[geary/bug/728002-webkit2: 97/140] Replace ad-hoc ComposerWebView cursor signal param w/ structured object.



commit 53caf43fca0528175ea97001da56a101c2638fa5
Author: Michael James Gratton <mike vee net>
Date:   Thu Jan 19 01:48:03 2017 +1100

    Replace ad-hoc ComposerWebView cursor signal param w/ structured object.
    
    This lets us notify of more cursor editing context state in the future
    without changing the signal signature.
    
    * src/client/composer/composer-web-view.vala (ComposerWebView): Replace
      cursor_style_changed signal and cursorStyleChanged JS message with
      cursor_context_changed signal and cursorContextChanged message, add new
      EditContext inner class and pass as arg to new signal, update call
      sites. Move parsing of JS message to new inner class. Add unit tests,
      fix a font-family bug revealed by tests.
    
    * ui/composer-web-view.js (ComposerPageState): Replace cursorFontFamily
      and cursorFontSize with a cursor context and new EditContext object to
      encapsulate them, update them from a node and serialise them. Add unit
      tests.

 src/client/composer/composer-web-view.vala       |  102 +++++++++++++---------
 src/client/composer/composer-widget.vala         |   11 ++-
 test/client/composer/composer-web-view-test.vala |    9 ++
 test/js/composer-page-state-test.vala            |   17 ++++
 ui/composer-web-view.js                          |   86 ++++++++++++++-----
 5 files changed, 157 insertions(+), 68 deletions(-)
