[geary/bug/728002-webkit2: 100/140] Replace composer link dialog with a popover.
- From: Michael Gratton <mjog src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [geary/bug/728002-webkit2: 100/140] Replace composer link dialog with a popover.
- Date: Tue, 31 Jan 2017 23:06:57 +0000 (UTC)
commit c476fdc6d1f02848e3f6ff3a6a8c2d929d5e4a64
Author: Michael James Gratton <mike vee net>
Date: Thu Jan 19 02:23:57 2017 +1100
Replace composer link dialog with a popover.
* src/client/composer/composer-link-popover.vala: New GtkPopover subclass
for creating/editing links.
* src/client/composer/composer-web-view.vala (EditContext): Add is_link
and link_uri properties, decode them from the message string, add
decoding tests.
(ComposerWebView): Add some as-yet un-implemented methods for
inserting/deleting links.
* src/client/composer/composer-widget.vala (ComposerWidget): Add
cursor_url for storing current text cursor link, update it from the
cursor_context_changed signal param, rename hover_url to pointer_url to
match. Add link_activated signal to let user's open links they are
adding, hook that up in the controller. Rename
::update_selection_actions to ::update_cursor_actions, since that's a
little more apt now, also enable insert link action if there is a
cursor_url set as well as a selection. Remove ::link_dialog, replace
with ::new_link_popover, hook up the new popover's signals there as
appropriate.
(ComposerWidget::on_insert_link): Create and show a lin popover instead
of a dialog.
* ui/composer-web-view.js: Take note of whther the context node is a link
and if so, also it's href. Include both when serialsing for the
cursorContextChanged message. Add serialisation tests.
* ui/composer-link-popover.ui: New UI for link popover.
po/POTFILES.in | 2 +
src/CMakeLists.txt | 1 +
src/client/application/geary-controller.vala | 1 +
src/client/composer/composer-link-popover.vala | 189 ++++++++++++++++++++++
src/client/composer/composer-web-view.vala | 29 +++-
src/client/composer/composer-widget.vala | 141 ++++++----------
test/client/composer/composer-web-view-test.vala | 12 +-
test/js/composer-page-state-test.vala | 19 ++-
ui/CMakeLists.txt | 1 +
ui/composer-link-popover.ui | 130 +++++++++++++++
ui/composer-web-view.js | 12 ++
11 files changed, 441 insertions(+), 96 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 054ffb2..f5939d4 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -38,6 +38,7 @@ src/client/composer/composer-box.vala
src/client/composer/composer-container.vala
src/client/composer/composer-embed.vala
src/client/composer/composer-headerbar.vala
+src/client/composer/composer-link-popover.vala
src/client/composer/composer-web-view.vala
src/client/composer/composer-widget.vala
src/client/composer/composer-window.vala
@@ -386,6 +387,7 @@ src/mailer/main.vala
[type: gettext/glade]ui/account_spinner.glade
[type: gettext/glade]ui/certificate_warning_dialog.glade
[type: gettext/glade]ui/composer-headerbar.ui
+[type: gettext/glade]ui/composer-link-popover.ui
[type: gettext/glade]ui/composer-menus.ui
[type: gettext/glade]ui/composer-widget.ui
[type: gettext/glade]ui/conversation-email.ui
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index a91beda..bb0ad05 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -350,6 +350,7 @@ client/composer/composer-box.vala
client/composer/composer-container.vala
client/composer/composer-embed.vala
client/composer/composer-headerbar.vala
+client/composer/composer-link-popover.vala
client/composer/composer-web-view.vala
client/composer/composer-widget.vala
client/composer/composer-window.vala
diff --git a/src/client/application/geary-controller.vala b/src/client/application/geary-controller.vala
index 1c780a0..8452fba 100644
--- a/src/client/application/geary-controller.vala
+++ b/src/client/application/geary-controller.vala
@@ -2330,6 +2330,7 @@ public class GearyController : Geary.BaseObject {
yield widget.restore_draft_state_async();
}
+ widget.link_activated.connect((uri) => { open_uri(uri); });
widget.show_all();
// We want to keep track of the open composer windows, so we can allow the user to cancel
diff --git a/src/client/composer/composer-link-popover.vala b/src/client/composer/composer-link-popover.vala
new file mode 100644
index 0000000..d51d312
--- /dev/null
+++ b/src/client/composer/composer-link-popover.vala
@@ -0,0 +1,189 @@
+/*
+ * Copyright 2017 Michael Gratton <mike vee net>
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+
+/**
+ * A popover for editing a link in the composer.
+ *
+ * The exact appearance of the popover will depend on the {@link
+ * Type} passed to the constructor:
+ *
+ * - For {@link Type.NEW_LINK}, the user will be presented with an
+ * insert button and an open button.
+ * - For {@link Type.EXISTING_LINK}, the user will be presented with
+ * an update, delete and open buttons.
+ */
+[GtkTemplate (ui = "/org/gnome/Geary/composer-link-popover.ui")]
+public class ComposerLinkPopover : Gtk.Popover {
+
+ private const string[] HTTP_SCHEMES = { "http", "https" };
+ private const string[] OTHER_SCHEMES = {
+ "aim", "apt", "bitcoin", "cvs", "ed2k", "ftp", "file", "finger",
+ "git", "gtalk", "irc", "ircs", "irc6", "lastfm", "ldap", "ldaps",
+ "magnet", "news", "nntp", "rsync", "sftp", "skype", "smb", "sms",
+ "svn", "telnet", "tftp", "ssh", "webcal", "xmpp"
+ };
+
+ /** Determines which version of the UI is presented to the user. */
+ public enum Type {
+ /** A new link is being created. */
+ NEW_LINK,
+
+ /** An existing link is being edited. */
+ EXISTING_LINK,
+ }
+
+ /** The URL displayed in the popover */
+ public string link_uri { get { return this.url.get_text(); } }
+
+ [GtkChild]
+ private Gtk.Entry url;
+
+ [GtkChild]
+ private Gtk.Button insert;
+
+ [GtkChild]
+ private Gtk.Button update;
+
+ [GtkChild]
+ private Gtk.Button delete;
+
+ [GtkChild]
+ private Gtk.Button open;
+
+ private Geary.TimeoutManager validation_timeout;
+
+
+ /** Emitted when the link URL has changed. */
+ public signal void link_changed(Soup.URI? uri, bool is_valid);
+
+ /** Emitted when the link URL was activated. */
+ public signal void link_activate();
+
+ /** Emitted when the open button was activated. */
+ public signal void link_open();
+
+ /** Emitted when the delete button was activated. */
+ public signal void link_delete();
+
+
+ public ComposerLinkPopover(Type type) {
+ set_default_widget(this.url);
+ set_focus_child(this.url);
+ switch (type) {
+ case Type.NEW_LINK:
+ this.update.hide();
+ this.delete.hide();
+ break;
+ case Type.EXISTING_LINK:
+ this.insert.hide();
+ break;
+ }
+ this.validation_timeout = new Geary.TimeoutManager.milliseconds(
+ 150, () => { validate(); }
+ );
+ }
+
+ ~ComposerLinkPopover() {
+ debug("Destructing...");
+ }
+
+ public override void destroy() {
+ this.validation_timeout.reset();
+ base.destroy();
+ }
+
+ public void set_link_url(string url) {
+ this.url.set_text(url);
+ this.validation_timeout.reset(); // Don't update on manual set
+ }
+
+ private void validate() {
+ string? text = this.url.get_text().strip();
+ bool is_empty = Geary.String.is_empty(text);
+ bool is_valid = false;
+ bool is_nominal = false;
+ bool is_mailto = false;
+ Soup.URI? url = null;
+ if (!is_empty) {
+ url = new Soup.URI(text);
+ if (url != null) {
+ is_valid = true;
+
+ string? scheme = url.get_scheme();
+ string? path = url.get_path();
+ if (scheme in HTTP_SCHEMES) {
+ is_nominal = Geary.Inet.is_valid_display_host(url.get_host());
+ } else if (scheme == "mailto") {
+ is_mailto = true;
+ is_nominal = (
+ !Geary.String.is_empty(path) &&
+ Geary.RFC822.MailboxAddress.is_valid_address(path)
+ );
+ } else if (scheme in OTHER_SCHEMES) {
+ is_nominal = !Geary.String.is_empty(path);
+ }
+ } else if (text == "http:/" || text == "https:/") {
+ // Don't let the URL entry switch to invalid and back
+ // between "http:" and "http://"
+ is_valid = true;
+ }
+ }
+
+ // Don't let the user open invalid and mailto links, it's not
+ // terribly useful
+ this.open.set_sensitive(is_nominal && !is_mailto);
+
+ Gtk.StyleContext style = this.url.get_style_context();
+ Gtk.EntryIconPosition pos = Gtk.EntryIconPosition.SECONDARY;
+ if (!is_valid) {
+ style.add_class(Gtk.STYLE_CLASS_ERROR);
+ style.remove_class(Gtk.STYLE_CLASS_WARNING);
+ this.url.set_icon_from_icon_name(pos, "dialog-error-symbolic");
+ this.url.set_tooltip_text(
+ _("Link URL is not correctly formatted, e.g. http://example.com")
+ );
+ } else if (!is_nominal) {
+ style.remove_class(Gtk.STYLE_CLASS_ERROR);
+ style.add_class(Gtk.STYLE_CLASS_WARNING);
+ this.url.set_icon_from_icon_name(pos, "dialog-warning-symbolic");
+ this.url.set_tooltip_text(
+ !is_mailto ? _("Invalid link URL") : _("Invalid email address")
+ );
+ } else {
+ style.remove_class(Gtk.STYLE_CLASS_ERROR);
+ style.remove_class(Gtk.STYLE_CLASS_WARNING);
+ this.url.set_icon_from_icon_name(pos, null);
+ this.url.set_tooltip_text(null);
+ }
+
+ link_changed(url, is_valid && is_nominal);
+ }
+
+ [GtkCallback]
+ private void on_url_changed() {
+ this.validation_timeout.start();
+ }
+
+ [GtkCallback]
+ private void on_activate_popover() {
+ link_activate();
+ this.popdown();
+ }
+
+ [GtkCallback]
+ private void on_delete_clicked() {
+ link_delete();
+ this.popdown();
+ }
+
+ [GtkCallback]
+ private void on_open_clicked() {
+ link_open();
+ }
+
+}
diff --git a/src/client/composer/composer-web-view.vala b/src/client/composer/composer-web-view.vala
index 79dc233..5546b35 100644
--- a/src/client/composer/composer-web-view.vala
+++ b/src/client/composer/composer-web-view.vala
@@ -99,13 +99,20 @@ public class ComposerWebView : ClientWebView {
}
+ public bool is_link { get { return (this.context & LINK_MASK) > 0; } }
+ public string link_url { get; private set; default = ""; }
public string font_family { get; private set; default = "sans"; }
public uint font_size { get; private set; default = 12; }
+ private uint context = 0;
+
public EditContext(string message) {
string[] values = message.split(",");
+ this.context = (uint) uint64.parse(values[0]);
+
+ this.link_url = values[1];
- string view_name = values[0].down();
+ string view_name = values[2].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];
@@ -113,7 +120,7 @@ public class ComposerWebView : ClientWebView {
}
}
- this.font_size = (uint) uint64.parse(values[1]);
+ this.font_size = (uint) uint64.parse(values[3]);
}
}
@@ -303,7 +310,23 @@ public class ComposerWebView : ClientWebView {
}
/**
- * Inserts an IMG with the given `src` at the current cursor location.
+ * Inserts or updates an A element at the current text cursor location.
+ *
+ * If the cursor is located on an A element, the element's HREF
+ * will be updated, else if some text is selected, an A element
+ * will be inserted wrapping the selection.
+ */
+ public void insert_link(string href) {
+ }
+
+ /**
+ * Removes any A element at the current text cursor location.
+ */
+ public void delete_link() {
+ }
+
+ /**
+ * Inserts an IMG element at the current text cursor location.
*/
public void insert_image(string src) {
// Use insertHTML instead of insertImage here so
diff --git a/src/client/composer/composer-widget.vala b/src/client/composer/composer-widget.vala
index dc716f0..d9c0a42 100644
--- a/src/client/composer/composer-widget.vala
+++ b/src/client/composer/composer-widget.vala
@@ -1,7 +1,9 @@
-/* Copyright 2016 Software Freedom Conservancy Inc.
+/*
+ * Copyright 2016 Software Freedom Conservancy Inc.
+ * Copyright 2017 Michael Gratton <mike vee net>
*
* This software is licensed under the GNU Lesser General Public License
- * (version 2.1 or later). See the COPYING file in this distribution.
+ * (version 2.1 or later). See the COPYING file in this distribution.
*/
private errordomain AttachmentError {
@@ -310,6 +312,8 @@ public class ComposerWidget : Gtk.EventBox {
[GtkChild]
private Gtk.Box font_style_buttons;
[GtkChild]
+ private Gtk.Button insert_link_button;
+ [GtkChild]
private Gtk.Button remove_format_button;
[GtkChild]
private Gtk.Button select_dictionary_button;
@@ -332,7 +336,8 @@ public class ComposerWidget : Gtk.EventBox {
private Menu context_menu_inspector;
private SpellCheckPopover? spell_check_popover = null;
- private string? hover_url = null;
+ private string? pointer_url = null;
+ private string? cursor_url = null;
private bool is_attachment_overlay_visible = false;
private Geary.RFC822.MailboxAddresses reply_to_addresses;
private Geary.RFC822.MailboxAddresses reply_cc_addresses;
@@ -361,8 +366,12 @@ public class ComposerWidget : Gtk.EventBox {
}
+ /** Fired when the current saved draft's id has changed. */
public signal void draft_id_changed(Geary.EmailIdentifier? id);
+ /** Fired when the user opens a link in the composer. */
+ public signal void link_activated(string url);
+
public ComposerWidget(Geary.Account account, ComposeType compose_type, Configuration config,
Geary.Email? referred = null, string? quote = null, bool is_referred_draft = false) {
@@ -506,9 +515,7 @@ public class ComposerWidget : Gtk.EventBox {
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((has_selection) => {
- update_selection_actions(has_selection);
- });
+ this.editor.selection_changed.connect((has_selection) => { update_cursor_actions(); });
this.editor.load_html(this.body_html, this.signature_html, this.top_posting);
@@ -792,17 +799,20 @@ public class ComposerWidget : Gtk.EventBox {
get_action(ACTION_UNDO).set_enabled(false);
get_action(ACTION_REDO).set_enabled(false);
- // No initial selection
- update_selection_actions(false);
+ update_cursor_actions();
}
- private void update_selection_actions(bool has_selection) {
+ private void update_cursor_actions() {
+ bool has_selection = this.editor.has_selection;
get_action(ACTION_CUT).set_enabled(has_selection);
get_action(ACTION_COPY).set_enabled(has_selection);
- bool rich_text_selected = has_selection && this.editor.is_rich_text;
- get_action(ACTION_INSERT_LINK).set_enabled(rich_text_selected);
- get_action(ACTION_REMOVE_FORMAT).set_enabled(rich_text_selected);
+ get_action(ACTION_INSERT_LINK).set_enabled(
+ this.editor.is_rich_text && (has_selection || this.cursor_url != null)
+ );
+ get_action(ACTION_REMOVE_FORMAT).set_enabled(
+ this.editor.is_rich_text && has_selection
+ );
}
private bool check_preferred_from_address(Gee.List<Geary.RFC822.MailboxAddress> account_addresses,
@@ -1724,7 +1734,9 @@ public class ComposerWidget : Gtk.EventBox {
private void on_copy_link(SimpleAction action, Variant? param) {
Gtk.Clipboard c = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD);
- c.set_text(this.hover_url, -1);
+ // XXX could this also be the cursor URL? We should be getting
+ // the target URL as from the action param
+ c.set_text(this.pointer_url, -1);
c.store();
}
@@ -1764,7 +1776,7 @@ public class ComposerWidget : Gtk.EventBox {
foreach (string html_action in html_actions)
get_action(html_action).set_enabled(compose_as_html);
- update_selection_actions(this.editor.has_selection);
+ update_cursor_actions();
this.insert_buttons.visible = compose_as_html;
this.font_style_buttons.visible = compose_as_html;
@@ -1825,82 +1837,12 @@ public class ComposerWidget : Gtk.EventBox {
this.editor.undo_blockquote_style();
}
- private void link_dialog(string link) {
- // Gtk.Dialog dialog = new Gtk.Dialog();
- // bool existing_link = false;
-
- // // Save information needed to re-establish selection
- // WebKit.DOM.DOMSelection selection = this.editor.get_dom_document().get_default_view().
- // get_selection();
- // WebKit.DOM.Node anchor_node = selection.anchor_node;
- // long anchor_offset = selection.anchor_offset;
- // WebKit.DOM.Node focus_node = selection.focus_node;
- // long focus_offset = selection.focus_offset;
-
- // // Allow user to remove link if they're editing an existing one.
- // if (focus_node != null && (focus_node is WebKit.DOM.HTMLAnchorElement ||
- // focus_node.get_parent_element() is WebKit.DOM.HTMLAnchorElement)) {
- // existing_link = true;
- // dialog.add_buttons(Stock._REMOVE, Gtk.ResponseType.REJECT);
- // }
-
- // dialog.add_buttons(Stock._CANCEL, Gtk.ResponseType.CANCEL, Stock._OK,
- // Gtk.ResponseType.OK);
-
- // Gtk.Entry entry = new Gtk.Entry();
- // entry.changed.connect(() => {
- // // Only allow OK when there's text in the box.
- // dialog.set_response_sensitive(Gtk.ResponseType.OK,
- // !Geary.String.is_empty(entry.text.strip()));
- // });
-
- // dialog.width_request = 350;
- // dialog.get_content_area().spacing = 7;
- // dialog.get_content_area().border_width = 10;
- // dialog.get_content_area().pack_start(new Gtk.Label("Link URL:"));
- // dialog.get_content_area().pack_start(entry);
- // dialog.get_widget_for_response(Gtk.ResponseType.OK).can_default = true;
- // dialog.set_default_response(Gtk.ResponseType.OK);
- // dialog.show_all();
-
- // entry.set_text(link);
- // entry.activates_default = true;
- // entry.move_cursor(Gtk.MovementStep.BUFFER_ENDS, 0, false);
-
- // int response = dialog.run();
-
- // // Re-establish selection, since selecting text in the Entry will de-select all
- // // in the WebView.
- // try {
- // selection.set_base_and_extent(anchor_node, anchor_offset, focus_node, focus_offset);
- // } catch (Error e) {
- // debug("Error re-establishing selection: %s", e.message);
- // }
-
- // if (response == Gtk.ResponseType.OK)
- // this.editor.execute_editing_command_with_argument("createLink", entry.text);
- // else if (response == Gtk.ResponseType.REJECT)
- // this.editor.execute_editing_command("unlink");
-
- // dialog.destroy();
-
- // Re-bind to anchor links. This must be done every time link have changed.
- //Util.DOM.bind_event(this.editor,"a", "click", (Callback) on_link_clicked, this);
- }
-
-
private void on_mouse_target_changed(WebKit.WebView web_view,
WebKit.HitTestResult hit_test,
uint modifiers) {
- bool copy_link_enabled = false;
- if (hit_test.context_is_link()) {
- copy_link_enabled = true;
- this.hover_url = hit_test.get_link_uri();
- this.message_overlay_label.label = this.hover_url;
- } else {
- this.hover_url = null;
- this.message_overlay_label.label = "";
- }
+ bool copy_link_enabled = hit_test.context_is_link();
+ this.pointer_url = copy_link_enabled ? hit_test.get_link_uri() : null;
+ this.message_overlay_label.label = this.pointer_url ?? "";
get_action(ACTION_COPY_LINK).set_enabled(copy_link_enabled);
}
@@ -2220,6 +2162,23 @@ public class ComposerWidget : Gtk.EventBox {
this.signature_html = account_sig;
}
+ private ComposerLinkPopover new_link_popover(ComposerLinkPopover.Type type,
+ string url) {
+ ComposerLinkPopover popover = new ComposerLinkPopover(type);
+ popover.set_link_url(url);
+ popover.hide.connect(() => {
+ Idle.add(() => { popover.destroy(); return Source.REMOVE; });
+ });
+ popover.link_activate.connect((link_uri) => {
+ this.editor.insert_link(popover.link_uri);
+ });
+ popover.link_delete.connect(() => {
+ this.editor.delete_link();
+ });
+ popover.link_open.connect(() => { link_activated(popover.link_uri); });
+ return popover;
+ }
+
private void on_command_state_changed(bool can_undo, bool can_redo) {
get_action(ACTION_UNDO).set_enabled(can_undo);
get_action(ACTION_REDO).set_enabled(can_redo);
@@ -2255,6 +2214,8 @@ public class ComposerWidget : Gtk.EventBox {
}
private void on_cursor_context_changed(ComposerWebView.EditContext context) {
+ this.cursor_url = context.is_link ? context.link_url : null;
+ update_cursor_actions();
this.actions.change_action_state(ACTION_FONT_FAMILY, context.font_family);
@@ -2333,7 +2294,11 @@ public class ComposerWidget : Gtk.EventBox {
}
private void on_insert_link(SimpleAction action, Variant? param) {
- link_dialog("http://");
+ ComposerLinkPopover popover = this.cursor_url == null
+ ? new_link_popover(ComposerLinkPopover.Type.NEW_LINK, "http://")
+ : new_link_popover(ComposerLinkPopover.Type.EXISTING_LINK, this.cursor_url);
+ popover.set_relative_to(this.insert_link_button);
+ popover.show();
}
private void on_open_inspector(SimpleAction action, Variant? param) {
diff --git a/test/client/composer/composer-web-view-test.vala
b/test/client/composer/composer-web-view-test.vala
index 1ad2c06..5383e48 100644
--- a/test/client/composer/composer-web-view-test.vala
+++ b/test/client/composer/composer-web-view-test.vala
@@ -20,11 +20,15 @@ public class ComposerWebViewTest : ClientWebViewTestCase<ComposerWebView> {
}
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("0,,,").is_link));
+ assert(new ComposerWebView.EditContext("1,,,").is_link);
+ assert(new ComposerWebView.EditContext("1,url,,").link_url == "url");
- assert(new ComposerWebView.EditContext(",12").font_size == 12);
+ assert(new ComposerWebView.EditContext("0,,Helvetica,").font_family == "sans");
+ assert(new ComposerWebView.EditContext("0,,Times New Roman,").font_family == "serif");
+ assert(new ComposerWebView.EditContext("0,,Courier,").font_family == "monospace");
+
+ assert(new ComposerWebView.EditContext("0,,,12").font_size == 12);
}
public void get_html() {
diff --git a/test/js/composer-page-state-test.vala b/test/js/composer-page-state-test.vala
index 09eef3c..68b7279 100644
--- a/test/js/composer-page-state-test.vala
+++ b/test/js/composer-page-state-test.vala
@@ -10,6 +10,7 @@ class ComposerPageStateTest : ClientWebViewTestCase<ComposerWebView> {
public ComposerPageStateTest() {
base("ComposerPageStateTest");
add_test("edit_context_font", edit_context_font);
+ add_test("edit_context_link", edit_context_link);
add_test("get_html", get_html);
add_test("get_text", get_text);
add_test("get_text_with_quote", get_text_with_quote);
@@ -19,13 +20,29 @@ class ComposerPageStateTest : ClientWebViewTestCase<ComposerWebView> {
add_test("replace_non_breaking_space", replace_non_breaking_space);
}
+ public void edit_context_link() {
+ string html = "<a id=\"test\" href=\"url\">para</a>";
+ load_body_fixture(html);
+
+ try {
+ assert(run_javascript(@"new EditContext(document.getElementById('test')).encode()")
+ .has_prefix("1,url,"));
+ } 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 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"));
+ == ("0,,Comic Sans,144"));
} catch (Geary.JS.Error err) {
print("Geary.JS.Error: %s\n", err.message);
assert_not_reached();
diff --git a/ui/CMakeLists.txt b/ui/CMakeLists.txt
index c95061e..bc5c57d 100644
--- a/ui/CMakeLists.txt
+++ b/ui/CMakeLists.txt
@@ -8,6 +8,7 @@ set(RESOURCE_LIST
"client-web-view.js"
"client-web-view-allow-remote-images.js"
STRIPBLANKS "composer-headerbar.ui"
+ STRIPBLANKS "composer-link-popover.ui"
STRIPBLANKS "composer-menus.ui"
STRIPBLANKS "composer-widget.ui"
"composer-web-view.js"
diff --git a/ui/composer-link-popover.ui b/ui/composer-link-popover.ui
new file mode 100644
index 0000000..6ed29c7
--- /dev/null
+++ b/ui/composer-link-popover.ui
@@ -0,0 +1,130 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.20.0 -->
+<interface>
+ <requires lib="gtk+" version="3.14"/>
+ <template class="ComposerLinkPopover" parent="GtkPopover">
+ <property name="can_focus">False</property>
+ <property name="position">bottom</property>
+ <child>
+ <object class="GtkGrid">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_left">6</property>
+ <property name="margin_right">6</property>
+ <property name="margin_top">6</property>
+ <property name="margin_bottom">6</property>
+ <property name="row_spacing">6</property>
+ <property name="column_spacing">6</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Link URL:</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="url">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="width_chars">40</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="secondary_icon_activatable">False</property>
+ <property name="placeholder_text">http://</property>
+ <property name="input_purpose">url</property>
+ <signal name="activate" handler="on_activate_popover" swapped="no"/>
+ <signal name="changed" handler="on_url_changed" swapped="no"/>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="insert">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="tooltip_text" translatable="yes">Insert a new link with this URL</property>
+ <signal name="clicked" handler="on_activate_popover" swapped="no"/>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="icon_name">emblem-ok-symbolic</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="update">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="tooltip_text" translatable="yes">Update the link URL</property>
+ <signal name="clicked" handler="on_activate_popover" swapped="no"/>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="icon_name">emblem-ok-symbolic</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">3</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="delete">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="tooltip_text" translatable="yes">Delete this link</property>
+ <signal name="clicked" handler="on_delete_clicked" swapped="no"/>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="icon_name">user-trash-symbolic</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">4</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="open">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="tooltip_text" translatable="yes">Open this link</property>
+ <signal name="clicked" handler="on_open_clicked" swapped="no"/>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="icon_name">document-open-symbolic</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">5</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/ui/composer-web-view.js b/ui/composer-web-view.js
index 5922855..32f2dca 100644
--- a/ui/composer-web-view.js
+++ b/ui/composer-web-view.js
@@ -314,6 +314,14 @@ EditContext.LINK_MASK = 1 << 0;
EditContext.prototype = {
init: function(node) {
+ this.context = 0;
+ this.linkUrl = "";
+
+ if (node.nodeName == "A") {
+ this.context |= EditContext.LINK_MASK;
+ this.linkUrl = node.href;
+ }
+
let styles = window.getComputedStyle(node);
let fontFamily = styles.getPropertyValue("font-family");
if (fontFamily.charAt() == "'") {
@@ -324,11 +332,15 @@ EditContext.prototype = {
},
equals: function(other) {
return other != null
+ && this.context == other.context
+ && this.linkUrl == other.linkUrl
&& this.fontFamily == other.fontFamily
&& this.fontSize == other.fontSize;
},
encode: function() {
return [
+ this.context.toString(16),
+ this.linkUrl,
this.fontFamily,
this.fontSize
].join(",");
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]