[geary/wip/778728-split-composer: 12/13] Split body editing code in ComposerWidget into new ComposerEditor class.
- From: Michael Gratton <mjog src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [geary/wip/778728-split-composer: 12/13] Split body editing code in ComposerWidget into new ComposerEditor class.
- Date: Wed, 11 Oct 2017 17:17:02 +0000 (UTC)
commit 7d790f95aaa5b03f2850bfb99f10448c26fde18c
Author: Michael James Gratton <mike vee net>
Date: Tue Oct 10 12:53:17 2017 -0700
Split body editing code in ComposerWidget into new ComposerEditor class.
* src/client/composer/composer-editor.vala: New ComposerEditor class that
encapsulates all composer message body editing functionality, including
the ComposerWebView used to edit the body itself.
* src/client/composer/composer-widget.vala: Move all message body related
code from here to new ComposerEditor class. Make internal editor
property an instance of ComposerEditor rather than ComposerEditor,
update all call sites as needed.
* src/client/composer/composer-window.vala (ComposerWindow): Manage
binding composer subject to window title here rather than in
ComposerWidget, since it's not useful for other composers.
* src/client/composer/composer-web-view.vala (ClientWebView): Move
is_rich_text property into the new editor, it's more useful there.
* ui/composer-widget.ui: Move all editor related widgets to new
composer-editor.ui file.
* ui/composer-menus.ui: Update toolbar and menu actions to reflect new
editor action group name.
src/CMakeLists.txt | 1 +
src/client/composer/composer-box.vala | 14 +-
src/client/composer/composer-editor.vala | 670 +++++++++++++++++++++++++
src/client/composer/composer-embed.vala | 18 +-
src/client/composer/composer-web-view.vala | 4 -
src/client/composer/composer-widget.vala | 738 +++------------------------
src/client/composer/composer-window.vala | 30 +-
ui/CMakeLists.txt | 1 +
ui/composer-editor.ui | 479 ++++++++++++++++++
ui/composer-menus.ui | 40 +-
ui/composer-widget.ui | 457 +-----------------
11 files changed, 1307 insertions(+), 1145 deletions(-)
---
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 4e8a0c4..0cb40bb 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -350,6 +350,7 @@ client/components/stock.vala
client/composer/composer-box.vala
client/composer/composer-container.vala
+client/composer/composer-editor.vala
client/composer/composer-embed.vala
client/composer/composer-headerbar.vala
client/composer/composer-link-popover.vala
diff --git a/src/client/composer/composer-box.vala b/src/client/composer/composer-box.vala
index c4ae911..3cb9ba3 100644
--- a/src/client/composer/composer-box.vala
+++ b/src/client/composer/composer-box.vala
@@ -37,16 +37,16 @@ public class ComposerBox : Gtk.Frame, ComposerContainer {
add(this.composer);
this.main_toolbar.set_conversation_header(composer.header);
- this.composer.editor.focus_in_event.connect(on_focus_in);
- this.composer.editor.focus_out_event.connect(on_focus_out);
+ this.composer.editor.body.focus_in_event.connect(on_focus_in);
+ this.composer.editor.body.focus_out_event.connect(on_focus_out);
show();
}
public void remove_composer() {
- if (this.composer.editor.has_focus)
+ if (this.composer.editor.body.has_focus)
on_focus_out();
- this.composer.editor.focus_in_event.disconnect(on_focus_in);
- this.composer.editor.focus_out_event.disconnect(on_focus_out);
+ this.composer.editor.body.focus_in_event.disconnect(on_focus_in);
+ this.composer.editor.body.focus_out_event.disconnect(on_focus_out);
remove(this.composer);
close_container();
@@ -56,8 +56,8 @@ public class ComposerBox : Gtk.Frame, ComposerContainer {
hide();
this.main_toolbar.remove_conversation_header(composer.header);
this.composer.state = ComposerWidget.ComposerState.DETACHED;
- this.composer.editor.focus_in_event.disconnect(on_focus_in);
- this.composer.editor.focus_out_event.disconnect(on_focus_out);
+ this.composer.editor.body.focus_in_event.disconnect(on_focus_in);
+ this.composer.editor.body.focus_out_event.disconnect(on_focus_out);
vanished();
}
diff --git a/src/client/composer/composer-editor.vala b/src/client/composer/composer-editor.vala
new file mode 100644
index 0000000..574594e
--- /dev/null
+++ b/src/client/composer/composer-editor.vala
@@ -0,0 +1,670 @@
+/*
+ * 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.
+ */
+
+/**
+ * Widget for formatting and editing a message body.
+ */
+[GtkTemplate (ui = "/org/gnome/Geary/composer-editor.ui")]
+public class ComposerEditor : Gtk.Grid {
+
+ private const string BACKSPACE_TEXT = _("Press Backspace to delete quote");
+
+ private const string ACTION_UNDO = "undo";
+ private const string ACTION_REDO = "redo";
+ private const string ACTION_CUT = "cut";
+ private const string ACTION_COPY = "copy";
+ private const string ACTION_COPY_LINK = "copy-link";
+ private const string ACTION_PASTE = "paste";
+ private const string ACTION_PASTE_WITH_FORMATTING = "paste-with-formatting";
+ private const string ACTION_SELECT_ALL = "select-all";
+ private const string ACTION_BOLD = "bold";
+ private const string ACTION_ITALIC = "italic";
+ private const string ACTION_UNDERLINE = "underline";
+ private const string ACTION_STRIKETHROUGH = "strikethrough";
+ private const string ACTION_FONT_SIZE = "font-size";
+ private const string ACTION_FONT_FAMILY = "font-family";
+ private const string ACTION_REMOVE_FORMAT = "remove-format";
+ private const string ACTION_INDENT = "indent";
+ private const string ACTION_OUTDENT = "outdent";
+ private const string ACTION_JUSTIFY = "justify";
+ private const string ACTION_COLOR = "color";
+ private const string ACTION_INSERT_IMAGE = "insert-image";
+ private const string ACTION_INSERT_LINK = "insert-link";
+ // this is internal for Bug 771812 workaround in ComposerWidget
+ internal const string ACTION_COMPOSE_AS_HTML = "compose-as-html";
+ private const string ACTION_SELECT_DICTIONARY = "select-dictionary";
+ private const string ACTION_OPEN_INSPECTOR = "open_inspector";
+
+ // ACTION_INSERT_LINK and ACTION_REMOVE_FORMAT are missing from
+ // here since they are handled in update_selection_actions
+ private const string[] html_actions = {
+ ACTION_BOLD, ACTION_ITALIC, ACTION_UNDERLINE, ACTION_STRIKETHROUGH,
+ ACTION_FONT_SIZE, ACTION_FONT_FAMILY, ACTION_COLOR, ACTION_JUSTIFY,
+ ACTION_INSERT_IMAGE, ACTION_COPY_LINK, ACTION_PASTE_WITH_FORMATTING
+ };
+
+ private const ActionEntry[] action_entries = {
+ {ACTION_UNDO, on_undo },
+ {ACTION_REDO, on_redo },
+ {ACTION_CUT, on_cut },
+ {ACTION_COPY, on_copy },
+ {ACTION_COPY_LINK, on_copy_link },
+ {ACTION_PASTE, on_paste },
+ {ACTION_PASTE_WITH_FORMATTING, on_paste_with_formatting },
+ {ACTION_SELECT_ALL, on_select_all },
+ {ACTION_BOLD, on_action, null, "false" },
+ {ACTION_ITALIC, on_action, null, "false" },
+ {ACTION_UNDERLINE, on_action, null, "false" },
+ {ACTION_STRIKETHROUGH, on_action, null, "false" },
+ {ACTION_FONT_SIZE, on_font_size, "s", "'medium'" },
+ {ACTION_FONT_FAMILY, on_font_family, "s", "'sans'" },
+ {ACTION_REMOVE_FORMAT, on_remove_format, null, "false" },
+ {ACTION_INDENT, on_indent },
+ {ACTION_OUTDENT, on_action },
+ {ACTION_JUSTIFY, on_justify, "s", "'left'" },
+ {ACTION_COLOR, on_select_color },
+ {ACTION_INSERT_IMAGE, on_insert_image },
+ {ACTION_INSERT_LINK, on_insert_link },
+ {ACTION_COMPOSE_AS_HTML, on_toggle_action, null, "true",
on_compose_as_html_toggled },
+ {ACTION_SELECT_DICTIONARY, on_select_dictionary
},
+ {ACTION_OPEN_INSPECTOR, on_open_inspector
}
+ };
+
+ public static Gee.MultiMap<string, string> action_accelerators = new Gee.HashMultiMap<string, string>();
+ static construct {
+ action_accelerators.set(ACTION_UNDO, "<Ctrl>z");
+ action_accelerators.set(ACTION_REDO, "<Ctrl><Shift>z");
+ action_accelerators.set(ACTION_CUT, "<Ctrl>x");
+ action_accelerators.set(ACTION_COPY, "<Ctrl>c");
+ action_accelerators.set(ACTION_PASTE, "<Ctrl>v");
+ action_accelerators.set(ACTION_PASTE_WITH_FORMATTING, "<Ctrl><Shift>v");
+ action_accelerators.set(ACTION_INSERT_IMAGE, "<Ctrl>g");
+ action_accelerators.set(ACTION_INSERT_LINK, "<Ctrl>l");
+ action_accelerators.set(ACTION_INDENT, "<Ctrl>bracketright");
+ action_accelerators.set(ACTION_OUTDENT, "<Ctrl>bracketleft");
+ action_accelerators.set(ACTION_REMOVE_FORMAT, "<Ctrl>space");
+ action_accelerators.set(ACTION_BOLD, "<Ctrl>b");
+ action_accelerators.set(ACTION_ITALIC, "<Ctrl>i");
+ action_accelerators.set(ACTION_UNDERLINE, "<Ctrl>u");
+ action_accelerators.set(ACTION_STRIKETHROUGH, "<Ctrl>k");
+ }
+
+ /** Determines if the view is in rich text mode. */
+ public bool is_rich_text { get; private set; default = true; }
+
+ /** The HTML editor for the message's body text. */
+ public ComposerWebView body { get; private set; }
+
+ // this is internal for Bug 771812 workaround in ComposerWidget
+ internal SimpleActionGroup actions = new SimpleActionGroup();
+
+ private Configuration config { get; set; }
+
+ private bool can_delete_quote { get; private set; default = false; }
+
+ [GtkChild]
+ private Gtk.Grid body_container;
+
+ [GtkChild]
+ private Gtk.Box composer_toolbar;
+ [GtkChild]
+ private Gtk.Box insert_buttons;
+ [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;
+ [GtkChild]
+ private Gtk.MenuButton menu_button;
+ [GtkChild]
+ private Gtk.Label info_label;
+ [GtkChild]
+ private Gtk.Label message_overlay_label;
+
+ [GtkChild]
+ private Gtk.Box message_area;
+
+ private Menu html_menu;
+ private Menu plain_menu;
+
+ private Menu context_menu_model;
+ private Menu context_menu_rich_text;
+ private Menu context_menu_plain_text;
+ private Menu context_menu_webkit_spelling;
+ private Menu context_menu_webkit_text_entry;
+ private Menu context_menu_inspector;
+
+ private SpellCheckPopover? spell_check_popover = null;
+ private string? pointer_url = null;
+ private string? cursor_url = null;
+
+ /** Fired when the user opens a link in the composer. */
+ public signal void link_activated(string url);
+
+ /** Fired when the user invokes the insert image action. */
+ public signal void insert_image();
+
+
+ public ComposerEditor(Configuration config) {
+ this.config = config;
+ this.body = new ComposerWebView(config);
+ this.body.set_hexpand(true);
+ this.body.set_vexpand(true);
+ this.body.show();
+
+ this.body_container.add(this.body);
+
+ // Initialize menus
+ Gtk.Builder builder = new Gtk.Builder.from_resource(
+ "/org/gnome/Geary/composer-menus.ui"
+ );
+ this.html_menu = (Menu) builder.get_object("html_menu_model");
+ this.plain_menu = (Menu) builder.get_object("plain_menu_model");
+ this.context_menu_model = (Menu) builder.get_object("context_menu_model");
+ this.context_menu_rich_text = (Menu) builder.get_object("context_menu_rich_text");
+ this.context_menu_plain_text = (Menu) builder.get_object("context_menu_plain_text");
+ this.context_menu_inspector = (Menu) builder.get_object("context_menu_inspector");
+ this.context_menu_webkit_spelling = (Menu) builder.get_object("context_menu_webkit_spelling");
+ this.context_menu_webkit_text_entry = (Menu) builder.get_object("context_menu_webkit_text_entry");
+
+ // Add actions once every element has been initialized and added
+ this.actions.add_action_entries(action_entries, this);
+
+ insert_action_group("cpe", this.actions);
+ get_action(ACTION_UNDO).set_enabled(false);
+ get_action(ACTION_REDO).set_enabled(false);
+ update_cursor_actions();
+
+ this.body.command_stack_changed.connect(on_command_state_changed);
+ this.body.button_release_event_done.connect(on_button_release);
+ this.body.context_menu.connect(on_context_menu);
+ this.body.cursor_context_changed.connect(on_cursor_context_changed);
+ this.body.get_editor_state().notify["typing-attributes"].connect(on_typing_attributes_changed);
+ this.body.key_press_event.connect(on_editor_key_press_event);
+ this.body.load_changed.connect(on_load_changed);
+ this.body.mouse_target_changed.connect(on_mouse_target_changed);
+ this.body.selection_changed.connect(on_selection_changed);
+
+ // Place the message area before the compose toolbar in the
+ // focus chain, so that the user can tab directly from the
+ // Subject: field to the message area. TODO: after bumping
+ // the min. GTK+ version to 3.16, we can/should do this in the
+ // UI file.
+ List<Gtk.Widget> chain = new List<Gtk.Widget>();
+ chain.append(this.message_area);
+ chain.append(this.composer_toolbar);
+ set_focus_chain(chain);
+ }
+
+ /**
+ * Enables deleting the quote a reply is first loaded.
+ */
+ public void enable_quote_delete() {
+ this.can_delete_quote = true;
+ set_info_text(BACKSPACE_TEXT);
+ }
+
+ /**
+ * Sets informational text shown to the user on the toolbar.
+ */
+ public void set_info_text(string info) {
+ this.info_label.set_text(info);
+ }
+
+ private void update_cursor_actions() {
+ bool has_selection = this.body.has_selection;
+ get_action(ACTION_CUT).set_enabled(has_selection);
+ get_action(ACTION_COPY).set_enabled(has_selection);
+
+ get_action(ACTION_INSERT_LINK).set_enabled(
+ this.is_rich_text && (has_selection || this.cursor_url != null)
+ );
+ get_action(ACTION_REMOVE_FORMAT).set_enabled(
+ this.is_rich_text && has_selection
+ );
+ }
+
+ private async ComposerLinkPopover new_link_popover(ComposerLinkPopover.Type type,
+ string url) {
+ var selection_id = "";
+ try {
+ selection_id = yield this.body.save_selection();
+ } catch (Error err) {
+ debug("Error saving selection: %s", err.message);
+ }
+ ComposerLinkPopover popover = new ComposerLinkPopover(type);
+ popover.set_link_url(url);
+ popover.closed.connect(() => {
+ this.body.free_selection(selection_id);
+ Idle.add(() => { popover.destroy(); return Source.REMOVE; });
+ });
+ popover.link_activate.connect((link_uri) => {
+ this.body.insert_link(popover.link_uri, selection_id);
+ });
+ popover.link_delete.connect(() => {
+ this.body.delete_link();
+ });
+ popover.link_open.connect(() => { link_activated(popover.link_uri); });
+ return popover;
+ }
+
+ private SimpleAction? get_action(string action_name) {
+ return this.actions.lookup_action(action_name) as SimpleAction;
+ }
+
+ private void on_load_changed(WebKit.WebView view, WebKit.LoadEvent event) {
+ if (event == WebKit.LoadEvent.FINISHED) {
+ if (get_realized())
+ on_load_finished_and_realized();
+ else
+ realize.connect(on_load_finished_and_realized);
+ }
+ }
+
+ private void on_load_finished_and_realized() {
+ // This is safe to call even when this connection hasn't been made.
+ realize.disconnect(on_load_finished_and_realized);
+
+ this.actions.change_action_state(
+ ACTION_COMPOSE_AS_HTML, this.config.compose_as_html
+ );
+
+ if (this.can_delete_quote) {
+ // Would be nice to clean this up an bit.
+ this.notify["can-delete-quote"].connect(() => {
+ if (this.info_label.get_text() == BACKSPACE_TEXT) {
+ set_info_text("");
+ }
+ });
+ this.body.selection_changed.connect(
+ () => { this.can_delete_quote = false; }
+ );
+ }
+ }
+
+ private void on_action(SimpleAction action, Variant? param) {
+ if (!action.enabled)
+ return;
+
+ // We need the unprefixed name to send as a command to the editor
+ string[] prefixed_action_name = action.get_name().split(".");
+ string action_name = prefixed_action_name[prefixed_action_name.length - 1];
+ this.body.execute_editing_command(action_name);
+ }
+
+ // Use this for toggle actions, and use the change-state signal to respond to these state changes
+ private void on_toggle_action(SimpleAction? action, Variant? param) {
+ action.change_state(!action.state.get_boolean());
+ }
+
+ private void on_compose_as_html_toggled(SimpleAction? action, Variant? new_state) {
+ bool compose_as_html = new_state.get_boolean();
+ action.set_state(compose_as_html);
+
+ foreach (string html_action in html_actions)
+ get_action(html_action).set_enabled(compose_as_html);
+
+ update_cursor_actions();
+
+ this.insert_buttons.visible = compose_as_html;
+ this.font_style_buttons.visible = compose_as_html;
+ this.remove_format_button.visible = compose_as_html;
+
+ this.menu_button.menu_model = (compose_as_html) ? this.html_menu : this.plain_menu;
+
+ this.is_rich_text = compose_as_html;
+ this.body.set_rich_text(compose_as_html);
+
+ this.config.compose_as_html = compose_as_html;
+ }
+
+ private void on_undo(SimpleAction action, Variant? param) {
+ this.body.undo();
+ }
+
+ private void on_redo(SimpleAction action, Variant? param) {
+ this.body.redo();
+ }
+
+ private void on_cut(SimpleAction action, Variant? param) {
+ this.body.cut_clipboard();
+ }
+
+ private void on_copy(SimpleAction action, Variant? param) {
+ this.body.copy_clipboard();
+ }
+
+ private void on_copy_link(SimpleAction action, Variant? param) {
+ Gtk.Clipboard c = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD);
+ // 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();
+ }
+
+ private void on_paste(SimpleAction action, Variant? param) {
+ this.body.paste_plain_text();
+ }
+
+ private void on_paste_with_formatting(SimpleAction action, Variant? param) {
+ this.body.paste_rich_text();
+ }
+
+ private void on_select_all(SimpleAction action, Variant? param) {
+ this.body.select_all();
+ }
+
+ private void on_remove_format(SimpleAction action, Variant? param) {
+ this.body.execute_editing_command("removeformat");
+ this.body.execute_editing_command("removeparaformat");
+ this.body.execute_editing_command("unlink");
+ this.body.execute_editing_command_with_argument("backcolor", "#ffffff");
+ this.body.execute_editing_command_with_argument("forecolor", "#000000");
+ }
+
+ private void on_indent(SimpleAction action, Variant? param) {
+ this.body.indent_line();
+ }
+
+ private void on_justify(SimpleAction action, Variant? param) {
+ this.body.execute_editing_command("justify" + param.get_string());
+ }
+
+ private void on_font_family(SimpleAction action, Variant? param) {
+ this.body.execute_editing_command_with_argument(
+ "fontname", param.get_string()
+ );
+ action.set_state(param.get_string());
+ }
+
+ private void on_font_size(SimpleAction action, Variant? param) {
+ string size = "";
+ if (param.get_string() == "small")
+ size = "1";
+ else if (param.get_string() == "medium")
+ size = "3";
+ else // Large
+ size = "7";
+
+ this.body.execute_editing_command_with_argument("fontsize", size);
+ action.set_state(param.get_string());
+ }
+
+ private void on_select_color() {
+ Gtk.ColorChooserDialog dialog = new Gtk.ColorChooserDialog(
+ _("Select Color"), get_toplevel() as Gtk.Window
+ );
+ if (dialog.run() == Gtk.ResponseType.OK) {
+ this.body.execute_editing_command_with_argument(
+ "forecolor", dialog.get_rgba().to_string()
+ );
+ }
+ dialog.destroy();
+ }
+
+ private void on_mouse_target_changed(WebKit.WebView web_view,
+ WebKit.HitTestResult hit_test,
+ uint modifiers) {
+ 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);
+ }
+
+ private void update_message_overlay_label_style() {
+ Gtk.Window? window = get_toplevel() as Gtk.Window;
+ if (window != null) {
+ Gdk.RGBA window_background = window.get_style_context()
+ .get_background_color(Gtk.StateFlags.NORMAL);
+ Gdk.RGBA label_background = this.message_overlay_label.get_style_context()
+ .get_background_color(Gtk.StateFlags.NORMAL);
+
+ if (label_background == window_background)
+ return;
+
+ message_overlay_label.get_style_context().changed.disconnect(
+ on_message_overlay_label_style_changed);
+ message_overlay_label.override_background_color(Gtk.StateFlags.NORMAL, window_background);
+ message_overlay_label.get_style_context().changed.connect(
+ on_message_overlay_label_style_changed);
+ }
+ }
+
+ [GtkCallback]
+ private void on_message_overlay_label_realize() {
+ update_message_overlay_label_style();
+ }
+
+ private void on_message_overlay_label_style_changed() {
+ update_message_overlay_label_style();
+ }
+
+ private bool on_context_menu(WebKit.WebView view,
+ WebKit.ContextMenu context_menu,
+ Gdk.Event event,
+ WebKit.HitTestResult hit_test_result) {
+ // This is a three step process:
+ // 1. Work out what existing menu items exist that we want to keep
+ // 2. Clear the existing menu
+ // 3. Rebuild it based on our GMenu specification
+
+ // Step 1.
+
+ const WebKit.ContextMenuAction[] SPELLING_ACTIONS = {
+ WebKit.ContextMenuAction.SPELLING_GUESS,
+ WebKit.ContextMenuAction.NO_GUESSES_FOUND,
+ WebKit.ContextMenuAction.IGNORE_SPELLING,
+ WebKit.ContextMenuAction.IGNORE_GRAMMAR,
+ WebKit.ContextMenuAction.LEARN_SPELLING,
+ };
+ const WebKit.ContextMenuAction[] TEXT_INPUT_ACTIONS = {
+ WebKit.ContextMenuAction.INPUT_METHODS,
+ WebKit.ContextMenuAction.UNICODE,
+ };
+
+ Gee.List<WebKit.ContextMenuItem> existing_spelling =
+ new Gee.LinkedList<WebKit.ContextMenuItem>();
+ Gee.List<WebKit.ContextMenuItem> existing_text_entry =
+ new Gee.LinkedList<WebKit.ContextMenuItem>();
+
+ foreach (WebKit.ContextMenuItem item in context_menu.get_items()) {
+ if (item.get_stock_action() in SPELLING_ACTIONS) {
+ existing_spelling.add(item);
+ } else if (item.get_stock_action() in TEXT_INPUT_ACTIONS) {
+ existing_text_entry.add(item);
+ }
+ }
+
+ // Step 2.
+
+ context_menu.remove_all();
+
+ // Step 3.
+
+ GtkUtil.menu_foreach(context_menu_model, (label, name, target, section) => {
+ if (context_menu.last() != null) {
+ context_menu.append(new WebKit.ContextMenuItem.separator());
+ }
+
+ if (section == this.context_menu_webkit_spelling) {
+ foreach (WebKit.ContextMenuItem item in existing_spelling)
+ context_menu.append(item);
+ } else if (section == this.context_menu_webkit_text_entry) {
+ foreach (WebKit.ContextMenuItem item in existing_text_entry)
+ context_menu.append(item);
+ } else if (section == this.context_menu_rich_text) {
+ if (this.is_rich_text)
+ append_menu_section(context_menu, section);
+ } else if (section == this.context_menu_plain_text) {
+ if (!this.is_rich_text)
+ append_menu_section(context_menu, section);
+ } else if (section == this.context_menu_inspector) {
+ if (Args.inspector)
+ append_menu_section(context_menu, section);
+ } else {
+ append_menu_section(context_menu, section);
+ }
+ });
+
+ // 4. Update the clipboard
+ // get_clipboard(Gdk.SELECTION_CLIPBOARD).request_targets(
+ // (_, targets) => {
+ // foreach (Gdk.Atom atom in targets) {
+ // debug("atom name: %s", atom.name());
+ // }
+ // });
+
+ return Gdk.EVENT_PROPAGATE;
+ }
+
+ private inline void append_menu_section(WebKit.ContextMenu context_menu,
+ Menu section) {
+ GtkUtil.menu_foreach(section, (label, name, target, section) => {
+ if ("." in name)
+ name = name.split(".")[1];
+
+ Gtk.Action action = new Gtk.Action(name, label, null, null);
+ action.set_sensitive(get_action(name).enabled);
+ action.activate.connect((action) => {
+ this.actions.activate_action(name, target);
+ });
+ context_menu.append(new WebKit.ContextMenuItem(action));
+ });
+ }
+
+ private void on_select_dictionary(SimpleAction action, Variant? param) {
+ if (this.spell_check_popover == null) {
+ this.spell_check_popover = new SpellCheckPopover(
+ this.select_dictionary_button, this.config
+ );
+ this.spell_check_popover.selection_changed.connect((active_langs) => {
+ this.config.spell_check_languages = active_langs;
+ });
+ }
+ this.spell_check_popover.toggle();
+ }
+
+ private bool on_editor_key_press_event(Gdk.EventKey event) {
+ if (this.can_delete_quote) {
+ this.can_delete_quote = false;
+ if (event.is_modifier == 0 &&
+ event.keyval == Gdk.Key.BackSpace) {
+ this.body.delete_quoted_message();
+ return Gdk.EVENT_STOP;
+ }
+ }
+
+ return Gdk.EVENT_PROPAGATE;
+ }
+
+ 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);
+ }
+
+ private bool on_button_release(Gdk.Event event) {
+ // Show the link popover on mouse release (instead of press)
+ // so the user can still select text with a link in it,
+ // without the popover immediately appearing and raining on
+ // their text selection parade.
+ if (this.pointer_url != null &&
+ this.actions.get_action_state(ACTION_COMPOSE_AS_HTML).get_boolean()) {
+ Gdk.EventButton? button = (Gdk.EventButton) event;
+ Gdk.Rectangle location = new Gdk.Rectangle();
+ location.x = (int) button.x;
+ location.y = (int) button.y;
+
+ this.new_link_popover.begin(
+ ComposerLinkPopover.Type.EXISTING_LINK, this.pointer_url,
+ (obj, res) => {
+ ComposerLinkPopover popover = this.new_link_popover.end(res);
+ popover.set_relative_to(this.body);
+ popover.set_pointing_to(location);
+ popover.show();
+ });
+ }
+ return Gdk.EVENT_PROPAGATE;
+ }
+
+ 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);
+
+ if (context.font_size < 11)
+ this.actions.change_action_state(ACTION_FONT_SIZE, "small");
+ 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");
+ }
+
+ private void on_typing_attributes_changed() {
+ uint mask = this.body.get_editor_state().get_typing_attributes();
+ this.actions.change_action_state(
+ ACTION_BOLD,
+ (mask & WebKit.EditorTypingAttributes.BOLD) == WebKit.EditorTypingAttributes.BOLD
+ );
+ this.actions.change_action_state(
+ ACTION_ITALIC,
+ (mask & WebKit.EditorTypingAttributes.ITALIC) == WebKit.EditorTypingAttributes.ITALIC
+ );
+ this.actions.change_action_state(
+ ACTION_UNDERLINE,
+ (mask & WebKit.EditorTypingAttributes.UNDERLINE) == WebKit.EditorTypingAttributes.UNDERLINE
+ );
+ this.actions.change_action_state(
+ ACTION_STRIKETHROUGH,
+ (mask & WebKit.EditorTypingAttributes.STRIKETHROUGH) ==
WebKit.EditorTypingAttributes.STRIKETHROUGH
+ );
+ }
+
+ private void on_insert_image(SimpleAction action, Variant? param) {
+ insert_image();
+ }
+
+ private void on_insert_link(SimpleAction action, Variant? param) {
+ ComposerLinkPopover.Type type = ComposerLinkPopover.Type.NEW_LINK;
+ string url = "http://";
+ if (this.cursor_url != null) {
+ type = ComposerLinkPopover.Type.EXISTING_LINK;
+ url = this.cursor_url;
+ }
+
+ this.new_link_popover.begin(type, url, (obj, res) => {
+ ComposerLinkPopover popover = this.new_link_popover.end(res);
+
+ // We have to disconnect then reconnect the selection
+ // changed signal for the duration of the popover
+ // being active since if the user selects the text in
+ // the URL entry, then the editor will lose its
+ // selection, the inset link action will become
+ // disabled, and the popover will disappear
+ this.body.selection_changed.disconnect(on_selection_changed);
+ popover.closed.connect(() => {
+ this.body.selection_changed.connect(on_selection_changed);
+ });
+
+ popover.set_relative_to(this.insert_link_button);
+ popover.show();
+ });
+ }
+
+ private void on_open_inspector(SimpleAction action, Variant? param) {
+ this.body.get_inspector().show();
+ }
+
+ private void on_selection_changed(bool has_selection) {
+ update_cursor_actions();
+ }
+
+}
diff --git a/src/client/composer/composer-embed.vala b/src/client/composer/composer-embed.vala
index 9b4bcc8..89116b0 100644
--- a/src/client/composer/composer-embed.vala
+++ b/src/client/composer/composer-embed.vala
@@ -42,8 +42,8 @@ public class ComposerEmbed : Gtk.EventBox, ComposerContainer {
add(composer);
realize.connect(on_realize);
- this.composer.editor.focus_in_event.connect(on_focus_in);
- this.composer.editor.focus_out_event.connect(on_focus_out);
+ this.composer.editor.body.focus_in_event.connect(on_focus_in);
+ this.composer.editor.body.focus_out_event.connect(on_focus_out);
show();
}
@@ -71,11 +71,11 @@ public class ComposerEmbed : Gtk.EventBox, ComposerContainer {
}
public void remove_composer() {
- if (this.composer.editor.has_focus)
+ if (this.composer.editor.body.has_focus)
on_focus_out();
- this.composer.editor.focus_in_event.disconnect(on_focus_in);
- this.composer.editor.focus_out_event.disconnect(on_focus_out);
+ this.composer.editor.body.focus_in_event.disconnect(on_focus_in);
+ this.composer.editor.body.focus_out_event.disconnect(on_focus_out);
disable_scroll_reroute(this);
@@ -136,8 +136,8 @@ public class ComposerEmbed : Gtk.EventBox, ComposerContainer {
// Outer scroller didn't use the complete delta,
// so work out what to do with the remainder.
- int editor_height = this.composer.editor.get_allocated_height();
- int editor_preferred = this.composer.editor.preferred_height;
+ int editor_height = this.composer.editor.body.get_allocated_height();
+ int editor_preferred = this.composer.editor.body.preferred_height;
int scrolled_height = this.outer_scroller.get_allocated_height();
if (alloc.height < scrolled_height &&
@@ -189,8 +189,8 @@ public class ComposerEmbed : Gtk.EventBox, ComposerContainer {
public void vanish() {
hide();
this.composer.state = ComposerWidget.ComposerState.DETACHED;
- this.composer.editor.focus_in_event.disconnect(on_focus_in);
- this.composer.editor.focus_out_event.disconnect(on_focus_out);
+ this.composer.editor.body.focus_in_event.disconnect(on_focus_in);
+ this.composer.editor.body.focus_out_event.disconnect(on_focus_out);
vanished();
}
diff --git a/src/client/composer/composer-web-view.vala b/src/client/composer/composer-web-view.vala
index 08972b2..166eca3 100644
--- a/src/client/composer/composer-web-view.vala
+++ b/src/client/composer/composer-web-view.vala
@@ -101,9 +101,6 @@ public class ComposerWebView : ClientWebView {
*/
public bool is_empty { get; private set; default = true; }
- /** Determines if the view is in rich text mode. */
- public bool is_rich_text { get; private set; default = true; }
-
// Determines if signals should be sent, useful for e.g. stopping
// document_modified being sent when the editor content is being
// updated before sending.
@@ -203,7 +200,6 @@ public class ComposerWebView : ClientWebView {
* Sets whether the editor is in rich text or plain text mode.
*/
public void set_rich_text(bool enabled) {
- this.is_rich_text = enabled;
this.call.begin(Geary.JS.callable("geary.setRichText").bool(enabled), null);
}
diff --git a/src/client/composer/composer-widget.vala b/src/client/composer/composer-widget.vala
index 9183268..fec86fa 100644
--- a/src/client/composer/composer-widget.vala
+++ b/src/client/composer/composer-widget.vala
@@ -11,7 +11,11 @@ private errordomain AttachmentError {
DUPLICATE
}
-// The actual widget for sending messages. Should be put in a ComposerContainer
+/**
+ * Main widget for composing new messages.
+ *
+ * Should be put in a ComposerContainer.
+ */
[GtkTemplate (ui = "/org/gnome/Geary/composer-widget.ui")]
public class ComposerWidget : Gtk.EventBox {
@@ -48,30 +52,6 @@ public class ComposerWidget : Gtk.EventBox {
}
}
- private SimpleActionGroup actions = new SimpleActionGroup();
-
- private const string ACTION_UNDO = "undo";
- private const string ACTION_REDO = "redo";
- private const string ACTION_CUT = "cut";
- private const string ACTION_COPY = "copy";
- private const string ACTION_COPY_LINK = "copy-link";
- private const string ACTION_PASTE = "paste";
- private const string ACTION_PASTE_WITH_FORMATTING = "paste-with-formatting";
- private const string ACTION_SELECT_ALL = "select-all";
- private const string ACTION_BOLD = "bold";
- private const string ACTION_ITALIC = "italic";
- private const string ACTION_UNDERLINE = "underline";
- private const string ACTION_STRIKETHROUGH = "strikethrough";
- private const string ACTION_FONT_SIZE = "font-size";
- private const string ACTION_FONT_FAMILY = "font-family";
- private const string ACTION_REMOVE_FORMAT = "remove-format";
- private const string ACTION_INDENT = "indent";
- private const string ACTION_OUTDENT = "outdent";
- private const string ACTION_JUSTIFY = "justify";
- private const string ACTION_COLOR = "color";
- private const string ACTION_INSERT_IMAGE = "insert-image";
- private const string ACTION_INSERT_LINK = "insert-link";
- private const string ACTION_COMPOSE_AS_HTML = "compose-as-html";
private const string ACTION_SHOW_EXTENDED = "show-extended";
private const string ACTION_CLOSE = "close";
private const string ACTION_CLOSE_AND_SAVE = "close-and-save";
@@ -80,42 +60,9 @@ public class ComposerWidget : Gtk.EventBox {
private const string ACTION_SEND = "send";
private const string ACTION_ADD_ATTACHMENT = "add-attachment";
private const string ACTION_ADD_ORIGINAL_ATTACHMENTS = "add-original-attachments";
- private const string ACTION_SELECT_DICTIONARY = "select-dictionary";
- private const string ACTION_OPEN_INSPECTOR = "open_inspector";
-
- // ACTION_INSERT_LINK and ACTION_REMOVE_FORMAT are missing from
- // here since they are handled in update_selection_actions
- private const string[] html_actions = {
- ACTION_BOLD, ACTION_ITALIC, ACTION_UNDERLINE, ACTION_STRIKETHROUGH,
- ACTION_FONT_SIZE, ACTION_FONT_FAMILY, ACTION_COLOR, ACTION_JUSTIFY,
- ACTION_INSERT_IMAGE, ACTION_COPY_LINK, ACTION_PASTE_WITH_FORMATTING
- };
private const ActionEntry[] action_entries = {
- // Editor commands
- {ACTION_UNDO, on_undo },
- {ACTION_REDO, on_redo },
- {ACTION_CUT, on_cut },
- {ACTION_COPY, on_copy },
- {ACTION_COPY_LINK, on_copy_link },
- {ACTION_PASTE, on_paste },
- {ACTION_PASTE_WITH_FORMATTING, on_paste_with_formatting },
- {ACTION_SELECT_ALL, on_select_all },
- {ACTION_BOLD, on_action, null, "false" },
- {ACTION_ITALIC, on_action, null, "false" },
- {ACTION_UNDERLINE, on_action, null, "false" },
- {ACTION_STRIKETHROUGH, on_action, null, "false" },
- {ACTION_FONT_SIZE, on_font_size, "s", "'medium'" },
- {ACTION_FONT_FAMILY, on_font_family, "s", "'sans'" },
- {ACTION_REMOVE_FORMAT, on_remove_format, null, "false" },
- {ACTION_INDENT, on_indent },
- {ACTION_OUTDENT, on_action },
- {ACTION_JUSTIFY, on_justify, "s", "'left'" },
- {ACTION_COLOR, on_select_color },
- {ACTION_INSERT_IMAGE, on_insert_image },
- {ACTION_INSERT_LINK, on_insert_link },
// Composer commands
- {ACTION_COMPOSE_AS_HTML, on_toggle_action, null, "true",
on_compose_as_html_toggled },
{ACTION_SHOW_EXTENDED, on_toggle_action, null, "false", on_show_extended_toggled
},
{ACTION_CLOSE, on_close
},
{ACTION_CLOSE_AND_SAVE, on_close_and_save
},
@@ -124,27 +71,10 @@ public class ComposerWidget : Gtk.EventBox {
{ACTION_SEND, on_send
},
{ACTION_ADD_ATTACHMENT, on_add_attachment
},
{ACTION_ADD_ORIGINAL_ATTACHMENTS, on_pending_attachments
},
- {ACTION_SELECT_DICTIONARY, on_select_dictionary
},
- {ACTION_OPEN_INSPECTOR, on_open_inspector
},
};
public static Gee.MultiMap<string, string> action_accelerators = new Gee.HashMultiMap<string, string>();
static construct {
- action_accelerators.set(ACTION_UNDO, "<Ctrl>z");
- action_accelerators.set(ACTION_REDO, "<Ctrl><Shift>z");
- action_accelerators.set(ACTION_CUT, "<Ctrl>x");
- action_accelerators.set(ACTION_COPY, "<Ctrl>c");
- action_accelerators.set(ACTION_PASTE, "<Ctrl>v");
- action_accelerators.set(ACTION_PASTE_WITH_FORMATTING, "<Ctrl><Shift>v");
- action_accelerators.set(ACTION_INSERT_IMAGE, "<Ctrl>g");
- action_accelerators.set(ACTION_INSERT_LINK, "<Ctrl>l");
- action_accelerators.set(ACTION_INDENT, "<Ctrl>bracketright");
- action_accelerators.set(ACTION_OUTDENT, "<Ctrl>bracketleft");
- action_accelerators.set(ACTION_REMOVE_FORMAT, "<Ctrl>space");
- action_accelerators.set(ACTION_BOLD, "<Ctrl>b");
- action_accelerators.set(ACTION_ITALIC, "<Ctrl>i");
- action_accelerators.set(ACTION_UNDERLINE, "<Ctrl>u");
- action_accelerators.set(ACTION_STRIKETHROUGH, "<Ctrl>k");
action_accelerators.set(ACTION_CLOSE, "<Ctrl>w");
action_accelerators.set(ACTION_CLOSE, "Escape");
action_accelerators.set(ACTION_ADD_ATTACHMENT, "<Ctrl>t");
@@ -154,8 +84,6 @@ public class ComposerWidget : Gtk.EventBox {
private const string DRAFT_SAVED_TEXT = _("Saved");
private const string DRAFT_SAVING_TEXT = _("Saving");
private const string DRAFT_ERROR_TEXT = _("Error saving");
- private const string BACKSPACE_TEXT = _("Press Backspace to delete quote");
- private const string DEFAULT_TITLE = _("New Message");
private const string URI_LIST_MIME_TYPE = "text/uri-list";
private const string FILE_URI_PREFIX = "file://";
@@ -212,11 +140,17 @@ public class ComposerWidget : Gtk.EventBox {
&& this.bcc_entry.empty
&& this.reply_to_entry.empty
&& this.subject_entry.buffer.length == 0
- && this.editor.is_empty
+ && this.editor.body.is_empty
&& this.attached_files.size == 0;
}
}
+ /** The composer's custom header bar */
+ internal ComposerHeaderbar header { get; private set; }
+
+ /** The composer's body editor bar */
+ internal ComposerEditor editor { get; private set; }
+
/** Determines if the composer can currently save a draft. */
private bool can_save {
get { return this.draft_manager != null; }
@@ -231,29 +165,18 @@ public class ComposerWidget : Gtk.EventBox {
}
}
- public ComposerHeaderbar header { get; private set; }
-
- public ComposerWebView editor { get; private set; }
-
- public string draft_save_text { get; private set; }
-
- public bool can_delete_quote { get; private set; default = false; }
-
- public string toolbar_text { get; set; }
+ private SimpleActionGroup actions = new SimpleActionGroup();
- public string window_title { get; set; }
+ private string draft_save_text { get; private set; }
- public Configuration config { get; set; }
+ private Configuration config { get; set; }
private ContactListStore? contact_list_store = null;
private string body_html = "";
[GtkChild]
- private Gtk.Box composer_container;
-
- [GtkChild]
- internal Gtk.Grid editor_container;
+ private Gtk.Grid editor_container;
[GtkChild]
private Gtk.Label from_label;
@@ -287,8 +210,6 @@ public class ComposerWidget : Gtk.EventBox {
[GtkChild]
private Gtk.Entry subject_entry;
[GtkChild]
- private Gtk.Label message_overlay_label;
- [GtkChild]
private Gtk.Box attachments_box;
[GtkChild]
private Gtk.Box hidden_on_attachment_drag_over;
@@ -302,48 +223,16 @@ public class ComposerWidget : Gtk.EventBox {
private Gtk.Widget recipients;
[GtkChild]
private Gtk.Box header_area;
- [GtkChild]
-
- private Gtk.Box composer_toolbar;
- [GtkChild]
- private Gtk.Box insert_buttons;
- [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;
- [GtkChild]
- private Gtk.MenuButton menu_button;
- [GtkChild]
- private Gtk.Label info_label;
-
- [GtkChild]
- private Gtk.Box message_area;
-
- private Menu html_menu;
- private Menu plain_menu;
-
- private Menu context_menu_model;
- private Menu context_menu_rich_text;
- private Menu context_menu_plain_text;
- private Menu context_menu_webkit_spelling;
- private Menu context_menu_webkit_text_entry;
- private Menu context_menu_inspector;
- private SpellCheckPopover? spell_check_popover = 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;
private string reply_subject = "";
private string forward_subject = "";
+
private bool top_posting = true;
private string? last_quote = null;
+ private bool is_attachment_overlay_visible = false;
private Gee.List<Geary.Attachment>? pending_attachments = null;
private AttachPending pending_include = AttachPending.INLINE_ONLY;
private Gee.Set<File> attached_files = new Gee.HashSet<File>(Geary.Files.nullable_hash,
@@ -402,17 +291,6 @@ public class ComposerWidget : Gtk.EventBox {
this.visible_on_attachment_drag_over.remove(this.visible_on_attachment_drag_over_child);
- BindingTransformFunc set_toolbar_text = (binding, source_value, ref target_value) => {
- if (draft_save_text == "" && can_delete_quote)
- target_value = BACKSPACE_TEXT;
- else
- target_value = draft_save_text;
- return true;
- };
- bind_property("draft-save-text", this, "toolbar-text", BindingFlags.SYNC_CREATE,
- set_toolbar_text);
- bind_property("can-delete-quote", this, "toolbar-text", BindingFlags.SYNC_CREATE,
- set_toolbar_text);
this.to_entry = new EmailEntry(this);
this.to_entry.changed.connect(on_envelope_changed);
this.to_box.add(to_entry);
@@ -433,33 +311,20 @@ public class ComposerWidget : Gtk.EventBox {
this.to_entry.margin_top = this.cc_entry.margin_top = this.bcc_entry.margin_top =
this.reply_to_entry.margin_top = 6;
- this.editor = new ComposerWebView(config);
- this.editor.set_hexpand(true);
- this.editor.set_vexpand(true);
+ this.subject_entry.notify["text"].connect(() => {
+ notify_property("subject");
+ });
+
+ this.editor = new ComposerEditor(config);
+ this.editor.body.document_modified.connect(() => { draft_changed(); });
+ this.editor.body.load_changed.connect(on_load_changed);
+ this.editor.body.key_press_event.connect(on_editor_key_press_event);
+ this.editor.link_activated.connect((url) => { this.link_activated(url); });
+ this.editor.insert_image.connect(on_insert_image);
this.editor.show();
this.editor_container.add(this.editor);
- // Initialize menus
- Gtk.Builder builder = new Gtk.Builder.from_resource(
- "/org/gnome/Geary/composer-menus.ui"
- );
- this.html_menu = (Menu) builder.get_object("html_menu_model");
- this.plain_menu = (Menu) builder.get_object("plain_menu_model");
- this.context_menu_model = (Menu) builder.get_object("context_menu_model");
- this.context_menu_rich_text = (Menu) builder.get_object("context_menu_rich_text");
- this.context_menu_plain_text = (Menu) builder.get_object("context_menu_plain_text");
- this.context_menu_inspector = (Menu) builder.get_object("context_menu_inspector");
- this.context_menu_webkit_spelling = (Menu) builder.get_object("context_menu_webkit_spelling");
- this.context_menu_webkit_text_entry = (Menu) builder.get_object("context_menu_webkit_text_entry");
-
- this.subject_entry.bind_property("text", this, "window-title", BindingFlags.SYNC_CREATE,
- (binding, source_value, ref target_value) => {
- target_value = Geary.String.is_empty_or_whitespace(this.subject_entry.text)
- ? DEFAULT_TITLE : this.subject_entry.text.strip();
- return true;
- });
-
embed_header();
// Listen to account signals to update from menu.
@@ -473,8 +338,6 @@ public class ComposerWidget : Gtk.EventBox {
});
// TODO: also listen for account updates to allow adding identities while writing an email
- bind_property("toolbar-text", this.info_label, "label", BindingFlags.SYNC_CREATE);
-
this.from = new Geary.RFC822.MailboxAddresses.single(account.information.primary_mailbox);
this.draft_timer = new Geary.TimeoutManager.seconds(
@@ -482,7 +345,11 @@ public class ComposerWidget : Gtk.EventBox {
);
// Add actions once every element has been initialized and added
- initialize_actions();
+ this.actions.add_action_entries(action_entries, this);
+ insert_action_group("cmp", this.actions);
+ this.header.insert_action_group("cmh", this.actions);
+ get_action(ACTION_CLOSE_AND_SAVE).set_enabled(false);
+
validate_send_button();
// Connect everything (can only happen after actions were added)
@@ -491,27 +358,6 @@ public class ComposerWidget : Gtk.EventBox {
this.bcc_entry.changed.connect(validate_send_button);
this.reply_to_entry.changed.connect(validate_send_button);
- this.editor.command_stack_changed.connect(on_command_state_changed);
- this.editor.button_release_event_done.connect(on_button_release);
- this.editor.context_menu.connect(on_context_menu);
- 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);
- this.editor.load_changed.connect(on_load_changed);
- this.editor.mouse_target_changed.connect(on_mouse_target_changed);
- this.editor.selection_changed.connect(on_selection_changed);
-
- // Place the message area before the compose toolbar in the focus chain, so that
- // the user can tab directly from the Subject: field to the message area.
- // TODO: after bumping the min. GTK+ version to 3.16, we can/should do this in the UI file.
- List<Gtk.Widget> chain = new List<Gtk.Widget>();
- chain.append(this.hidden_on_attachment_drag_over);
- chain.append(this.message_area);
- chain.append(this.composer_toolbar);
- chain.append(this.attachments_box);
- this.composer_container.set_focus_chain(chain);
-
update_composer_view();
}
@@ -601,7 +447,7 @@ public class ComposerWidget : Gtk.EventBox {
update_attachments_view();
string signature = yield load_signature(cancellable);
- this.editor.load_html(
+ this.editor.body.load_html(
this.body_html,
signature,
referred_quote,
@@ -791,8 +637,9 @@ public class ComposerWidget : Gtk.EventBox {
Geary.RFC822.TextFormat.HTML);
if (!Geary.String.is_empty(quote))
this.top_posting = false;
- else
- this.can_delete_quote = true;
+ else {
+ this.editor.enable_quote_delete();
+ }
break;
case ComposeType.FORWARD:
@@ -811,36 +658,7 @@ public class ComposerWidget : Gtk.EventBox {
else if (not_compact && Geary.String.is_empty(subject))
this.subject_entry.grab_focus();
else
- this.editor.grab_focus();
- }
-
- // Initializes all actions and adds them to the action group
- private void initialize_actions() {
- this.actions.add_action_entries(action_entries, this);
-
- // for some reason, we can't use the same prefix.
- insert_action_group("cmp", this.actions);
- this.header.insert_action_group("cmh", this.actions);
-
- get_action(ACTION_CLOSE_AND_SAVE).set_enabled(false);
-
- get_action(ACTION_UNDO).set_enabled(false);
- get_action(ACTION_REDO).set_enabled(false);
-
- update_cursor_actions();
- }
-
- 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);
-
- 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
- );
+ this.editor.body.grab_focus();
}
private bool check_preferred_from_address(Gee.List<Geary.RFC822.MailboxAddress> account_addresses,
@@ -872,14 +690,6 @@ public class ComposerWidget : Gtk.EventBox {
this.actions.change_action_state(
ACTION_SHOW_EXTENDED, false
);
- this.actions.change_action_state(
- ACTION_COMPOSE_AS_HTML, this.config.compose_as_html
- );
-
- if (can_delete_quote)
- this.editor.selection_changed.connect(
- () => { this.can_delete_quote = false; }
- );
}
private void show_attachment_overlay(bool visible) {
@@ -987,9 +797,9 @@ public class ComposerWidget : Gtk.EventBox {
try {
if (this.editor.is_rich_text || only_html)
- email.body_html = yield this.editor.get_html();
+ email.body_html = yield this.editor.body.get_html();
if (!only_html)
- email.body_text = yield this.editor.get_text();
+ email.body_text = yield this.editor.body.get_text();
} catch (Error error) {
debug("Error getting composer message body: %s", error.message);
}
@@ -1005,7 +815,7 @@ public class ComposerWidget : Gtk.EventBox {
if (referred != null && quote != null && quote != this.last_quote) {
this.last_quote = quote;
// Always use reply styling, since forward styling doesn't work for inline quotes
- this.editor.insert_html(
+ this.editor.body.insert_html(
Geary.RFC822.Utils.quote_email_for_reply(referred, quote, Geary.RFC822.TextFormat.HTML)
);
@@ -1200,8 +1010,10 @@ public class ComposerWidget : Gtk.EventBox {
// conversation back in the main window. The workaround here
// sets a new menu model and hence the menu_button constructs
// a new popover.
- this.actions.change_action_state(ACTION_COMPOSE_AS_HTML,
- GearyApplication.instance.config.compose_as_html);
+ this.editor.actions.change_action_state(
+ ComposerEditor.ACTION_COMPOSE_AS_HTML,
+ this.config.compose_as_html
+ );
this.state = ComposerWidget.ComposerState.DETACHED;
this.header.detached();
@@ -1234,6 +1046,14 @@ public class ComposerWidget : Gtk.EventBox {
return check_send_on_return(event) && base.key_press_event(event);
}
+ /**
+ * Helper method, returns a composer action.
+ * @param action_name - The name of the action (as found in action_entries)
+ */
+ internal SimpleAction? get_action(string action_name) {
+ return this.actions.lookup_action(action_name) as SimpleAction;
+ }
+
// Updates the composer's UI after its state has changed
private void update_composer_view() {
this.recipients.set_visible(this.state != ComposerState.INLINE_COMPACT);
@@ -1254,7 +1074,7 @@ public class ComposerWidget : Gtk.EventBox {
bool has_body = true;
try {
- has_body = !Geary.String.is_empty(yield this.editor.get_html());
+ has_body = !Geary.String.is_empty(yield this.editor.body.get_html());
} catch (Error err) {
debug("Failed to get message body: %s", err.message);
}
@@ -1267,7 +1087,7 @@ public class ComposerWidget : Gtk.EventBox {
} else if (!has_body && !has_attachment) {
confirmation = _("Send message with an empty body?");
} else if (!has_attachment &&
- yield this.editor.contains_attachment_keywords(
+ yield this.editor.body.contains_attachment_keywords(
ATTACHMENT_KEYWORDS_LOCALIZED, this.subject)) {
confirmation = _("Send message without an attachment?");
}
@@ -1291,13 +1111,14 @@ public class ComposerWidget : Gtk.EventBox {
// Used internally by on_send()
private async void on_send_async() {
- this.editor.disable();
+ this.editor.set_sensitive(false);
+ this.editor.body.disable();
this.container.vanish();
this.is_closing = true;
// Perform send.
try {
- yield this.editor.clean_content();
+ yield this.editor.body.clean_content();
yield this.account.send_email_async(yield get_composed_email());
} catch (Error e) {
GLib.message("Error sending email: %s", e.message);
@@ -1316,6 +1137,19 @@ public class ComposerWidget : Gtk.EventBox {
this.container.close_container();
}
+ private bool on_editor_key_press_event(Gdk.EventKey event) {
+ // Widget's keypress override doesn't receive non-modifier
+ // keys when the editor processes them, regardless if true or
+ // false is called; this deals with that issue (specifically
+ // so Ctrl+Enter will send the message)
+ bool ret = Gdk.EVENT_PROPAGATE;
+ if (event.is_modifier == 0) {
+ if (check_send_on_return(event) == Gdk.EVENT_STOP)
+ ret = Gdk.EVENT_STOP;
+ }
+ return ret;
+ }
+
/**
* Closes current draft manager, if any, then opens a new one.
*/
@@ -1358,22 +1192,22 @@ public class ComposerWidget : Gtk.EventBox {
private void update_draft_state() {
switch (this.draft_manager.draft_state) {
case Geary.App.DraftManager.DraftState.STORED:
- this.draft_save_text = DRAFT_SAVED_TEXT;
+ this.editor.set_info_text(DRAFT_SAVED_TEXT);
this.is_draft_saved = true;
break;
case Geary.App.DraftManager.DraftState.STORING:
- this.draft_save_text = DRAFT_SAVING_TEXT;
+ this.editor.set_info_text(DRAFT_SAVING_TEXT);
this.is_draft_saved = true;
break;
case Geary.App.DraftManager.DraftState.NOT_STORED:
- this.draft_save_text = "";
+ this.editor.set_info_text("");
this.is_draft_saved = false;
break;
case Geary.App.DraftManager.DraftState.ERROR:
- this.draft_save_text = DRAFT_ERROR_TEXT;
+ this.editor.set_info_text(DRAFT_ERROR_TEXT);
this.is_draft_saved = false;
break;
@@ -1491,7 +1325,7 @@ public class ComposerWidget : Gtk.EventBox {
// attachment instead.
if (content_id != null) {
this.cid_files[content_id] = file;
- this.editor.add_internal_resource(
+ this.editor.body.add_internal_resource(
content_id, new Geary.Memory.FileBuffer(file, true)
);
} else {
@@ -1560,7 +1394,7 @@ public class ComposerWidget : Gtk.EventBox {
check_attachment_file(target);
this.inline_files[content_id] = target;
try {
- this.editor.add_internal_resource(
+ this.editor.body.add_internal_resource(
content_id, new Geary.Memory.FileBuffer(target, true)
);
} catch (Error err) {
@@ -1677,89 +1511,11 @@ public class ComposerWidget : Gtk.EventBox {
this.header.set_recipients(label, tooltip.str.slice(0, -1)); // Remove trailing \n
}
- private void on_justify(SimpleAction action, Variant? param) {
- this.editor.execute_editing_command("justify" + param.get_string());
- }
-
- private void on_action(SimpleAction action, Variant? param) {
- if (!action.enabled)
- return;
-
- // We need the unprefixed name to send as a command to the editor
- string[] prefixed_action_name = action.get_name().split(".");
- string action_name = prefixed_action_name[prefixed_action_name.length - 1];
- this.editor.execute_editing_command(action_name);
- }
-
- private void on_undo(SimpleAction action, Variant? param) {
- this.editor.undo();
- }
-
- private void on_redo(SimpleAction action, Variant? param) {
- this.editor.redo();
- }
-
- private void on_cut(SimpleAction action, Variant? param) {
- this.editor.cut_clipboard();
- }
-
- private void on_copy(SimpleAction action, Variant? param) {
- this.editor.copy_clipboard();
- }
-
- private void on_copy_link(SimpleAction action, Variant? param) {
- Gtk.Clipboard c = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD);
- // 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();
- }
-
- private void on_paste(SimpleAction action, Variant? param) {
- this.editor.paste_plain_text();
- }
-
- private void on_paste_with_formatting(SimpleAction action, Variant? param) {
- this.editor.paste_rich_text();
- }
-
- private void on_select_all(SimpleAction action, Variant? param) {
- this.editor.select_all();
- }
-
- private void on_remove_format(SimpleAction action, Variant? param) {
- this.editor.execute_editing_command("removeformat");
- this.editor.execute_editing_command("removeparaformat");
- this.editor.execute_editing_command("unlink");
- this.editor.execute_editing_command_with_argument("backcolor", "#ffffff");
- this.editor.execute_editing_command_with_argument("forecolor", "#000000");
- }
-
// Use this for toggle actions, and use the change-state signal to respond to these state changes
private void on_toggle_action(SimpleAction? action, Variant? param) {
action.change_state(!action.state.get_boolean());
}
- private void on_compose_as_html_toggled(SimpleAction? action, Variant? new_state) {
- bool compose_as_html = new_state.get_boolean();
- action.set_state(compose_as_html);
-
- foreach (string html_action in html_actions)
- get_action(html_action).set_enabled(compose_as_html);
-
- update_cursor_actions();
-
- this.insert_buttons.visible = compose_as_html;
- this.font_style_buttons.visible = compose_as_html;
- this.remove_format_button.visible = compose_as_html;
-
- this.menu_button.menu_model = (compose_as_html) ? this.html_menu : this.plain_menu;
-
- this.editor.set_rich_text(compose_as_html);
-
- GearyApplication.instance.config.compose_as_html = compose_as_html;
- }
-
private void on_show_extended_toggled(SimpleAction? action, Variant? new_state) {
bool show_extended = new_state.get_boolean();
action.set_state(show_extended);
@@ -1774,214 +1530,6 @@ public class ComposerWidget : Gtk.EventBox {
}
}
- private void on_font_family(SimpleAction action, Variant? param) {
- this.editor.execute_editing_command_with_argument(
- "fontname", param.get_string()
- );
- action.set_state(param.get_string());
- }
-
- private void on_font_size(SimpleAction action, Variant? param) {
- string size = "";
- if (param.get_string() == "small")
- size = "1";
- else if (param.get_string() == "medium")
- size = "3";
- else // Large
- size = "7";
-
- this.editor.execute_editing_command_with_argument("fontsize", size);
- action.set_state(param.get_string());
- }
-
- private void on_select_color() {
- Gtk.ColorChooserDialog dialog = new Gtk.ColorChooserDialog(
- _("Select Color"),
- get_toplevel() as Gtk.Window
- );
- if (dialog.run() == Gtk.ResponseType.OK) {
- this.editor.execute_editing_command_with_argument(
- "forecolor", dialog.get_rgba().to_string()
- );
- }
- dialog.destroy();
- }
-
- private void on_indent(SimpleAction action, Variant? param) {
- this.editor.indent_line();
- }
-
- private void on_mouse_target_changed(WebKit.WebView web_view,
- WebKit.HitTestResult hit_test,
- uint modifiers) {
- 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);
- }
-
- private void update_message_overlay_label_style() {
- Gtk.Window? window = get_toplevel() as Gtk.Window;
- if (window != null) {
- Gdk.RGBA window_background = window.get_style_context()
- .get_background_color(Gtk.StateFlags.NORMAL);
- Gdk.RGBA label_background = message_overlay_label.get_style_context()
- .get_background_color(Gtk.StateFlags.NORMAL);
-
- if (label_background == window_background)
- return;
-
- message_overlay_label.get_style_context().changed.disconnect(
- on_message_overlay_label_style_changed);
- message_overlay_label.override_background_color(Gtk.StateFlags.NORMAL, window_background);
- message_overlay_label.get_style_context().changed.connect(
- on_message_overlay_label_style_changed);
- }
- }
-
- [GtkCallback]
- private void on_message_overlay_label_realize() {
- update_message_overlay_label_style();
- }
-
- private void on_message_overlay_label_style_changed() {
- update_message_overlay_label_style();
- }
-
- private bool on_context_menu(WebKit.WebView view,
- WebKit.ContextMenu context_menu,
- Gdk.Event event,
- WebKit.HitTestResult hit_test_result) {
- // This is a three step process:
- // 1. Work out what existing menu items exist that we want to keep
- // 2. Clear the existing menu
- // 3. Rebuild it based on our GMenu specification
-
- // Step 1.
-
- const WebKit.ContextMenuAction[] SPELLING_ACTIONS = {
- WebKit.ContextMenuAction.SPELLING_GUESS,
- WebKit.ContextMenuAction.NO_GUESSES_FOUND,
- WebKit.ContextMenuAction.IGNORE_SPELLING,
- WebKit.ContextMenuAction.IGNORE_GRAMMAR,
- WebKit.ContextMenuAction.LEARN_SPELLING,
- };
- const WebKit.ContextMenuAction[] TEXT_INPUT_ACTIONS = {
- WebKit.ContextMenuAction.INPUT_METHODS,
- WebKit.ContextMenuAction.UNICODE,
- };
-
- Gee.List<WebKit.ContextMenuItem> existing_spelling =
- new Gee.LinkedList<WebKit.ContextMenuItem>();
- Gee.List<WebKit.ContextMenuItem> existing_text_entry =
- new Gee.LinkedList<WebKit.ContextMenuItem>();
-
- foreach (WebKit.ContextMenuItem item in context_menu.get_items()) {
- if (item.get_stock_action() in SPELLING_ACTIONS) {
- existing_spelling.add(item);
- } else if (item.get_stock_action() in TEXT_INPUT_ACTIONS) {
- existing_text_entry.add(item);
- }
- }
-
- // Step 2.
-
- context_menu.remove_all();
-
- // Step 3.
-
- GtkUtil.menu_foreach(context_menu_model, (label, name, target, section) => {
- if (context_menu.last() != null) {
- context_menu.append(new WebKit.ContextMenuItem.separator());
- }
-
- if (section == this.context_menu_webkit_spelling) {
- foreach (WebKit.ContextMenuItem item in existing_spelling)
- context_menu.append(item);
- } else if (section == this.context_menu_webkit_text_entry) {
- foreach (WebKit.ContextMenuItem item in existing_text_entry)
- context_menu.append(item);
- } else if (section == this.context_menu_rich_text) {
- if (this.editor.is_rich_text)
- append_menu_section(context_menu, section);
- } else if (section == this.context_menu_plain_text) {
- if (!this.editor.is_rich_text)
- append_menu_section(context_menu, section);
- } else if (section == this.context_menu_inspector) {
- if (Args.inspector)
- append_menu_section(context_menu, section);
- } else {
- append_menu_section(context_menu, section);
- }
- });
-
- // 4. Update the clipboard
- // get_clipboard(Gdk.SELECTION_CLIPBOARD).request_targets(
- // (_, targets) => {
- // foreach (Gdk.Atom atom in targets) {
- // debug("atom name: %s", atom.name());
- // }
- // });
-
- return Gdk.EVENT_PROPAGATE;
- }
-
- private inline void append_menu_section(WebKit.ContextMenu context_menu,
- Menu section) {
- GtkUtil.menu_foreach(section, (label, name, target, section) => {
- if ("." in name)
- name = name.split(".")[1];
-
- Gtk.Action action = new Gtk.Action(name, label, null, null);
- action.set_sensitive(get_action(name).enabled);
- action.activate.connect((action) => {
- this.actions.activate_action(name, target);
- });
- context_menu.append(new WebKit.ContextMenuItem(action));
- });
- }
-
- private void on_select_dictionary(SimpleAction action, Variant? param) {
- if (this.spell_check_popover == null) {
- this.spell_check_popover = new SpellCheckPopover(
- this.select_dictionary_button, this.config
- );
- this.spell_check_popover.selection_changed.connect((active_langs) => {
- this.config.spell_check_languages = active_langs;
- });
- }
- this.spell_check_popover.toggle();
- }
-
- private bool on_editor_key_press_event(Gdk.EventKey event) {
- // Widget's keypress override doesn't receive non-modifier
- // keys when the editor processes them, regardless if true or
- // false is called; this deals with that issue (specifically
- // so Ctrl+Enter will send the message)
- if (event.is_modifier == 0) {
- if (check_send_on_return(event) == Gdk.EVENT_STOP)
- return Gdk.EVENT_STOP;
- }
-
- if (this.can_delete_quote) {
- this.can_delete_quote = false;
- if (event.is_modifier == 0 && event.keyval == Gdk.Key.BackSpace) {
- this.editor.delete_quoted_message();
- return Gdk.EVENT_STOP;
- }
- }
-
- return Gdk.EVENT_PROPAGATE;
- }
-
- /**
- * Helper method, returns a composer action.
- * @param action_name - The name of the action (as found in action_entries)
- */
- public SimpleAction? get_action(string action_name) {
- return this.actions.lookup_action(action_name) as SimpleAction;
- }
-
private bool add_account_emails_to_from_list(Geary.Account other_account, bool set_active = false) {
Geary.RFC822.MailboxAddresses primary_address = new Geary.RFC822.MailboxAddresses.single(
other_account.information.primary_mailbox);
@@ -2089,7 +1637,7 @@ public class ComposerWidget : Gtk.EventBox {
this.account = new_account;
this.load_signature.begin(null, (obj, res) => {
- this.editor.update_signature(this.load_signature.end(res));
+ this.editor.body.update_signature(this.load_signature.end(res));
});
load_entry_completions.begin();
@@ -2124,35 +1672,6 @@ public class ComposerWidget : Gtk.EventBox {
return account_sig;
}
- private async ComposerLinkPopover new_link_popover(ComposerLinkPopover.Type type,
- string url) {
- var selection_id = "";
- try {
- selection_id = yield this.editor.save_selection();
- } catch (Error err) {
- debug("Error saving selection: %s", err.message);
- }
- ComposerLinkPopover popover = new ComposerLinkPopover(type);
- popover.set_link_url(url);
- popover.closed.connect(() => {
- this.editor.free_selection(selection_id);
- Idle.add(() => { popover.destroy(); return Source.REMOVE; });
- });
- popover.link_activate.connect((link_uri) => {
- this.editor.insert_link(popover.link_uri, selection_id);
- });
- 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);
- }
-
private void on_draft_id_changed() {
draft_id_changed(this.draft_manager.current_draft_id);
}
@@ -2182,64 +1701,6 @@ public class ComposerWidget : Gtk.EventBox {
}
}
- private bool on_button_release(Gdk.Event event) {
- // Show the link popover on mouse release (instead of press)
- // so the user can still select text with a link in it,
- // without the popover immediately appearing and raining on
- // their text selection parade.
- if (this.pointer_url != null &&
- this.actions.get_action_state(ACTION_COMPOSE_AS_HTML).get_boolean()) {
- Gdk.EventButton? button = (Gdk.EventButton) event;
- Gdk.Rectangle location = new Gdk.Rectangle();
- location.x = (int) button.x;
- location.y = (int) button.y;
-
- this.new_link_popover.begin(
- ComposerLinkPopover.Type.EXISTING_LINK, this.pointer_url,
- (obj, res) => {
- ComposerLinkPopover popover = this.new_link_popover.end(res);
- popover.set_relative_to(this.editor);
- popover.set_pointing_to(location);
- popover.show();
- });
- }
- return Gdk.EVENT_PROPAGATE;
- }
-
- 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);
-
- if (context.font_size < 11)
- this.actions.change_action_state(ACTION_FONT_SIZE, "small");
- 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");
- }
-
- private void on_typing_attributes_changed() {
- uint mask = this.editor.get_editor_state().get_typing_attributes();
- this.actions.change_action_state(
- ACTION_BOLD,
- (mask & WebKit.EditorTypingAttributes.BOLD) == WebKit.EditorTypingAttributes.BOLD
- );
- this.actions.change_action_state(
- ACTION_ITALIC,
- (mask & WebKit.EditorTypingAttributes.ITALIC) == WebKit.EditorTypingAttributes.ITALIC
- );
- this.actions.change_action_state(
- ACTION_UNDERLINE,
- (mask & WebKit.EditorTypingAttributes.UNDERLINE) == WebKit.EditorTypingAttributes.UNDERLINE
- );
- this.actions.change_action_state(
- ACTION_STRIKETHROUGH,
- (mask & WebKit.EditorTypingAttributes.STRIKETHROUGH) ==
WebKit.EditorTypingAttributes.STRIKETHROUGH
- );
- }
-
private void on_add_attachment() {
AttachmentDialog dialog = new AttachmentDialog(
// Translators: Title of add attachment dialog
@@ -2264,11 +1725,7 @@ public class ComposerWidget : Gtk.EventBox {
dialog.destroy();
}
- private void on_pending_attachments() {
- update_pending_attachments(AttachPending.ALL, true);
- }
-
- private void on_insert_image(SimpleAction action, Variant? param) {
+ private void on_insert_image() {
AttachmentDialog dialog = new AttachmentDialog(
// Translators: Title of insert image dialog
_("Insert an image"),
@@ -2289,7 +1746,7 @@ public class ComposerWidget : Gtk.EventBox {
try {
string path = file.get_path();
add_inline_part(file, path);
- this.editor.insert_image(
+ this.editor.body.insert_image(
ClientWebView.INTERNAL_URL_PREFIX + path
);
} catch (Error err) {
@@ -2301,39 +1758,8 @@ public class ComposerWidget : Gtk.EventBox {
dialog.destroy();
}
- private void on_insert_link(SimpleAction action, Variant? param) {
- ComposerLinkPopover.Type type = ComposerLinkPopover.Type.NEW_LINK;
- string url = "http://";
- if (this.cursor_url != null) {
- type = ComposerLinkPopover.Type.EXISTING_LINK;
- url = this.cursor_url;
- }
-
- this.new_link_popover.begin(type, url, (obj, res) => {
- ComposerLinkPopover popover = this.new_link_popover.end(res);
-
- // We have to disconnect then reconnect the selection
- // changed signal for the duration of the popover
- // being active since if the user selects the text in
- // the URL entry, then the editor will lose its
- // selection, the inset link action will become
- // disabled, and the popover will disappear
- this.editor.selection_changed.disconnect(on_selection_changed);
- popover.closed.connect(() => {
- this.editor.selection_changed.connect(on_selection_changed);
- });
-
- popover.set_relative_to(this.insert_link_button);
- popover.show();
- });
- }
-
- private void on_open_inspector(SimpleAction action, Variant? param) {
- this.editor.get_inspector().show();
- }
-
- private void on_selection_changed(bool has_selection) {
- update_cursor_actions();
+ private void on_pending_attachments() {
+ update_pending_attachments(AttachPending.ALL, true);
}
}
diff --git a/src/client/composer/composer-window.vala b/src/client/composer/composer-window.vala
index b4ffd19..1d87825 100644
--- a/src/client/composer/composer-window.vala
+++ b/src/client/composer/composer-window.vala
@@ -10,6 +10,10 @@
*/
public class ComposerWindow : Gtk.ApplicationWindow, ComposerContainer {
+ // Translators: The composer window's default title
+ private const string DEFAULT_TITLE = _("New Message");
+
+ public string window_title { get; set; }
internal ComposerWidget composer { get; set; }
@@ -35,14 +39,28 @@ public class ComposerWindow : Gtk.ApplicationWindow, ComposerContainer {
focus_in_event.connect(on_focus_in);
focus_out_event.connect(on_focus_out);
+ composer.bind_property(
+ "subject",
+ this, "window-title",
+ BindingFlags.SYNC_CREATE,
+ (binding, source_value, ref target_value) => {
+ target_value = Geary.String.is_empty_or_whitespace(composer.subject)
+ ? DEFAULT_TITLE
+ : composer.subject.strip();
+ return true;
+ });
+
if (config.desktop_environment == Configuration.DesktopEnvironment.UNITY) {
composer.embed_header();
- composer.bind_property("window-title", this, "title", BindingFlags.SYNC_CREATE);
+ this.bind_property("window-title",
+ this, "title",
+ BindingFlags.SYNC_CREATE);
} else {
- this.composer.header.show_close_button = true;
- this.composer.free_header();
+ composer.header.show_close_button = true;
+ composer.free_header();
set_titlebar(this.composer.header);
- composer.bind_property("window-title", this.composer.header, "title",
+ this.bind_property("window-title",
+ this.composer.header, "title",
BindingFlags.SYNC_CREATE);
}
@@ -57,8 +75,8 @@ public class ComposerWindow : Gtk.ApplicationWindow, ComposerContainer {
public void close_container() {
on_focus_out();
- this.composer.editor.focus_in_event.disconnect(on_focus_in);
- this.composer.editor.focus_out_event.disconnect(on_focus_out);
+ this.composer.editor.body.focus_in_event.disconnect(on_focus_in);
+ this.composer.editor.body.focus_out_event.disconnect(on_focus_out);
this.closing = true;
destroy();
diff --git a/ui/CMakeLists.txt b/ui/CMakeLists.txt
index dc41091..c0ce34a 100644
--- a/ui/CMakeLists.txt
+++ b/ui/CMakeLists.txt
@@ -7,6 +7,7 @@ set(RESOURCE_LIST
STRIPBLANKS "certificate_warning_dialog.glade"
"client-web-view.js"
"client-web-view-allow-remote-images.js"
+ STRIPBLANKS "composer-editor.ui"
STRIPBLANKS "composer-headerbar.ui"
STRIPBLANKS "composer-link-popover.ui"
STRIPBLANKS "composer-menus.ui"
diff --git a/ui/composer-editor.ui b/ui/composer-editor.ui
new file mode 100644
index 0000000..be42cbb
--- /dev/null
+++ b/ui/composer-editor.ui
@@ -0,0 +1,479 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.20.0 -->
+<interface domain="geary">
+ <requires lib="gtk+" version="3.20"/>
+ <template class="ComposerEditor" parent="GtkGrid">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkBox" id="composer_toolbar">
+ <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_start">6</property>
+ <property name="margin_end">6</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkBox" id="command_buttons">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkButton">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="receives_default">False</property>
+ <property name="tooltip_text" translatable="yes">Undo last edit (Ctrl+Z)</property>
+ <property name="action_name">cpe.undo</property>
+ <property name="always_show_image">True</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="pixel_size">16</property>
+ <property name="icon_name">edit-undo-symbolic</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="receives_default">False</property>
+ <property name="tooltip_text" translatable="yes">Redo last edit (Ctrl+Shift+Z)</property>
+ <property name="action_name">cpe.redo</property>
+ <property name="always_show_image">True</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="pixel_size">16</property>
+ <property name="icon_name">edit-redo-symbolic</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <style>
+ <class name="linked"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="font_style_buttons">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkToggleButton" id="bold_button">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="receives_default">False</property>
+ <property name="tooltip_text" translatable="yes">Bold (Ctrl+B)</property>
+ <property name="action_name">cpe.bold</property>
+ <property name="always_show_image">True</property>
+ <child>
+ <object class="GtkImage" id="bold_image">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="pixel_size">16</property>
+ <property name="icon_name">format-text-bold-symbolic</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="italics_button">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="receives_default">False</property>
+ <property name="tooltip_text" translatable="yes">Italic (Ctrl+I)</property>
+ <property name="action_name">cpe.italic</property>
+ <property name="always_show_image">True</property>
+ <child>
+ <object class="GtkImage" id="italics_image">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="pixel_size">16</property>
+ <property name="icon_name">format-text-italic-symbolic</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="underline_button">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="receives_default">False</property>
+ <property name="tooltip_text" translatable="yes">Underline (Ctrl+U)</property>
+ <property name="action_name">cpe.underline</property>
+ <property name="always_show_image">True</property>
+ <child>
+ <object class="GtkImage" id="underline_image">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="pixel_size">16</property>
+ <property name="icon_name">format-text-underline-symbolic</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="strikethrough_button">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="receives_default">False</property>
+ <property name="tooltip_text" translatable="yes">Strikethrough (Ctrl+K)</property>
+ <property name="action_name">cpe.strikethrough</property>
+ <property name="always_show_image">True</property>
+ <child>
+ <object class="GtkImage" id="strikethrough_image">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="pixel_size">16</property>
+ <property name="icon_name">format-text-strikethrough-symbolic</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ <style>
+ <class name="linked"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="indentation_buttons">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkButton" id="indent_button">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="receives_default">False</property>
+ <property name="tooltip_text" translatable="yes">Quote text (Ctrl+])</property>
+ <property name="action_name">cpe.indent</property>
+ <property name="always_show_image">True</property>
+ <child>
+ <object class="GtkImage" id="indent_image">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="pixel_size">16</property>
+ <property name="icon_name">format-indent-more-symbolic</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="outdent_button">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="receives_default">False</property>
+ <property name="tooltip_text" translatable="yes">Unquote text (Ctrl+[)</property>
+ <property name="action_name">cpe.outdent</property>
+ <property name="always_show_image">True</property>
+ <child>
+ <object class="GtkImage" id="outdent_image">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="pixel_size">16</property>
+ <property name="icon_name">format-indent-less-symbolic</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <style>
+ <class name="linked"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="insert_buttons">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkButton" id="insert_link_button">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="receives_default">False</property>
+ <property name="tooltip_text" translatable="yes">Insert or update selection link
(Ctrl+L)</property>
+ <property name="action_name">cpe.insert-link</property>
+ <property name="always_show_image">True</property>
+ <child>
+ <object class="GtkImage" id="insert_link_image">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="pixel_size">16</property>
+ <property name="icon_name">insert-link-symbolic</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="insert_image_button">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="receives_default">False</property>
+ <property name="tooltip_text" translatable="yes">Insert an image (Ctrl+G)</property>
+ <property name="action_name">cpe.insert-image</property>
+ <property name="always_show_image">True</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="pixel_size">16</property>
+ <property name="icon_name">insert-image-symbolic</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <style>
+ <class name="linked"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="remove_format_button">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="receives_default">False</property>
+ <property name="tooltip_text" translatable="yes">Remove selection formatting
(Ctrl+Space)</property>
+ <property name="action_name">cpe.remove-format</property>
+ <property name="always_show_image">True</property>
+ <child>
+ <object class="GtkImage" id="remove_format_image">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="pixel_size">16</property>
+ <property name="icon_name">format-text-remove-symbolic</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">4</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="select_dictionary_button">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="receives_default">False</property>
+ <property name="tooltip_text" translatable="yes">Select spell checking languages</property>
+ <property name="action_name">cpe.select-dictionary</property>
+ <property name="always_show_image">True</property>
+ <child>
+ <object class="GtkImage" id="select_dictionary_image">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="pixel_size">16</property>
+ <property name="icon_name">accessories-dictionary-symbolic</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">5</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkMenuButton" id="menu_button">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="receives_default">False</property>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="pack_type">end</property>
+ <property name="position">5</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="info_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="pack_type">end</property>
+ <property name="position">6</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkFrame">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label_xalign">0</property>
+ <property name="shadow_type">in</property>
+ <child>
+ <object class="GtkBox" id="message_area">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkOverlay" id="message_overlay">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkGrid" id="body_container">
+ <property name="height_request">250</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="index">-1</property>
+ </packing>
+ </child>
+ <child type="overlay">
+ <object class="GtkLabel" id="message_overlay_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="valign">end</property>
+ <property name="ellipsize">middle</property>
+ <signal name="realize" handler="on_message_overlay_label_realize" swapped="no"/>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <style>
+ <class name="geary-composer-body"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ </template>
+</interface>
diff --git a/ui/composer-menus.ui b/ui/composer-menus.ui
index f9f7c64..ab244f7 100644
--- a/ui/composer-menus.ui
+++ b/ui/composer-menus.ui
@@ -5,47 +5,47 @@
<section>
<item>
<attribute name="label" translatable="yes">S_ans Serif</attribute>
- <attribute name="action">cmp.font-family</attribute>
+ <attribute name="action">cpe.font-family</attribute>
<attribute name="target">sans</attribute>
</item>
<item>
<attribute name="label" translatable="yes">S_erif</attribute>
- <attribute name="action">cmp.font-family</attribute>
+ <attribute name="action">cpe.font-family</attribute>
<attribute name="target">serif</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Fixed Width</attribute>
- <attribute name="action">cmp.font-family</attribute>
+ <attribute name="action">cpe.font-family</attribute>
<attribute name="target">monospace</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">_Small</attribute>
- <attribute name="action">cmp.font-size</attribute>
+ <attribute name="action">cpe.font-size</attribute>
<attribute name="target">small</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Medium</attribute>
- <attribute name="action">cmp.font-size</attribute>
+ <attribute name="action">cpe.font-size</attribute>
<attribute name="target">medium</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Lar_ge</attribute>
- <attribute name="action">cmp.font-size</attribute>
+ <attribute name="action">cpe.font-size</attribute>
<attribute name="target">large</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">C_olor</attribute>
- <attribute name="action">cmp.color</attribute>
+ <attribute name="action">cpe.color</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">_Rich Text</attribute>
- <attribute name="action">cmp.compose-as-html</attribute>
+ <attribute name="action">cpe.compose-as-html</attribute>
</item>
</section>
<section>
@@ -60,7 +60,7 @@
<section>
<item>
<attribute name="label" translatable="yes">_Rich Text</attribute>
- <attribute name="action">cmp.compose-as-html</attribute>
+ <attribute name="action">cpe.compose-as-html</attribute>
</item>
</section>
<section>
@@ -76,56 +76,56 @@
<section>
<item>
<attribute name="label" translatable="yes">_Undo</attribute>
- <attribute name="action">cmp.undo</attribute>
+ <attribute name="action">cpe.undo</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Redo</attribute>
- <attribute name="action">cmp.redo</attribute>
+ <attribute name="action">cpe.redo</attribute>
</item>
</section>
<section id="context_menu_rich_text">
<item>
<attribute name="label" translatable="yes">Cu_t</attribute>
- <attribute name="action">cmp.cut</attribute>
+ <attribute name="action">cpe.cut</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Copy</attribute>
- <attribute name="action">cmp.copy</attribute>
+ <attribute name="action">cpe.copy</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Paste</attribute>
- <attribute name="action">cmp.paste</attribute>
+ <attribute name="action">cpe.paste</attribute>
</item>
<item>
<attribute name="label" translatable="yes" context="Clipboard paste with rich text">Paste _With
Formatting</attribute>
- <attribute name="action">cmp.paste-with-formatting</attribute>
+ <attribute name="action">cpe.paste-with-formatting</attribute>
</item>
</section>
<section id="context_menu_plain_text">
<item>
<attribute name="label" translatable="yes">Cu_t</attribute>
- <attribute name="action">cmp.cut</attribute>
+ <attribute name="action">cpe.cut</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Copy</attribute>
- <attribute name="action">cmp.copy</attribute>
+ <attribute name="action">cpe.copy</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Paste</attribute>
- <attribute name="action">cmp.paste</attribute>
+ <attribute name="action">cpe.paste</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">Select _All</attribute>
- <attribute name="action">cmp.select-all</attribute>
+ <attribute name="action">cpe.select-all</attribute>
</item>
</section>
<section id="context_menu_webkit_text_entry"/>
<section id="context_menu_inspector">
<item>
<attribute name="label" translatable="yes">_Inspect…</attribute>
- <attribute name="action">cmp.open_inspector</attribute>
+ <attribute name="action">cpe.open_inspector</attribute>
</item>
</section>
</menu>
diff --git a/ui/composer-widget.ui b/ui/composer-widget.ui
index 50fd22e..3e604dc 100644
--- a/ui/composer-widget.ui
+++ b/ui/composer-widget.ui
@@ -1,13 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.20.0 -->
-<interface>
- <requires lib="gtk+" version="3.14"/>
+<interface domain="geary">
+ <requires lib="gtk+" version="3.20"/>
<template class="ComposerWidget" parent="GtkEventBox">
<property name="visible">True</property>
- <signal name="drag_data_received" handler="on_drag_data_received"/>
- <signal name="drag_drop" handler="on_drag_drop"/>
- <signal name="drag_motion" handler="on_drag_motion"/>
- <signal name="drag_leave" handler="on_drag_leave"/>
+ <property name="can_focus">False</property>
+ <signal name="drag-data-received" handler="on_drag_data_received" swapped="no"/>
+ <signal name="drag-drop" handler="on_drag_drop" swapped="no"/>
+ <signal name="drag-leave" handler="on_drag_leave" swapped="no"/>
+ <signal name="drag-motion" handler="on_drag_motion" swapped="no"/>
<child>
<object class="GtkBox" id="composer_container">
<property name="visible">True</property>
@@ -323,460 +324,30 @@
</object>
<packing>
<property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">2</property>
- </packing>
- </child>
- <child>
- <object class="GtkBox" id="composer_toolbar">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="margin_start">6</property>
- <property name="margin_end">6</property>
- <property name="spacing">6</property>
- <child>
- <object class="GtkBox" id="command_buttons">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <child>
- <object class="GtkButton">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="focus_on_click">False</property>
- <property name="receives_default">False</property>
- <property name="tooltip_text" translatable="yes">Undo last edit (Ctrl+Z)</property>
- <property name="action_name">cmp.undo</property>
- <property name="always_show_image">True</property>
- <child>
- <object class="GtkImage">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="pixel_size">16</property>
- <property name="icon_name">edit-undo-symbolic</property>
- </object>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkButton">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="focus_on_click">False</property>
- <property name="receives_default">False</property>
- <property name="tooltip_text" translatable="yes">Redo last edit
(Ctrl+Shift+Z)</property>
- <property name="action_name">cmp.redo</property>
- <property name="always_show_image">True</property>
- <child>
- <object class="GtkImage">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="pixel_size">16</property>
- <property name="icon_name">edit-redo-symbolic</property>
- </object>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">1</property>
- </packing>
- </child>
- <style>
- <class name="linked"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkBox" id="font_style_buttons">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <child>
- <object class="GtkToggleButton" id="bold_button">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="focus_on_click">False</property>
- <property name="receives_default">False</property>
- <property name="tooltip_text" translatable="yes">Bold (Ctrl+B)</property>
- <property name="action_name">cmp.bold</property>
- <property name="always_show_image">True</property>
- <child>
- <object class="GtkImage" id="bold_image">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="pixel_size">16</property>
- <property name="icon_name">format-text-bold-symbolic</property>
- </object>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkToggleButton" id="italics_button">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="focus_on_click">False</property>
- <property name="receives_default">False</property>
- <property name="tooltip_text" translatable="yes">Italic (Ctrl+I)</property>
- <property name="action_name">cmp.italic</property>
- <property name="always_show_image">True</property>
- <child>
- <object class="GtkImage" id="italics_image">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="pixel_size">16</property>
- <property name="icon_name">format-text-italic-symbolic</property>
- </object>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">1</property>
- </packing>
- </child>
- <child>
- <object class="GtkToggleButton" id="underline_button">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="focus_on_click">False</property>
- <property name="receives_default">False</property>
- <property name="tooltip_text" translatable="yes">Underline (Ctrl+U)</property>
- <property name="action_name">cmp.underline</property>
- <property name="always_show_image">True</property>
- <child>
- <object class="GtkImage" id="underline_image">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="pixel_size">16</property>
- <property name="icon_name">format-text-underline-symbolic</property>
- </object>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">2</property>
- </packing>
- </child>
- <child>
- <object class="GtkToggleButton" id="strikethrough_button">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="focus_on_click">False</property>
- <property name="receives_default">False</property>
- <property name="tooltip_text" translatable="yes">Strikethrough (Ctrl+K)</property>
- <property name="action_name">cmp.strikethrough</property>
- <property name="always_show_image">True</property>
- <child>
- <object class="GtkImage" id="strikethrough_image">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="pixel_size">16</property>
- <property name="icon_name">format-text-strikethrough-symbolic</property>
- </object>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">3</property>
- </packing>
- </child>
- <style>
- <class name="linked"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">1</property>
- </packing>
- </child>
- <child>
- <object class="GtkBox" id="indentation_buttons">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <child>
- <object class="GtkButton" id="indent_button">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="focus_on_click">False</property>
- <property name="receives_default">False</property>
- <property name="tooltip_text" translatable="yes">Quote text (Ctrl+])</property>
- <property name="action_name">cmp.indent</property>
- <property name="always_show_image">True</property>
- <child>
- <object class="GtkImage" id="indent_image">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="pixel_size">16</property>
- <property name="icon_name">format-indent-more-symbolic</property>
- </object>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkButton" id="outdent_button">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="focus_on_click">False</property>
- <property name="receives_default">False</property>
- <property name="tooltip_text" translatable="yes">Unquote text (Ctrl+[)</property>
- <property name="action_name">cmp.outdent</property>
- <property name="always_show_image">True</property>
- <child>
- <object class="GtkImage" id="outdent_image">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="pixel_size">16</property>
- <property name="icon_name">format-indent-less-symbolic</property>
- </object>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">1</property>
- </packing>
- </child>
- <style>
- <class name="linked"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">2</property>
- </packing>
- </child>
- <child>
- <object class="GtkBox" id="insert_buttons">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <child>
- <object class="GtkButton" id="insert_link_button">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="focus_on_click">False</property>
- <property name="receives_default">False</property>
- <property name="tooltip_text" translatable="yes">Insert or update selection link
(Ctrl+L)</property>
- <property name="action_name">cmp.insert-link</property>
- <property name="always_show_image">True</property>
- <child>
- <object class="GtkImage" id="insert_link_image">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="pixel_size">16</property>
- <property name="icon_name">insert-link-symbolic</property>
- </object>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkButton" id="insert_image_button">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="focus_on_click">False</property>
- <property name="receives_default">False</property>
- <property name="tooltip_text" translatable="yes">Insert an image (Ctrl+G)</property>
- <property name="action_name">cmp.insert-image</property>
- <property name="always_show_image">True</property>
- <child>
- <object class="GtkImage">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="pixel_size">16</property>
- <property name="icon_name">insert-image-symbolic</property>
- </object>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">1</property>
- </packing>
- </child>
- <style>
- <class name="linked"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">3</property>
- </packing>
- </child>
- <child>
- <object class="GtkButton" id="remove_format_button">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="focus_on_click">False</property>
- <property name="receives_default">False</property>
- <property name="tooltip_text" translatable="yes">Remove selection formatting
(Ctrl+Space)</property>
- <property name="action_name">cmp.remove-format</property>
- <property name="always_show_image">True</property>
- <child>
- <object class="GtkImage" id="remove_format_image">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="pixel_size">16</property>
- <property name="icon_name">format-text-remove-symbolic</property>
- </object>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">4</property>
- </packing>
- </child>
- <child>
- <object class="GtkButton" id="select_dictionary_button">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="focus_on_click">False</property>
- <property name="receives_default">False</property>
- <property name="tooltip_text" translatable="yes">Select spell checking languages</property>
- <property name="action_name">cmp.select-dictionary</property>
- <property name="always_show_image">True</property>
- <child>
- <object class="GtkImage" id="select_dictionary_image">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="pixel_size">16</property>
- <property name="icon_name">accessories-dictionary-symbolic</property>
- </object>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">5</property>
- </packing>
- </child>
- <child>
- <object class="GtkMenuButton" id="menu_button">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="focus_on_click">False</property>
- <property name="receives_default">False</property>
- <child>
- <placeholder/>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="pack_type">end</property>
- <property name="position">5</property>
- </packing>
- </child>
- <child>
- <object class="GtkLabel" id="info_label">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <style>
- <class name="dim-label"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="pack_type">end</property>
- <property name="position">6</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
<property name="fill">True</property>
- <property name="position">3</property>
+ <property name="position">2</property>
</packing>
</child>
<child>
- <object class="GtkFrame">
+ <object class="GtkGrid" id="editor_container">
<property name="visible">True</property>
<property name="can_focus">False</property>
- <property name="label_xalign">0</property>
- <property name="shadow_type">in</property>
<child>
- <object class="GtkBox" id="message_area">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <child>
- <object class="GtkOverlay" id="message_overlay">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <child>
- <object class="GtkGrid" id="editor_container">
- <property name="height_request">250</property>
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <child>
- <placeholder/>
- </child>
- </object>
- <packing>
- <property name="index">-1</property>
- </packing>
- </child>
- <child type="overlay">
- <object class="GtkLabel" id="message_overlay_label">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="halign">start</property>
- <property name="valign">end</property>
- <property name="ellipsize">middle</property>
- <signal name="realize" handler="on_message_overlay_label_realize" swapped="no"/>
- </object>
- </child>
- </object>
- <packing>
- <property name="expand">True</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- </object>
+ <placeholder/>
</child>
- <style>
- <class name="geary-composer-body"/>
- </style>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
- <property name="position">4</property>
+ <property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkBox" id="attachments_box">
<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_start">6</property>
<property name="margin_end">6</property>
<property name="margin_top">6</property>
@@ -790,7 +361,7 @@
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
- <property name="position">5</property>
+ <property name="position">4</property>
</packing>
</child>
</object>
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]