[geary/wip/714104-refine-account-dialog: 24/69] Push WebView editing machinery from the composer to the client.



commit f7ad510dcce26b9037868f59de9bb48b3c473926
Author: Michael James Gratton <mike vee net>
Date:   Wed Jun 13 17:41:55 2018 +1000

    Push WebView editing machinery from the composer to the client.
    
    This allows using the same code for editing account signatures as for the
    composer.
    
    * src/client/components/client-web-view.vala (ClientWebView): Move both
      command_stack_changed and document_modified signals here from
      ComposerWebView, since they use the same underlying machinery to
      emit. Move get_html() from ComposerWebView since we want to get to the
      HTML when editing signatures. Override WebView.set_editable() so we can
      enable/disable the signal machinery and hence not get change signals
      emitted when enabling/disabling editing.
    
    * ui/client-web-view.js (PageState): Mirror the changes above made to
      ClientWebView.
    
    * src/meson.build (geary_web_process): Ensure C args are passed through
      web compiling the web extension.

 src/client/components/client-web-view.vala | 45 ++++++++++++++++++++++
 src/client/composer/composer-web-view.vala | 62 +++++-------------------------
 src/meson.build                            |  1 +
 ui/client-web-view.js                      | 56 +++++++++++++++++++++++++++
 ui/composer-web-view.js                    | 59 +++++++++-------------------
 5 files changed, 131 insertions(+), 92 deletions(-)
---
diff --git a/src/client/components/client-web-view.vala b/src/client/components/client-web-view.vala
index fb6656b7..3af0d7c2 100644
--- a/src/client/components/client-web-view.vala
+++ b/src/client/components/client-web-view.vala
@@ -26,7 +26,10 @@ public class ClientWebView : WebKit.WebView, Geary.BaseInterface {
     /** URI Scheme and delimiter for images loaded by Content-ID. */
     public const string CID_URL_PREFIX = "cid:";
 
+    // WebKit message handler names
+    private const string COMMAND_STACK_CHANGED = "commandStackChanged";
     private const string CONTENT_LOADED = "contentLoaded";
+    private const string DOCUMENT_MODIFIED = "documentModified";
     private const string PREFERRED_HEIGHT_CHANGED = "preferredHeightChanged";
     private const string REMOTE_IMAGE_LOAD_BLOCKED = "remoteImageLoadBlocked";
     private const string SELECTION_CHANGED = "selectionChanged";
@@ -260,6 +263,12 @@ public class ClientWebView : WebKit.WebView, Geary.BaseInterface {
      */
     public signal void content_loaded();
 
+    /** 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 web view's content has changed. */
+    public signal void document_modified();
+
     /** Emitted when the view's selection has changed. */
     public signal void selection_changed(bool has_selection);
 
@@ -308,9 +317,15 @@ public class ClientWebView : WebKit.WebView, Geary.BaseInterface {
                 warning("Web process crashed: %s", reason.to_string());
             });
 
+        register_message_handler(
+            COMMAND_STACK_CHANGED, on_command_stack_changed
+        );
         register_message_handler(
             CONTENT_LOADED, on_content_loaded
         );
+        register_message_handler(
+            DOCUMENT_MODIFIED, on_document_modified
+        );
         register_message_handler(
             PREFERRED_HEIGHT_CHANGED, on_preferred_height_changed
         );
@@ -358,6 +373,15 @@ public class ClientWebView : WebKit.WebView, Geary.BaseInterface {
         base.load_html(body, base_uri ?? INTERNAL_URL_BODY);
     }
 
+    /**
+     * Returns the view's content as an HTML string.
+     */
+    public async string? get_html() throws Error {
+        return WebKitUtil.to_string(
+            yield call(Geary.JS.callable("geary.getHtml"), null)
+        );
+    }
+
     /**
      * Adds an resource that may be accessed from the view via a URL.
      *
@@ -441,6 +465,14 @@ public class ClientWebView : WebKit.WebView, Geary.BaseInterface {
         notify_property("preferred-height");
     }
 
+    public new async void set_editable(bool enabled,
+                                       Cancellable? cancellable)
+        throws Error {
+        yield call(
+            Geary.JS.callable("geary.setEditable").bool(enabled), cancellable
+        );
+    }
+
     /**
      * Invokes a {@link Geary.JS.Callable} on this web view.
      */
@@ -582,6 +614,19 @@ public class ClientWebView : WebKit.WebView, Geary.BaseInterface {
         }
     }
 
