[geary/bug/728002-webkit2: 79/140] Re-enable composer empty body checking and draft save timer.



commit fcf5be297e26a2c9d6bf81380baa4a0de6c01374
Author: Michael James Gratton <mike vee net>
Date:   Fri Jan 6 10:48:40 2017 +1100

    Re-enable composer empty body checking and draft save timer.
    
    * src/client/composer/composer-web-view.vala (ClientWebView): Add a
      ::document_modified signal and a documentModified JS message listener,
      fire it when the JS message is receieved. Update value of ::is_empty
      based on whether a non-empty HTML body was provided in the first place,
      and if it has been subsequently modified. Update related doc comments a
      bit.
    
    * src/client/composer/composer-widget.vala (ComposerWidget): Rename
      `blank` property to `is_blank`, fix sense of editor.is_blank check,
      update call sites. Convert ::can_save method into a property, include
      the this.is_blank check since there's no point saving a blank message,
      updtae call sites. Replace use of GLib.Timeout with
      Geary.TimeoutManager, tidy up resulting code, hook up timer to new
      document_modified signal.
    
    * ui/composer-web-view.js: Use body mutation observer to send
      documentModified messages to the client, coalescing consecutive events
      over a period of 1s into a single message.

 src/client/application/geary-controller.vala |    2 +-
 src/client/composer/composer-web-view.vala   |   36 ++++++++--
 src/client/composer/composer-widget.vala     |   96 +++++++++++--------------
 ui/composer-web-view.js                      |   38 +++++------
 4 files changed, 92 insertions(+), 80 deletions(-)
