Add autosave feature with backup of unsaved new projects (#221)

* Add autosave feature with backup of unsaved new projects.

* Fix wrong indentation on line 205.

* Store backup for every opened file in user://. Some other improvements.

* Remove unnecessary variable.

* Update Translations.pot

Co-authored-by: Manolis Papadeas <35376950+OverloadedOrama@users.noreply.github.com>
This commit is contained in:
Martin Novák 2020-04-30 19:33:24 +02:00 committed by GitHub
parent c82c54d096
commit 82fe186b65
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 343 additions and 110 deletions

View file

@ -41,6 +41,9 @@ func _ready() -> void:
# Disable input until the shortcut selector is displayed
set_process_input(false)
# Replace OK with Close since preference changes are being applied immediately, not after OK confirmation
get_ok().text = tr("Close")
for child in languages.get_children():
if child is Button:
child.connect("pressed", self, "_on_Language_pressed", [child])
@ -76,6 +79,16 @@ func _ready() -> void:
Global.show_right_tool_icon = Global.config_cache.get_value("preferences", "show_right_tool_icon")
right_tool_icon.pressed = Global.show_right_tool_icon
# Get autosave settings
if Global.config_cache.has_section_key("preferences", "autosave_interval"):
var autosave_interval = Global.config_cache.get_value("preferences", "autosave_interval")
OpenSave.set_autosave_interval(autosave_interval)
general.get_node("AutosaveInterval/AutosaveInterval").value = autosave_interval
if Global.config_cache.has_section_key("preferences", "enable_autosave"):
var enable_autosave = Global.config_cache.get_value("preferences", "enable_autosave")
OpenSave.toggle_autosave(enable_autosave)
general.get_node("EnableAutosave").pressed = enable_autosave
# Set default values for Canvas options
if Global.config_cache.has_section_key("preferences", "grid_size"):
var grid_size = Global.config_cache.get_value("preferences", "grid_size")
@ -527,3 +540,15 @@ func _on_OpenLastProject_pressed():
Global.open_last_project = !Global.open_last_project
Global.config_cache.set_value("preferences", "open_last_project", Global.open_last_project)
Global.config_cache.save("user://cache.ini")
func _on_EnableAutosave_toggled(button_pressed : bool) -> void:
OpenSave.toggle_autosave(button_pressed)
Global.config_cache.set_value("preferences", "enable_autosave", button_pressed)
Global.config_cache.save("user://cache.ini")
func _on_AutosaveInterval_value_changed(value : float) -> void:
OpenSave.set_autosave_interval(value)
Global.config_cache.set_value("preferences", "autosave_interval", value)
Global.config_cache.save("user://cache.ini")

View file

@ -193,8 +193,28 @@ func _ready() -> void:
Global.config_cache.set_value("preferences", "open_last_project", true)
if Global.config_cache.get_value("preferences", "open_last_project"):
Global.open_last_project = Global.config_cache.get_value("preferences", "open_last_project")
# If backup file exists then Pixelorama was not closed properly (probably crashed) - reopen backup
$BackupConfirmation.get_cancel().text = tr("Delete")
if Global.config_cache.has_section("backups"):
var project_paths = Global.config_cache.get_section_keys("backups")
if project_paths.size() > 0:
# Get backup path
var backup_path = Global.config_cache.get_value("backups", project_paths[0])
# Temporatily stop autosave until user confirms backup
OpenSave.autosave_timer.stop()
Global.can_draw = false
# For it's only possible to reload the first found backup
$BackupConfirmation.dialog_text = $BackupConfirmation.dialog_text % project_paths[0]
$BackupConfirmation.connect("confirmed", self, "_on_BackupConfirmation_confirmed", [project_paths[0], backup_path])
$BackupConfirmation.get_cancel().connect("pressed", self, "_on_BackupConfirmation_delete", [project_paths[0], backup_path])
$BackupConfirmation.popup_centered()
else:
load_last_project()
else:
load_last_project()
func _input(event : InputEvent) -> void:
Global.left_cursor.position = get_global_mouse_position() + Vector2(-32, 32)
Global.left_cursor.texture = Global.left_cursor_tool_texture
@ -420,16 +440,17 @@ func help_menu_id_pressed(id : int) -> void:
Global.can_draw = false
func load_last_project():
# Check if any project was saved or opened last time
if Global.config_cache.has_section_key("preferences", "last_project_path"):
# Check if file still exists on disk
var file_path = Global.config_cache.get_value("preferences", "last_project_path")
var file_check := File.new()
if file_check.file_exists(file_path): # If yes then load the file
_on_OpenSprite_file_selected(file_path)
else:
# If file doesn't exist on disk then warn user about this
$OpenLastProjectAlertDialog.popup_centered()
if Global.open_last_project:
# Check if any project was saved or opened last time
if Global.config_cache.has_section_key("preferences", "last_project_path"):
# Check if file still exists on disk
var file_path = Global.config_cache.get_value("preferences", "last_project_path")
var file_check := File.new()
if file_check.file_exists(file_path): # If yes then load the file
_on_OpenSprite_file_selected(file_path)
else:
# If file doesn't exist on disk then warn user about this
$OpenLastProjectAlertDialog.popup_centered()
func _on_UnsavedCanvasDialog_confirmed() -> void:
@ -451,7 +472,7 @@ func _on_OpenSprite_file_selected(path : String) -> void:
func _on_SaveSprite_file_selected(path : String) -> void:
OpenSave.save_pxo_file(path)
OpenSave.save_pxo_file(path, false)
# Set last opened project path and save
Global.config_cache.set_value("preferences", "last_project_path", path)
@ -782,16 +803,34 @@ func _on_QuitAndSaveDialog_custom_action(action : String) -> void:
$SaveSprite.popup_centered()
$QuitDialog.hide()
Global.can_draw = false
OpenSave.remove_backup()
func _on_QuitDialog_confirmed() -> void:
# Darken the UI to denote that the application is currently exiting
# (it won't respond to user input in this state).
modulate = Color(0.5, 0.5, 0.5)
OpenSave.remove_backup()
get_tree().quit()
func _on_BackupConfirmation_confirmed(project_path : String, backup_path : String) -> void:
OpenSave.reload_backup_file(project_path, backup_path)
OpenSave.autosave_timer.start()
$ExportDialog.file_name = OpenSave.current_save_path.get_file().trim_suffix(".pxo")
$ExportDialog.directory_path = OpenSave.current_save_path.get_base_dir()
$ExportDialog.was_exported = false
file_menu.set_item_text(3, tr("Save") + " %s" % OpenSave.current_save_path.get_file())
file_menu.set_item_text(6, tr("Export"))
func _on_BackupConfirmation_delete(project_path : String, backup_path : String) -> void:
OpenSave.remove_backup_by_path(project_path, backup_path)
OpenSave.autosave_timer.start()
# Reopen last project
load_last_project()
func _on_LeftPixelPerfectMode_toggled(button_pressed) -> void:
Global.left_pixel_perfect = button_pressed

View file

@ -1,17 +1,31 @@
extends Node
var current_save_path := ""
# Stores a filename of a backup file in user:// until user saves manually
var backup_save_path = ""
onready var autosave_timer : Timer
var default_autosave_interval := 5 # Minutes
func _ready():
autosave_timer = Timer.new()
autosave_timer.one_shot = false
autosave_timer.process_mode = Timer.TIMER_PROCESS_IDLE
autosave_timer.connect("timeout", self, "_on_Autosave_timeout")
add_child(autosave_timer)
set_autosave_interval(default_autosave_interval)
toggle_autosave(false) # Gets started from preferences dialog
func open_pxo_file(path : String) -> void:
func open_pxo_file(path : String, untitled_backup : bool = false) -> void:
var file := File.new()
var err := file.open_compressed(path, File.READ, File.COMPRESSION_ZSTD)
if err == ERR_FILE_UNRECOGNIZED:
err = file.open(path, File.READ) # If the file is not compressed open it raw (pre-v0.7)
if err != OK:
Global.notification_label("File failed to open")
file.close()
OS.alert("Can't load file")
return
var file_version := file.get_line() # Example, "v0.6"
@ -133,13 +147,13 @@ func open_pxo_file(path : String) -> void:
file.close()
current_save_path = path
Global.window_title = path.get_file() + " - Pixelorama"
if not untitled_backup:
# Untitled backup should not change window title and save path
current_save_path = path
Global.window_title = path.get_file() + " - Pixelorama"
func save_pxo_file(path : String) -> void:
current_save_path = path
func save_pxo_file(path : String, autosave : bool) -> void:
var file := File.new()
var err := file.open_compressed(path, File.WRITE, File.COMPRESSION_ZSTD)
if err == OK:
@ -209,10 +223,89 @@ func save_pxo_file(path : String) -> void:
file.store_8(tag[3]) # Tag "to", the last frame
file.store_line("END_FRAME_TAGS")
if !Global.saved:
file.close()
if !Global.saved and not autosave:
Global.saved = true
Global.window_title = current_save_path.get_file() + " - Pixelorama"
Global.notification_label("File saved")
file.close()
if autosave:
Global.notification_label("File autosaved")
else:
# First remove backup then set current save path
remove_backup()
current_save_path = path
Global.notification_label("File saved")
if backup_save_path == "":
Global.window_title = path.get_file() + " - Pixelorama"
else:
Global.notification_label("File failed to save")
func toggle_autosave(enable : bool) -> void:
if enable:
autosave_timer.start()
else:
autosave_timer.stop()
func set_autosave_interval(interval : float) -> void:
autosave_timer.wait_time = interval * 60 # Interval parameter is in minutes, wait_time is seconds
autosave_timer.start()
func _on_Autosave_timeout() -> void:
if backup_save_path == "":
# Create a new backup file if it doesn't exist yet
backup_save_path = "user://backup-" + String(OS.get_unix_time())
store_backup_path()
save_pxo_file(backup_save_path, true)
# Backup paths are stored in two ways:
# 1) User already manually saved and defined a save path -> {current_save_path, backup_save_path}
# 2) User didn't manually saved, "untitled" backup is stored -> {backup_save_path, backup_save_path}
func store_backup_path() -> void:
if current_save_path != "":
# Remove "untitled" backup if it existed on this project instance
if Global.config_cache.has_section_key("backups", backup_save_path):
Global.config_cache.erase_section_key("backups", backup_save_path)
Global.config_cache.set_value("backups", current_save_path, backup_save_path)
else:
Global.config_cache.set_value("backups", backup_save_path, backup_save_path)
Global.config_cache.save("user://cache.ini")
func remove_backup() -> void:
# Remove backup file
if backup_save_path != "":
if current_save_path != "":
remove_backup_by_path(current_save_path, backup_save_path)
else:
# If manual save was not yet done - remove "untitled" backup
remove_backup_by_path(backup_save_path, backup_save_path)
backup_save_path = ""
func remove_backup_by_path(project_path : String, backup_path : String) -> void:
Directory.new().remove(backup_path)
Global.config_cache.erase_section_key("backups", project_path)
Global.config_cache.save("user://cache.ini")
func reload_backup_file(project_path : String, backup_path : String) -> void:
# If project path is the same as backup save path -> the backup was untitled
open_pxo_file(backup_path, project_path == backup_path)
backup_save_path = backup_path
if project_path != backup_path:
current_save_path = project_path
Global.window_title = project_path.get_file() + " - Pixelorama(*)"
Global.saved = false
Global.notification_label("Backup reloaded")