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.
This commit is contained in:
Michael James Gratton 2017-01-19 01:48:03 +11:00
parent 617dd45393
commit 53caf43fca
5 changed files with 157 additions and 68 deletions

View file

@ -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;
/**
* 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 SERIF_FAMILY_NAMES) {
font_family_map["serif"] = name;
}
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);
}
}

View file

@ -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");

View file

@ -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);

View file

@ -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);

View file

@ -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();