---
diff --git a/src/client/application/geary-controller.vala b/src/client/application/geary-controller.vala
index 0e7ae7e..fcde502 100644
--- a/src/client/application/geary-controller.vala
+++ b/src/client/application/geary-controller.vala
@@ -2403,7 +2403,7 @@ public class GearyController : Geary.BaseObject {
         if (compose_type == ComposerWidget.ComposeType.NEW_MESSAGE) {
             foreach (ComposerWidget cw in composer_widgets) {
                 if (cw.state == ComposerWidget.ComposerState.NEW) {
-                    if (!cw.blank) {
+                    if (!cw.is_blank) {
                         inline = false;
                         return true;
                     } else {
diff --git a/src/client/composer/composer-web-view.vala b/src/client/composer/composer-web-view.vala
index 2b3a4cd..08a0dc0 100644
--- a/src/client/composer/composer-web-view.vala
+++ b/src/client/composer/composer-web-view.vala
@@ -14,6 +14,7 @@ public class ComposerWebView : ClientWebView {
 
     private const string COMMAND_STACK_CHANGED = "commandStackChanged";
     private const string CURSOR_STYLE_CHANGED = "cursorStyleChanged";
+    private const string DOCUMENT_MODIFIED = "documentModified";
 
     private const string[] SANS_FAMILY_NAMES = {
         "sans", "arial", "trebuchet", "helvetica"
@@ -96,14 +97,24 @@ public class ComposerWebView : ClientWebView {
         );
     }
 
-    /** Determines if the view contains any edited text */
-    public bool is_empty { get; private set; default = false; }
+    /**
+     * Determines if the body contains any non-boilerplate content.
+     *
+     * Currently, only a signatures are considered to be boilerplate.
+     * Any user-made changes or message body content from a
+     * forwarded/replied-to message present will make the view
+     * considered to be non-empty.
+     */
+    public bool is_empty { get; private set; default = true; }
 
-    /** Determines if the view is in rich text mode */
+    /** Determines if the view is in rich text mode. */
     public bool is_rich_text { get; private set; default = true; }
 
 
-    /** Emitted when the web view's undo/redo stack has changed. */
+    /** Emitted when the web view's content has changed. */
+    public signal void document_modified();
+
+    /** Emitted when the web view's undo/redo stack state changes. */
     public signal void command_stack_changed(bool can_undo, bool can_redo);
 
     /** Emitted when the style under the cursor has changed. */
@@ -124,9 +135,13 @@ public class ComposerWebView : ClientWebView {
         this.user_content_manager.script_message_received[CURSOR_STYLE_CHANGED].connect(
             on_cursor_style_changed_message
         );
+        this.user_content_manager.script_message_received[DOCUMENT_MODIFIED].connect(
+            on_document_modified_message
+        );
 
         register_message_handler(COMMAND_STACK_CHANGED);
         register_message_handler(CURSOR_STYLE_CHANGED);
+        register_message_handler(DOCUMENT_MODIFIED);
     }
 
     /**
@@ -136,7 +151,8 @@ public class ComposerWebView : ClientWebView {
         string html = "";
         signature = signature ?? "";
 
-        if (body == null)
+        this.is_empty = Geary.String.is_empty(body);
+        if (this.is_empty)
             html = CURSOR + "<br /><br />" + signature;
         else if (top_posting)
             html = CURSOR + "<br /><br />" + signature + body;
@@ -404,4 +420,14 @@ public class ComposerWebView : ClientWebView {
         }
     }
 
+    private void on_document_modified_message(WebKit.JavascriptResult result) {
+        result.unref();
+
+        // Only modify actually changed to avoid excessive notify
+        // signals being fired.
+        if (this.is_empty) {
+            this.is_empty = false;
+        }
+        document_modified();
+    }
 }
diff --git a/src/client/composer/composer-widget.vala b/src/client/composer/composer-widget.vala
index 87c13a4..f95b91d 100644
--- a/src/client/composer/composer-widget.vala
+++ b/src/client/composer/composer-widget.vala
@@ -155,9 +155,7 @@ public class ComposerWidget : Gtk.EventBox {
 
     private const string URI_LIST_MIME_TYPE = "text/uri-list";
     private const string FILE_URI_PREFIX = "file://";
-    
-    private const int DRAFT_TIMEOUT_SEC = 10;
-    
+
     public const string ATTACHMENT_KEYWORDS_SUFFIX = ".doc|.pdf|.xls|.ppt|.rtf|.pps";
     
     // A list of keywords, separated by pipe ("|") characters, that suggest an attachment; since
@@ -204,15 +202,26 @@ public class ComposerWidget : Gtk.EventBox {
 
     public Gee.Set<Geary.EmailIdentifier> referred_ids = new Gee.HashSet<Geary.EmailIdentifier>();
 
-    public bool blank {
+    /** Determines if the composer is completely empty. */
+    public bool is_blank {
+        get {
+            return this.to_entry.empty
+                && this.cc_entry.empty
+                && this.bcc_entry.empty
+                && this.reply_to_entry.empty
+                && this.subject_entry.buffer.length == 0
+                && this.editor.is_empty
+                && this.attached_files.size == 0;
+        }
+    }
+
+    /** Determines if current message can be saved as draft. */
+    private bool can_save {
         get {
-            return this.to_entry.empty &&
-                this.cc_entry.empty &&
-                this.bcc_entry.empty &&
-                this.reply_to_entry.empty &&
-                this.subject_entry.buffer.length == 0 &&
-                !this.editor.is_empty &&
-                this.attached_files.size == 0;
+            return this.draft_manager != null
+                && this.draft_manager.is_open
+                && this.account.information.save_drafts
+                && !this.is_blank;
         }
     }
 
@@ -336,7 +345,9 @@ public class ComposerWidget : Gtk.EventBox {
 
     private Geary.App.DraftManager? draft_manager = null;
     private Geary.EmailFlags draft_flags = new Geary.EmailFlags.with(Geary.EmailFlags.DRAFT);
-    private uint draft_save_timeout_id = 0;
+    private Geary.TimeoutManager draft_timer;
+
+    // Is the composer closing (e.g. saving a draft or sending)?
     private bool is_closing = false;
 
     private ComposerContainer container {
@@ -462,6 +473,10 @@ public class ComposerWidget : Gtk.EventBox {
         update_signature();
         update_pending_attachments(this.pending_include, true);
 
+        this.draft_timer = new Geary.TimeoutManager.seconds(
+            10, () => { this.save_draft.begin(); }
+        );
+
         // Add actions once every element has been initialized and added
         initialize_actions();
 
@@ -476,12 +491,12 @@ public class ComposerWidget : Gtk.EventBox {
         this.editor.command_stack_changed.connect(on_command_state_changed);
         this.editor.context_menu.connect(on_context_menu);
         this.editor.cursor_style_changed.connect(on_cursor_style_changed);
+        this.editor.document_modified.connect(() => { draft_changed(); });
         this.editor.get_editor_state().notify["typing-attributes"].connect(on_typing_attributes_changed);
         this.editor.key_press_event.connect(on_editor_key_press_event);
         this.editor.load_changed.connect(on_load_changed);
         this.editor.mouse_target_changed.connect(on_mouse_target_changed);
         this.editor.selection_changed.connect(on_selection_changed);
-        //this.editor.user_changed_contents.connect(reset_draft_timer);
 
         this.editor.load_html(this.body_html, this.signature_html, this.top_posting);
 
@@ -1034,23 +1049,16 @@ public class ComposerWidget : Gtk.EventBox {
                     this.to_entry.addresses);
             this.to_entry.modified = this.cc_entry.modified = false;
         }
-        
+
         in_reply_to.add(referred.message_id);
         referred_ids.add(referred.id);
     }
-    
-    private bool can_save() {
-        return this.draft_manager != null
-            && this.draft_manager.is_open
-            && this.editor.is_empty
-            && this.account.information.save_drafts;
-    }
 
     public CloseStatus should_close() {
         if (this.is_closing)
             return CloseStatus.PENDING_CLOSE;
 
-        bool try_to_save = can_save();
+        bool try_to_save = this.can_save;
 
         this.container.present();
         AlertDialog dialog;
@@ -1085,7 +1093,7 @@ public class ComposerWidget : Gtk.EventBox {
     }
 
     private void on_close_and_save(SimpleAction action, Variant? param) {
-        if (can_save())
+        if (this.can_save)
             save_and_exit_async.begin();
         else
             on_close(action, param);
@@ -1339,37 +1347,17 @@ public class ComposerWidget : Gtk.EventBox {
         debug("Draft manager closed");
     }
 
-    // Resets the draft save timeout.
-    private void reset_draft_timer() {
+    private inline void draft_changed() {
         this.draft_save_text = "";
-        cancel_draft_timer();
-        
-        if (can_save())
-            draft_save_timeout_id = Timeout.add_seconds(DRAFT_TIMEOUT_SEC, on_save_draft_timeout);
-    }
-
-    // Cancels the draft save timeout
-    private void cancel_draft_timer() {
-        if (this.draft_save_timeout_id == 0)
-            return;
-        
-        Source.remove(this.draft_save_timeout_id);
-        this.draft_save_timeout_id = 0;
-    }
-
-    private bool on_save_draft_timeout() {
-        // this is not rescheduled by the event loop, so kill the timeout id
-        this.draft_save_timeout_id = 0;
-        
-        save_draft.begin();
-        
-        return false;
+        if (this.can_save) {
+            this.draft_timer.start();
+        }
     }
 
     // Note that drafts are NOT "linkified."
     private async void save_draft() {
         // cancel timer in favor of just doing it now
-        cancel_draft_timer();
+        this.draft_timer.reset();
 
         if (this.draft_manager != null) {
             try {
@@ -1385,8 +1373,8 @@ public class ComposerWidget : Gtk.EventBox {
 
     private Geary.Nonblocking.Semaphore? discard_draft() {
         // cancel timer in favor of this operation
-        cancel_draft_timer();
-        
+        this.draft_timer.reset();
+
         try {
             if (this.draft_manager != null)
                 return this.draft_manager.discard();
@@ -1400,7 +1388,7 @@ public class ComposerWidget : Gtk.EventBox {
     // Used while waiting for draft to save before closing widget.
     private void make_gui_insensitive() {
         this.container.vanish();
-        cancel_draft_timer();
+        this.draft_timer.reset();
     }
 
     private async void save_and_exit_async() {
@@ -1598,7 +1586,7 @@ public class ComposerWidget : Gtk.EventBox {
 
     [GtkCallback]
     private void on_subject_changed() {
-        reset_draft_timer();
+        draft_changed();
     }
 
     private void validate_send_button() {
@@ -1627,7 +1615,7 @@ public class ComposerWidget : Gtk.EventBox {
             this.header.set_recipients(label, tooltip.str.slice(0, -1));  // Remove trailing \n
         }
 
-        reset_draft_timer();
+        draft_changed();
     }
 
     private void on_justify(SimpleAction action, Variant? param) {
@@ -2130,7 +2118,7 @@ public class ComposerWidget : Gtk.EventBox {
         this.open_draft_manager_async.begin(null, null, (obj, res) => {
                 try {
                     this.open_draft_manager_async.end(res);
-                    reset_draft_timer();
+                    draft_changed();
                 } catch (Error e) {
                     // Oh well?
                 }
diff --git a/ui/composer-web-view.js b/ui/composer-web-view.js
index 98324d1..ae39790 100644
--- a/ui/composer-web-view.js
+++ b/ui/composer-web-view.js
@@ -38,8 +38,15 @@ ComposerPageState.prototype = {
             }
         }, true);
 
+        let modifiedId = null;
         this.bodyObserver = new MutationObserver(function() {
-            state.checkCommandStack();
+            if (modifiedId == null) {
+                modifiedId = window.setTimeout(function() {
+                    state.documentModified();
+                    state.checkCommandStack();
+                    modifiedId = null;
+                }, 1000);
+            }
         });
     },
     loaded: function() {
@@ -88,7 +95,13 @@ ComposerPageState.prototype = {
         // Enable editing and observation machinery only after
         // modifying the body above.
         this.messageBody.contentEditable = true;
-        this.setBodyObserverEnabled(true);
+        let config = {
+            attributes: true,
+            childList: true,
+            characterData: true,
+            subtree: true
+        };
+        this.bodyObserver.observe(this.messageBody, config);
 
         // Chain up here so we continue to a preferred size update
         // after munging the HTML above.
@@ -133,28 +146,10 @@ ComposerPageState.prototype = {
             document.body.classList.add("plain");
         }
     },
-    setBodyObserverEnabled: function(enabled) {
-        if (enabled) {
-            let config = {
-                attributes: true,
-                childList: true,
-                characterData: true,
-                subtree: true
-            };
-            this.bodyObserver.observe(this.messageBody, config);
-        } else {
-            this.bodyObserver.disconnect();
-        }
-    },
     checkCommandStack: function() {
         let canUndo = document.queryCommandEnabled("undo");
         let canRedo = document.queryCommandEnabled("redo");
 
-        // Update the body observer - if we can undo we don't need to
-        // keep an eye on mutations any more, until we can't undo
-        // again.
-        this.setBodyObserverEnabled(!canUndo);
-
         if (canUndo != this.undoEnabled || canRedo != this.redoEnabled) {
             this.undoEnabled = canUndo;
             this.redoEnabled = canRedo;
@@ -173,6 +168,9 @@ ComposerPageState.prototype = {
             element.setAttribute("type", "cite");
         }
     },
+    documentModified: function(element) {
+        window.webkit.messageHandlers.documentModified.postMessage(null);
+    },
     linkClicked: function(element) {
         window.getSelection().selectAllChildren(element);
     },


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]