+    private void on_command_stack_changed(WebKit.JavascriptResult result) {
+        try {
+            string[] values = WebKitUtil.to_string(result).split(",");
+            command_stack_changed(values[0] == "true", values[1] == "true");
+        } catch (Geary.JS.Error err) {
+            debug("Could not get command stack state: %s", err.message);
+        }
+    }
+
+    private void on_document_modified(WebKit.JavascriptResult result) {
+        document_modified();
+    }
+
     private void on_remote_image_load_blocked(WebKit.JavascriptResult result) {
         remote_image_load_blocked();
     }
diff --git a/src/client/composer/composer-web-view.vala b/src/client/composer/composer-web-view.vala
index 59baf4bd..efe83986 100644
--- a/src/client/composer/composer-web-view.vala
+++ b/src/client/composer/composer-web-view.vala
@@ -12,9 +12,9 @@
 public class ComposerWebView : ClientWebView {
 
 
-    private const string COMMAND_STACK_CHANGED = "commandStackChanged";
+    // WebKit message handler names
     private const string CURSOR_CONTEXT_CHANGED = "cursorContextChanged";
-    private const string DOCUMENT_MODIFIED = "documentModified";
+
 
     /**
      * Encapsulates editing-related state for a specific DOM node.
@@ -104,17 +104,6 @@ public class ComposerWebView : ClientWebView {
     /** Determines if the view is in rich text mode. */
     public bool is_rich_text { get; private set; default = true; }
 
-    // Determines if signals should be sent, useful for e.g. stopping
-    // document_modified being sent when the editor content is being
-    // updated before sending.
-    private bool signals_enabled = true;
-
-
-    /** 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 cursor's edit context has changed. */
     public signal void cursor_context_changed(EditContext cursor_context);
@@ -131,9 +120,13 @@ public class ComposerWebView : ClientWebView {
         this.user_content_manager.add_style_sheet(ComposerWebView.app_style);
         this.user_content_manager.add_script(ComposerWebView.app_script);
 
-        register_message_handler(COMMAND_STACK_CHANGED, on_command_stack_changed);
         register_message_handler(CURSOR_CONTEXT_CHANGED, on_cursor_context_changed);
-        register_message_handler(DOCUMENT_MODIFIED, on_document_modified);
+
+        // XXX this is a bit of a hack given the docs for is_empty,
+        // above
+        this.command_stack_changed.connect((can_undo, can_redo) => {
+                this.is_empty = !can_redo;
+            });
     }
 
     /**
@@ -197,7 +190,6 @@ public class ComposerWebView : ClientWebView {
      */
     public void disable() {
         set_sensitive(false);
-        this.signals_enabled = false;
     }
 
     /**
@@ -429,15 +421,6 @@ public class ComposerWebView : ClientWebView {
         this.call.begin(Geary.JS.callable("geary.cleanContent"), null);
     }
 
-    /**
-     * Returns the editor content as an HTML string.
-     */
-    public async string? get_html() throws Error {
-        return WebKitUtil.to_string(
-            yield call(Geary.JS.callable("geary.getHtml"), null)
-        );
-    }
-
     /**
      * Returns the editor text as RFC 3676 format=flowed text.
      */
@@ -515,41 +498,16 @@ public class ComposerWebView : ClientWebView {
         // to show a link popopver after the view has processed one,
         // we need to emit our own.
         bool ret = base.button_release_event(event);
-        if (this.signals_enabled) {
-            button_release_event_done(event);
-        }
+        button_release_event_done(event);
         return ret;
     }
 
-    private void on_command_stack_changed(WebKit.JavascriptResult result) {
-        try {
-            if (this.signals_enabled) {
-                string[] values = WebKitUtil.to_string(result).split(",");
-                command_stack_changed(values[0] == "true", values[1] == "true");
-            }
-        } catch (Geary.JS.Error err) {
-            debug("Could not get command stack state: %s", err.message);
-        }
-    }
-
     private void on_cursor_context_changed(WebKit.JavascriptResult result) {
         try {
-            if (this.signals_enabled) {
-                cursor_context_changed(new EditContext(WebKitUtil.to_string(result)));
-            }
+            cursor_context_changed(new EditContext(WebKitUtil.to_string(result)));
         } catch (Geary.JS.Error err) {
             debug("Could not get text cursor style: %s", err.message);
         }
     }
 
-    private void on_document_modified(WebKit.JavascriptResult result) {
-        // Only modify actually changed to avoid excessive notify
-        // signals being fired.
-        if (this.signals_enabled) {
-            if (this.is_empty) {
-                this.is_empty = false;
-            }
-            document_modified();
-        }
-    }
 }
