From e871c76c986a629211b170f91b29b712f4b72c8c Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 6 Apr 2022 18:21:31 +0000 Subject: [PATCH 01/12] Improve saving * Make mostly async * Simplify functions - remove app_closing and force parameters * Better function names * Only destroy window after saving completed --- src/MainWindow.vala | 45 ++++------ src/Services/Document.vala | 151 ++++++++++++++++---------------- src/Services/PluginManager.vala | 2 +- src/Widgets/DocumentView.vala | 73 +++++++++------ 4 files changed, 143 insertions(+), 128 deletions(-) diff --git a/src/MainWindow.vala b/src/MainWindow.vala index fef536c599..68384b0103 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -343,7 +343,7 @@ namespace Scratch { var docs = document_view.docs.copy (); docs.foreach ((doc) => { if (doc.file.get_path ().has_prefix (a)) { - document_view.close_document (doc); + document_view.close_document.begin (doc); } }); }); @@ -524,8 +524,9 @@ namespace Scratch { } protected override bool delete_event (Gdk.EventAny event) { - handle_quit (); - return !check_unsaved_changes (); + handle_quit.begin (); + // Do not want the signal to be propagated, handle_quit will destroy the app if possible. + return Gdk.EVENT_STOP; } // Set sensitive property for 'delicate' Widgets/GtkActions while @@ -571,21 +572,8 @@ namespace Scratch { } // Close a document - public void close_document (Scratch.Services.Document doc) { - document_view.close_document (doc); - } - - // Check if there no unsaved changes - private bool check_unsaved_changes () { - document_view.is_closing = true; - foreach (var doc in document_view.docs) { - if (!doc.do_close (true)) { - document_view.current_document = doc; - return false; - } - } - - return true; + public async void close_document (Scratch.Services.Document doc) { + yield document_view.close_document (doc); } // Save session information different from window state @@ -635,13 +623,21 @@ namespace Scratch { // SIGTERM/SIGINT Handling public bool quit_source_func () { action_quit (); - return false; + return Source.REMOVE; } // For exit cleanup - private void handle_quit () { - document_view.save_opened_files (); - update_saved_state (); + private async void handle_quit () { + if (yield document_view.prepare_to_close ()) { + do_quit (); + } else { + //TODO Give user option to force close + } + } + + private void do_quit () { + update_saved_state (); // Remember window state + destroy (); // For now, destroy under all circumstances } public void set_default_zoom () { @@ -726,10 +722,7 @@ namespace Scratch { } private void action_quit () { - handle_quit (); - if (check_unsaved_changes ()) { - destroy (); - } + handle_quit.begin (); } private void action_open () { diff --git a/src/Services/Document.vala b/src/Services/Document.vala index 3c6479ca43..37ad50c934 100644 --- a/src/Services/Document.vala +++ b/src/Services/Document.vala @@ -92,7 +92,7 @@ namespace Scratch.Services { public string original_content; private string last_save_content; - public bool saved = true; + private Gtk.ScrolledWindow scroll; private Gtk.InfoBar info_bar; @@ -103,6 +103,7 @@ namespace Scratch.Services { private ulong onchange_handler_id = 0; // It is used to not mark files as changed on load private bool loaded = false; private bool mounted = true; // Mount state of the file + private bool saved = true; private Mount mount; private static Pango.FontDescription? builder_blocks_font = null; @@ -180,11 +181,11 @@ namespace Scratch.Services { // Focus out event for SourceView this.source_view.focus_out_event.connect (() => { - if (Scratch.settings.get_boolean ("autosave")) { + if (!saved && Scratch.settings.get_boolean ("autosave")) { save.begin (); } - return false; + return Gdk.EVENT_PROPAGATE; }); source_view.buffer.changed.connect (() => { @@ -361,93 +362,100 @@ namespace Scratch.Services { return; } - public bool do_close (bool app_closing = false) { - debug ("Closing \"%s\"", get_basename ()); - + // Call before destroying the document + public async bool ensure_saved () { if (!loaded) { load_cancellable.cancel (); return true; } - bool ret_value = true; - if (Scratch.settings.get_boolean ("autosave") && !saved) { - save_with_hold (); - } else if (app_closing && is_file_temporary && !delete_temporary_file ()) { - debug ("Save temporary file!"); - save_with_hold (); - } - // Check for unsaved changes - else if (!this.saved || (!app_closing && is_file_temporary && !delete_temporary_file ())) { - var parent_window = source_view.get_toplevel () as Gtk.Window; - - var dialog = new Granite.MessageDialog ( - _("Save changes to \"%s\" before closing?").printf (this.get_basename ()), - _("If you don't save, changes will be permanently lost."), - new ThemedIcon ("dialog-warning"), - Gtk.ButtonsType.NONE - ); - dialog.transient_for = parent_window; - - var no_save_button = (Gtk.Button) dialog.add_button (_("Close Without Saving"), Gtk.ResponseType.NO); - no_save_button.get_style_context ().add_class (Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION); - - dialog.add_button (_("Cancel"), Gtk.ResponseType.CANCEL); - dialog.add_button (_("Save"), Gtk.ResponseType.YES); - dialog.set_default_response (Gtk.ResponseType.YES); - - int response = dialog.run (); - switch (response) { - case Gtk.ResponseType.CANCEL: - case Gtk.ResponseType.DELETE_EVENT: - ret_value = false; - break; - case Gtk.ResponseType.YES: - if (this.is_file_temporary) - save_as_with_hold (); - else - save_with_hold (); - break; - case Gtk.ResponseType.NO: - if (this.is_file_temporary) - delete_temporary_file (true); - break; + bool success = true; // True if saved or does not need saving + bool autosave = Scratch.settings.get_boolean ("autosave"); + if (is_file_temporary) { + if (get_text ().length > 0) { + success = yield confirm_save (); + } else { + delete_temporary_file (); } - dialog.destroy (); + } else if (autosave) { + success = yield save_with_hold (); + } else if (!saved) { + success = yield confirm_save (); } - if (ret_value) { - // Delete backup copy file - delete_backup (); - doc_closed (); + return success; + } + + private async bool confirm_save () { + var dialog = new Granite.MessageDialog ( + _("Save changes to \"%s\" before closing?").printf (this.get_basename ()), + _("If you don't save, changes will be permanently lost."), + new ThemedIcon ("dialog-warning"), + Gtk.ButtonsType.NONE + ) { + transient_for = (Gtk.Window)source_view.get_toplevel () + }; + + var no_save_button = (Gtk.Button) dialog.add_button (_("Close Without Saving"), Gtk.ResponseType.NO); + no_save_button.get_style_context ().add_class (Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION); + + dialog.add_button (_("Cancel"), Gtk.ResponseType.CANCEL); + dialog.add_button (_("Save"), Gtk.ResponseType.YES); + dialog.set_default_response (Gtk.ResponseType.YES); + + int response = dialog.run (); + dialog.destroy (); + + switch (response) { + case Gtk.ResponseType.YES: + bool success; + if (is_file_temporary) { + success = yield save_as_with_hold (); + } else { + success = yield save_with_hold (); + } + + return success; + case Gtk.ResponseType.NO: + if (is_file_temporary) { + delete_temporary_file (); + } + + return true; + default: // Cancelled + return false; } - return ret_value; } - public bool save_with_hold (bool force = false) { + // Should only be called after doc has been prepared for closing. + public void do_close () { + debug ("Closing \"%s\"", get_basename ()); + delete_backup (); + doc_closed (); + } + + private async bool save_with_hold () { GLib.Application.get_default ().hold (); bool result = false; - save.begin (force, (obj, res) => { - result = save.end (res); - GLib.Application.get_default ().release (); - }); + result = yield save (); + GLib.Application.get_default ().release (); return result; } - public bool save_as_with_hold () { + private async bool save_as_with_hold () { GLib.Application.get_default ().hold (); bool result = false; - save_as.begin ((obj, res) => { - result = save_as.end (res); - GLib.Application.get_default ().release (); - }); + result = yield save_as (); + GLib.Application.get_default ().release (); return result; } - public async bool save (bool force = false) { - if (!force && (source_view.buffer.get_modified () == false || this.loaded == false)) { + // It is callers responsibility to ensure document should be saved + public async bool save () { + if (!loaded) { return false; } @@ -467,13 +475,12 @@ namespace Scratch.Services { } source_view.buffer.set_modified (false); - - doc_saved (); this.set_saved_status (true); last_save_content = source_view.buffer.text; debug ("File \"%s\" saved successfully", get_basename ()); + doc_saved (); return true; } @@ -804,7 +811,7 @@ namespace Scratch.Services { } // Set saved status - public void set_saved_status (bool val) { + private void set_saved_status (bool val) { this.saved = val; string unsaved_identifier = "* "; @@ -859,11 +866,7 @@ namespace Scratch.Services { } } - private bool delete_temporary_file (bool force = false) { - if (!is_file_temporary || (get_text ().length > 0 && !force)) { - return false; - } - + private bool delete_temporary_file () requires (is_file_temporary) { try { file.delete (); return true; diff --git a/src/Services/PluginManager.vala b/src/Services/PluginManager.vala index c8036d5db5..2d166f0be2 100644 --- a/src/Services/PluginManager.vala +++ b/src/Services/PluginManager.vala @@ -47,7 +47,7 @@ namespace Scratch.Services { } public void close_document (Document doc) { - manager.window.close_document (doc); + manager.window.close_document.begin (doc); } } diff --git a/src/Widgets/DocumentView.vala b/src/Widgets/DocumentView.vala index 06e50b450f..2248609d89 100644 --- a/src/Widgets/DocumentView.vala +++ b/src/Widgets/DocumentView.vala @@ -35,8 +35,6 @@ public class Scratch.Widgets.DocumentView : Granite.Widgets.DynamicNotebook { public GLib.List docs; - public bool is_closing = false; - private Gtk.CssProvider style_provider; public DocumentView (MainWindow window) { @@ -64,12 +62,15 @@ public class Scratch.Widgets.DocumentView : Granite.Widgets.DynamicNotebook { }); close_tab_requested.connect ((tab) => { - var document = tab as Services.Document; - if (!document.is_file_temporary && document.file != null) { - tab.restore_data = document.get_uri (); + var doc = tab as Services.Document; + + // Saving restore_data in close_document callback does not work + if (!doc.is_file_temporary && doc.file != null) { + tab.restore_data = doc.get_uri (); } - return document.do_close (); + close_document.begin (doc); + return true; // tab will be removed if/when doc properly closed }); tab_switched.connect ((old_tab, new_tab) => { @@ -151,7 +152,7 @@ public class Scratch.Widgets.DocumentView : Granite.Widgets.DynamicNotebook { current_document = doc; doc.focus (); - save_opened_files (); + remember_opened_files (); } catch (Error e) { critical (e.message); } @@ -171,7 +172,7 @@ public class Scratch.Widgets.DocumentView : Granite.Widgets.DynamicNotebook { current_document = doc; doc.focus (); - save_opened_files (); + remember_opened_files (); } catch (Error e) { critical ("Cannot insert clipboard: %s", clipboard); } @@ -209,7 +210,8 @@ public class Scratch.Widgets.DocumentView : Granite.Widgets.DynamicNotebook { if (cursor_position > 0) { doc.source_view.cursor_position = cursor_position; } - save_opened_files (); + + remember_opened_files (); }); return false; @@ -223,11 +225,9 @@ public class Scratch.Widgets.DocumentView : Granite.Widgets.DynamicNotebook { file.create (FileCreateFlags.PRIVATE); var doc = new Services.Document (window.actions, file); - doc.source_view.set_text (original.get_text ()); + doc.source_view.set_text (original.get_text ()); // Does not generate changed signal + doc.source_view.buffer.changed (); // Trigger autosave or update tab label doc.source_view.language = original.source_view.language; - if (Scratch.settings.get_boolean ("autosave")) { - doc.save.begin (true); - } insert_tab (doc, -1); current_document = doc; @@ -263,18 +263,38 @@ public class Scratch.Widgets.DocumentView : Granite.Widgets.DynamicNotebook { } } - public void close_document (Services.Document doc) { - remove_tab (doc); - doc.do_close (); + // Only use for single documents when app remains open + public async bool close_document (Services.Document doc) { + if (yield doc.ensure_saved ()) { + doc.do_close (); + remove_tab (doc); + return true; + } else { + return false; + } } - public void close_current_document () { - var doc = current_document; - if (doc != null) { - if (close_tab_requested (doc)) { - remove_tab (doc); - } + // Must call before the app is closed + public async bool prepare_to_close () { + tab_removed.disconnect (on_doc_removed); + + var docs = docs.copy (); + bool success = true; + foreach (var doc in docs) { + success = success && yield doc.ensure_saved (); // This may rename the document + }; + + if (!success) { + return false; // Either a document could not be saved or the operation was cancelled } + + remember_opened_files (); + foreach (var doc in docs) { + doc.do_close (); + remove_tab (doc); + }; + + return true; } public void request_placeholder_if_empty () { @@ -320,6 +340,7 @@ public class Scratch.Widgets.DocumentView : Granite.Widgets.DynamicNotebook { doc.source_view.drag_data_received.connect (drag_received); } + // Must disconnect this handler before closing the app else open docs not remembered properly private void on_doc_removed (Granite.Widgets.Tab tab) { var doc = tab as Services.Document; @@ -337,9 +358,7 @@ public class Scratch.Widgets.DocumentView : Granite.Widgets.DynamicNotebook { } } - if (!is_closing) { - save_opened_files (); - } + remember_opened_files (); } private void on_doc_moved (Granite.Widgets.Tab tab, int x, int y) { @@ -366,7 +385,7 @@ public class Scratch.Widgets.DocumentView : Granite.Widgets.DynamicNotebook { doc.focus (); - save_opened_files (); + remember_opened_files (); } private bool on_focus_in_event () { @@ -398,7 +417,7 @@ public class Scratch.Widgets.DocumentView : Granite.Widgets.DynamicNotebook { Gtk.drag_finish (ctx, true, false, time); } - public void save_opened_files () { + public void remember_opened_files () { if (privacy_settings.get_boolean ("remember-recent-files")) { var vb = new VariantBuilder (new VariantType ("a(si)")); tabs.foreach ((tab) => { From 56b8ee2d575e38933bbba70f5f54010476934fff Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 6 Apr 2022 18:37:25 +0000 Subject: [PATCH 02/12] Always hold application while saving --- src/MainWindow.vala | 4 ++-- src/Services/Document.vala | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 68384b0103..5a580205b6 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -794,7 +794,7 @@ namespace Scratch { if (doc.is_file_temporary == true) { action_save_as (); } else { - doc.save.begin (); + doc.save_with_hold.begin (); } } } @@ -802,7 +802,7 @@ namespace Scratch { private void action_save_as () { var doc = get_current_document (); if (doc != null) { - doc.save_as.begin (); + doc.save_as_with_hold.begin (); } } diff --git a/src/Services/Document.vala b/src/Services/Document.vala index 37ad50c934..0466983abd 100644 --- a/src/Services/Document.vala +++ b/src/Services/Document.vala @@ -435,7 +435,8 @@ namespace Scratch.Services { doc_closed (); } - private async bool save_with_hold () { + // Call directly to force saving + public async bool save_with_hold () { GLib.Application.get_default ().hold (); bool result = false; result = yield save (); @@ -444,7 +445,7 @@ namespace Scratch.Services { return result; } - private async bool save_as_with_hold () { + public async bool save_as_with_hold () { GLib.Application.get_default ().hold (); bool result = false; result = yield save_as (); @@ -454,7 +455,7 @@ namespace Scratch.Services { } // It is callers responsibility to ensure document should be saved - public async bool save () { + private async bool save () { if (!loaded) { return false; } @@ -484,7 +485,7 @@ namespace Scratch.Services { return true; } - public async bool save_as () { + private async bool save_as () { // New file if (!loaded) { return false; From 588d485cd09bfbc595ca298bc4850545473931d8 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Thu, 7 Apr 2022 11:17:31 +0000 Subject: [PATCH 03/12] Disconnect tab switch signal before closing --- src/Widgets/DocumentView.vala | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Widgets/DocumentView.vala b/src/Widgets/DocumentView.vala index 2248609d89..6a998f1f5b 100644 --- a/src/Widgets/DocumentView.vala +++ b/src/Widgets/DocumentView.vala @@ -56,6 +56,7 @@ public class Scratch.Widgets.DocumentView : Granite.Widgets.DynamicNotebook { tab_removed.connect (on_doc_removed); tab_reordered.connect (on_doc_reordered); tab_moved.connect (on_doc_moved); + tab_switched.connect (on_tab_switched); new_tab_requested.connect (() => { new_document (); @@ -73,11 +74,6 @@ public class Scratch.Widgets.DocumentView : Granite.Widgets.DynamicNotebook { return true; // tab will be removed if/when doc properly closed }); - tab_switched.connect ((old_tab, new_tab) => { - /* The 'document_change' signal is emitted when the document is focused. We do not need to emit it here */ - save_focused_document_uri (new_tab as Services.Document); - }); - tab_restored.connect ((label, restore_data, icon) => { var doc = new Services.Document (window.actions, File.new_for_uri (restore_data)); open_document (doc); @@ -277,6 +273,7 @@ public class Scratch.Widgets.DocumentView : Granite.Widgets.DynamicNotebook { // Must call before the app is closed public async bool prepare_to_close () { tab_removed.disconnect (on_doc_removed); + tab_switched.disconnect (on_tab_switched); var docs = docs.copy (); bool success = true; @@ -431,7 +428,8 @@ public class Scratch.Widgets.DocumentView : Granite.Widgets.DynamicNotebook { } } - private void save_focused_document_uri (Services.Document? current_document) { + private void on_tab_switched (Granite.Widgets.Tab? old_tab, Granite.Widgets.Tab? new_tab) { + var current_document = (Services.Document?)new_tab; if (privacy_settings.get_boolean ("remember-recent-files")) { var file_uri = ""; From b32d73d2a1253b07ade4d7eec9150c050d43b126 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Thu, 7 Apr 2022 11:35:33 +0000 Subject: [PATCH 04/12] Fix close tab --- src/Widgets/DocumentView.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Widgets/DocumentView.vala b/src/Widgets/DocumentView.vala index 6a998f1f5b..e9ca3c8910 100644 --- a/src/Widgets/DocumentView.vala +++ b/src/Widgets/DocumentView.vala @@ -71,7 +71,7 @@ public class Scratch.Widgets.DocumentView : Granite.Widgets.DynamicNotebook { } close_document.begin (doc); - return true; // tab will be removed if/when doc properly closed + return false; // tab will be removed if/when doc properly closed }); tab_restored.connect ((label, restore_data, icon) => { From 81067214ce6a14f3803206e22f386d99f4ad8990 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Fri, 8 Apr 2022 12:07:29 +0000 Subject: [PATCH 05/12] Handle save failure better --- src/Services/Document.vala | 67 +++++++++++++++++++++++++------------- 1 file changed, 45 insertions(+), 22 deletions(-) diff --git a/src/Services/Document.vala b/src/Services/Document.vala index 0466983abd..8fc54720b4 100644 --- a/src/Services/Document.vala +++ b/src/Services/Document.vala @@ -469,10 +469,33 @@ namespace Scratch.Services { try { yield source_file_saver.save_async (GLib.Priority.DEFAULT, save_cancellable, null); } catch (Error e) { - // We don't need to send an error message at cancellation (corresponding to error code 19) - if (e.code != 19) - warning ("Cannot save \"%s\": %s", get_basename (), e.message); - return false; + // We don't need to send an error message at cancellation + if (e.code != IOError.CANCELLED) { + var primary = _("Cannot save \"%s\"").printf (get_basename ()); + var secondary = _("%s").printf (e.message); + var dialog = new Granite.MessageDialog.with_image_from_icon_name ( + primary, secondary, "dialog-warning", Gtk.ButtonsType.CANCEL) { + transient_for = (Gtk.Window)source_view.get_toplevel () + }; + + var no_save_button = (Gtk.Button) dialog.add_button (_("Ignore"), Gtk.ResponseType.NO); + no_save_button.get_style_context ().add_class (Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION); + + var save_as_button = dialog.add_button ("Save As", Gtk.ResponseType.YES); + save_as_button.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION); + + + var response = dialog.run (); + dialog.destroy (); + switch (response) { + case Gtk.ResponseType.YES: + return yield save_as (); + case Gtk.ResponseType.NO: + return true; + default: + return false; + } + } } source_view.buffer.set_modified (false); @@ -508,35 +531,35 @@ namespace Scratch.Services { ); file_chooser.add_filter (all_files_filter); file_chooser.add_filter (text_files_filter); + //NOTE: Filechooser portal will always check overwrite (but not permissions) in future file_chooser.do_overwrite_confirmation = true; file_chooser.set_current_folder_uri (Utils.last_path ?? GLib.Environment.get_home_dir ()); var success = false; - var current_file = file.get_path (); + var current_file_uri = file.get_uri (); var is_current_file_temporary = this.is_file_temporary; if (file_chooser.run () == Gtk.ResponseType.ACCEPT) { + source_view.buffer.set_modified (true); file = File.new_for_uri (file_chooser.get_uri ()); // Update last visited path - Utils.last_path = Path.get_dirname (file_chooser.get_file ().get_uri ()); - success = true; - } - - if (success) { - source_view.buffer.set_modified (true); - var is_saved = yield save (); - - if (is_saved && is_current_file_temporary) { - try { - // Delete temporary file - File.new_for_path (current_file).delete (); - } catch (Error err) { - warning ("Temporary file cannot be deleted: %s", current_file); + Utils.last_path = Path.get_dirname (file_chooser.get_uri ()); + if (yield save ()) { + if (is_current_file_temporary) { + try { + // Delete temporary file + File.new_for_path (current_file_uri).delete (); + } catch (Error err) { + warning ("Temporary file cannot be deleted: %s", current_file_uri); + } } - } - delete_backup (current_file + "~"); - this.source_view.change_syntax_highlight_from_file (this.file); + delete_backup (current_file_uri + "~"); + this.source_view.change_syntax_highlight_from_file (this.file); + } else { + // Revert to original file is cannot save to new file. + file = File.new_for_uri (current_file_uri); + } } /* We delay destruction of file chooser dialog til to avoid the document focussing in, From c603761fbf34f576a9c2fc5fc2159ae6843b2ec7 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Fri, 8 Apr 2022 15:42:47 +0000 Subject: [PATCH 06/12] DRY deleting temporary file; minor cleanups --- src/Services/Document.vala | 40 ++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/src/Services/Document.vala b/src/Services/Document.vala index 8fc54720b4..14bb61d308 100644 --- a/src/Services/Document.vala +++ b/src/Services/Document.vala @@ -454,15 +454,15 @@ namespace Scratch.Services { return result; } - // It is callers responsibility to ensure document should be saved + private async bool save () { if (!loaded) { return false; } + // No check is made whether the file can be saved, we just deal with any error arising by giving + // chance to save with different name, cancel or ignore the error (e.g. so that app or tab can close) this.create_backup (); - - // Replace old content with the new one save_cancellable.cancel (); save_cancellable = new GLib.Cancellable (); var source_file_saver = new Gtk.SourceFileSaver ((Gtk.SourceBuffer) source_view.buffer, source_file); @@ -491,7 +491,7 @@ namespace Scratch.Services { case Gtk.ResponseType.YES: return yield save_as (); case Gtk.ResponseType.NO: - return true; + break; default: return false; } @@ -508,6 +508,7 @@ namespace Scratch.Services { return true; } + //This function is re-entrant. private async bool save_as () { // New file if (!loaded) { @@ -537,21 +538,16 @@ namespace Scratch.Services { var success = false; var current_file_uri = file.get_uri (); - var is_current_file_temporary = this.is_file_temporary; - + var is_current_file_temporary = is_file_temporary; if (file_chooser.run () == Gtk.ResponseType.ACCEPT) { source_view.buffer.set_modified (true); file = File.new_for_uri (file_chooser.get_uri ()); // Update last visited path Utils.last_path = Path.get_dirname (file_chooser.get_uri ()); - if (yield save ()) { + if (yield save ()) { // This can cause "save_as ()" to be called again + success = true; if (is_current_file_temporary) { - try { - // Delete temporary file - File.new_for_path (current_file_uri).delete (); - } catch (Error err) { - warning ("Temporary file cannot be deleted: %s", current_file_uri); - } + delete_temporary_file (current_file_uri); } delete_backup (current_file_uri + "~"); @@ -560,9 +556,11 @@ namespace Scratch.Services { // Revert to original file is cannot save to new file. file = File.new_for_uri (current_file_uri); } + } else { + warning ("Save failed"); } - /* We delay destruction of file chooser dialog til to avoid the document focussing in, + /* We delay destruction of file chooser dialog til now to avoid the document focussing in, * which triggers premature loading of overwritten content. */ file_chooser.destroy (); @@ -890,12 +888,20 @@ namespace Scratch.Services { } } - private bool delete_temporary_file () requires (is_file_temporary) { + private bool delete_temporary_file (string? temporary_uri = null) { + File temp_file; + if (temporary_uri != null) { + temp_file = File.new_for_uri (temporary_uri); + } else { + temp_file = file; + } try { - file.delete (); + temp_file.delete (); return true; } catch (Error e) { - warning ("Cannot delete temporary file \"%s\": %s", file.get_uri (), e.message); + if (e.code != IOError.NOT_FOUND) { + warning ("Cannot delete temporary file \"%s\": %s", file.get_uri (), e.message); + } } return false; From affec77257d889008ba270fbc54246addccbef87 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Fri, 8 Apr 2022 16:00:55 +0000 Subject: [PATCH 07/12] Provide forced quit option --- src/MainWindow.vala | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 5a580205b6..59f96abbd0 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -631,13 +631,29 @@ namespace Scratch { if (yield document_view.prepare_to_close ()) { do_quit (); } else { - //TODO Give user option to force close + //Should not reach here as any problems should be dealt with earlier but provided as a + //fallback to ensure application can be closed. + var primary = _("Cannot close all the documents"); + var secondary = _("There was a problem saving changes in one or more open documents"); + var dialog = new Granite.MessageDialog.with_image_from_icon_name ( + primary, secondary, "dialog-warning", Gtk.ButtonsType.CANCEL) { + transient_for = this + }; + + var force_close_button = (Gtk.Button) dialog.add_button (_("Close Anyway"), Gtk.ResponseType.YES); + force_close_button.get_style_context ().add_class (Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION); + + var response = dialog.run (); + dialog.destroy (); + if (response == Gtk.ResponseType.YES) { + do_quit (); + } } } private void do_quit () { update_saved_state (); // Remember window state - destroy (); // For now, destroy under all circumstances + destroy (); } public void set_default_zoom () { From 8e07d4c46bbd5fa4c30f854fcf699f051177dac0 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 22 Jun 2022 10:31:42 +0100 Subject: [PATCH 08/12] Deactivate plugins before ensure save --- src/MainWindow.vala | 4 ++++ src/Services/Document.vala | 9 +++++---- src/Services/PluginManager.vala | 12 +++++++++++- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 59f96abbd0..9e3a3deebf 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -18,6 +18,7 @@ * Boston, MA 02110-1301 USA */ + namespace Scratch { public class MainWindow : Hdy.Window { public const int FONT_SIZE_MAX = 72; @@ -628,6 +629,9 @@ namespace Scratch { // For exit cleanup private async void handle_quit () { + // Prevent plugins possibly leaving output streams on disk. + plugins.deactivate_plugins (); + if (yield document_view.prepare_to_close ()) { do_quit (); } else { diff --git a/src/Services/Document.vala b/src/Services/Document.vala index 2fcf1c2841..35606b8916 100644 --- a/src/Services/Document.vala +++ b/src/Services/Document.vala @@ -372,12 +372,13 @@ namespace Scratch.Services { } else { delete_temporary_file (); } - } else if (autosave) { - success = yield save_with_hold (); } else if (!saved) { - success = yield confirm_save (); + if (autosave) { + success = yield save_with_hold (); + } else { + success = yield confirm_save (); + } } - return success; } diff --git a/src/Services/PluginManager.vala b/src/Services/PluginManager.vala index 2d166f0be2..186a1c1bca 100644 --- a/src/Services/PluginManager.vala +++ b/src/Services/PluginManager.vala @@ -1,7 +1,7 @@ // -*- Mode: vala; indent-tabs-mode: nil; tab-width: 4 -*- /*** BEGIN LICENSE - + Copyright (C) 2013 Mario Guerriero This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License version 3, as published @@ -153,5 +153,15 @@ namespace Scratch.Services { bottom_box.no_show_all = true; return view; } + + public void deactivate_plugins () { + exts.foreach (extension_deactivate); + exts_core.foreach (extension_deactivate); + } + + private void extension_deactivate (Peas.ExtensionSet set, Peas.PluginInfo info, Peas.Extension extension) { + ((Peas.Activatable)extension).deactivate (); + } + } } From d3ee0dd145288487d84fb7cbbb097cc79746a644 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 18 Jan 2023 19:58:45 +0000 Subject: [PATCH 09/12] Simplifications, reduced scope, fix warning --- src/MainWindow.vala | 28 ++-------- src/Services/Document.vala | 99 +++++++++++++++++++++-------------- src/Widgets/DocumentView.vala | 24 ++++----- 3 files changed, 75 insertions(+), 76 deletions(-) diff --git a/src/MainWindow.vala b/src/MainWindow.vala index ae8c2a8f0f..d17b02d1ae 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -694,33 +694,11 @@ namespace Scratch { plugins.deactivate_plugins (); if (yield document_view.prepare_to_close ()) { - do_quit (); - } else { - //Should not reach here as any problems should be dealt with earlier but provided as a - //fallback to ensure application can be closed. - var primary = _("Cannot close all the documents"); - var secondary = _("There was a problem saving changes in one or more open documents"); - var dialog = new Granite.MessageDialog.with_image_from_icon_name ( - primary, secondary, "dialog-warning", Gtk.ButtonsType.CANCEL) { - transient_for = this - }; - - var force_close_button = (Gtk.Button) dialog.add_button (_("Close Anyway"), Gtk.ResponseType.YES); - force_close_button.get_style_context ().add_class (Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION); - - var response = dialog.run (); - dialog.destroy (); - if (response == Gtk.ResponseType.YES) { - do_quit (); - } + update_saved_state (); // Remember window state + destroy (); } } - private void do_quit () { - update_saved_state (); // Remember window state - destroy (); - } - public void set_default_zoom () { terminal.set_default_font_size (); Scratch.settings.set_string ("font", get_current_font () + " " + get_default_font_size ().to_string ()); @@ -955,7 +933,7 @@ namespace Scratch { unowned var docs = document_view.docs; docs.foreach ((doc) => { if (doc.file.get_path ().has_prefix (project_path)) { - document_view.close_document (doc); + document_view.close_document.begin (doc); if (make_restorable) { document_manager.make_restorable (doc); } diff --git a/src/Services/Document.vala b/src/Services/Document.vala index bfbd8419ee..8ec90ce85f 100644 --- a/src/Services/Document.vala +++ b/src/Services/Document.vala @@ -371,15 +371,14 @@ namespace Scratch.Services { return; } - // Call before destroying the document - public async bool ensure_saved () { + public async bool ensure_saved (bool confirm = false) { if (!loaded) { load_cancellable.cancel (); return true; } - bool success = true; // True if saved or does not need saving - bool autosave = Scratch.settings.get_boolean ("autosave"); + var success = true; + var autosave = Scratch.settings.get_boolean ("autosave"); if (is_file_temporary) { if (get_text ().length > 0) { success = yield confirm_save (); @@ -387,26 +386,31 @@ namespace Scratch.Services { delete_temporary_file (); } } else if (!saved) { - if (autosave) { + if (autosave || !confirm) { success = yield save_with_hold (); } else { success = yield confirm_save (); } } + + // Success means the file has been saved or it does not need + // saving or (if confirm == true) the user has confirmed it should not saved return success; } private async bool confirm_save () { var dialog = new Granite.MessageDialog ( _("Save changes to \"%s\" before closing?").printf (this.get_basename ()), - _("If you don't save, changes will be permanently lost."), + _("If you do not save, then changes will be permanently lost."), new ThemedIcon ("dialog-warning"), Gtk.ButtonsType.NONE ) { transient_for = (Gtk.Window)source_view.get_toplevel () }; - var no_save_button = (Gtk.Button) dialog.add_button (_("Close Without Saving"), Gtk.ResponseType.NO); + var no_save_button = (Gtk.Button) dialog.add_button ( + _("Close Without Saving"), Gtk.ResponseType.NO + ); no_save_button.get_style_context ().add_class (Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION); dialog.add_button (_("Cancel"), Gtk.ResponseType.CANCEL); @@ -435,7 +439,6 @@ namespace Scratch.Services { default: // Cancelled return false; } - } // Should only be called after doc has been prepared for closing. @@ -445,57 +448,72 @@ namespace Scratch.Services { doc_closed (); } - // Call directly to force saving + // Only return false if user cancels the process public async bool save_with_hold () { GLib.Application.get_default ().hold (); - bool result = false; - result = yield save (); + var result = yield save (); GLib.Application.get_default ().release (); return result; } + // Only return false if user cancels the process public async bool save_as_with_hold () { GLib.Application.get_default ().hold (); - bool result = false; - result = yield save_as (); + var result = yield save_as (); GLib.Application.get_default ().release (); return result; } - public async bool save (bool force = false) { - if (completion_shown || !loaded) { - return false; + // This function must return "true" unless the user has cancelled the process + private async bool save () { + if (!loaded) { + return true; } - // No check is made whether the file can be saved, we just deal with any error arising by giving - // chance to save with different name, cancel or ignore the error (e.g. so that app or tab can close) + source_view.completion.hide (); + // No check is made whether the file can be saved, + // We just deal with any error arising by giving a chance to save with + // a different name, cancel or ignore the error (i.e.so that app or tab can close) this.create_backup (); - if (Scratch.settings.get_boolean ("strip-trailing-on-save") && force) { + if (Scratch.settings.get_boolean ("strip-trailing-on-save")) { strip_trailing_spaces (); } + // Guard against this function being called again before save is complete save_cancellable.cancel (); save_cancellable = new GLib.Cancellable (); - var source_file_saver = new Gtk.SourceFileSaver ((Gtk.SourceBuffer) source_view.buffer, source_file); + var source_file_saver = new Gtk.SourceFileSaver ( + (Gtk.SourceBuffer) source_view.buffer, source_file + ); + + bool success = true; try { - yield source_file_saver.save_async (GLib.Priority.DEFAULT, save_cancellable, null); + // Assume anything causing an unsuccessful save will throw an error + yield source_file_saver.save_async ( + GLib.Priority.DEFAULT, save_cancellable, null + ); } catch (Error e) { // We don't need to send an error message at cancellation if (e.code != IOError.CANCELLED) { var primary = _("Cannot save \"%s\"").printf (get_basename ()); var secondary = _("%s").printf (e.message); var dialog = new Granite.MessageDialog.with_image_from_icon_name ( - primary, secondary, "dialog-warning", Gtk.ButtonsType.CANCEL) { + primary, secondary, "dialog-warning", Gtk.ButtonsType.CANCEL + ) { transient_for = (Gtk.Window)source_view.get_toplevel () }; - var no_save_button = (Gtk.Button) dialog.add_button (_("Ignore"), Gtk.ResponseType.NO); + var no_save_button = (Gtk.Button) dialog.add_button ( + _("Ignore"), Gtk.ResponseType.NO + ); no_save_button.get_style_context ().add_class (Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION); - var save_as_button = dialog.add_button ("Save As", Gtk.ResponseType.YES); + var save_as_button = dialog.add_button ( + "Save As", Gtk.ResponseType.YES + ); save_as_button.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION); @@ -503,32 +521,35 @@ namespace Scratch.Services { dialog.destroy (); switch (response) { case Gtk.ResponseType.YES: - return yield save_as (); + success = yield save_as (); + break; case Gtk.ResponseType.NO: + success = true; + break; + default: // CANCEL pressed + success = false; break; - default: - return false; } } } - source_view.buffer.set_modified (false); - - if (outline != null) { - outline.parse_symbols (); - } + // If success is true then the file can be regarded as saved + if (success) { + source_view.buffer.set_modified (false); + if (outline != null) { + outline.parse_symbols (); + } - this.set_saved_status (true); - last_save_content = source_view.buffer.text; + this.set_saved_status (true); + last_save_content = source_view.buffer.text; - debug ("File \"%s\" saved successfully", get_basename ()); + debug ("File \"%s\" saved successfully", get_basename ()); + } - return true; + return success; } - //This function is re-entrant. private async bool save_as () { - // New file if (!loaded) { return false; } @@ -543,7 +564,7 @@ namespace Scratch.Services { var file_chooser = new Gtk.FileChooserNative ( _("Save File"), - (Gtk.Window) this.get_toplevel (), + (Gtk.Window)(this.get_toplevel ()), Gtk.FileChooserAction.SAVE, _("Save"), _("Cancel") diff --git a/src/Widgets/DocumentView.vala b/src/Widgets/DocumentView.vala index 352fe7ce1c..42afe81a1e 100644 --- a/src/Widgets/DocumentView.vala +++ b/src/Widgets/DocumentView.vala @@ -301,7 +301,7 @@ public class Scratch.Widgets.DocumentView : Granite.Widgets.DynamicNotebook { // Only use for single documents when app remains open public async bool close_document (Services.Document doc) { - if (yield doc.ensure_saved ()) { + if (yield doc.ensure_saved (true)) { doc.do_close (); remove_tab (doc); return true; @@ -310,7 +310,8 @@ public class Scratch.Widgets.DocumentView : Granite.Widgets.DynamicNotebook { } } - // Must call before the app is closed + // Must call before the app is closed. This function must either leave the app in a + // closable state or the user has cancelled closing public async bool prepare_to_close () { tab_removed.disconnect (on_doc_removed); tab_switched.disconnect (on_tab_switched); @@ -318,20 +319,19 @@ public class Scratch.Widgets.DocumentView : Granite.Widgets.DynamicNotebook { var docs = docs.copy (); bool success = true; foreach (var doc in docs) { - success = success && yield doc.ensure_saved (); // This may rename the document + // Give the user the chance to cancel or rename + success = success && yield doc.ensure_saved (true); }; - if (!success) { - return false; // Either a document could not be saved or the operation was cancelled + if (success) { + remember_opened_files (); + foreach (var doc in docs) { + doc.do_close (); + remove_tab (doc); + }; } - remember_opened_files (); - foreach (var doc in docs) { - doc.do_close (); - remove_tab (doc); - }; - - return true; + return success; } public void request_placeholder_if_empty () { From 0fc9a2f9b0feaf88b57d86f71af2512d861a98fc Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Thu, 19 Jan 2023 17:48:38 +0000 Subject: [PATCH 10/12] Simplify, comment, fix strip trailing & autosave --- src/MainWindow.vala | 27 ++++----- src/Services/Document.vala | 107 +++++++++++++++++++--------------- src/Widgets/DocumentView.vala | 3 +- 3 files changed, 75 insertions(+), 62 deletions(-) diff --git a/src/MainWindow.vala b/src/MainWindow.vala index d17b02d1ae..5e7254ef18 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -18,7 +18,6 @@ * Boston, MA 02110-1301 USA */ - namespace Scratch { public class MainWindow : Hdy.Window { public const int FONT_SIZE_MAX = 72; @@ -588,7 +587,7 @@ namespace Scratch { } protected override bool delete_event (Gdk.EventAny event) { - handle_quit.begin (); + handle_quit (); // Do not want the signal to be propagated, handle_quit will destroy the app if possible. return Gdk.EVENT_STOP; } @@ -689,14 +688,20 @@ namespace Scratch { } // For exit cleanup - private async void handle_quit () { + //This will close the app after saving content as needed + // and recording open documents (if history on) + // unless the user cancels the process + // When closing due to logout ??? + private void handle_quit () { // Prevent plugins possibly leaving output streams on disk. plugins.deactivate_plugins (); - if (yield document_view.prepare_to_close ()) { - update_saved_state (); // Remember window state - destroy (); - } + document_view.prepare_to_close.begin ((obj, res) => { + if (document_view.prepare_to_close.end (res)) { + update_saved_state (); // Remember window state + destroy (); + } + }); } public void set_default_zoom () { @@ -784,7 +789,7 @@ namespace Scratch { } private void action_quit () { - handle_quit.begin (); + handle_quit (); } private void action_open () { @@ -853,11 +858,7 @@ namespace Scratch { private void action_save () { var doc = get_current_document (); /* may return null */ if (doc != null) { - if (doc.is_file_temporary == true) { - action_save_as (); - } else { - doc.save_with_hold.begin (); - } + doc.ensure_saved.begin (false, true); // No confirm, strip } } diff --git a/src/Services/Document.vala b/src/Services/Document.vala index 8ec90ce85f..9a668974f2 100644 --- a/src/Services/Document.vala +++ b/src/Services/Document.vala @@ -213,6 +213,8 @@ namespace Scratch.Services { ellipsize_mode = Pango.EllipsizeMode.MIDDLE; } + // Changed handler is disabled during programmed text changes + // currentl only search and replace all public void toggle_changed_handlers (bool enabled) { if (enabled) { onchange_handler_id = this.source_view.buffer.changed.connect (() => { @@ -232,7 +234,8 @@ namespace Scratch.Services { timeout_saving = 0; } timeout_saving = Timeout.add (1000, () => { - save.begin (); + //Not closing not stripping + ensure_saved.begin (false, false); timeout_saving = 0; return false; }); @@ -371,34 +374,44 @@ namespace Scratch.Services { return; } - public async bool ensure_saved (bool confirm = false) { + // By default (e.g. during autosave) this saves without stripping or confirmation + // When saving due to user action, stripping occurs subject to setting but no confirmation + // When closing tab or app, stripping occurs subject to setting and a confirmation dialog + // shows for documents that have not already been saved. + public async bool ensure_saved ( + bool closing = false, + bool strip = false + ) { + var success = true; if (!loaded) { load_cancellable.cancel (); - return true; - } - - var success = true; - var autosave = Scratch.settings.get_boolean ("autosave"); - if (is_file_temporary) { - if (get_text ().length > 0) { - success = yield confirm_save (); - } else { + } else { + if (is_file_temporary && get_text ().length == 0) { delete_temporary_file (); - } - } else if (!saved) { - if (autosave || !confirm) { - success = yield save_with_hold (); - } else { - success = yield confirm_save (); + } else if (!saved || strip) { // If stripping have to save after + //Should not strip while editing - annoying, but should strip + // when manually saved + if (strip) { + strip_trailing_spaces (); + } + + if (closing) { + success = yield confirm_save_before_close (); + } else { + success = yield save_with_hold (); + } } } - // Success means the file has been saved or it does not need // saving or (if confirm == true) the user has confirmed it should not saved + if (!saved) { + critical ("%s Not saved after ensure saved", file.get_basename ()); + } + return success; } - private async bool confirm_save () { + private async bool confirm_save_before_close () { var dialog = new Granite.MessageDialog ( _("Save changes to \"%s\" before closing?").printf (this.get_basename ()), _("If you do not save, then changes will be permanently lost."), @@ -443,6 +456,7 @@ namespace Scratch.Services { // Should only be called after doc has been prepared for closing. public void do_close () { + assert (!loaded || saved); debug ("Closing \"%s\"", get_basename ()); delete_backup (); doc_closed (); @@ -460,6 +474,7 @@ namespace Scratch.Services { // Only return false if user cancels the process public async bool save_as_with_hold () { GLib.Application.get_default ().hold (); + strip_trailing_spaces (); var result = yield save_as (); GLib.Application.get_default ().release (); @@ -476,10 +491,20 @@ namespace Scratch.Services { // No check is made whether the file can be saved, // We just deal with any error arising by giving a chance to save with // a different name, cancel or ignore the error (i.e.so that app or tab can close) - this.create_backup (); - if (Scratch.settings.get_boolean ("strip-trailing-on-save")) { - strip_trailing_spaces (); + // Backup functions + if (!can_write ()) { + return true; + } + + var backup = File.new_for_path (this.file.get_path () + "~"); + + if (!backup.query_exists ()) { + try { + file.copy (backup, FileCopyFlags.NONE); + } catch (Error e) { + warning ("Cannot create backup copy for file \"%s\": %s", get_basename (), e.message); + } } // Guard against this function being called again before save is complete @@ -516,7 +541,6 @@ namespace Scratch.Services { ); save_as_button.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION); - var response = dialog.run (); dialog.destroy (); switch (response) { @@ -623,9 +647,7 @@ namespace Scratch.Services { scroll.vscrollbar_policy = Gtk.PolicyType.AUTOMATIC; } - if (Scratch.settings.get_boolean ("strip-trailing-on-save")) { - strip_trailing_spaces (); - } + strip_trailing_spaces (); } // Focus the SourceView @@ -791,7 +813,7 @@ namespace Scratch.Services { ).printf ("%s".printf (get_basename ())); set_message (Gtk.MessageType.WARNING, message, _("Save As…"), () => { - this.save_as.begin (); + this.save_as_with_hold.begin (); hide_info_bar (); }); } else { @@ -800,7 +822,7 @@ namespace Scratch.Services { ).printf ("%s".printf (get_basename ())); set_message (Gtk.MessageType.WARNING, message, _("Save"), () => { - this.save.begin (); + this.ensure_saved.begin (false, true); hide_info_bar (); }); } @@ -890,23 +912,6 @@ namespace Scratch.Services { } } - // Backup functions - private void create_backup () { - if (!can_write ()) { - return; - } - - var backup = File.new_for_path (this.file.get_path () + "~"); - - if (!backup.query_exists ()) { - try { - file.copy (backup, FileCopyFlags.NONE); - } catch (Error e) { - warning ("Cannot create backup copy for file \"%s\": %s", get_basename (), e.message); - } - } - } - private void delete_backup (string? backup_path = null) { string backup_file; @@ -961,7 +966,7 @@ namespace Scratch.Services { writable = info.get_attribute_boolean (FileAttribute.ACCESS_CAN_WRITE); return writable; } catch (Error e) { - warning ("query_info failed, but filename appears to be correct, allowing as new file"); + debug ("query_info failed, but filename appears to be correct, allowing as new file"); writable = true; return writable; } @@ -1030,8 +1035,11 @@ namespace Scratch.Services { /* Pull the buffer into an array and then work out which parts are to be deleted. * Do not strip line currently being edited unless forced */ private void strip_trailing_spaces () { - if (!loaded || source_view.language == null) { - return; + if (!loaded || + source_view.language == null || + !Scratch.settings.get_boolean ("strip-trailing-on-save")) { + + return; } var source_buffer = (Gtk.SourceBuffer)source_view.buffer; @@ -1066,6 +1074,7 @@ namespace Scratch.Services { return; } + toggle_changed_handlers (false); // Avoid triggering autosave for (int line_no = 0; line_no < lines.length; line_no++) { if (whitespace.match (lines[line_no], 0, out info)) { @@ -1082,6 +1091,8 @@ namespace Scratch.Services { source_buffer.get_iter_at_line_offset (out iter, orig_line, orig_offset); source_buffer.place_cursor (iter); + + toggle_changed_handlers (true); } } } diff --git a/src/Widgets/DocumentView.vala b/src/Widgets/DocumentView.vala index 42afe81a1e..1b1fe7acb1 100644 --- a/src/Widgets/DocumentView.vala +++ b/src/Widgets/DocumentView.vala @@ -172,7 +172,7 @@ public class Scratch.Widgets.DocumentView : Granite.Widgets.DynamicNotebook { private void insert_document (Scratch.Services.Document doc, int pos) { insert_tab (doc, pos); if (Scratch.saved_state.get_boolean ("outline-visible")) { - warning ("setting outline visible"); + debug ("setting outline visible"); doc.show_outline (true); } } @@ -320,6 +320,7 @@ public class Scratch.Widgets.DocumentView : Granite.Widgets.DynamicNotebook { bool success = true; foreach (var doc in docs) { // Give the user the chance to cancel or rename + // Once success is false, no ensure_saved is not called again success = success && yield doc.ensure_saved (true); }; From 8e0458d7ebd2b8c15ff1201e63080e839751c7e4 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Thu, 19 Jan 2023 18:10:08 +0000 Subject: [PATCH 11/12] Strip trailing spaces on focus out --- src/Services/Document.vala | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Services/Document.vala b/src/Services/Document.vala index 9a668974f2..122edf1471 100644 --- a/src/Services/Document.vala +++ b/src/Services/Document.vala @@ -182,8 +182,10 @@ namespace Scratch.Services { // Focus out event for SourceView this.source_view.focus_out_event.connect (() => { - if (!saved && Scratch.settings.get_boolean ("autosave")) { - save.begin (); + if (!saved && Scratch.settings.get_boolean ("autosave") || + Scratch.settings.get_boolean ("strip-trailing-on-save")) { + + ensure_saved.begin (false, true); } return Gdk.EVENT_PROPAGATE; From e35f0892fe02b275464194d73d68ca905bf40075 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Thu, 19 Jan 2023 18:45:57 +0000 Subject: [PATCH 12/12] Zero timeout and handler ids after removal/disconnection --- plugins/word-completion/plugin.vala | 4 +++- src/Services/Document.vala | 15 +++++++++------ src/Widgets/SourceView.vala | 2 +- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/plugins/word-completion/plugin.vala b/plugins/word-completion/plugin.vala index d227d12a6a..55a2b29bad 100644 --- a/plugins/word-completion/plugin.vala +++ b/plugins/word-completion/plugin.vala @@ -68,8 +68,10 @@ public class Scratch.Plugins.Completion : Peas.ExtensionBase, Peas.Activatable { parser.cancel_parsing (); - if (timeout_id > 0) + if (timeout_id > 0) { GLib.Source.remove (timeout_id); + timeout_id = 0; + } cleanup (current_view); } diff --git a/src/Services/Document.vala b/src/Services/Document.vala index 122edf1471..4f785114e8 100644 --- a/src/Services/Document.vala +++ b/src/Services/Document.vala @@ -222,6 +222,7 @@ namespace Scratch.Services { onchange_handler_id = this.source_view.buffer.changed.connect (() => { if (onchange_handler_id != 0) { this.source_view.buffer.disconnect (onchange_handler_id); + onchange_handler_id = 0; } // Signals for SourceView @@ -246,10 +247,11 @@ namespace Scratch.Services { }); } else if (onchange_handler_id != 0) { this.source_view.buffer.disconnect (onchange_handler_id); + onchange_handler_id = 0; } } - private uint load_timout_id = 0; + private uint load_timeout_id = 0; public async void open (bool force = false) { /* Loading improper files may hang so we cancel after a certain time as a fallback. * In most cases, an error will be thrown and caught. */ @@ -298,7 +300,7 @@ namespace Scratch.Services { var buffer = new Gtk.SourceBuffer (null); /* Faster to load into a separate buffer */ - load_timout_id = Timeout.add_seconds_full (GLib.Priority.HIGH, 5, () => { + load_timeout_id = Timeout.add_seconds_full (GLib.Priority.HIGH, 5, () => { if (load_cancellable != null && !load_cancellable.is_cancelled ()) { var title = _("Loading File \"%s\" Is Taking a Long Time").printf (get_basename ()); var description = _("Please wait while Code is loading the file."); @@ -311,12 +313,12 @@ namespace Scratch.Services { load_cancellable.cancel (); doc_closed (); }); - load_timout_id = 0; + load_timeout_id = 0; return GLib.Source.REMOVE; } - load_timout_id = 0; + load_timeout_id = 0; return GLib.Source.REMOVE; }); @@ -340,8 +342,9 @@ namespace Scratch.Services { return; } finally { load_cancellable = null; - if (load_timout_id > 0) { - Source.remove (load_timout_id); + if (load_timeout_id > 0) { + Source.remove (load_timeout_id); + load_timeout_id = 0; } } diff --git a/src/Widgets/SourceView.vala b/src/Widgets/SourceView.vala index 9bfabf0e69..686b84217f 100644 --- a/src/Widgets/SourceView.vala +++ b/src/Widgets/SourceView.vala @@ -587,7 +587,6 @@ namespace Scratch.Widgets { } else { selection_changed_timer = Timeout.add (THROTTLE_MS, selection_changed_event); } - } bool selection_changed_event () { @@ -607,6 +606,7 @@ namespace Scratch.Widgets { private void schedule_refresh_diff () { if (refresh_diff_timeout_id > 0) { Source.remove (refresh_diff_timeout_id); + refresh_diff_timeout_id = 0; } refresh_diff_timeout_id = Timeout.add (250, () => {