diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 04db2ee..6bb7dec 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -9,12 +9,8 @@ * It contains a ``Adw.NavigationSplitView`` as a view which has a {@link View.FilesView} and * {@link View.EditView} inside. * - * It also has a instance of {@link Model.DesktopFileModel} and calls {@link Model.DesktopFileModel.load} when: - * - * * constructed - * * {@link View.FilesView.deleted} is emitted - * * {@link View.EditView.saved} is emitted - * * the action ``win.new`` is called + * It also has a instance of {@link Model.DesktopFileModel} and calls {@link Model.DesktopFileModel.load} when + * constructed. * * If {@link Model.DesktopFileModel.load} succeeded, this calls {@link View.FilesView.set_list_data} to reflect * load result.<
> @@ -30,8 +26,6 @@ public class MainWindow : Adw.ApplicationWindow { private Adw.NavigationSplitView split_view; private Model.DesktopFileModel model; - private Model.DesktopFile desktop_file; - private Model.DesktopFile backup_desktop_file; public MainWindow () { Object ( @@ -83,18 +77,32 @@ public class MainWindow : Adw.ApplicationWindow { on_new_activate (); }); - files_view.deleted.connect ((is_success) => { - desktop_file = null; - backup_desktop_file = null; + files_view.delete_activated.connect ((file) => { edit_view.hide_all (); - model.load.begin (); - if (is_success) { - var deleted_toast = new Adw.Toast (_("Entry deleted.")) { - timeout = 5 - }; - overlay.add_toast (deleted_toast); + bool ret = model.delete_file (file); + if (!ret) { + var error_dialog = new Adw.MessageDialog ( + (Gtk.Window) get_root (), + _("Failed to Delete Entry of ā€œ%sā€").printf (file.value_name), + _("There was an error while removing the app entry.") + ); + + error_dialog.add_response (Define.DialogResponse.CLOSE, _("_Close")); + + error_dialog.default_response = Define.DialogResponse.CLOSE; + error_dialog.close_response = Define.DialogResponse.CLOSE; + + error_dialog.present (); + return; } + + show_files_view (); + + var deleted_toast = new Adw.Toast (_("Entry deleted.")) { + timeout = 5 + }; + overlay.add_toast (deleted_toast); }); files_view.selected.connect ((entry) => { @@ -102,8 +110,7 @@ public class MainWindow : Adw.ApplicationWindow { }); edit_view.saved.connect (() => { - desktop_file.copy_to (backup_desktop_file); - model.load.begin (); + files_view.set_list_data (model.files_list); var updated_toast = new Adw.Toast (_("Entry updated.")) { timeout = 5 @@ -144,18 +151,20 @@ public class MainWindow : Adw.ApplicationWindow { /** * Preprocess before destruction of this. * - * Just destroy this if we never edited entries or no changes made for desktop files. + * Just destroy this if no changes made for desktop files. * Otherwise, tell the user unsaved work through a dialog. */ public void prep_destroy () { - // Never edited entries - if (desktop_file == null || backup_desktop_file == null) { - destroy (); - return; + // Check if there is a desktop file with changes + bool is_changed = false; + foreach (Model.DesktopFile file in model.files_list) { + if (!file.is_clean) { + is_changed = true; + break; + } } - // No changes made - if (desktop_file.equals (backup_desktop_file)) { + if (!is_changed) { destroy (); return; } @@ -193,47 +202,29 @@ public class MainWindow : Adw.ApplicationWindow { * Reload and show the file list. */ public void show_files_view () { - model.load.begin (); + files_view.set_list_data (model.files_list); split_view.show_content = false; } /** * Start editing the given {@link Model.DesktopFile}. * - * It first backups the given {@link Model.DesktopFile} and then calls {@link View.EditView.load_file} to start - * editing, so that the app can recognize unsaved changes before destruction. - * * @param file The {@link Model.DesktopFile} to edit */ public void show_edit_view (Model.DesktopFile file) { - desktop_file = file; - backup_desktop_file = new Model.DesktopFile (desktop_file.path); - desktop_file.copy_to (backup_desktop_file); - - edit_view.load_file (desktop_file); + edit_view.load_file (file); split_view.show_content = true; } /** * The callback for new file. * - * Create a new DesktopFile with random filename and start editing it. + * Create a new DesktopFile and start editing it. */ private void on_new_activate () { - string filename = Config.APP_ID + "." + Uuid.string_random (); - string path = Path.build_filename ( - Environment.get_home_dir (), ".local/share/applications", - filename + Model.DesktopFile.DESKTOP_SUFFIX - ); - - var file = new Model.DesktopFile (path); + Model.DesktopFile file = model.create_file (); - bool ret = file.save_file (); - if (!ret) { - return; - } - - model.load.begin (); + files_view.set_list_data (model.files_list); show_edit_view (file); } } diff --git a/src/Model/DesktopFile.vala b/src/Model/DesktopFile.vala index 4e6a4fb..8b63ff3 100644 --- a/src/Model/DesktopFile.vala +++ b/src/Model/DesktopFile.vala @@ -22,7 +22,7 @@ public class Model.DesktopFile : Object { */ public string value_type { set { - Util.KeyFileUtil.set_string (keyfile, KeyFileDesktop.KEY_TYPE, value); + Util.KeyFileUtil.set_string (keyfile_dirty, KeyFileDesktop.KEY_TYPE, value); } } @@ -31,14 +31,14 @@ public class Model.DesktopFile : Object { */ public string value_name { owned get { - string? locale = Util.KeyFileUtil.get_locale_for_key (keyfile, KeyFileDesktop.KEY_NAME, Application.preferred_language); - string name = Util.KeyFileUtil.get_locale_string (keyfile, KeyFileDesktop.KEY_NAME, locale); + string? locale = Util.KeyFileUtil.get_locale_for_key (keyfile_dirty, KeyFileDesktop.KEY_NAME, Application.preferred_language); + string name = Util.KeyFileUtil.get_locale_string (keyfile_dirty, KeyFileDesktop.KEY_NAME, locale); return name; } set { - Util.KeyFileUtil.set_string (keyfile, KeyFileDesktop.KEY_NAME, value); + Util.KeyFileUtil.set_string (keyfile_dirty, KeyFileDesktop.KEY_NAME, value); } } @@ -47,11 +47,11 @@ public class Model.DesktopFile : Object { */ public string value_exec { owned get { - return Util.KeyFileUtil.get_string (keyfile, KeyFileDesktop.KEY_EXEC); + return Util.KeyFileUtil.get_string (keyfile_dirty, KeyFileDesktop.KEY_EXEC); } set { - Util.KeyFileUtil.set_string (keyfile, KeyFileDesktop.KEY_EXEC, value); + Util.KeyFileUtil.set_string (keyfile_dirty, KeyFileDesktop.KEY_EXEC, value); } } @@ -60,11 +60,11 @@ public class Model.DesktopFile : Object { */ public string value_icon { owned get { - return Util.KeyFileUtil.get_string (keyfile, KeyFileDesktop.KEY_ICON); + return Util.KeyFileUtil.get_string (keyfile_dirty, KeyFileDesktop.KEY_ICON); } set { - Util.KeyFileUtil.set_string (keyfile, KeyFileDesktop.KEY_ICON, value); + Util.KeyFileUtil.set_string (keyfile_dirty, KeyFileDesktop.KEY_ICON, value); } } @@ -73,14 +73,14 @@ public class Model.DesktopFile : Object { */ public string value_generic_name { owned get { - string? locale = Util.KeyFileUtil.get_locale_for_key (keyfile, KeyFileDesktop.KEY_GENERIC_NAME, Application.preferred_language); - string generic_name = Util.KeyFileUtil.get_locale_string (keyfile, KeyFileDesktop.KEY_GENERIC_NAME, locale); + string? locale = Util.KeyFileUtil.get_locale_for_key (keyfile_dirty, KeyFileDesktop.KEY_GENERIC_NAME, Application.preferred_language); + string generic_name = Util.KeyFileUtil.get_locale_string (keyfile_dirty, KeyFileDesktop.KEY_GENERIC_NAME, locale); return generic_name; } set { - Util.KeyFileUtil.set_string (keyfile, KeyFileDesktop.KEY_GENERIC_NAME, value); + Util.KeyFileUtil.set_string (keyfile_dirty, KeyFileDesktop.KEY_GENERIC_NAME, value); } } @@ -89,14 +89,14 @@ public class Model.DesktopFile : Object { */ public string value_comment { owned get { - string? locale = Util.KeyFileUtil.get_locale_for_key (keyfile, KeyFileDesktop.KEY_COMMENT, Application.preferred_language); - string comment = Util.KeyFileUtil.get_locale_string (keyfile, KeyFileDesktop.KEY_COMMENT, locale); + string? locale = Util.KeyFileUtil.get_locale_for_key (keyfile_dirty, KeyFileDesktop.KEY_COMMENT, Application.preferred_language); + string comment = Util.KeyFileUtil.get_locale_string (keyfile_dirty, KeyFileDesktop.KEY_COMMENT, locale); return comment; } set { - Util.KeyFileUtil.set_string (keyfile, KeyFileDesktop.KEY_COMMENT, value); + Util.KeyFileUtil.set_string (keyfile_dirty, KeyFileDesktop.KEY_COMMENT, value); } } @@ -105,11 +105,11 @@ public class Model.DesktopFile : Object { */ public string[] value_categories { owned get { - return Util.KeyFileUtil.get_string_list (keyfile, KeyFileDesktop.KEY_CATEGORIES); + return Util.KeyFileUtil.get_string_list (keyfile_dirty, KeyFileDesktop.KEY_CATEGORIES); } set { - Util.KeyFileUtil.set_string_list (keyfile, KeyFileDesktop.KEY_CATEGORIES, value); + Util.KeyFileUtil.set_string_list (keyfile_dirty, KeyFileDesktop.KEY_CATEGORIES, value); } } @@ -118,11 +118,11 @@ public class Model.DesktopFile : Object { */ public string[] value_keywords { owned get { - return Util.KeyFileUtil.get_string_list (keyfile, Util.KeyFileUtil.KEY_KEYWORDS); + return Util.KeyFileUtil.get_string_list (keyfile_dirty, Util.KeyFileUtil.KEY_KEYWORDS); } set { - Util.KeyFileUtil.set_string_list (keyfile, Util.KeyFileUtil.KEY_KEYWORDS, value); + Util.KeyFileUtil.set_string_list (keyfile_dirty, Util.KeyFileUtil.KEY_KEYWORDS, value); } } @@ -131,11 +131,11 @@ public class Model.DesktopFile : Object { */ public string value_startup_wm_class { owned get { - return Util.KeyFileUtil.get_string (keyfile, KeyFileDesktop.KEY_STARTUP_WM_CLASS); + return Util.KeyFileUtil.get_string (keyfile_dirty, KeyFileDesktop.KEY_STARTUP_WM_CLASS); } set { - Util.KeyFileUtil.set_string (keyfile, KeyFileDesktop.KEY_STARTUP_WM_CLASS, value); + Util.KeyFileUtil.set_string (keyfile_dirty, KeyFileDesktop.KEY_STARTUP_WM_CLASS, value); } } @@ -144,73 +144,79 @@ public class Model.DesktopFile : Object { */ public bool value_terminal { get { - return Util.KeyFileUtil.get_boolean (keyfile, KeyFileDesktop.KEY_TERMINAL); + return Util.KeyFileUtil.get_boolean (keyfile_dirty, KeyFileDesktop.KEY_TERMINAL); } set { - Util.KeyFileUtil.set_boolean (keyfile, KeyFileDesktop.KEY_TERMINAL, value); + Util.KeyFileUtil.set_boolean (keyfile_dirty, KeyFileDesktop.KEY_TERMINAL, value); } } /** - * Store data in a single desktop file. + * Value of "Name" entry without unsaved changes. */ - private KeyFile keyfile; + public string saved_value_name { + owned get { + string? locale = Util.KeyFileUtil.get_locale_for_key (keyfile_clean, KeyFileDesktop.KEY_NAME, Application.preferred_language); + string name = Util.KeyFileUtil.get_locale_string (keyfile_clean, KeyFileDesktop.KEY_NAME, locale); - /** - * The constructor. - * - * @param path the absolute path to the desktop file - */ - public DesktopFile (string path) { - Object ( - path: path - ); + return name; + } } - construct { - keyfile = new KeyFile (); + /** + * Value of "Icon" entry without unsaved changes. + */ + public string saved_value_icon { + owned get { + return Util.KeyFileUtil.get_string (keyfile_clean, KeyFileDesktop.KEY_ICON); + } } /** - * Check if this and other contains the same values as desktop files. - * - * @param other another DesktopFile - * @return true if this and other contains the same values + * Value of "Comment" entry without unsaved changes. */ - public bool equals (DesktopFile other) { - // Compare other than the path - string this_data = this.to_data (); - string other_data = other.to_data (); + public string saved_value_comment { + owned get { + string? locale = Util.KeyFileUtil.get_locale_for_key (keyfile_clean, KeyFileDesktop.KEY_COMMENT, Application.preferred_language); + string comment = Util.KeyFileUtil.get_locale_string (keyfile_clean, KeyFileDesktop.KEY_COMMENT, locale); - return this_data == other_data; + return comment; + } } /** - * Copy and set data from this to another DesktopFile. - * - * @param dest another DesktopFile to copy this data to - * @return true if successfully copied, false otherwise + * Returns if this contains unsaved changes to the disk. */ - public bool copy_to (DesktopFile dest) { - string data = to_data (); - return dest.load_from_data (data); - } - - public string to_data () { - return keyfile.to_data (); + public bool is_clean { + get { + return Util.KeyFileUtil.equals (keyfile_dirty, keyfile_clean); + } } - public bool load_from_data (string data) { - bool ret = false; + /** + * Data in a single desktop file that loaded from the disk. + */ + private KeyFile keyfile_clean; + /** + * Data in a single desktop file that may contain unsaved changes to the disk. + */ + private KeyFile keyfile_dirty; - try { - ret = keyfile.load_from_data (data, data.length, KeyFileFlags.KEEP_TRANSLATIONS); - } catch (KeyFileError err) { - warning ("Failed to KeyFile.load_from_data: %s", err.message); - } + /** + * The constructor. + * + * @param path the absolute path to the desktop file + */ + public DesktopFile (string path) { + Object ( + path: path + ); + } - return ret; + construct { + keyfile_clean = new KeyFile (); + keyfile_dirty = new KeyFile (); } //////////////////////////////////////////////////////////////////////////// @@ -220,23 +226,20 @@ public class Model.DesktopFile : Object { //////////////////////////////////////////////////////////////////////////// public bool load_file () { - return Util.KeyFileUtil.load_file (keyfile, path, KeyFileFlags.KEEP_TRANSLATIONS); - } + bool ret = Util.KeyFileUtil.load_file (keyfile_clean, path, KeyFileFlags.KEEP_TRANSLATIONS); + if (!ret) { + return false; + } - public bool save_file () { - return Util.KeyFileUtil.save_file (keyfile, path); + return Util.KeyFileUtil.copy (keyfile_dirty, keyfile_clean); } - public bool delete_file () { - var file = File.new_for_path (path); - bool ret = false; - - try { - ret = file.delete (); - } catch (Error err) { - warning ("Failed to delete file. path=%s: %s", path, err.message); + public bool save_file () { + bool ret = Util.KeyFileUtil.save_file (keyfile_dirty, path); + if (!ret) { + return false; } - return ret; + return Util.KeyFileUtil.copy (keyfile_clean, keyfile_dirty); } } diff --git a/src/Model/DesktopFileModel.vala b/src/Model/DesktopFileModel.vala index 0abe4ab..a6ebc6b 100644 --- a/src/Model/DesktopFileModel.vala +++ b/src/Model/DesktopFileModel.vala @@ -120,4 +120,63 @@ public class Model.DesktopFileModel : Object { load_failure (); } } + + /** + * Create a new {@link DesktopFile} with random filename. + * + * @return Created {@link DesktopFile} + */ + public Model.DesktopFile create_file () { + string filename = Config.APP_ID + "." + Uuid.string_random (); + string path = Path.build_filename ( + desktop_files_path, + filename + Model.DesktopFile.DESKTOP_SUFFIX + ); + + var file = new Model.DesktopFile (path); + + files_list.add (file); + + return file; + } + + /** + * Delete desktop file from list and the disk. + * + * @param file A {@link DesktopFile} to delete + * @return true if succeeded, false otherwise + */ + public bool delete_file (Model.DesktopFile file) { + bool ret = delete_from_disk (file.path); + if (!ret) { + return false; + } + + files_list.remove (file); + + return true; + } + + /** + * Delete file at path from the disk. + * + * @param path A file to delete + * @return true if succeeded, false otherwise + */ + private bool delete_from_disk (string path) { + var file = File.new_for_path (path); + + if (!file.query_exists ()) { + return true; + } + + bool ret = false; + try { + ret = file.delete (); + } catch (Error err) { + warning ("Failed to delete file. path=%s: %s", path, err.message); + } + + return ret; + } } diff --git a/src/Util/KeyFileUtil.vala b/src/Util/KeyFileUtil.vala index 8036223..8c02f40 100644 --- a/src/Util/KeyFileUtil.vala +++ b/src/Util/KeyFileUtil.vala @@ -23,6 +23,40 @@ namespace Util.KeyFileUtil { */ public const string KEY_KEYWORDS = "Keywords"; + /** + * Check if two KeyFiles have the same content. + * + * @param a KeyFile to compare + * @param b KeyFile to compare + * @return true if two KeyFiles have the same content + */ + public static bool equals (KeyFile a, KeyFile b) { + string a_data = a.to_data (); + string b_data = b.to_data (); + + return a_data == b_data; + } + + /** + * Sync content of KeyFile between. + * + * @param src KeyFile to be copied from + * @param dst KeyFile to be copied to + * @return true if successfully copied, false otherwise + */ + public static bool copy (KeyFile dst, KeyFile src) { + string data = src.to_data (); + + try { + dst.load_from_data (data, data.length, KeyFileFlags.KEEP_TRANSLATIONS); + } catch (KeyFileError err) { + warning ("Failed to KeyFile.load_from_data: %s", err.message); + return false; + } + + return true; + } + //////////////////////////////////////////////////////////////////////////// // // Key Opearations diff --git a/src/View/FilesView.vala b/src/View/FilesView.vala index 59d1c3e..4b4ed68 100644 --- a/src/View/FilesView.vala +++ b/src/View/FilesView.vala @@ -5,7 +5,7 @@ public class View.FilesView : Adw.NavigationPage { public signal void new_activated (); - public signal void deleted (bool is_success); + public signal void delete_activated (Model.DesktopFile file); public signal void selected (Model.DesktopFile file); private ListStore list_store; @@ -112,7 +112,7 @@ public class View.FilesView : Adw.NavigationPage { margin_bottom = 6 }; try { - app_icon.gicon = Icon.new_for_string (file.value_icon); + app_icon.gicon = Icon.new_for_string (file.saved_value_icon); } catch (Error err) { warning ("Failed to update app_icon: %s", err.message); } @@ -123,40 +123,24 @@ public class View.FilesView : Adw.NavigationPage { }; delete_button.add_css_class ("flat"); delete_button.clicked.connect (() => { - var delete_dialog = setup_delete_dialog (file.value_name); + var delete_dialog = setup_delete_dialog (file.saved_value_name); delete_dialog.response.connect ((response_id) => { if (response_id != Define.DialogResponse.OK) { return; } - bool ret = file.delete_file (); - if (!ret) { - var error_dialog = new Adw.MessageDialog ( - (Gtk.Window) get_root (), - _("Failed to Delete Entry of ā€œ%sā€").printf (file.value_name), - _("There was an error while removing the app entry.") - ); - - error_dialog.add_response (Define.DialogResponse.CLOSE, _("_Close")); - - error_dialog.default_response = Define.DialogResponse.CLOSE; - error_dialog.close_response = Define.DialogResponse.CLOSE; - - error_dialog.present (); - } - - deleted (ret); - delete_dialog.destroy (); + + delete_activated (file); }); delete_dialog.present (); }); var row = new Adw.ActionRow () { - title = file.value_name, - subtitle = file.value_comment, + title = file.saved_value_name, + subtitle = file.saved_value_comment, title_lines = 1, subtitle_lines = 1, activatable = true