---
diff --git a/src/client/composer/composer-web-view.vala b/src/client/composer/composer-web-view.vala
index 4d1bb08..79dc233 100644
--- a/src/client/composer/composer-web-view.vala
+++ b/src/client/composer/composer-web-view.vala
@@ -13,19 +13,9 @@ public class ComposerWebView : ClientWebView {
 
 
     private const string COMMAND_STACK_CHANGED = "commandStackChanged";
-    private const string CURSOR_STYLE_CHANGED = "cursorStyleChanged";
+    private const string CURSOR_CONTEXT_CHANGED = "cursorContextChanged";
     private const string DOCUMENT_MODIFIED = "documentModified";
 
-    private const string[] SANS_FAMILY_NAMES = {
-        "sans", "arial", "trebuchet", "helvetica"
-    };
-    private const string[] SERIF_FAMILY_NAMES = {
-        "serif", "georgia", "times"
-    };
-    private const string[] MONO_FAMILY_NAMES = {
-        "monospace", "courier", "console"
-    };
-
     private const string HTML_BODY = """
         <html><head><title></title>
         <style>
@@ -73,21 +63,62 @@ public class ComposerWebView : ClientWebView {
         </body></html>""";
     private const string CURSOR = "<span id=\"cursormarker\"></span>";
 
-    private static Gee.HashMap<string,string> font_family_map =
-        new Gee.HashMap<string,string>();
 
-    static construct {
-        foreach (string name in SANS_FAMILY_NAMES) {
-            font_family_map["sans"] = name;
-        }
-        foreach (string name in SERIF_FAMILY_NAMES) {
-            font_family_map["serif"] = name;
+    /**
+     * Encapsulates editing-related state for a specific DOM node.
+     *
+     * This must be kept in sync with the JS object of the same name.
+     */
+    public class EditContext : Object {
+
+        private const uint LINK_MASK = 1 << 0;
+
+        private const string[] SANS_FAMILY_NAMES = {
+            "sans", "arial", "trebuchet", "helvetica"
+        };
+        private const string[] SERIF_FAMILY_NAMES = {
+            "serif", "georgia", "times"
+        };
+        private const string[] MONO_FAMILY_NAMES = {
+            "monospace", "courier", "console"
+        };
+
+        private static Gee.HashMap<string,string> font_family_map =
+            new Gee.HashMap<string,string>();
+
+        static construct {
+            foreach (string name in SANS_FAMILY_NAMES) {
+                font_family_map[name] = "sans";
+            }
+            foreach (string name in SERIF_FAMILY_NAMES) {
+                font_family_map[name] = "serif";
+            }
+            foreach (string name in MONO_FAMILY_NAMES) {
+                font_family_map[name] = "monospace";
+            }
         }
-        foreach (string name in MONO_FAMILY_NAMES) {
-            font_family_map["monospace"] = name;
+
+
+        public string font_family { get; private set; default = "sans"; }
+        public uint font_size { get; private set; default = 12; }
+
+        public EditContext(string message) {
+            string[] values = message.split(",");
+
+            string view_name = values[0].down();
+            foreach (string specific_name in EditContext.font_family_map.keys) {
+                if (specific_name in view_name) {
+                    this.font_family = EditContext.font_family_map[specific_name];
+                    break;
+                }
+            }
+
+            this.font_size = (uint) uint64.parse(values[1]);
         }
+
     }
 
+
     private static WebKit.UserScript? app_script = null;
 
     public static void load_resources()
@@ -117,8 +148,8 @@ public class ComposerWebView : ClientWebView {
     /** 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. */
-    public signal void cursor_style_changed(string face, uint size);
+    /** Emitted when the cursor's edit context has changed. */
+    public signal void cursor_context_changed(EditContext cursor_context);
 
 
     public ComposerWebView(Configuration config) {
@@ -130,7 +161,7 @@ public class ComposerWebView : ClientWebView {
         // this.should_insert_text.connect(on_should_insert_text);
 
         register_message_handler(COMMAND_STACK_CHANGED, on_command_stack_changed);
-        register_message_handler(CURSOR_STYLE_CHANGED, on_cursor_style_changed);
+        register_message_handler(CURSOR_CONTEXT_CHANGED, on_cursor_context_changed);
         register_message_handler(DOCUMENT_MODIFIED, on_document_modified);
     }
 
@@ -202,7 +233,7 @@ public class ComposerWebView : ClientWebView {
     }
 
     /**
-     * Inserts some text at the current cursor location.
+     * Inserts some text at the current text cursor location.
      */
     public void insert_text(string text) {
         execute_editing_command_with_argument("inserttext", text);
@@ -265,7 +296,7 @@ public class ComposerWebView : ClientWebView {
     // }
 
     /**
-     * Inserts some HTML at the current cursor location.
+     * Inserts some HTML at the current text cursor location.
      */
     public void insert_html(string markup) {
         execute_editing_command_with_argument("insertHTML", markup);
@@ -398,24 +429,11 @@ public class ComposerWebView : ClientWebView {
         }
     }
 
-    private void on_cursor_style_changed(WebKit.JavascriptResult result) {
+    private void on_cursor_context_changed(WebKit.JavascriptResult result) {
         try {
-            string[] values = WebKitUtil.to_string(result).split(",");
-            string view_name = values[0].down();
-            string? font_family = "sans";
-            foreach (string name in ComposerWebView.font_family_map.keys) {
-                if (name in view_name) {
-                    font_family = ComposerWebView.font_family_map[name];
-                    break;
-                }
-            }
-
-            uint font_size = 12;
-            values[1].scanf("%dpx", out font_size);
-
-            cursor_style_changed(font_family, font_size);
+            cursor_context_changed(new EditContext(WebKitUtil.to_string(result)));
         } catch (Geary.JS.Error err) {
-            debug("Could not get cursor style: %s", err.message);
+            debug("Could not get text cursor style: %s", err.message);
         }
     }
 
diff --git a/src/client/composer/composer-widget.vala b/src/client/composer/composer-widget.vala
index e982475..dc716f0 100644
--- a/src/client/composer/composer-widget.vala
+++ b/src/client/composer/composer-widget.vala
@@ -500,7 +500,7 @@ 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.cursor_context_changed.connect(on_cursor_context_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);
@@ -2254,12 +2254,13 @@ public class ComposerWidget : Gtk.EventBox {
         }
     }
 
-    private void on_cursor_style_changed(string font_family, uint font_size) {
-        this.actions.change_action_state(ACTION_FONT_FAMILY, font_family);
+    private void on_cursor_context_changed(ComposerWebView.EditContext context) {
 
-        if (font_size < 11)
+        this.actions.change_action_state(ACTION_FONT_FAMILY, context.font_family);
+
+        if (context.font_size < 11)
             this.actions.change_action_state(ACTION_FONT_SIZE, "small");
-        else if (font_size > 20)
+        else if (context.font_size > 20)
             this.actions.change_action_state(ACTION_FONT_SIZE, "large");
         else
             this.actions.change_action_state(ACTION_FONT_SIZE, "medium");
diff --git a/test/client/composer/composer-web-view-test.vala 
b/test/client/composer/composer-web-view-test.vala
index 3af1e30..1ad2c06 100644
--- a/test/client/composer/composer-web-view-test.vala
+++ b/test/client/composer/composer-web-view-test.vala
@@ -9,6 +9,7 @@ public class ComposerWebViewTest : ClientWebViewTestCase<ComposerWebView> {
 
     public ComposerWebViewTest() {
         base("ComposerWebViewTest");
+        add_test("edit_context", edit_context);
         add_test("get_html", get_html);
         add_test("get_text", get_text);
         add_test("get_text_with_quote", get_text_with_quote);
@@ -18,6 +19,14 @@ public class ComposerWebViewTest : ClientWebViewTestCase<ComposerWebView> {
         add_test("get_text_with_nbsp", get_text_with_nbsp);
     }
 
+    public void edit_context() {
+        assert(new ComposerWebView.EditContext("Helvetica,").font_family == "sans");
+        assert(new ComposerWebView.EditContext("Times New Roman,").font_family == "serif");
+        assert(new ComposerWebView.EditContext("Courier,").font_family == "monospace");
+
+        assert(new ComposerWebView.EditContext(",12").font_size == 12);
+    }
+
     public void get_html() {
         string html = "<p>para</p>";
         load_body_fixture(html);
diff --git a/test/js/composer-page-state-test.vala b/test/js/composer-page-state-test.vala
index a4d580d..09eef3c 100644
--- a/test/js/composer-page-state-test.vala
+++ b/test/js/composer-page-state-test.vala
@@ -9,6 +9,7 @@ class ComposerPageStateTest : ClientWebViewTestCase<ComposerWebView> {
 
     public ComposerPageStateTest() {
         base("ComposerPageStateTest");
+        add_test("edit_context_font", edit_context_font);
         add_test("get_html", get_html);
         add_test("get_text", get_text);
         add_test("get_text_with_quote", get_text_with_quote);
@@ -18,6 +19,22 @@ class ComposerPageStateTest : ClientWebViewTestCase<ComposerWebView> {
         add_test("replace_non_breaking_space", replace_non_breaking_space);
     }
 
+    public void edit_context_font() {
+        string html = "<p id=\"test\" style=\"font-family: Comic Sans; font-size: 144\">para</p>";
+        load_body_fixture(html);
+
+        try {
+            assert(run_javascript(@"new EditContext(document.getElementById('test')).encode()")
+                   == ("Comic Sans,144"));
+        } catch (Geary.JS.Error err) {
+            print("Geary.JS.Error: %s\n", err.message);
+            assert_not_reached();
+        } catch (Error err) {
+            print("WKError: %s\n", err.message);
+            assert_not_reached();
+        }
+    }
+
     public void get_html() {
         string html = "<p>para</p>";
         load_body_fixture(html);
diff --git a/ui/composer-web-view.js b/ui/composer-web-view.js
index ae39790..5922855 100644
--- a/ui/composer-web-view.js
+++ b/ui/composer-web-view.js
@@ -27,8 +27,7 @@ ComposerPageState.prototype = {
         this.undoEnabled = false;
         this.redoEnabled = false;
 
-        this.cursorFontFamily = null;
-        this.cursorFontSize = null;
+        this.cursorContext = null;
 
         let state = this;
 
@@ -79,7 +78,7 @@ ComposerPageState.prototype = {
         // Focus within the HTML document
         document.body.focus();
 
-        // Set cursor at appropriate position
+        // Set text cursor at appropriate position
         let cursor = document.getElementById("cursormarker");
         if (cursor != null) {
             let range = document.createRange();
@@ -122,7 +121,7 @@ ComposerPageState.prototype = {
     },
     tabIn: function() {
         // If there is no selection and the character before the
-        // cursor is tab, delete it.
+        // text cursor is tab, delete it.
         let selection = window.getSelection();
         if (selection.isCollapsed) {
             selection.modify("extend", "backward", "character");
@@ -177,23 +176,13 @@ ComposerPageState.prototype = {
     selectionChanged: function() {
         PageState.prototype.selectionChanged.apply(this, []);
 
-        let selection = window.getSelection();
-        let active = selection.focusNode;
-        if (active != null && active.nodeType != Node.ELEMENT_TYPE) {
-            active = active.parentNode;
-        }
-
-        if (active != null) {
-            let styles = window.getComputedStyle(active);
-            let fontFamily = styles.getPropertyValue("font-family");
-            let fontSize = styles.getPropertyValue("font-size");
-
-            if (fontFamily != this.cursorFontFamily ||
-                fontSize != this.cursorFontSize) {
-                this.cursorFontFamily = fontFamily;
-                this.cursorFontSize = fontSize;
-                window.webkit.messageHandlers.cursorStyleChanged.postMessage(
-                    fontFamily + "," + fontSize
+        let cursor = SelectionUtil.getCursorElement();
+        if (cursor != null) {
+            let newContext = new EditContext(cursor);
+            if (!newContext.equals(this.cursorContext)) {
+                this.cursorContext = newContext;
+                window.webkit.messageHandlers.cursorContextChanged.postMessage(
+                    newContext.encode()
                 );
             }
         }
@@ -313,6 +302,61 @@ ComposerPageState.replaceNonBreakingSpace = function(text) {
 };
 
 
+/**
+ * Encapsulates editing-related state for a specific DOM node.
+ *
+ * This must be kept in sync with the vala object of the same name.
+ */
+let EditContext = function() {
+    this.init.apply(this, arguments);
+};
+EditContext.LINK_MASK = 1 << 0;
+
+EditContext.prototype = {
+    init: function(node) {
+        let styles = window.getComputedStyle(node);
+        let fontFamily = styles.getPropertyValue("font-family");
+        if (fontFamily.charAt() == "'") {
+            fontFamily = fontFamily.substr(1, fontFamily.length - 2);
+        }
+        this.fontFamily = fontFamily;
+        this.fontSize = styles.getPropertyValue("font-size").replace("px", "");
+    },
+    equals: function(other) {
+        return other != null
+            && this.fontFamily == other.fontFamily
+            && this.fontSize == other.fontSize;
+    },
+    encode: function() {
+        return [
+            this.fontFamily,
+            this.fontSize
+        ].join(",");
+    }
+};
+
+
+/**
+ * Utility methods for managing the DOM Selection.
+ */
+let SelectionUtil = {
+    /**
+     * Returns the element immediately under the text cursor.
+     *
+     * If there is a non-empty selection, the element at the end of the
+     * selection is returned.
+     */
+    getCursorElement: function() {
+        let selection = window.getSelection();
+        let node = selection.focusNode;
+        if (node != null && node.nodeType != Node.ELEMENT_TYPE) {
+            node = node.parentNode;
+        }
+        return node;
+    }
+};
+
+
 var geary = new ComposerPageState();
 window.onload = function() {
     geary.loaded();


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