diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 4dab94987d..0ea5804d3f 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 d01b5ac753..42ba537bf2 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 ignored. Unlock when content is loaded private bool _locked = true; public bool locked { get { @@ -115,13 +116,12 @@ 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; private Gtk.ScrolledWindow scroll; - private Gtk.InfoBar info_bar; private Gtk.SourceMap source_map; private Gtk.Paned outline_widget_pane; @@ -167,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); @@ -179,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); @@ -200,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 (); @@ -208,6 +205,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")) { @@ -218,6 +226,10 @@ namespace Scratch.Services { }); 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 @@ -237,9 +249,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; } @@ -261,8 +271,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; }); @@ -371,16 +383,6 @@ 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; - }); - // Change syntax highlight this.source_view.change_syntax_highlight_from_file (this.file); @@ -474,6 +476,13 @@ 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 @@ -619,6 +628,7 @@ namespace Scratch.Services { // Should not set "modified" state of the buffer to true - this is automatic is_saved = yield save (true, true); if (is_saved) { + source_view.buffer.set_modified (false); if (is_current_file_temporary) { try { // Delete temporary file @@ -706,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 () { @@ -861,23 +812,37 @@ 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) { // 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 + // 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 ()))); } else { - // Check for external changes (can load even if locked or unwritable) + // 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. + + //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, @@ -896,36 +861,37 @@ namespace Scratch.Services { return; } - if (source_view.buffer.text == new_buffer.text) { + if (last_save_content == new_buffer.text) { 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 (); - }) - ; - } + 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? + loaded = false; // Block certain actions. Will be set `true` when `paste-done` sigal received. + 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; + } + + var primary_text = _("File “%s” was modified by an external application").printf (file.get_uri ()); + string secondary_text; + + if (source_view.buffer.get_modified ()) { + secondary_text = _( + "There are also unsaved changes. Reloading the document will overwrite the unsaved changes." + ); } else { - //TODO Handle conflicting changes (dialog?) + secondary_text = _( + "The document changed externally since you last saved it." + ); } + + ask_external_changes (primary_text, secondary_text, new_buffer.text); } ); } @@ -981,6 +947,77 @@ namespace Scratch.Services { dialog.present (); } + private void ask_external_changes ( + string primary_text, + string secondary_text, + string external_content + ) { + locked = true; + + 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 (); + 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, unlock to allow saving to same location + locked = false; + save_with_hold.begin (true, false, (obj, res) => { + if (!save_with_hold.end (res)) { + locked = true; + } + }); + 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/Widgets/DocumentView.vala b/src/Widgets/DocumentView.vala index 7a5c1809db..80a4e65666 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); }