diff --git a/src/meson.build b/src/meson.build
index 40abe05b..2eef6927 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -51,6 +51,7 @@ subdir('mailer')
 # Web process extension library
 geary_web_process = library('geary-web-process',
   join_paths('client', 'web-process', 'web-process-extension.vala'),
+  c_args: geary_c_options,
   dependencies: [
     gee,
     gmime,
diff --git a/ui/client-web-view.js b/ui/client-web-view.js
index a4542133..c2a9d24a 100644
--- a/ui/client-web-view.js
+++ b/ui/client-web-view.js
@@ -16,11 +16,26 @@ PageState.prototype = {
     init: function() {
         this.allowRemoteImages = false;
         this.isLoaded = false;
+        this.undoEnabled = false;
+        this.redoEnabled = false;
         this.hasSelection = false;
         this.lastPreferredHeight = 0;
 
         let state = this;
 
+        // Set up an observer to keep track of modifications made to
+        // the document when editing.
+        let modifiedId = null;
+        this.bodyObserver = new MutationObserver(function(records) {
+            if (modifiedId == null) {
+                modifiedId = window.setTimeout(function() {
+                    state.documentModified();
+                    state.checkCommandStack();
+                    modifiedId = null;
+                }, 1000);
+            }
+        });
+
         // Coalesce multiple calls to updatePreferredHeight using a
         // timeout to avoid the overhead of multiple JS messages sent
         // to the app and hence view multiple resizes being queued.
@@ -73,6 +88,9 @@ PageState.prototype = {
     getPreferredHeight: function() {
         return window.document.documentElement.scrollHeight;
     },
+    getHtml: function() {
+        return document.body.innerHTML;
+    },
     loaded: function() {
         this.isLoaded = true;
         window.webkit.messageHandlers.contentLoaded.postMessage(null);
@@ -87,6 +105,29 @@ PageState.prototype = {
             img.src = src;
         }
     },
+    setEditable: function(enabled) {
+        if (!enabled) {
+            this.stopBodyObserver();
+        }
+        document.body.contentEditable = enabled;
+        if (enabled) {
+            // Enable modification observation only after the document
+            // has been set editable as WebKit will alter some attrs
+            this.startBodyObserver();
+        }
+    },
+    startBodyObserver: function() {
+        let config = {
+            attributes: true,
+            childList: true,
+            characterData: true,
+            subtree: true
+        };
+        this.bodyObserver.observe(document.body, config);
+    },
+    stopBodyObserver: function() {
+        this.bodyObserver.disconnect();
+    },
     remoteImageLoadBlocked: function() {
         window.webkit.messageHandlers.remoteImageLoadBlocked.postMessage(null);
     },
@@ -108,6 +149,21 @@ PageState.prototype = {
         }
         return updated;
     },
+    checkCommandStack: function() {
+        let canUndo = document.queryCommandEnabled("undo");
+        let canRedo = document.queryCommandEnabled("redo");
+
+        if (canUndo != this.undoEnabled || canRedo != this.redoEnabled) {
+            this.undoEnabled = canUndo;
+            this.redoEnabled = canRedo;
+            window.webkit.messageHandlers.commandStackChanged.postMessage(
+                this.undoEnabled + "," + this.redoEnabled
+            );
+        }
+    },
+    documentModified: function(element) {
+        window.webkit.messageHandlers.documentModified.postMessage(null);
+    },
     selectionChanged: function() {
         let hasSelection = !window.getSelection().isCollapsed;
         if (this.hasSelection != hasSelection) {
diff --git a/ui/composer-web-view.js b/ui/composer-web-view.js
index 678ce929..3e2439d6 100644
--- a/ui/composer-web-view.js
+++ b/ui/composer-web-view.js
@@ -30,8 +30,6 @@ ComposerPageState.prototype = {
         this.quotePart = null;
         this.focusedPart = null;
 
-        this.undoEnabled = false;
-        this.redoEnabled = false;
         this.selections = new Map();
         this.nextSelectionId = 0;
         this.cursorContext = null;
@@ -43,17 +41,6 @@ ComposerPageState.prototype = {
                 e.preventDefault();
             }
         }, true);
-
-        let modifiedId = null;
-        this.bodyObserver = new MutationObserver(function() {
-            if (modifiedId == null) {
-                modifiedId = window.setTimeout(function() {
-                    state.documentModified();
-                    state.checkCommandStack();
-                    modifiedId = null;
-                }, 1000);
-            }
-        });
     },
     loaded: function() {
         let state = this;
@@ -138,8 +125,16 @@ ComposerPageState.prototype = {
             cursor.parentNode.removeChild(cursor);
         }
 
-        // Enable editing and observation machinery only after
-        // modifying the body above.
+
+        // Enable editing only after modifying the body above.
+        this.setEditable(true);
+
+        PageState.prototype.loaded.apply(this, []);
+    },
+    setEditable: function(enabled) {
+        if (!enabled) {
+            this.stopBodyObserver();
+        }
         this.bodyPart.contentEditable = true;
         if (this.signaturePart != null) {
             this.signaturePart.contentEditable = true;
@@ -147,16 +142,11 @@ ComposerPageState.prototype = {
         if (this.quotePart != null) {
             this.quotePart.contentEditable = true;
         }
-        let config = {
-            attributes: true,
-            childList: true,
-            characterData: true,
-            subtree: true
-        };
-        this.bodyObserver.observe(document.body, config);
-
-        // Chain up
-        PageState.prototype.loaded.apply(this, []);
+        if (enabled) {
+            // Enable modification observation only after the document
+            // has been set editable as WebKit will alter some attrs
+            this.startBodyObserver();
+        }
     },
     undo: function() {
         document.execCommand("undo", false, null);
@@ -305,6 +295,10 @@ ComposerPageState.prototype = {
         }
     },
     cleanContent: function() {
+        // Prevent any modification signals being sent when mutating
+        // the document below.
+        this.stopBodyObserver();
+
         ComposerPageState.cleanPart(this.bodyPart, false);
         ComposerPageState.linkify(this.bodyPart);
 
@@ -345,21 +339,6 @@ ComposerPageState.prototype = {
             document.body.classList.add("plain");
         }
     },
-    checkCommandStack: function() {
-        let canUndo = document.queryCommandEnabled("undo");
-        let canRedo = document.queryCommandEnabled("redo");
-
-        if (canUndo != this.undoEnabled || canRedo != this.redoEnabled) {
-            this.undoEnabled = canUndo;
-            this.redoEnabled = canRedo;
-            window.webkit.messageHandlers.commandStackChanged.postMessage(
-                this.undoEnabled + "," + this.redoEnabled
-            );
-        }
-    },
-    documentModified: function(element) {
-        window.webkit.messageHandlers.documentModified.postMessage(null);
-    },
     selectionChanged: function() {
         PageState.prototype.selectionChanged.apply(this, []);
 


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