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:
parent
805a052f1f
commit
c476fdc6d1
11 changed files with 441 additions and 96 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
189
src/client/composer/composer-link-popover.vala
Normal file
189
src/client/composer/composer-link-popover.vala
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
130
ui/composer-link-popover.ui
Normal 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>
|
||||
|
|
@ -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(",");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue