From 0987e69b440b010e687612bf1965748aaa290c6f Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Thu, 23 Mar 2023 18:23:40 +0000 Subject: [PATCH 01/31] Ignore external changes after "Continue" --- src/Services/Document.vala | 49 ++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/src/Services/Document.vala b/src/Services/Document.vala index 9496d73446..1e9120359c 100644 --- a/src/Services/Document.vala +++ b/src/Services/Document.vala @@ -93,6 +93,7 @@ namespace Scratch.Services { private string last_save_content; public bool saved = true; private bool completion_shown = false; + private bool ignore_external_changes = false; private Gtk.ScrolledWindow scroll; private Gtk.InfoBar info_bar; @@ -251,6 +252,8 @@ namespace Scratch.Services { /* 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. */ loaded = false; + ignore_external_changes = false; + if (load_cancellable != null) { /* just in case */ load_cancellable.cancel (); } @@ -557,9 +560,9 @@ namespace Scratch.Services { var is_saved = false; if (success) { - source_view.buffer.set_modified (true); is_saved = yield save (true, true); if (is_saved) { + source_view.buffer.set_modified (false); if (is_current_file_temporary) { try { // Delete temporary file @@ -806,8 +809,14 @@ namespace Scratch.Services { this.source_view.editable = true; } - // Detect external changes - if (loaded) { + // Detect external changes by comparing file content with buffer content. + // Only done when no unsaved internal changes else difference from saved + // file are to be expected. If user selects to continue regardless then no further + // check made for this document - external changes will be overwritten on next (auto) save + if (loaded && + !source_view.buffer.get_modified () && + !ignore_external_changes) { + var new_buffer = new Gtk.SourceBuffer (null); var source_file_loader = new Gtk.SourceFileLoader (new_buffer, source_file); source_file_loader.load_async.begin (GLib.Priority.DEFAULT, null, null, (obj, res) => { @@ -823,22 +832,26 @@ namespace Scratch.Services { return; } - if (!source_view.buffer.get_modified ()) { - if (Scratch.settings.get_boolean ("autosave")) { - source_view.set_text (new_buffer.text, false); - } else { - string message = _( - "File \"%s\" was modified by an external application. Do you want to load it again or continue your editing?" - ).printf ("%s".printf (get_basename ())); - - set_message (Gtk.MessageType.WARNING, message, _("Load"), () => { - this.source_view.set_text (new_buffer.text, false); - hide_info_bar (); - }, _("Continue"), () => { - hide_info_bar (); - }); - } + string message; + if (Scratch.settings.get_boolean ("autosave")) { + message = _( + "File \"%s\" was modified by an external application. Do you want to reload the document? \nOtherwise, if you continue, the external changes will be overwritten by your next edit and any further external changes will be ignored." + ).printf ("%s".printf (get_basename ())); + } else { + message = _( + "File \"%s\" was modified by an external application. Do you want to reload the document? \nOtherwise, if you continue, the external changes will be overwritten by your next save and any further external changes will be ignored." + ).printf ("%s".printf (get_basename ())); } + + set_message (Gtk.MessageType.WARNING, message, _("Reload"), () => { + // this.source_view.set_text (new_buffer.text, false); + // set_modified (false); + hide_info_bar (); + this.open.begin (true); + }, _("Continue"), () => { + hide_info_bar (); + ignore_external_changes = true; + }); }); } } From 3cc29cf07a5c0e8fe86014d92198ebce4ea24f61 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Fri, 24 Mar 2023 11:18:26 +0000 Subject: [PATCH 02/31] Reload or Overwrite options --- src/Services/Document.vala | 59 ++++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/src/Services/Document.vala b/src/Services/Document.vala index 1e9120359c..09e9900839 100644 --- a/src/Services/Document.vala +++ b/src/Services/Document.vala @@ -93,7 +93,7 @@ namespace Scratch.Services { private string last_save_content; public bool saved = true; private bool completion_shown = false; - private bool ignore_external_changes = false; + private bool inhibit_autosave = false; private Gtk.ScrolledWindow scroll; private Gtk.InfoBar info_bar; @@ -184,7 +184,7 @@ namespace Scratch.Services { // Focus out event for SourceView this.source_view.focus_out_event.connect (() => { - if (Scratch.settings.get_boolean ("autosave")) { + if (Scratch.settings.get_boolean ("autosave") && !inhibit_autosave) { save.begin (); } @@ -228,7 +228,7 @@ namespace Scratch.Services { onchange_handler_id = source_view.buffer.changed.connect (() => { check_undoable_actions (); // Save if autosave is ON - if (Scratch.settings.get_boolean ("autosave")) { + if (Scratch.settings.get_boolean ("autosave") && !inhibit_autosave) { if (timeout_saving > 0) { Source.remove (timeout_saving); timeout_saving = 0; @@ -252,7 +252,7 @@ namespace Scratch.Services { /* 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. */ loaded = false; - ignore_external_changes = false; + inhibit_autosave = false; if (load_cancellable != null) { /* just in case */ load_cancellable.cancel (); @@ -515,6 +515,11 @@ namespace Scratch.Services { this.set_saved_status (true); last_save_content = source_view.buffer.text; + // If saving in response to external changes hide the infobar now. + if (inhibit_autosave) { + inhibit_autosave = false; + hide_info_bar (); + } debug ("File \"%s\" saved successfully", get_basename ()); @@ -813,10 +818,7 @@ namespace Scratch.Services { // Only done when no unsaved internal changes else difference from saved // file are to be expected. If user selects to continue regardless then no further // check made for this document - external changes will be overwritten on next (auto) save - if (loaded && - !source_view.buffer.get_modified () && - !ignore_external_changes) { - + if (loaded) { var new_buffer = new Gtk.SourceBuffer (null); var source_file_loader = new Gtk.SourceFileLoader (new_buffer, source_file); source_file_loader.load_async.begin (GLib.Priority.DEFAULT, null, null, (obj, res) => { @@ -828,30 +830,37 @@ namespace Scratch.Services { return; } - if (source_view.buffer.text == new_buffer.text) { + if (last_save_content == new_buffer.text) { return; } string message; - if (Scratch.settings.get_boolean ("autosave")) { - message = _( - "File \"%s\" was modified by an external application. Do you want to reload the document? \nOtherwise, if you continue, the external changes will be overwritten by your next edit and any further external changes will be ignored." - ).printf ("%s".printf (get_basename ())); + if (source_view.buffer.get_modified ()) { + message = _( + "File \"%s\" was modified by an external application. \nThere are also unsaved changes. \nReload the document and lose the unsaved changes? \nOtherwise, overwrite the external changes or save with a different name." + ).printf ("%s".printf (get_basename ())); } else { - message = _( - "File \"%s\" was modified by an external application. Do you want to reload the document? \nOtherwise, if you continue, the external changes will be overwritten by your next save and any further external changes will be ignored." - ).printf ("%s".printf (get_basename ())); + message = _( + "File \"%s\" was modified by an external application. \nReload the document? \nOtherwise, overwrite the external changes or save with a different name." + ).printf ("%s".printf (get_basename ())); } - set_message (Gtk.MessageType.WARNING, message, _("Reload"), () => { - // this.source_view.set_text (new_buffer.text, false); - // set_modified (false); - hide_info_bar (); - this.open.begin (true); - }, _("Continue"), () => { - hide_info_bar (); - ignore_external_changes = true; - }); + inhibit_autosave = true; + set_message ( + Gtk.MessageType.WARNING, + message, + _("Reload"), () => { + source_view.buffer.text = new_buffer.text; + source_view.buffer.set_modified (false); + last_save_content = source_view.buffer.text; + set_saved_status (true); + inhibit_autosave = false; + hide_info_bar (); + }, + _("Overwrite"), () => { + save_with_hold.begin (true, false); + } + ); }); } } From d6d16f450c02e29b7e06b7e70f0575528646d601 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Tue, 11 Apr 2023 16:27:35 +0100 Subject: [PATCH 03/31] Add AskSaveDialog.vala --- src/Dialogs/AskSaveLocationDialog.vala | 28 ++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/Dialogs/AskSaveLocationDialog.vala diff --git a/src/Dialogs/AskSaveLocationDialog.vala b/src/Dialogs/AskSaveLocationDialog.vala new file mode 100644 index 0000000000..66fbf9c5f0 --- /dev/null +++ b/src/Dialogs/AskSaveLocationDialog.vala @@ -0,0 +1,28 @@ +/* Copyright 2023 elementary, Inc. + * SPDX-License-Identifier: GPL-3.0-or-later + */ +public class Scratch.Dialogs.AskSaveLocationDialog : Granite.MessageDialog { + + public AskSaveLocationDialog (string primary_text, string secondary_text, string error_message = "") { + Object ( + buttons: Gtk.ButtonsType.NONE, + primary_text: primary_text, + secondary_text: secondary_text + ); + + if (error_message != "") { + show_error_details (error_message); + } + } + + construct { + var app_instance = (Gtk.Application) GLib.Application.get_default (); + transient_for = app_instance.active_window; + image_icon = new ThemedIcon ("dialog-warning"); + + add_button (_("Ignore"), Gtk.ResponseType.REJECT); + + var saveas_button = (Gtk.Button) add_button (_("Save Document elsewhere"), Gtk.ResponseType.ACCEPT); + saveas_button.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION); + } +} From e9f3e3a0b8324d63f303538d7120a38285289a87 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Tue, 11 Apr 2023 19:50:35 +0100 Subject: [PATCH 04/31] Introduce "locked" document state --- src/Services/Document.vala | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/Services/Document.vala b/src/Services/Document.vala index c6069d9900..9cf99e7c08 100644 --- a/src/Services/Document.vala +++ b/src/Services/Document.vala @@ -86,6 +86,26 @@ namespace Scratch.Services { } } + // Locked documents can be edited but cannot be (auto)saved to the current file. + // Locked documents can be saved to a different file (when they will be unlocked) + private bool _locked = true; + public bool locked { + get { + return _locked; + } + + set { + _locked = value; + Utils.action_from_group (MainWindow.ACTION_SAVE, actions).set_enabled (!value); + toggle_changed_handlers (!value); //Do not autosave locked documents + if (locked) { + icon = locked_icon; + } else { + icon = null; + } + } + } + public Gtk.Stack main_stack; public Scratch.Widgets.SourceView source_view; private Scratch.Services.SymbolOutline? outline = null; @@ -105,6 +125,7 @@ namespace Scratch.Services { private bool loaded = false; private bool mounted = true; // Mount state of the file private Mount mount; + private Icon locked_icon; private static Pango.FontDescription? builder_blocks_font = null; private static Pango.FontMap? builder_font_map = null; @@ -132,6 +153,7 @@ namespace Scratch.Services { } construct { + locked_icon = new ThemedIcon ("locked"); main_stack = new Gtk.Stack (); source_view = new Scratch.Widgets.SourceView (); @@ -211,6 +233,7 @@ namespace Scratch.Services { // /* Create as loaded - could be new document */ loaded = file == null; + locked = false; ellipsize_mode = Pango.EllipsizeMode.MIDDLE; } From 3cedc1fa9687cdef2921f320c8eef1b07aa93c25 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Tue, 11 Apr 2023 19:55:10 +0100 Subject: [PATCH 05/31] Lock document during loading; unlock after if writable --- src/Services/Document.vala | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Services/Document.vala b/src/Services/Document.vala index 9cf99e7c08..658eff553c 100644 --- a/src/Services/Document.vala +++ b/src/Services/Document.vala @@ -231,7 +231,7 @@ namespace Scratch.Services { completion_shown = false; }); - // /* Create as loaded - could be new document */ + // /* Create as loaded and unlocked - could be new document */ loaded = file == null; locked = false; ellipsize_mode = Pango.EllipsizeMode.MIDDLE; @@ -273,6 +273,7 @@ namespace Scratch.Services { 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. */ + locked = true; loaded = false; if (load_cancellable != null) { /* just in case */ load_cancellable.cancel (); @@ -366,8 +367,10 @@ namespace Scratch.Services { // Focus in event for SourceView this.source_view.focus_in_event.connect (() => { - check_file_status (); - check_undoable_actions (); + if (!locked) { + check_file_status (); + check_undoable_actions (); + } return false; }); @@ -388,6 +391,7 @@ namespace Scratch.Services { Idle.add (() => { working = false; loaded = true; + check_file_status (); return false; }); From 88c2bbf217f5ef406f44f38504ff47247a910f4d Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Tue, 11 Apr 2023 20:00:39 +0100 Subject: [PATCH 06/31] Stop some functions if locked --- src/Services/Document.vala | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/Services/Document.vala b/src/Services/Document.vala index 658eff553c..bd2a9e88d3 100644 --- a/src/Services/Document.vala +++ b/src/Services/Document.vala @@ -201,11 +201,9 @@ namespace Scratch.Services { this.source_view.buffer.create_tag ("highlight_search_all", "background", "yellow", null); - toggle_changed_handlers (true); - // Focus out event for SourceView this.source_view.focus_out_event.connect (() => { - if (Scratch.settings.get_boolean ("autosave")) { + if (!locked && Scratch.settings.get_boolean ("autosave")) { save.begin (); } @@ -215,7 +213,8 @@ namespace Scratch.Services { source_view.buffer.changed.connect (() => { if (source_view.buffer.text != last_save_content) { saved = false; - if (!Scratch.settings.get_boolean ("autosave")) { + // Autosave does not work on locked document + if (locked || !Scratch.settings.get_boolean ("autosave")) { set_saved_status (false); } } else { @@ -401,7 +400,7 @@ namespace Scratch.Services { public async bool do_close (bool app_closing = false) { debug ("Closing \"%s\"", get_basename ()); - if (!loaded) { + if (!loaded || locked) { load_cancellable.cancel (); return true; } @@ -466,7 +465,7 @@ namespace Scratch.Services { private bool is_saving = false; public async bool save_with_hold (bool force = false, bool saving_as = false) { // Prevent reentry which could result in mismatched holds on Application - if (is_saving) { + if (is_saving || locked) { return true; } else { is_saving = true; @@ -485,20 +484,24 @@ namespace Scratch.Services { is_saving = false; } + return result; } public async bool save_as_with_hold () { var old_uri = file.get_uri (); + var old_locked = locked; + locked = false; // Can always try to save as a different file var result = yield save_with_hold (true, true); if (!result) { file = File.new_for_uri (old_uri); + locked = old_locked; } return result; } - private async bool save (bool force = false, bool saving_as = false) { + private async bool save (bool force = false, bool saving_as = false) requires (!locked) { if (completion_shown || !force && (source_view.buffer.get_modified () == false || !loaded)) { @@ -543,7 +546,7 @@ namespace Scratch.Services { return true; } - public async bool save_as () { + private async bool save_as () requires (!locked) { // New file if (!loaded) { return false; From bad0f37b9cc55dc347fb277bfb6800e26966c11a Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Tue, 11 Apr 2023 20:02:19 +0100 Subject: [PATCH 07/31] Modify save and save as functions for locked --- src/Services/Document.vala | 39 +++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/src/Services/Document.vala b/src/Services/Document.vala index bd2a9e88d3..e65e5271a3 100644 --- a/src/Services/Document.vala +++ b/src/Services/Document.vala @@ -516,19 +516,35 @@ namespace Scratch.Services { save_cancellable.cancel (); save_cancellable = new GLib.Cancellable (); var source_file_saver = new Gtk.SourceFileSaver ((Gtk.SourceBuffer) source_view.buffer, source_file); + var success = false; + var error = ""; try { - yield source_file_saver.save_async (GLib.Priority.DEFAULT, save_cancellable, null); + success = yield source_file_saver.save_async (GLib.Priority.DEFAULT, save_cancellable, null); // Only create backup once save successful - this.create_backup (); + if (success) { + create_backup (); + } } 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); - // If called by `save_as ()` then that function will show infobar - if (!saving_as) { - ask_save_location (false); - } + if (e.code != 19) { // Not cancelled + error = e.message; + } else { + return false; } + } + + if (!success) { + warning ("Cannot save “%s”: %s", get_uri (), error); + locked = true; + // Allow save process to complete before showing dialog + Idle.add (() => { + ask_save_location ( + _("Saving to “%s” failed.").printf (get_uri ()), + error + ); + + return Source.REMOVE; + }); + return false; } @@ -599,11 +615,8 @@ namespace Scratch.Services { delete_backup (current_file.get_uri () + "~"); this.source_view.change_syntax_highlight_from_file (this.file); - } else { - // Restore original file - file = current_file; - ask_save_location (true); } + // Calling function responsible for restoring original } /* We delay destruction of file chooser dialog til to avoid the document focussing in, From 3c821f27ac49cb8393b122be20cfcbb9ef6ff5c8 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Tue, 11 Apr 2023 20:03:05 +0100 Subject: [PATCH 08/31] Make some funcs private --- src/Services/Document.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Services/Document.vala b/src/Services/Document.vala index e65e5271a3..2af6f66bf6 100644 --- a/src/Services/Document.vala +++ b/src/Services/Document.vala @@ -686,7 +686,7 @@ namespace Scratch.Services { } // Set InfoBars message - public void set_message (Gtk.MessageType type, string label, + private void set_message (Gtk.MessageType type, string label, string? button1 = null, owned VoidFunc? callback1 = null, string? button2 = null, owned VoidFunc? callback2 = null) { From 0d3509375e9890e64270d6172689f47794b04b93 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Tue, 11 Apr 2023 20:05:42 +0100 Subject: [PATCH 09/31] Modify check file status function for locked --- src/Services/Document.vala | 87 ++++++++++++++++++++++++++++---------- 1 file changed, 65 insertions(+), 22 deletions(-) diff --git a/src/Services/Document.vala b/src/Services/Document.vala index 2af6f66bf6..da5204b722 100644 --- a/src/Services/Document.vala +++ b/src/Services/Document.vala @@ -837,37 +837,80 @@ namespace Scratch.Services { } // Check if the file was deleted/changed by an external source - public void check_file_status () { + private void check_file_status () { // If the file does not exist anymore if (!exists ()) { + locked = true; + string details; if (mounted == false) { - string message = _( - "The location containing the file “%s” was unmounted. Do you want to save somewhere else?" - ).printf ("%s".printf (get_basename ())); + details = _("The location containing the file “%s” was unmounted."); + } else { + details = _("File “%s” was deleted."); + } - set_message (Gtk.MessageType.WARNING, message, _("Save As…"), () => { - this.save_as.begin (); - hide_info_bar (); - }); + ask_save_location (details.printf ("%s".printf (get_basename ()))); + } else if (loaded) { // Check external changes after loading + if (!locked && !can_write () && source_view.buffer.get_modified ()) { + // The file has become unwritable while changes are pending + locked = true; + var details = _("File “%s” was does not have write permission."); + ask_save_location (details.printf ("%s".printf (get_basename ()))); } else { - string message = _( - "File “%s” was deleted. Do you want to save it anyway?" - ).printf ("%s".printf (get_basename ())); + // Check for external changes (can load even if locked or unwritable) + var new_buffer = new Gtk.SourceBuffer (null); + var source_file_loader = new Gtk.SourceFileLoader ( + new_buffer, + source_file + ); + source_file_loader.load_async.begin ( + GLib.Priority.DEFAULT, + null, + null, + (obj, res) => { + try { + source_file_loader.load_async.end (res); + } catch (Error e) { + critical (e.message); + show_default_load_error_view (); + return; + } - set_message (Gtk.MessageType.WARNING, message, _("Save"), () => { - this.save.begin (); - hide_info_bar (); - }); - } + if (source_view.buffer.text == new_buffer.text) { + return; + } - Utils.action_from_group (MainWindow.ACTION_SAVE, actions).set_enabled (false); - this.source_view.editable = false; - return; + if (!source_view.buffer.get_modified ()) { + //FIXME Should block editing until responded? + if (Scratch.settings.get_boolean ("autosave")) { + source_view.set_text (new_buffer.text, false); + } else { + string message = _( + "File “%s” was modified by an external application." + ).printf ("%s".printf (get_uri ())); + + set_message ( + Gtk.MessageType.WARNING, + message, + _("Reload"), () => { + this.source_view.set_text ( + new_buffer.text, false + ); + hide_info_bar (); + }, + _("Continue"), () => { + hide_info_bar (); + }) + ; + } + } else { + //TODO Handle conflicting changes (dialog?) + } + } + ); + } } + } - // If the file can't be written - if (!can_write ()) { - ask_save_location (); } else { Utils.action_from_group (MainWindow.ACTION_SAVE, actions).set_enabled (true); this.source_view.editable = true; From d3e300e7b0fab4cfd43f472420ea57e50054d52d Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Tue, 11 Apr 2023 20:06:32 +0100 Subject: [PATCH 10/31] Rewrite ask save location for dialog and locked --- src/Services/Document.vala | 99 ++++++++++++-------------------------- 1 file changed, 31 insertions(+), 68 deletions(-) diff --git a/src/Services/Document.vala b/src/Services/Document.vala index da5204b722..3427acb4b1 100644 --- a/src/Services/Document.vala +++ b/src/Services/Document.vala @@ -911,83 +911,46 @@ namespace Scratch.Services { } } + private void ask_save_location ( + string details, + string error_text = "" + ) { + locked = true; + string primary_text; + if (source_view.buffer.get_modified ()) { + primary_text = _("The document cannot be saved"); } else { - Utils.action_from_group (MainWindow.ACTION_SAVE, actions).set_enabled (true); - this.source_view.editable = true; + primary_text = _("The changes to the document cannot be saved"); } - // Detect external changes - if (loaded) { - var new_buffer = new Gtk.SourceBuffer (null); - var source_file_loader = new Gtk.SourceFileLoader (new_buffer, source_file); - source_file_loader.load_async.begin (GLib.Priority.DEFAULT, null, null, (obj, res) => { - try { - source_file_loader.load_async.end (res); - } catch (Error e) { - critical (e.message); - show_default_load_error_view (); - return; - } - - if (source_view.buffer.text == new_buffer.text) { - return; - } + var dialog = new Scratch.Dialogs.AskSaveLocationDialog ( + primary_text, + details, + error_text + ); - if (!source_view.buffer.get_modified ()) { - if (Scratch.settings.get_boolean ("autosave")) { - source_view.set_text (new_buffer.text, false); - } else { - string message = _( - "File “%s” was modified by an external application. Do you want to load it again or continue your editing?" - ).printf ("%s".printf (get_basename ())); - - set_message (Gtk.MessageType.WARNING, message, _("Load"), () => { - this.source_view.set_text (new_buffer.text, false); - hide_info_bar (); - }, _("Continue"), () => { - hide_info_bar (); + dialog.response.connect ((id) => { + dialog.destroy (); + Idle.add (() => { + switch (id) { + case Gtk.ResponseType.ACCEPT: + save_as_with_hold.begin ((obj, res) => { + if (save_as_with_hold.end (res)) { + locked = false; + } }); - } + break; + case Gtk.ResponseType.REJECT: + break; + default: + assert_not_reached (); } - }); - } - } - private void save_as_and_hide_infobar () { - save_as.begin ((obj, res) => { - if (save_as.end (res)) { - hide_info_bar (); - } + return false; + }); }); - } - - private void ask_save_location (bool save_as = false) { - // We must assume that already asking for save location if infobar is - // visible. - if (info_bar.visible == true) { - return; - } - - string message; - if (save_as) { - message = _( - "You cannot save the document to “%s”. Do you want to save the file somewhere else?" - ).printf ("%s".printf (get_directory ())); - } else { - message = _( - "You cannot save changes to the file “%s”. Do you want to save the changes somewhere else?" - ).printf ("%s".printf (get_basename ())); - } - - set_message ( - Gtk.MessageType.WARNING, - message, - _("Save the document elsewhere"), - save_as_and_hide_infobar - ); - Utils.action_from_group (MainWindow.ACTION_SAVE, actions).set_enabled (false); - this.source_view.editable = !Scratch.settings.get_boolean ("autosave"); + dialog.present (); } // Set Undo/Redo action sensitive property From eef32abe0914724bb3282a6b67c373b6fea61bf3 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Tue, 11 Apr 2023 20:07:14 +0100 Subject: [PATCH 11/31] Compile dialog --- src/meson.build | 1 + 1 file changed, 1 insertion(+) diff --git a/src/meson.build b/src/meson.build index 55f6b3510a..47e13c324f 100644 --- a/src/meson.build +++ b/src/meson.build @@ -20,6 +20,7 @@ code_files = files( 'Utils.vala', 'Dialogs/PreferencesDialog.vala', 'Dialogs/RestoreConfirmationDialog.vala', + 'Dialogs/AskSaveLocationDialog.vala', 'Dialogs/GlobalSearchDialog.vala', 'Dialogs/NewBranchDialog.vala', 'FolderManager/ContractMenuItem.vala', From 453ccc45e0f89f9bae6e7870bb1fea3ffafb1f2a Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Tue, 11 Apr 2023 20:07:37 +0100 Subject: [PATCH 12/31] Minor improvements --- src/Dialogs/AskSaveLocationDialog.vala | 2 +- src/Services/Document.vala | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Dialogs/AskSaveLocationDialog.vala b/src/Dialogs/AskSaveLocationDialog.vala index 66fbf9c5f0..61d43ee5d5 100644 --- a/src/Dialogs/AskSaveLocationDialog.vala +++ b/src/Dialogs/AskSaveLocationDialog.vala @@ -3,7 +3,7 @@ */ public class Scratch.Dialogs.AskSaveLocationDialog : Granite.MessageDialog { - public AskSaveLocationDialog (string primary_text, string secondary_text, string error_message = "") { + public AskSaveLocationDialog (string primary_text, string secondary_text, string error_message) { Object ( buttons: Gtk.ButtonsType.NONE, primary_text: primary_text, diff --git a/src/Services/Document.vala b/src/Services/Document.vala index 3427acb4b1..1645f88143 100644 --- a/src/Services/Document.vala +++ b/src/Services/Document.vala @@ -179,8 +179,8 @@ namespace Scratch.Services { settings.changed.connect (restore_settings); /* Block user editing while working */ - source_view.key_press_event.connect (() => { - return working; + notify["working"].connect (() => { + source_view.sensitive = !working; }); var source_grid = new Gtk.Grid () { @@ -601,7 +601,7 @@ namespace Scratch.Services { var is_saved = false; if (success) { - source_view.buffer.set_modified (true); + // Should not set "modified" state of the buffer to true - this is automatic is_saved = yield save (true, true); if (is_saved) { if (is_current_file_temporary) { @@ -989,8 +989,7 @@ namespace Scratch.Services { try { file.copy (backup, FileCopyFlags.NONE); } catch (Error e) { - warning ("Cannot create backup copy for file “%s”: %s", get_basename (), e.message); - ask_save_location (); + warning ("Cannot create backup copy for file “%s”: %s", get_uri (), e.message); } } } From 24111241e95cefddabb4f63eacb475792295953b Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 12 Apr 2023 11:09:45 +0100 Subject: [PATCH 13/31] Start unlocked after loading --- src/Services/Document.vala | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Services/Document.vala b/src/Services/Document.vala index 1645f88143..457989d099 100644 --- a/src/Services/Document.vala +++ b/src/Services/Document.vala @@ -390,6 +390,7 @@ namespace Scratch.Services { Idle.add (() => { working = false; loaded = true; + locked = false; // Assume writable until status checked check_file_status (); return false; }); From 3826760d3a565f4bef5a84b3d7b0015b242119d7 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 12 Apr 2023 12:17:01 +0100 Subject: [PATCH 14/31] Handle closing changed locked documents --- src/Services/Document.vala | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Services/Document.vala b/src/Services/Document.vala index 457989d099..ca0ca98c3e 100644 --- a/src/Services/Document.vala +++ b/src/Services/Document.vala @@ -401,19 +401,22 @@ namespace Scratch.Services { public async bool do_close (bool app_closing = false) { debug ("Closing \"%s\"", get_basename ()); - if (!loaded || locked) { + if (!loaded) { load_cancellable.cancel (); return true; } bool ret_value = true; - if (Scratch.settings.get_boolean ("autosave") && !saved) { + // Prevent trying to save locked document to current location + if (!locked && Scratch.settings.get_boolean ("autosave") && !saved) { ret_value = yield save_with_hold (); - } else if (app_closing && is_file_temporary && !delete_temporary_file ()) { + } else if (!locked && app_closing && is_file_temporary && !delete_temporary_file ()) { debug ("Save temporary file!"); ret_value = yield save_with_hold (); - } else if (!this.saved || (!app_closing && is_file_temporary && !delete_temporary_file ())) { - // Check for unsaved changes + } else if (!this.saved || // Even locked documents can be modified + (!app_closing && is_file_temporary && !delete_temporary_file ())) { + + // Ask whether to save changes var parent_window = source_view.get_toplevel () as Gtk.Window; var dialog = new Granite.MessageDialog ( @@ -438,7 +441,8 @@ namespace Scratch.Services { ret_value = false; break; case Gtk.ResponseType.YES: - if (this.is_file_temporary) { + // Must save locked or temporary documents to a different location + if (locked || this.is_file_temporary) { ret_value = yield save_as_with_hold (); } else { ret_value = yield save_with_hold (); From ee5cbdb34bb8b681ff45eb78c1236f2cef7a8f05 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 12 Apr 2023 12:25:06 +0100 Subject: [PATCH 15/31] Update saved status when locked status changes --- src/Services/Document.vala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Services/Document.vala b/src/Services/Document.vala index ca0ca98c3e..2df1cc5ef8 100644 --- a/src/Services/Document.vala +++ b/src/Services/Document.vala @@ -103,6 +103,8 @@ namespace Scratch.Services { } else { icon = null; } + // Show "unsaved" marker on tab when locked even when autosave is ON + set_saved_status (!source_view.buffer.get_modified ()); } } From a2c3a6d357d257774dc65a06b9318c909ff88f96 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 12 Apr 2023 12:31:15 +0100 Subject: [PATCH 16/31] Amend tab tooltip when locked --- src/Services/Document.vala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Services/Document.vala b/src/Services/Document.vala index 2df1cc5ef8..181a0fe1f9 100644 --- a/src/Services/Document.vala +++ b/src/Services/Document.vala @@ -687,6 +687,8 @@ namespace Scratch.Services { public string get_tab_tooltip () { if (is_file_temporary) { return _("New Document"); //No path for a new document + } else if (locked) { + return _("Cannot save this document to %s").printf (Scratch.Utils.replace_home_with_tilde (file.get_path ())); } else { return Scratch.Utils.replace_home_with_tilde (file.get_path ()); } From fb3e17398e4e8cb1c63a5d45ece97f951fc90e9f Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Sun, 16 Apr 2023 13:29:07 +0100 Subject: [PATCH 17/31] Rework external changes with dialog --- src/Dialogs/AskExternalChangesDialog.vala | 30 ++++++ src/MainWindow.vala | 2 +- src/Services/Document.vala | 114 ++++++++++++++++------ src/meson.build | 1 + 4 files changed, 116 insertions(+), 31 deletions(-) create mode 100644 src/Dialogs/AskExternalChangesDialog.vala diff --git a/src/Dialogs/AskExternalChangesDialog.vala b/src/Dialogs/AskExternalChangesDialog.vala new file mode 100644 index 0000000000..6207f92d3c --- /dev/null +++ b/src/Dialogs/AskExternalChangesDialog.vala @@ -0,0 +1,30 @@ +/* Copyright 2023 elementary, Inc. + * SPDX-License-Identifier: GPL-3.0-or-later + */ +public class Scratch.Dialogs.AskExternalChangesDialog : Granite.MessageDialog { + + public AskExternalChangesDialog (string primary_text, string secondary_text) { + Object ( + buttons: Gtk.ButtonsType.NONE, + primary_text: primary_text, + secondary_text: secondary_text + ); + } + + construct { + var app_instance = (Gtk.Application) GLib.Application.get_default (); + transient_for = app_instance.active_window; + image_icon = new ThemedIcon ("dialog-warning"); + + add_button (_("Continue"), Gtk.ResponseType.REJECT); + + var reload_button = (Gtk.Button) add_button (_("Reload"), 0); + reload_button.get_style_context ().add_class (Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION); + + var overwrite_button = (Gtk.Button) add_button (_("Overwrite"), 1); + overwrite_button.get_style_context ().add_class (Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION); + + var saveas_button = (Gtk.Button) add_button (_("Save Document elsewhere"), Gtk.ResponseType.ACCEPT); + saveas_button.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION); + } +} diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 57ded41e28..a74d13c3f6 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -896,7 +896,7 @@ namespace Scratch { if (doc.is_file_temporary == true) { action_save_as (); } else { - doc.save_with_hold.begin (true); + doc.save_request (); } } } diff --git a/src/Services/Document.vala b/src/Services/Document.vala index 645f7bc1b5..7bc35f81f1 100644 --- a/src/Services/Document.vala +++ b/src/Services/Document.vala @@ -256,8 +256,10 @@ namespace Scratch.Services { Source.remove (timeout_saving); timeout_saving = 0; } + timeout_saving = Timeout.add (1000, () => { - save.begin (); + check_file_status (); + save.begin (); // Not forced timeout_saving = 0; return false; }); @@ -469,10 +471,17 @@ namespace Scratch.Services { return ret_value; } + // Handle save action (only use for user interaction) + public void save_request () { + check_undoable_actions (); + check_file_status (); // Need to check for external changes before forcing save + save_with_hold.begin (true); + } + private bool is_saving = false; public async bool save_with_hold (bool force = false, bool saving_as = false) { // Prevent reentry which could result in mismatched holds on Application - if (is_saving || locked) { + if (is_saving || (locked && !force)) { return true; } else { is_saving = true; @@ -508,7 +517,7 @@ namespace Scratch.Services { return result; } - private async bool save (bool force = false, bool saving_as = false) requires (!locked) { + private async bool save (bool force = false, bool saving_as = false) { if (completion_shown || !force && (source_view.buffer.get_modified () == false || !loaded)) { @@ -859,7 +868,7 @@ namespace Scratch.Services { } ask_save_location (details.printf ("%s".printf (get_basename ()))); - } else if (loaded) { // Check external changes after loading + } else if (loaded && !is_saving) { // Check external changes after loading if (!locked && !can_write () && source_view.buffer.get_modified ()) { // The file has become unwritable while changes are pending locked = true; @@ -867,9 +876,13 @@ namespace Scratch.Services { ask_save_location (details.printf ("%s".printf (get_basename ()))); } else { // Detect external changes by comparing file content with buffer content. - // Only done when no unsaved internal changes else difference from saved - // file are to be expected. If user selects to continue regardless then no further - // check made for this document - external changes will be overwritten on next (auto) save + // Only done when no unsaved internal changes else difference from saved + // file are to be expected. + + //TODO Check required behaviour on continue + // If user selects to continue regardless then no further + // check made for this document + // External changes will be overwritten on next (auto) save var new_buffer = new Gtk.SourceBuffer (null); var source_file_loader = new Gtk.SourceFileLoader ( new_buffer, @@ -892,33 +905,20 @@ namespace Scratch.Services { return; } - string message; + var primary_text = _("File “%s” was modified by an external application"); + string secondary_text; + if (source_view.buffer.get_modified ()) { - message = _( - "File \"%s\" was modified by an external application. \nThere are also unsaved changes. \nReload the document and lose the unsaved changes? \nOtherwise, overwrite the external changes or save with a different name." - ).printf ("%s".printf (get_basename ())); + secondary_text = _( + "There are also unsaved changes. Reloading the document will overwrite the unsaved changes." + ); } else { - message = _( - "File \"%s\" was modified by an external application. \nReload the document? \nOtherwise, overwrite the external changes or save with a different name." - ).printf ("%s".printf (get_basename ())); + secondary_text = _( + "The document changed externally since you last saved it." + ); } - locked = true; - set_message ( - Gtk.MessageType.WARNING, - message, - _("Reload"), () => { - source_view.buffer.text = new_buffer.text; - source_view.buffer.set_modified (false); - last_save_content = source_view.buffer.text; - set_saved_status (true); - locked = false; - hide_info_bar (); - }, - _("Continue"), () => { - hide_info_bar (); - } - ); + ask_external_changes (primary_text, secondary_text, new_buffer.text); } ); } @@ -967,6 +967,60 @@ namespace Scratch.Services { dialog.present (); } + private void ask_external_changes ( + string primary_text, + string secondary_text, + string external_content + ) { + locked = true; + + var dialog = new Scratch.Dialogs.AskExternalChangesDialog ( + primary_text, + secondary_text + ); + + dialog.response.connect ((id) => { + dialog.destroy (); + Idle.add (() => { + switch (id) { + case Gtk.ResponseType.ACCEPT: // Save as + save_as_with_hold.begin ((obj, res) => { + if (save_as_with_hold.end (res)) { + locked = false; + } + }); + break; + case Gtk.ResponseType.REJECT: // Ignore + // Document remains locked while conflicts exist + // The user must resolve some other way. To overwrite + // external changes use "Save As" with same name + break; + case 0: // Reload + source_view.buffer.text = external_content; + source_view.buffer.set_modified (false); + last_save_content = source_view.buffer.text; + set_saved_status (true); + locked = false; + break; + case 1: // Overwrite + // Force save + // locked = false; + save_with_hold.begin (true, false, (obj, res) => { + if (save_with_hold.end (res)) { + locked = false; + } + }); + break; + default: + assert_not_reached (); + } + + return Source.REMOVE; + }); + }); + + dialog.present (); + } // Set Undo/Redo action sensitive property public void check_undoable_actions () { var source_buffer = (Gtk.SourceBuffer) source_view.buffer; diff --git a/src/meson.build b/src/meson.build index 47e13c324f..c87c2fd87e 100644 --- a/src/meson.build +++ b/src/meson.build @@ -21,6 +21,7 @@ code_files = files( 'Dialogs/PreferencesDialog.vala', 'Dialogs/RestoreConfirmationDialog.vala', 'Dialogs/AskSaveLocationDialog.vala', + 'Dialogs/AskExternalChangesDialog.vala', 'Dialogs/GlobalSearchDialog.vala', 'Dialogs/NewBranchDialog.vala', 'FolderManager/ContractMenuItem.vala', From 56aa99fc255b31a36399ff9bcefa25f770de50b6 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Mon, 17 Apr 2023 11:10:25 +0100 Subject: [PATCH 18/31] Connect focus-in handler with focus-out handler --- src/Services/Document.vala | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Services/Document.vala b/src/Services/Document.vala index 7bc35f81f1..97b3fefce6 100644 --- a/src/Services/Document.vala +++ b/src/Services/Document.vala @@ -203,6 +203,16 @@ namespace Scratch.Services { this.source_view.buffer.create_tag ("highlight_search_all", "background", "yellow", null); + // Focus in event for SourceView + this.source_view.focus_in_event.connect (() => { + if (!locked && !is_file_temporary) { + check_file_status (); + check_undoable_actions (); + } + + return false; + }); + // Focus out event for SourceView this.source_view.focus_out_event.connect (() => { if (!locked && Scratch.settings.get_boolean ("autosave")) { @@ -368,16 +378,6 @@ namespace Scratch.Services { } } - // Focus in event for SourceView - this.source_view.focus_in_event.connect (() => { - if (!locked) { - check_file_status (); - check_undoable_actions (); - } - - return false; - }); - // Change syntax highlight this.source_view.change_syntax_highlight_from_file (this.file); From 4b876007766448234aaec9505d063a602d5ff2ca Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 31 May 2023 11:48:04 +0100 Subject: [PATCH 19/31] Fix focus in and overwrite --- src/Services/Document.vala | 43 ++++++++++++++++++----------------- src/Widgets/DocumentView.vala | 8 ++----- 2 files changed, 24 insertions(+), 27 deletions(-) diff --git a/src/Services/Document.vala b/src/Services/Document.vala index 7a8d82c2fd..c01dd1dbf1 100644 --- a/src/Services/Document.vala +++ b/src/Services/Document.vala @@ -92,6 +92,7 @@ namespace Scratch.Services { // Locked documents can be edited but cannot be (auto)saved to the current file. // Locked documents can be saved to a different file (when they will be unlocked) + // Create as locked so focus events ingb=nored. Unlock when content is loaded private bool _locked = true; public bool locked { get { @@ -115,8 +116,8 @@ namespace Scratch.Services { public Gtk.Stack main_stack; public Scratch.Widgets.SourceView source_view; private Scratch.Services.SymbolOutline? outline = null; - public string original_content; - private string last_save_content; + public string original_content = ""; + private string last_save_content = ""; public bool saved = true; private bool completion_shown = false; @@ -207,6 +208,17 @@ namespace Scratch.Services { this.source_view.buffer.create_tag ("highlight_search_all", "background", "yellow", null); + // Focus in event for SourceView + // Check if file changed externally or permissions changed + this.source_view.focus_in_event.connect (() => { + if (!locked && !is_file_temporary) { + check_file_status (); + check_undoable_actions (); + } + + return false; + }); + // Focus out event for SourceView this.source_view.focus_out_event.connect (() => { if (!locked && Scratch.settings.get_boolean ("autosave")) { @@ -236,9 +248,7 @@ namespace Scratch.Services { completion_shown = false; }); - // /* Create as loaded and unlocked - could be new document */ loaded = file == null; - locked = false; ellipsize_mode = Pango.EllipsizeMode.MIDDLE; } @@ -372,22 +382,13 @@ namespace Scratch.Services { } } - // Focus in event for SourceView - this.source_view.focus_in_event.connect (() => { - if (!working && !locked) { - check_file_status (); - check_undoable_actions (); - } - - return false; - }); + source_view.buffer.set_modified (false); + original_content = source_view.buffer.text; + last_save_content = source_view.buffer.text; // Change syntax highlight this.source_view.change_syntax_highlight_from_file (this.file); - source_view.buffer.set_modified (false); - original_content = source_view.buffer.text; - last_save_content = source_view.buffer.text; set_saved_status (true); doc_opened (); source_view.sensitive = true; @@ -915,7 +916,7 @@ namespace Scratch.Services { return; } - var primary_text = _("File “%s” was modified by an external application"); + var primary_text = _("File “%s” was modified by an external application").printf (file.get_uri ()); string secondary_text; if (source_view.buffer.get_modified ()) { @@ -1020,11 +1021,11 @@ namespace Scratch.Services { locked = false; break; case 1: // Overwrite - // Force save - // locked = false; + // Force save, unlock to allow saving to same location + locked = false; save_with_hold.begin (true, false, (obj, res) => { - if (save_with_hold.end (res)) { - locked = false; + if (!save_with_hold.end (res)) { + locked = true; } }); break; diff --git a/src/Widgets/DocumentView.vala b/src/Widgets/DocumentView.vala index 123ca7c38f..3fbc166bd4 100644 --- a/src/Widgets/DocumentView.vala +++ b/src/Widgets/DocumentView.vala @@ -187,12 +187,8 @@ public class Scratch.Widgets.DocumentView : Granite.Widgets.DynamicNotebook { file.create (FileCreateFlags.PRIVATE); var doc = new Services.Document (window.actions, file); - - insert_document (doc, -1); - current_document = doc; - - doc.focus (); - save_opened_files (); + // Must open document in order to unlock it. + open_document (doc); } catch (Error e) { critical (e.message); } From b4ae35be06db3a827538755f27f325392cbc4687 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 31 May 2023 18:36:59 +0100 Subject: [PATCH 20/31] Inline AskExternalChanges dialog --- src/Dialogs/AskExternalChangesDialog.vala | 30 ----------------------- src/Services/Document.vala | 25 ++++++++++++++++--- src/meson.build | 1 - 3 files changed, 21 insertions(+), 35 deletions(-) delete mode 100644 src/Dialogs/AskExternalChangesDialog.vala diff --git a/src/Dialogs/AskExternalChangesDialog.vala b/src/Dialogs/AskExternalChangesDialog.vala deleted file mode 100644 index 6207f92d3c..0000000000 --- a/src/Dialogs/AskExternalChangesDialog.vala +++ /dev/null @@ -1,30 +0,0 @@ -/* Copyright 2023 elementary, Inc. - * SPDX-License-Identifier: GPL-3.0-or-later - */ -public class Scratch.Dialogs.AskExternalChangesDialog : Granite.MessageDialog { - - public AskExternalChangesDialog (string primary_text, string secondary_text) { - Object ( - buttons: Gtk.ButtonsType.NONE, - primary_text: primary_text, - secondary_text: secondary_text - ); - } - - construct { - var app_instance = (Gtk.Application) GLib.Application.get_default (); - transient_for = app_instance.active_window; - image_icon = new ThemedIcon ("dialog-warning"); - - add_button (_("Continue"), Gtk.ResponseType.REJECT); - - var reload_button = (Gtk.Button) add_button (_("Reload"), 0); - reload_button.get_style_context ().add_class (Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION); - - var overwrite_button = (Gtk.Button) add_button (_("Overwrite"), 1); - overwrite_button.get_style_context ().add_class (Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION); - - var saveas_button = (Gtk.Button) add_button (_("Save Document elsewhere"), Gtk.ResponseType.ACCEPT); - saveas_button.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION); - } -} diff --git a/src/Services/Document.vala b/src/Services/Document.vala index c01dd1dbf1..6dbb38b6f5 100644 --- a/src/Services/Document.vala +++ b/src/Services/Document.vala @@ -992,10 +992,27 @@ namespace Scratch.Services { ) { locked = true; - var dialog = new Scratch.Dialogs.AskExternalChangesDialog ( - primary_text, - secondary_text - ); + var app_instance = (Gtk.Application) GLib.Application.get_default (); + var dialog = new Granite.MessageDialog ( + primary_text, + secondary_text, + new ThemedIcon ("dialog-warning"), + Gtk.ButtonsType.NONE + ) { + transient_for = app_instance.active_window + + }; + + dialog.add_button (_("Continue"), Gtk.ResponseType.REJECT); + + var reload_button = (Gtk.Button) (dialog.add_button (_("Reload"), 0)); + reload_button.get_style_context ().add_class (Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION); + + var overwrite_button = (Gtk.Button) (dialog.add_button (_("Overwrite"), 1)); + overwrite_button.get_style_context ().add_class (Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION); + + var saveas_button = (Gtk.Button) (dialog.add_button (_("Save Document elsewhere"), Gtk.ResponseType.ACCEPT)); + saveas_button.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION); dialog.response.connect ((id) => { dialog.destroy (); diff --git a/src/meson.build b/src/meson.build index c87c2fd87e..47e13c324f 100644 --- a/src/meson.build +++ b/src/meson.build @@ -21,7 +21,6 @@ code_files = files( 'Dialogs/PreferencesDialog.vala', 'Dialogs/RestoreConfirmationDialog.vala', 'Dialogs/AskSaveLocationDialog.vala', - 'Dialogs/AskExternalChangesDialog.vala', 'Dialogs/GlobalSearchDialog.vala', 'Dialogs/NewBranchDialog.vala', 'FolderManager/ContractMenuItem.vala', From 070314ac996f8e291e67b3798c02968340edfe23 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Sat, 3 Jun 2023 18:20:58 +0100 Subject: [PATCH 21/31] Do not throw dialog if there are no unsaved internal edits --- src/Services/Document.vala | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Services/Document.vala b/src/Services/Document.vala index 6dbb38b6f5..5ab41c3747 100644 --- a/src/Services/Document.vala +++ b/src/Services/Document.vala @@ -916,6 +916,15 @@ namespace Scratch.Services { return; } + if (last_save_content == source_view.buffer.text) { + // There are no unsaved internal edits so just load the external changes + //TODO Indicate to the user external changes loaded? + source_view.buffer.text = new_buffer.text; + // We know the content and file are now in sync so set unmodified + source_view.buffer.set_modified (false); + return; + } + var primary_text = _("File “%s” was modified by an external application").printf (file.get_uri ()); string secondary_text; From 7e17654978268475adbbc55bbd6d989d88126e56 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Sat, 3 Jun 2023 18:39:03 +0100 Subject: [PATCH 22/31] Remove unused class --- src/Dialogs/AskSaveLocationDialog.vala | 28 -------------------------- src/meson.build | 1 - 2 files changed, 29 deletions(-) delete mode 100644 src/Dialogs/AskSaveLocationDialog.vala diff --git a/src/Dialogs/AskSaveLocationDialog.vala b/src/Dialogs/AskSaveLocationDialog.vala deleted file mode 100644 index 61d43ee5d5..0000000000 --- a/src/Dialogs/AskSaveLocationDialog.vala +++ /dev/null @@ -1,28 +0,0 @@ -/* Copyright 2023 elementary, Inc. - * SPDX-License-Identifier: GPL-3.0-or-later - */ -public class Scratch.Dialogs.AskSaveLocationDialog : Granite.MessageDialog { - - public AskSaveLocationDialog (string primary_text, string secondary_text, string error_message) { - Object ( - buttons: Gtk.ButtonsType.NONE, - primary_text: primary_text, - secondary_text: secondary_text - ); - - if (error_message != "") { - show_error_details (error_message); - } - } - - construct { - var app_instance = (Gtk.Application) GLib.Application.get_default (); - transient_for = app_instance.active_window; - image_icon = new ThemedIcon ("dialog-warning"); - - add_button (_("Ignore"), Gtk.ResponseType.REJECT); - - var saveas_button = (Gtk.Button) add_button (_("Save Document elsewhere"), Gtk.ResponseType.ACCEPT); - saveas_button.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION); - } -} diff --git a/src/meson.build b/src/meson.build index 47e13c324f..55f6b3510a 100644 --- a/src/meson.build +++ b/src/meson.build @@ -20,7 +20,6 @@ code_files = files( 'Utils.vala', 'Dialogs/PreferencesDialog.vala', 'Dialogs/RestoreConfirmationDialog.vala', - 'Dialogs/AskSaveLocationDialog.vala', 'Dialogs/GlobalSearchDialog.vala', 'Dialogs/NewBranchDialog.vala', 'FolderManager/ContractMenuItem.vala', From 9534e3bf54f5e25f6d3aa1d50463ee33653373a3 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Sat, 3 Jun 2023 18:45:38 +0100 Subject: [PATCH 23/31] Move some code back to original position --- src/Services/Document.vala | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Services/Document.vala b/src/Services/Document.vala index 5ab41c3747..500abec4d4 100644 --- a/src/Services/Document.vala +++ b/src/Services/Document.vala @@ -382,13 +382,12 @@ namespace Scratch.Services { } } - source_view.buffer.set_modified (false); - original_content = source_view.buffer.text; - last_save_content = source_view.buffer.text; - // Change syntax highlight this.source_view.change_syntax_highlight_from_file (this.file); + source_view.buffer.set_modified (false); + original_content = source_view.buffer.text; + last_save_content = source_view.buffer.text; set_saved_status (true); doc_opened (); source_view.sensitive = true; From 3b141f92446a481e7f6d1feb8d95e27f52f07d0b Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Sat, 3 Jun 2023 19:10:36 +0100 Subject: [PATCH 24/31] Update last_save_content after loading external changes --- src/Services/Document.vala | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Services/Document.vala b/src/Services/Document.vala index 500abec4d4..88ee60fa93 100644 --- a/src/Services/Document.vala +++ b/src/Services/Document.vala @@ -919,6 +919,7 @@ namespace Scratch.Services { // There are no unsaved internal edits so just load the external changes //TODO Indicate to the user external changes loaded? source_view.buffer.text = new_buffer.text; + last_save_content = source_view.buffer.text; // We know the content and file are now in sync so set unmodified source_view.buffer.set_modified (false); return; From 694bd94c340365eaa7affca029697b9afc9d7848 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Sat, 3 Jun 2023 19:22:26 +0100 Subject: [PATCH 25/31] Check for external changes even when internal changes --- src/Services/Document.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Services/Document.vala b/src/Services/Document.vala index 88ee60fa93..49ff49a009 100644 --- a/src/Services/Document.vala +++ b/src/Services/Document.vala @@ -884,7 +884,7 @@ namespace Scratch.Services { locked = true; var details = _("File “%s” does not have write permission."); ask_save_location (details.printf ("%s".printf (get_basename ()))); - } else if (!source_view.buffer.get_modified ()) { + } else { // Detect external changes by comparing file content with buffer content. // Only done when no unsaved internal changes else difference from saved // file are to be expected. From 22710cc03b8ddf7d6db6d111fdb097a22e06c827 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Tue, 6 Jun 2023 11:41:00 +0100 Subject: [PATCH 26/31] Only save deleted file if there are unsaved changes --- src/Services/Document.vala | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/Services/Document.vala b/src/Services/Document.vala index 49ff49a009..7745a25b92 100644 --- a/src/Services/Document.vala +++ b/src/Services/Document.vala @@ -869,18 +869,25 @@ namespace Scratch.Services { private void check_file_status () { // If the file does not exist anymore if (!exists ()) { - locked = true; - string details; - if (mounted == false) { - details = _("The location containing the file “%s” was unmounted."); + if (source_view.buffer.get_modified ()) { + locked = true; + string details; + if (mounted == false) { + details = _("The location containing the file “%s” was unmounted and there are unsaved changes."); + } else { + details = _("File “%s” was deleted and there are unsaved changes."); + } + + ask_save_location (details.printf ("%s".printf (get_basename ()))); } else { - details = _("File “%s” was deleted."); + var close_tab_action = Utils.action_from_group (MainWindow.ACTION_CLOSE_TAB, actions); + close_tab_action.set_enabled (true); + this.saved = true; //Do not try to save + close_tab_action.activate (new Variant ("s", file.get_path ())); } - - ask_save_location (details.printf ("%s".printf (get_basename ()))); } else if (loaded && !is_saving) { // Check external changes after loading if (!locked && !can_write () && source_view.buffer.get_modified ()) { - // The file has become unwritable while changes are pending + // The file has become unwritable while changes are pending locked = true; var details = _("File “%s” does not have write permission."); ask_save_location (details.printf ("%s".printf (get_basename ()))); From 1a512383b4ca60c2de5bc01047cff2854ea142b2 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Mon, 3 Jul 2023 08:38:15 +0100 Subject: [PATCH 27/31] Update src/Services/Document.vala Fix typo Co-authored-by: Ryan Kornheisl --- src/Services/Document.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Services/Document.vala b/src/Services/Document.vala index 4b609750be..bee1252698 100644 --- a/src/Services/Document.vala +++ b/src/Services/Document.vala @@ -92,7 +92,7 @@ namespace Scratch.Services { // Locked documents can be edited but cannot be (auto)saved to the current file. // Locked documents can be saved to a different file (when they will be unlocked) - // Create as locked so focus events ingb=nored. Unlock when content is loaded + // Create as locked so focus events ignored. Unlock when content is loaded private bool _locked = true; public bool locked { get { From 7c214f6bca5444f9c3fb50c16b5db609ec86aca0 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Thu, 6 Jul 2023 11:54:21 +0100 Subject: [PATCH 28/31] Use copy paste to autoload changed text --- src/Services/Document.vala | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Services/Document.vala b/src/Services/Document.vala index bee1252698..c69af7aec1 100644 --- a/src/Services/Document.vala +++ b/src/Services/Document.vala @@ -928,8 +928,8 @@ namespace Scratch.Services { if (last_save_content == source_view.buffer.text) { // There are no unsaved internal edits so just load the external changes //TODO Indicate to the user external changes loaded? - source_view.buffer.text = new_buffer.text; - last_save_content = source_view.buffer.text; + replace_text_from_buffer (new_buffer); // TODO This is async, block further editing until complete + last_save_content = new_buffer.text; // Now in sync with file // We know the content and file are now in sync so set unmodified source_view.buffer.set_modified (false); return; @@ -955,6 +955,20 @@ namespace Scratch.Services { } } + private void replace_text_from_buffer (Gtk.TextBuffer new_buffer) { + // Use copy-paste so undoable in one step + Gtk.TextIter new_start, old_start, new_end, old_end; + var clipboard = Gtk.Clipboard.get_default (Gdk.Display.get_default ()); + new_buffer.get_start_iter (out new_start); + new_buffer.get_end_iter (out new_end); + source_view.buffer.get_start_iter (out old_start); + source_view.buffer.get_end_iter (out old_end); + new_buffer.select_range (new_start, new_end); + new_buffer.copy_clipboard (clipboard); + source_view.buffer.select_range (old_start, old_end); + source_view.buffer.paste_clipboard (clipboard, null, true); + } + private void ask_save_location ( string details, string error_text = "" From a80ad16065d73289baea08299a435e1b8b0f7e92 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Thu, 6 Jul 2023 12:23:58 +0100 Subject: [PATCH 29/31] Flag as not loaded while replacing text --- src/Services/Document.vala | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Services/Document.vala b/src/Services/Document.vala index c69af7aec1..0bef8f9349 100644 --- a/src/Services/Document.vala +++ b/src/Services/Document.vala @@ -229,7 +229,15 @@ namespace Scratch.Services { return false; }); + source_view.buffer.paste_done.connect (() => { + loaded = true; + }); + source_view.buffer.changed.connect (() => { + if (!loaded) { + return; + } + if (source_view.buffer.text != last_save_content) { saved = false; // Autosave does not work on locked document @@ -928,9 +936,11 @@ namespace Scratch.Services { if (last_save_content == source_view.buffer.text) { // There are no unsaved internal edits so just load the external changes //TODO Indicate to the user external changes loaded? - replace_text_from_buffer (new_buffer); // TODO This is async, block further editing until complete + loaded = false; // Block certain actions. Will be set `true` when `paste-done` sigal received. + replace_text_from_buffer (new_buffer); // Asynchronous last_save_content = new_buffer.text; // Now in sync with file - // We know the content and file are now in sync so set unmodified + // We know the content and file will be in sync after paste so set unmodified + set_saved_status (true); source_view.buffer.set_modified (false); return; } From 04e428d9396667aecae3d7b79ec9cefc5cfa688b Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Fri, 7 Jul 2023 09:15:33 +0100 Subject: [PATCH 30/31] No undo on autoload --- src/Services/Document.vala | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/src/Services/Document.vala b/src/Services/Document.vala index 0bef8f9349..99e14db815 100644 --- a/src/Services/Document.vala +++ b/src/Services/Document.vala @@ -229,10 +229,6 @@ namespace Scratch.Services { return false; }); - source_view.buffer.paste_done.connect (() => { - loaded = true; - }); - source_view.buffer.changed.connect (() => { if (!loaded) { return; @@ -937,11 +933,12 @@ namespace Scratch.Services { // There are no unsaved internal edits so just load the external changes //TODO Indicate to the user external changes loaded? loaded = false; // Block certain actions. Will be set `true` when `paste-done` sigal received. - replace_text_from_buffer (new_buffer); // Asynchronous + source_view.set_text (new_buffer.text); last_save_content = new_buffer.text; // Now in sync with file // We know the content and file will be in sync after paste so set unmodified set_saved_status (true); source_view.buffer.set_modified (false); + loaded = true; return; } @@ -965,20 +962,6 @@ namespace Scratch.Services { } } - private void replace_text_from_buffer (Gtk.TextBuffer new_buffer) { - // Use copy-paste so undoable in one step - Gtk.TextIter new_start, old_start, new_end, old_end; - var clipboard = Gtk.Clipboard.get_default (Gdk.Display.get_default ()); - new_buffer.get_start_iter (out new_start); - new_buffer.get_end_iter (out new_end); - source_view.buffer.get_start_iter (out old_start); - source_view.buffer.get_end_iter (out old_end); - new_buffer.select_range (new_start, new_end); - new_buffer.copy_clipboard (clipboard); - source_view.buffer.select_range (old_start, old_end); - source_view.buffer.paste_clipboard (clipboard, null, true); - } - private void ask_save_location ( string details, string error_text = "" From f5008b01b71808c5ed79610a6d267321dd38ea8e Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Fri, 7 Jul 2023 09:25:43 +0100 Subject: [PATCH 31/31] Lose unused infobar --- src/Services/Document.vala | 64 -------------------------------------- 1 file changed, 64 deletions(-) diff --git a/src/Services/Document.vala b/src/Services/Document.vala index 99e14db815..42ba537bf2 100644 --- a/src/Services/Document.vala +++ b/src/Services/Document.vala @@ -122,7 +122,6 @@ namespace Scratch.Services { private bool completion_shown = false; private Gtk.ScrolledWindow scroll; - private Gtk.InfoBar info_bar; private Gtk.SourceMap source_map; private Gtk.Paned outline_widget_pane; @@ -168,7 +167,6 @@ namespace Scratch.Services { expand = true }; scroll.add (source_view); - info_bar = new Gtk.InfoBar (); source_file = new Gtk.SourceFile (); source_map = new Gtk.SourceMap (); outline_widget_pane = new Gtk.Paned (Gtk.Orientation.HORIZONTAL); @@ -180,7 +178,6 @@ namespace Scratch.Services { source_map.set_view (source_view); - hide_info_bar (); set_minimap (); set_strip_trailing_whitespace (); settings.changed["show-mini-map"].connect (set_minimap); @@ -201,7 +198,6 @@ namespace Scratch.Services { var doc_grid = new Gtk.Grid (); doc_grid.orientation = Gtk.Orientation.VERTICAL; - doc_grid.add (info_bar); doc_grid.add (outline_widget_pane); doc_grid.show_all (); @@ -584,7 +580,6 @@ namespace Scratch.Services { this.set_saved_status (true); last_save_content = source_view.buffer.text; - hide_info_bar (); debug ("File “%s” saved successfully", get_basename ()); @@ -721,65 +716,6 @@ namespace Scratch.Services { } } - // Set InfoBars message - private void set_message (Gtk.MessageType type, string label, - string? button1 = null, owned VoidFunc? callback1 = null, - string? button2 = null, owned VoidFunc? callback2 = null) { - - // Show InfoBar - info_bar.no_show_all = false; - info_bar.visible = true; - - // Clear from useless widgets - info_bar.get_content_area ().get_children ().foreach ((widget) => { - if (widget != null) { - widget.destroy (); - } - }); - - ((Gtk.Container) info_bar.get_action_area ()).get_children ().foreach ((widget) => { - if (widget != null) { - widget.destroy (); - } - }); - - // Type - info_bar.message_type = type; - - // Layout - var l = new Gtk.Label (label); - l.ellipsize = Pango.EllipsizeMode.END; - l.use_markup = true; - l.set_markup (label); - ((Gtk.Box) info_bar.get_action_area ()).orientation = Gtk.Orientation.HORIZONTAL; - var main = info_bar.get_content_area () as Gtk.Box; - main.orientation = Gtk.Orientation.HORIZONTAL; - main.pack_start (l, false, false, 0); - if (button1 != null) { - info_bar.add_button (button1, 0); - } if (button2 != null) { - info_bar.add_button (button2, 1); - } - - // Response - info_bar.response.connect ((id) => { - if (id == 0) { - callback1 (); - } else if (id == 1) { - callback2 (); - } - }); - - // Show everything - info_bar.show_all (); - } - - // Hide InfoBar when not needed - public void hide_info_bar () { - info_bar.no_show_all = true; - info_bar.visible = false; - } - // SourceView related functions // Undo public void undo () {