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.
This commit is contained in:
Michael James Gratton 2017-01-19 02:23:57 +11:00
parent 805a052f1f
commit c476fdc6d1
11 changed files with 441 additions and 96 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -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]);
string view_name = values[0].down();
this.link_url = values[1];
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

View file

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

View file

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

View file

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

View file

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

130
ui/composer-link-popover.ui Normal file
View file

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

View file

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