[geary/mjog/fix-composer-actions] Composer.Widget: Split email body editing code out into separate widget




commit 369b1f1a4dc7b1807ed7e256b77cb8f17219bcb0
Author: Michael Gratton <mike vee net>
Date:   Fri Aug 28 17:56:55 2020 +1000

    Composer.Widget: Split email body editing code out into separate widget
    
    Create a new Composer.Editor widget and move all body web view and
    action bar related code from the main widget there.
    
    This helps to clearly delineate concerns of the two classes, it
    substantially reduces the complexity of the main widget, and should
    reduce the odds of further breakage like that fixed by the previous
    commit less likely in the future.

 po/POTFILES.in                                     |   4 +-
 src/client/application/application-client.vala     |   1 +
 .../application/application-plugin-manager.vala    |   6 +-
 src/client/composer/composer-editor.vala           | 707 +++++++++++++++
 src/client/composer/composer-embed.vala            |   7 +-
 src/client/composer/composer-widget.vala           | 976 ++++-----------------
 src/client/meson.build                             |   1 +
 ui/{composer-menus.ui => composer-editor-menus.ui} |   0
 ui/composer-editor.ui                              | 775 ++++++++++++++++
 ui/composer-widget.ui                              | 794 +----------------
 ui/org.gnome.Geary.gresource.xml                   |   3 +-
 11 files changed, 1668 insertions(+), 1606 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 6c67a9963..438069987 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -60,6 +60,7 @@ src/client/components/status-bar.vala
 src/client/components/stock.vala
 src/client/composer/composer-box.vala
 src/client/composer/composer-container.vala
+src/client/composer/composer-editor.vala
 src/client/composer/composer-email-entry.vala
 src/client/composer/composer-embed.vala
 src/client/composer/composer-headerbar.vala
@@ -445,9 +446,10 @@ ui/accounts_editor_remove_pane.ui
 ui/accounts_editor_servers_pane.ui
 ui/application-main-window.ui
 ui/certificate_warning_dialog.glade
+ui/composer-editor.ui
+ui/composer-editor-menus.ui
 ui/composer-headerbar.ui
 ui/composer-link-popover.ui
-ui/composer-menus.ui
 ui/composer-widget.ui
 ui/components-attachment-pane.ui
 ui/components-attachment-pane-menus.ui
diff --git a/src/client/application/application-client.vala b/src/client/application/application-client.vala
index cb1f04228..3dd66f6f8 100644
--- a/src/client/application/application-client.vala
+++ b/src/client/application/application-client.vala
@@ -409,6 +409,7 @@ public class Application.Client : Gtk.Application {
         );
 
         MainWindow.add_accelerators(this);
+        Composer.Editor.add_accelerators(this);
         Composer.Widget.add_accelerators(this);
         Components.Inspector.add_accelerators(this);
         Components.PreferencesWindow.add_accelerators(this);
diff --git a/src/client/application/application-plugin-manager.vala 
b/src/client/application/application-plugin-manager.vala
index 976471921..de1e01b0b 100644
--- a/src/client/application/application-plugin-manager.vala
+++ b/src/client/application/application-plugin-manager.vala
@@ -427,7 +427,7 @@ public class Application.PluginManager : GLib.Object {
             if (entry != null) {
                 entry.insert_at_cursor(plain_text);
             } else {
-                this.backing.editor.insert_text(plain_text);
+                this.backing.editor.body.insert_text(plain_text);
             }
         }
 
@@ -450,7 +450,7 @@ public class Application.PluginManager : GLib.Object {
         public void append_menu_item(Plugin.Actionable menu_item) {
             if (this.menu_items == null) {
                 this.menu_items = new GLib.Menu();
-                this.backing.insert_menu_section(this.menu_items);
+                this.backing.editor.insert_menu_section(this.menu_items);
             }
             this.menu_items.append(
                 menu_item.label,
@@ -494,7 +494,7 @@ public class Application.PluginManager : GLib.Object {
             }
 
             this.action_bar.show_all();
-            this.backing.add_action_bar(this.action_bar);
+            this.backing.editor.add_action_bar(this.action_bar);
         }
 
         private Gtk.Widget? widget_for_item(Plugin.ActionBar.Item item) {
diff --git a/src/client/composer/composer-editor.vala b/src/client/composer/composer-editor.vala
new file mode 100644
index 000000000..e47ba9405
--- /dev/null
+++ b/src/client/composer/composer-editor.vala
@@ -0,0 +1,707 @@
+/*
+ * Copyright © 2016 Software Freedom Conservancy Inc.
+ * Copyright © 2017-2020 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.
+ */
+
+[CCode (cname = "components_reflow_box_get_type")]
+private extern Type components_reflow_box_get_type();
+
+/**
+ * A widget for editing the body of an email message.
+ */
+[GtkTemplate (ui = "/org/gnome/Geary/composer-editor.ui")]
+public class Composer.Editor : Gtk.Grid, Geary.BaseInterface {
+
+    private const string ACTION_BOLD = "bold";
+    private const string ACTION_COLOR = "color";
+    private const string ACTION_COPY_LINK = "copy-link";
+    private const string ACTION_CUT = "cut";
+    private const string ACTION_FONT_FAMILY = "font-family";
+    private const string ACTION_FONT_SIZE = "font-size";
+    private const string ACTION_INDENT = "indent";
+    private const string ACTION_INSERT_IMAGE = "insert-image";
+    private const string ACTION_INSERT_LINK = "insert-link";
+    private const string ACTION_ITALIC = "italic";
+    private const string ACTION_JUSTIFY = "justify";
+    private const string ACTION_OLIST = "olist";
+    private const string ACTION_OPEN_INSPECTOR = "open_inspector";
+    private const string ACTION_OUTDENT = "outdent";
+    private const string ACTION_PASTE = "paste";
+    private const string ACTION_PASTE_WITHOUT_FORMATTING = "paste-without-formatting";
+    private const string ACTION_REMOVE_FORMAT = "remove-format";
+    private const string ACTION_SELECT_ALL = "select-all";
+    private const string ACTION_SELECT_DICTIONARY = "select-dictionary";
+    private const string ACTION_SHOW_FORMATTING = "show-formatting";
+    private const string ACTION_STRIKETHROUGH = "strikethrough";
+    internal const string ACTION_TEXT_FORMAT = "text-format";
+    private const string ACTION_ULIST = "ulist";
+    private const string ACTION_UNDERLINE = "underline";
+
+    // 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_OLIST, ACTION_ULIST
+    };
+
+    private const ActionEntry[] ACTIONS = {
+        { Action.Edit.COPY,                on_copy                            },
+        { Action.Edit.REDO,                on_redo                            },
+        { Action.Edit.UNDO,                on_undo                            },
+        { ACTION_BOLD,                     on_action,        null, "false"    },
+        { ACTION_COLOR,                    on_select_color                    },
+        { ACTION_COPY_LINK,                on_copy_link                       },
+        { ACTION_CUT,                      on_cut                             },
+        { ACTION_FONT_FAMILY,              on_font_family,   "s",  "'sans'"   },
+        { ACTION_FONT_SIZE,                on_font_size,     "s",  "'medium'" },
+        { ACTION_INDENT,                   on_indent                          },
+        { ACTION_INSERT_IMAGE,             on_insert_image                    },
+        { ACTION_INSERT_LINK,              on_insert_link                     },
+        { ACTION_ITALIC,                   on_action,        null, "false"    },
+        { ACTION_JUSTIFY,                  on_justify,       "s",  "'left'"   },
+        { ACTION_OLIST,                    on_olist                           },
+        { ACTION_OPEN_INSPECTOR,           on_open_inspector                  },
+        { ACTION_OUTDENT,                  on_action                          },
+        { ACTION_PASTE,                    on_paste                           },
+        { ACTION_PASTE_WITHOUT_FORMATTING, on_paste_without_formatting        },
+        { ACTION_REMOVE_FORMAT,            on_remove_format, null, "false"    },
+        { ACTION_SELECT_ALL,               on_select_all                      },
+        { ACTION_SELECT_DICTIONARY,        on_select_dictionary              },
+        { ACTION_SHOW_FORMATTING,          on_toggle_action, null, "false",
+                                           on_show_formatting                 },
+        { ACTION_STRIKETHROUGH,            on_action,        null, "false"    },
+        { ACTION_TEXT_FORMAT,              null,             "s", "'html'",
+                                           on_text_format                     },
+        { ACTION_ULIST,                    on_ulist                           },
+        { ACTION_UNDERLINE,                on_action,        null, "false"    },
+    };
+
+    public static void add_accelerators(Application.Client application) {
+        application.add_edit_accelerators(ACTION_CUT, { "<Ctrl>x" } );
+        application.add_edit_accelerators(ACTION_PASTE, { "<Ctrl>v" } );
+        application.add_edit_accelerators(ACTION_PASTE_WITHOUT_FORMATTING, { "<Ctrl><Shift>v" } );
+        application.add_edit_accelerators(ACTION_INSERT_IMAGE, { "<Ctrl>g" } );
+        application.add_edit_accelerators(ACTION_INSERT_LINK, { "<Ctrl>l" } );
+        application.add_edit_accelerators(ACTION_INDENT, { "<Ctrl>bracketright" } );
+        application.add_edit_accelerators(ACTION_OUTDENT, { "<Ctrl>bracketleft" } );
+        application.add_edit_accelerators(ACTION_REMOVE_FORMAT, { "<Ctrl>space" } );
+        application.add_edit_accelerators(ACTION_BOLD, { "<Ctrl>b" } );
+        application.add_edit_accelerators(ACTION_ITALIC, { "<Ctrl>i" } );
+        application.add_edit_accelerators(ACTION_UNDERLINE, { "<Ctrl>u" } );
+        application.add_edit_accelerators(ACTION_STRIKETHROUGH, { "<Ctrl>k" } );
+    }
+
+
+    /** The email body view. */
+    public WebView body { get; private set; }
+
+    internal GLib.SimpleActionGroup actions = new GLib.SimpleActionGroup();
+
+    [GtkChild] internal Gtk.Button new_message_attach_button;
+    [GtkChild] internal Gtk.Box conversation_attach_buttons;
+
+    private Application.Configuration config;
+
+    private string? pointer_url = null;
+    private string? cursor_url = null;
+
+    // Timeout for showing the slow image paste pulsing bar
+    private Geary.TimeoutManager show_background_work_timeout = null;
+    // Timer for pulsing progress bar
+    private Geary.TimeoutManager background_work_pulse;
+
+    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;
+
+    [GtkChild] private Gtk.Grid body_container;
+
+    [GtkChild] private Gtk.Label message_overlay_label;
+
+    [GtkChild] private Gtk.Box action_bar_box;
+
+    [GtkChild] private Gtk.Button insert_link_button;
+    [GtkChild] private Gtk.MenuButton select_dictionary_button;
+
+    [GtkChild] private Gtk.Label info_label;
+
+    [GtkChild] private Gtk.ProgressBar background_progress;
+
+    [GtkChild] private Gtk.Revealer formatting;
+    [GtkChild] private Gtk.MenuButton font_button;
+    [GtkChild] private Gtk.Stack font_button_stack;
+    [GtkChild] private Gtk.MenuButton font_size_button;
+    [GtkChild] private Gtk.Image font_color_icon;
+    [GtkChild] private Gtk.MenuButton more_options_button;
+
+
+    internal signal void insert_image(bool from_clipboard);
+
+
+    internal Editor(Application.Configuration config) {
+        base_ref();
+        components_reflow_box_get_type();
+        this.config = config;
+
+        Gtk.Builder builder = new Gtk.Builder.from_resource(
+            "/org/gnome/Geary/composer-editor-menus.ui"
+        );
+        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.body = new WebView(config);
+        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.mouse_target_changed.connect(on_mouse_target_changed);
+        this.body.selection_changed.connect(on_selection_changed);
+        this.body.set_hexpand(true);
+        this.body.set_vexpand(true);
+        this.body.show();
+        this.body_container.add(this.body);
+
+        this.actions.add_action_entries(ACTIONS, this);
+        this.actions.change_action_state(
+            ACTION_TEXT_FORMAT,
+            config.compose_as_html ? "html" : "plain"
+        );
+        this.actions.change_action_state(
+            ACTION_SHOW_FORMATTING,
+            config.formatting_toolbar_visible
+        );
+        insert_action_group(Action.Edit.GROUP_NAME, this.actions);
+        get_action(Action.Edit.UNDO).set_enabled(false);
+        get_action(Action.Edit.REDO).set_enabled(false);
+        update_cursor_actions();
+
+        var spell_check_popover = new SpellCheckPopover(
+            this.select_dictionary_button, config
+        );
+        spell_check_popover.selection_changed.connect((active_langs) => {
+            config.set_spell_check_languages(active_langs);
+        });
+
+        this.show_background_work_timeout = new Geary.TimeoutManager.milliseconds(
+            Util.Gtk.SHOW_PROGRESS_TIMEOUT_MSEC, this.on_background_work_timeout
+        );
+        this.background_work_pulse = new Geary.TimeoutManager.milliseconds(
+            Util.Gtk.PROGRESS_PULSE_TIMEOUT_MSEC, this.background_progress.pulse
+        );
+        this.background_work_pulse.repetition = FOREVER;
+    }
+
+    ~Editor() {
+        base_unref();
+    }
+
+    public override void destroy() {
+        this.show_background_work_timeout.reset();
+        this.background_work_pulse.reset();
+        base.destroy();
+    }
+
+    /** Adds an action bar to the composer. */
+    public void add_action_bar(Gtk.ActionBar to_add) {
+        this.action_bar_box.pack_start(to_add);
+        this.action_bar_box.reorder_child(to_add, 0);
+    }
+
+    /**
+     * Inserts a menu section into the editor's menu.
+     */
+    public void insert_menu_section(GLib.MenuModel section) {
+        var menu = this.more_options_button.menu_model as GLib.Menu;
+        if (menu != null) {
+            menu.insert_section(0, null, section);
+        }
+    }
+
+    /** Displays the given human readable text in the UI */
+    internal void set_info_label(string text) {
+        this.info_label.set_text(text);
+        this.info_label.set_tooltip_text(text);
+    }
+
+    /** Starts the progress meter timer. */
+    internal void start_background_work_pulse() {
+        this.show_background_work_timeout.start();
+    }
+
+    /** Hides and stops pulsing the progress meter. */
+    internal void stop_background_work_pulse() {
+        this.background_progress.hide();
+        this.background_work_pulse.reset();
+        this.show_background_work_timeout.reset();
+    }
+
+    private void update_cursor_actions() {
+        bool has_selection = this.body.has_selection;
+        get_action(ACTION_CUT).set_enabled(has_selection);
+        get_action(Action.Edit.COPY).set_enabled(has_selection);
+
+        get_action(ACTION_INSERT_LINK).set_enabled(
+            this.body.is_rich_text && (has_selection || this.cursor_url != null)
+        );
+        get_action(ACTION_REMOVE_FORMAT).set_enabled(
+            this.body.is_rich_text && has_selection
+        );
+    }
+
+    private async LinkPopover new_link_popover(LinkPopover.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);
+        }
+        LinkPopover popover = new LinkPopover(type);
+        popover.set_link_url(url);
+        popover.closed.connect(() => {
+                this.body.free_selection(selection_id);
+            });
+        popover.hide.connect(() => {
+                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(selection_id);
+            });
+        return popover;
+    }
+
+    private void update_formatting_toolbar() {
+        var show_formatting = (SimpleAction) this.actions.lookup_action(ACTION_SHOW_FORMATTING);
+        var text_format = (SimpleAction) this.actions.lookup_action(ACTION_TEXT_FORMAT);
+        this.formatting.reveal_child = text_format.get_state().get_string() == "html" && 
show_formatting.get_state().get_boolean();
+    }
+
+    private async void update_color_icon(Gdk.RGBA color) {
+        var theme = Gtk.IconTheme.get_default();
+        var icon = theme.lookup_icon("font-color-symbolic", 16, 0);
+        var fg_color = Util.Gtk.rgba(0, 0, 0, 1);
+        this.get_style_context().lookup_color("theme_fg_color", out fg_color);
+
+        try {
+            var pixbuf = yield icon.load_symbolic_async(
+                fg_color, color, null, null, null
+            );
+            this.font_color_icon.pixbuf = pixbuf;
+        } catch(Error e) {
+            warning("Could not load icon `font-color-symbolic`!");
+            this.font_color_icon.icon_name = "font-color-symbolic";
+        }
+    }
+
+    private GLib.SimpleAction? get_action(string action_name) {
+        return this.actions.lookup_action(action_name) as GLib.SimpleAction;
+    }
+
+    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.config.compose_as_html) {
+            Gdk.EventButton? button = (Gdk.EventButton) event;
+            Gdk.Rectangle location = Gdk.Rectangle();
+            location.x = (int) button.x;
+            location.y = (int) button.y;
+
+            this.new_link_popover.begin(
+                LinkPopover.Type.EXISTING_LINK, this.pointer_url,
+                (obj, res) => {
+                    LinkPopover popover = this.new_link_popover.end(res);
+                    popover.set_relative_to(this.body);
+                    popover.set_pointing_to(location);
+                    popover.popup();
+                });
+        }
+        return Gdk.EVENT_PROPAGATE;
+    }
+
+    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,
+            WebKit.ContextMenuAction.INSERT_EMOJI,
+        };
+
+        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.
+
+        Util.Gtk.menu_foreach(
+            this.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.body.is_rich_text)
+                        append_menu_section(context_menu, section);
+                } else if (section == this.context_menu_plain_text) {
+                    if (!this.body.is_rich_text)
+                        append_menu_section(context_menu, section);
+                } else if (section == this.context_menu_inspector) {
+                    if (this.config.enable_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) {
+        Util.Gtk.menu_foreach(section, (label, name, target, section) => {
+                string simple_name = name;
+                if ("." in simple_name) {
+                    simple_name = simple_name.split(".")[1];
+                }
+
+                GLib.SimpleAction? action = get_action(simple_name);
+                if (action != null) {
+                    context_menu.append(
+                        new WebKit.ContextMenuItem.from_gaction(
+                            action, label, target
+                        )
+                    );
+                } else {
+                    warning("Unknown action: %s/%s", name, label);
+                }
+            });
+    }
+
+    private void on_cursor_context_changed(WebView.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
+        );
+
+        this.update_color_icon.begin(context.font_color);
+
+        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_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 ?? "";
+        this.message_overlay_label.set_visible(copy_link_enabled);
+        get_action(ACTION_COPY_LINK).set_enabled(copy_link_enabled);
+    }
+
+    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
+        );
+    }
+
+    /** Shows and starts pulsing the progress meter. */
+    private void on_background_work_timeout() {
+        this.background_progress.fraction = 0.0;
+        this.background_work_pulse.start();
+        this.background_progress.show();
+    }
+
+    /////////////// Editing action callbacks /////////////////
+
+    private void on_text_format(SimpleAction? action, Variant? new_state) {
+        bool compose_as_html = new_state.get_string() == "html";
+        action.set_state(new_state.get_string());
+
+        foreach (string html_action in HTML_ACTIONS)
+            get_action(html_action).set_enabled(compose_as_html);
+
+        update_cursor_actions();
+
+        var show_formatting = get_action(ACTION_SHOW_FORMATTING);
+        show_formatting.set_enabled(compose_as_html);
+        update_formatting_toolbar();
+
+        this.body.set_rich_text(compose_as_html);
+
+        this.config.compose_as_html = compose_as_html;
+        this.more_options_button.popover.popdown();
+    }
+
+    private void on_show_formatting(GLib.SimpleAction? action,
+                                    GLib.Variant? new_state) {
+        bool show_formatting = new_state.get_boolean();
+        this.config.formatting_toolbar_visible = show_formatting;
+        action.set_state(new_state);
+
+        update_formatting_toolbar();
+        this.update_color_icon.begin(Util.Gtk.rgba(0, 0, 0, 0));
+    }
+
+    private void on_select_dictionary(SimpleAction action, Variant? param) {
+        this.select_dictionary_button.toggled();
+    }
+
+    private void on_command_state_changed(bool can_undo, bool can_redo) {
+        get_action(Action.Edit.UNDO).set_enabled(can_undo);
+        get_action(Action.Edit.REDO).set_enabled(can_redo);
+    }
+
+    private void on_selection_changed(bool has_selection) {
+        update_cursor_actions();
+    }
+
+    private void on_undo() {
+        this.body.undo();
+    }
+
+    private void on_redo() {
+        this.body.redo();
+    }
+
+    private void on_cut() {
+        this.body.cut_clipboard();
+    }
+
+    private void on_copy() {
+        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 URLn as from the action param
+        c.set_text(this.pointer_url, -1);
+        c.store();
+    }
+
+    private void on_paste() {
+        if (this.body.is_rich_text) {
+            // Check for pasted image in clipboard
+            Gtk.Clipboard clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD);
+            bool has_image = clipboard.wait_is_image_available();
+            if (has_image) {
+                insert_image(true);
+            } else {
+                this.body.paste_rich_text();
+            }
+        } else {
+            this.body.paste_plain_text();
+        }
+    }
+
+    private void on_paste_without_formatting(SimpleAction action, Variant? param) {
+        this.body.paste_plain_text();
+    }
+
+    private void on_select_all(SimpleAction action, Variant? param) {
+        this.body.select_all();
+    }
+
+    private void on_indent() {
+        this.body.indent_line();
+    }
+
+    private void on_olist() {
+        this.body.insert_olist();
+    }
+
+    private void on_ulist() {
+        this.body.insert_ulist();
+    }
+
+    private void on_justify(GLib.Action action, GLib.Variant? param) {
+        this.body.execute_editing_command("justify" + param.get_string());
+    }
+
+    private void on_insert_image() {
+        insert_image(false);
+    }
+
+    private void on_insert_link() {
+        LinkPopover.Type type = LinkPopover.Type.NEW_LINK;
+        string url = "https://";;
+        if (this.cursor_url != null) {
+            type = LinkPopover.Type.EXISTING_LINK;
+            url = this.cursor_url;
+        }
+
+        this.new_link_popover.begin(type, url, (obj, res) => {
+                LinkPopover popover = this.new_link_popover.end(res);
+
+                var style = this.insert_link_button.get_style_context();
+
+                // 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);
+                        style.set_state(NORMAL);
+                    });
+
+                popover.set_relative_to(this.insert_link_button);
+                popover.popup();
+                style.set_state(ACTIVE);
+            });
+    }
+
+    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_font_family(GLib.SimpleAction action, GLib.Variant? param) {
+        string font = param.get_string();
+        this.body.execute_editing_command_with_argument(
+            "fontname", font
+        );
+        action.set_state(font);
+
+        this.font_button_stack.visible_child_name = font;
+        this.font_button.popover.popdown();
+    }
+
+    private void on_font_size(GLib.SimpleAction action, GLib.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());
+
+        this.font_size_button.popover.popdown();
+    }
+
+    private void on_select_color() {
+        var dialog = new Gtk.ColorChooserDialog(
+            _("Select Color"),
+            get_toplevel() as Gtk.Window
+        );
+        if (dialog.run() == Gtk.ResponseType.OK) {
+            var rgba = dialog.get_rgba();
+            this.body.execute_editing_command_with_argument(
+                "forecolor", rgba.to_string()
+            );
+
+            this.update_color_icon.begin(rgba);
+        }
+        dialog.destroy();
+    }
+
+    private void on_action(GLib.SimpleAction action, GLib.Variant? param) {
+        // Uses the unprefixed name as a command for the web view
+        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);
+    }
+
+    private void on_toggle_action(GLib.SimpleAction? action,
+                                  GLib.Variant? param) {
+        action.change_state(!action.state.get_boolean());
+    }
+
+    private void on_open_inspector() {
+        this.body.get_inspector().show();
+    }
+
+}
diff --git a/src/client/composer/composer-embed.vala b/src/client/composer/composer-embed.vala
index 26f6a3367..229745d7a 100644
--- a/src/client/composer/composer-embed.vala
+++ b/src/client/composer/composer-embed.vala
@@ -147,9 +147,10 @@ public class Composer.Embed : Gtk.EventBox, Container {
                     // 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 scrolled_height = this.outer_scroller.get_allocated_height();
+                    var body = this.composer.editor.body;
+                    int editor_height = body.get_allocated_height();
+                    int editor_preferred = body.preferred_height;
+                    int scrolled_height = body.get_allocated_height();
 
                     if (alloc.height < scrolled_height &&
                         editor_height < editor_preferred) {
diff --git a/src/client/composer/composer-widget.vala b/src/client/composer/composer-widget.vala
index addda1277..ecc3fbfdc 100644
--- a/src/client/composer/composer-widget.vala
+++ b/src/client/composer/composer-widget.vala
@@ -11,9 +11,6 @@ private errordomain AttachmentError {
     DUPLICATE
 }
 
-[CCode (cname = "components_reflow_box_get_type")]
-private extern Type components_reflow_box_get_type();
-
 /**
  * A widget for editing an email message.
  *
@@ -118,7 +115,6 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
 
     private enum DraftPolicy { DISCARD, KEEP }
 
-
     private class FromAddressMap {
         public Application.AccountContext account;
         public Geary.RFC822.MailboxAddresses from;
@@ -135,105 +131,37 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
     // well. This could probably be fixed by pulling both the main
     // window's and composer's actions out of the 'win' action
     // namespace, leaving only common window actions there.
+    private const string ACTION_ADD_ATTACHMENT = "add-attachment";
+    private const string ACTION_ADD_ORIGINAL_ATTACHMENTS = "add-original-attachments";
     private const string ACTION_CLOSE = "composer-close";
     private const string ACTION_CUT = "cut";
-    private const string ACTION_COPY_LINK = "copy-link";
-    private const string ACTION_PASTE = "paste";
-    private const string ACTION_PASTE_WITHOUT_FORMATTING = "paste-without-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_OLIST = "olist";
-    private const string ACTION_ULIST = "ulist";
-    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_TEXT_FORMAT = "text-format";
-    private const string ACTION_SHOW_EXTENDED_HEADERS = "show-extended-headers";
-    private const string ACTION_SHOW_FORMATTING = "show-formatting";
-    private const string ACTION_DISCARD = "discard";
     private const string ACTION_DETACH = "detach";
+    private const string ACTION_DISCARD = "discard";
+    private const string ACTION_PASTE = "paste";
     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_OLIST, ACTION_ULIST
-    };
-
-    private const ActionEntry[] EDITOR_ACTIONS = {
-        { Action.Edit.COPY,                on_copy                            },
-        { Action.Edit.REDO,                on_redo                            },
-        { Action.Edit.UNDO,                on_undo                            },
-        { ACTION_BOLD,                     on_action,        null, "false"    },
-        { ACTION_COLOR,                    on_select_color                    },
-        { ACTION_COPY_LINK,                on_copy_link                       },
-        { ACTION_CUT,                      on_cut                             },
-        { ACTION_FONT_FAMILY,              on_font_family,   "s",  "'sans'"   },
-        { ACTION_FONT_SIZE,                on_font_size,     "s",  "'medium'" },
-        { ACTION_INDENT,                   on_indent                          },
-        { ACTION_INSERT_IMAGE,             on_insert_image                    },
-        { ACTION_INSERT_LINK,              on_insert_link                     },
-        { ACTION_ITALIC,                   on_action,        null, "false"    },
-        { ACTION_JUSTIFY,                  on_justify,       "s",  "'left'"   },
-        { ACTION_OLIST,                    on_olist                           },
-        { ACTION_OUTDENT,                  on_action                          },
-        { ACTION_PASTE,                    on_paste                           },
-        { ACTION_PASTE_WITHOUT_FORMATTING, on_paste_without_formatting        },
-        { ACTION_REMOVE_FORMAT,            on_remove_format, null, "false"    },
-        { ACTION_SELECT_ALL,               on_select_all                      },
-        { ACTION_STRIKETHROUGH,            on_action,        null, "false"    },
-        { ACTION_ULIST,                    on_ulist                           },
-        { ACTION_UNDERLINE,                on_action,        null, "false"    },
-    };
+    private const string ACTION_SHOW_EXTENDED_HEADERS = "show-extended-headers";
 
-    private const ActionEntry[] COMPOSER_ACTIONS = {
-        { Action.Window.CLOSE,             on_close                                                   },
-        { ACTION_ADD_ATTACHMENT,           on_add_attachment                                          },
-        { ACTION_ADD_ORIGINAL_ATTACHMENTS, on_pending_attachments                                     },
-        { ACTION_CLOSE,                    on_close                                                   },
-        { ACTION_DISCARD,                  on_discard                                       },
-        { ACTION_TEXT_FORMAT,              null,            "s", "'html'", on_text_format             },
-        { ACTION_DETACH,                   on_detach                                                  },
-        { ACTION_OPEN_INSPECTOR,           on_open_inspector                  },
-        { ACTION_SELECT_DICTIONARY,        on_select_dictionary                                       },
-        { ACTION_SEND,                     on_send                                                    },
-        { ACTION_SHOW_EXTENDED_HEADERS,    on_toggle_action, null, "false", on_show_extended_headers_toggled 
},
-        { ACTION_SHOW_FORMATTING,          on_toggle_action, null, "false", on_show_formatting        },
+    private const ActionEntry[] ACTIONS = {
+        { Action.Edit.COPY,                on_copy                          },
+        { Action.Window.CLOSE,             on_close                         },
+        { ACTION_ADD_ATTACHMENT,           on_add_attachment                },
+        { ACTION_ADD_ORIGINAL_ATTACHMENTS, on_pending_attachments           },
+        { ACTION_CLOSE,                    on_close                         },
+        { ACTION_CUT,                      on_cut                           },
+        { ACTION_DETACH,                   on_detach                        },
+        { ACTION_DISCARD,                  on_discard                       },
+        { ACTION_PASTE,                    on_paste                         },
+        { ACTION_SEND,                     on_send                          },
+        { ACTION_SHOW_EXTENDED_HEADERS,    on_toggle_action, null, "false",
+                                           on_show_extended_headers_toggled },
     };
 
     public static void add_accelerators(Application.Client application) {
         application.add_window_accelerators(ACTION_DISCARD, { "Escape" } );
         application.add_window_accelerators(ACTION_ADD_ATTACHMENT, { "<Ctrl>t" } );
         application.add_window_accelerators(ACTION_DETACH, { "<Ctrl>d" } );
-
-        application.add_edit_accelerators(ACTION_CUT, { "<Ctrl>x" } );
-        application.add_edit_accelerators(ACTION_PASTE, { "<Ctrl>v" } );
-        application.add_edit_accelerators(ACTION_PASTE_WITHOUT_FORMATTING, { "<Ctrl><Shift>v" } );
-        application.add_edit_accelerators(ACTION_INSERT_IMAGE, { "<Ctrl>g" } );
-        application.add_edit_accelerators(ACTION_INSERT_LINK, { "<Ctrl>l" } );
-        application.add_edit_accelerators(ACTION_INDENT, { "<Ctrl>bracketright" } );
-        application.add_edit_accelerators(ACTION_OUTDENT, { "<Ctrl>bracketleft" } );
-        application.add_edit_accelerators(ACTION_REMOVE_FORMAT, { "<Ctrl>space" } );
-        application.add_edit_accelerators(ACTION_BOLD, { "<Ctrl>b" } );
-        application.add_edit_accelerators(ACTION_ITALIC, { "<Ctrl>i" } );
-        application.add_edit_accelerators(ACTION_UNDERLINE, { "<Ctrl>u" } );
-        application.add_edit_accelerators(ACTION_STRIKETHROUGH, { "<Ctrl>k" } );
+        application.add_window_accelerators(ACTION_CUT, { "<Ctrl>x" } );
+        application.add_window_accelerators(ACTION_PASTE, { "<Ctrl>v" } );
     }
 
     private const string DRAFT_SAVED_TEXT = _("Saved");
@@ -259,7 +187,6 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
 
     private const string PASTED_IMAGE_FILENAME_TEMPLATE = "geary-pasted-image-%u.png";
 
-
     /** The account the email is being sent from. */
     public Application.AccountContext sender_context { get; private set; }
 
@@ -282,13 +209,13 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
                 && this.bcc_entry.is_empty
                 && this.reply_to_entry.is_empty
                 && this.subject_entry.buffer.length == 0
-                && this.editor.is_empty
+                && this.editor.body.is_empty
                 && this.attached_files.size == 0;
         }
     }
 
     /** The email body editor widget. */
-    public WebView editor { get; private set; }
+    public Editor editor { get; private set; }
 
     /**
      * The last focused text input widget.
@@ -371,9 +298,6 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
     [GtkChild]
     private Gtk.Grid editor_container;
 
-    [GtkChild]
-    private Gtk.Grid body_container;
-
     [GtkChild]
     private Gtk.Label from_label;
     [GtkChild] private Gtk.Box from_row;
@@ -426,10 +350,6 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
     private Gspell.Checker subject_spell_checker = new Gspell.Checker(null);
     private Gspell.Entry subject_spell_entry;
 
-    [GtkChild]
-    private Gtk.Label message_overlay_label;
-    [GtkChild]
-    private Gtk.Box action_bar_box;
     [GtkChild]
     private Gtk.Box attachments_box;
     [GtkChild]
@@ -445,35 +365,7 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
     [GtkChild]
     private Gtk.Box header_area;
 
-    [GtkChild] private Gtk.Button new_message_attach_button;
-    [GtkChild] private Gtk.Box conversation_attach_buttons;
-
-    [GtkChild] private Gtk.Revealer formatting;
-    [GtkChild] private Gtk.MenuButton font_button;
-    [GtkChild] private Gtk.Stack font_button_stack;
-    [GtkChild] private Gtk.MenuButton font_size_button;
-    [GtkChild] private Gtk.Image font_color_icon;
-    [GtkChild] private Gtk.MenuButton more_options_button;
-
-    [GtkChild]
-    private Gtk.Button insert_link_button;
-    [GtkChild]
-    private Gtk.MenuButton select_dictionary_button;
-    [GtkChild]
-    private Gtk.Label info_label;
-
-    [GtkChild]
-    private Gtk.ProgressBar background_progress;
-
-    private GLib.SimpleActionGroup composer_actions = new GLib.SimpleActionGroup();
-    private GLib.SimpleActionGroup editor_actions = new GLib.SimpleActionGroup();
-
-    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 GLib.SimpleActionGroup actions = new GLib.SimpleActionGroup();
 
     /** Determines if the composer can currently save a draft. */
     private bool can_save {
@@ -489,8 +381,6 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
         }
     }
 
-    private string? pointer_url = null;
-    private string? cursor_url = null;
     private bool is_attachment_overlay_visible = false;
     private bool top_posting = true;
 
@@ -535,12 +425,6 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
 
     private Application.Configuration config;
 
-    // Timeout for showing the slow image paste pulsing bar
-    private Geary.TimeoutManager show_background_work_timeout = null;
-
-    // Timer for pulsing progress bar
-    private Geary.TimeoutManager background_work_pulse;
-
 
     internal Widget(ApplicationInterface application,
                     Application.Configuration config,
@@ -598,26 +482,28 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
         this.subject_spell_entry = Gspell.Entry.get_from_gtk_entry(
             this.subject_entry
         );
+        config.settings.changed[
+            Application.Configuration.SPELL_CHECK_LANGUAGES
+        ].connect(() => {
+                update_subject_spell_checker();
+            });
         update_subject_spell_checker();
 
-        this.editor = new WebView(config);
-        this.editor.set_hexpand(true);
-        this.editor.set_vexpand(true);
-        this.editor.content_loaded.connect(on_editor_content_loaded);
-        this.editor.show();
-
-        this.body_container.add(this.editor);
-
-        // Initialize menus
-        Gtk.Builder builder = new Gtk.Builder.from_resource(
-            "/org/gnome/Geary/composer-menus.ui"
+        this.editor = new Editor(config);
+        this.editor.insert_image.connect(
+            (from_clipboard) => {
+                if (from_clipboard) {
+                    paste_image();
+                } else {
+                    insert_image();
+                }
+            }
         );
-        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.editor.body.content_loaded.connect(on_content_loaded);
+        this.editor.body.document_modified.connect(() => { draft_changed(); });
+        this.editor.body.key_press_event.connect(on_editor_key_press_event);
+        this.editor.show();
+        this.editor_container.add(this.editor);
 
         // Listen to account signals to update from menu.
         this.application.account_available.connect(
@@ -628,7 +514,7 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
         );
 
         // Listen for drag and dropped image file
-        this.editor.image_file_dropped.connect(
+        this.editor.body.image_file_dropped.connect(
             on_image_file_dropped
         );
 
@@ -643,7 +529,16 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
         );
 
         // Add actions once every element has been initialized and added
-        initialize_actions();
+        // Composer actions
+        this.actions.add_action_entries(ACTIONS, this);
+        this.actions.change_action_state(
+            ACTION_SHOW_EXTENDED_HEADERS, false
+        );
+        // Main actions use the window prefix so they override main
+        // window actions. But for some reason, we can't use the same
+        // prefix for the headerbar.
+        insert_action_group(Action.Window.GROUP_NAME, this.actions);
+        this.header.insert_action_group("cmh", this.actions);
         validate_send_button();
 
         // Connect everything (can only happen after actions were added)
@@ -652,42 +547,12 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
         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.content_loaded.connect(on_content_loaded);
-        this.editor.mouse_target_changed.connect(on_mouse_target_changed);
-        this.editor.selection_changed.connect(on_selection_changed);
-
-        this.show_background_work_timeout = new Geary.TimeoutManager.milliseconds(
-            Util.Gtk.SHOW_PROGRESS_TIMEOUT_MSEC, this.on_background_work_timeout
-        );
-        this.background_work_pulse = new Geary.TimeoutManager.milliseconds(
-            Util.Gtk.PROGRESS_PULSE_TIMEOUT_MSEC, this.background_progress.pulse
-        );
-        this.background_work_pulse.repetition = FOREVER;
-
         // Set the from_multiple combo box to ellipsize. This can't be done
         // from the .ui file.
         var cells = this.from_multiple.get_cells();
         ((Gtk.CellRendererText) cells.data).ellipsize = END;
 
-        // Create spellcheck popover
-        var spell_check_popover = new SpellCheckPopover(
-            this.select_dictionary_button, config
-        );
-        spell_check_popover.selection_changed.connect((active_langs) => {
-            config.set_spell_check_languages(active_langs);
-            update_subject_spell_checker();
-        });
-
         load_entry_completions();
-
-        update_color_icon.begin(Util.Gtk.rgba(0, 0, 0, 0));
     }
 
     ~Widget() {
@@ -935,8 +800,8 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
         // the main window. The workaround here sets a new menu
         // model and hence the menu_button constructs a new
         // popover.
-        this.composer_actions.change_action_state(
-            ACTION_TEXT_FORMAT,
+        this.editor.actions.change_action_state(
+            Editor.ACTION_TEXT_FORMAT,
             this.config.compose_as_html ? "html" : "plain"
         );
 
@@ -1085,10 +950,6 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
         this.application.account_unavailable.disconnect(
             on_account_unavailable
         );
-
-        this.show_background_work_timeout.reset();
-        this.background_work_pulse.reset();
-
         base.destroy();
     }
 
@@ -1116,22 +977,6 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
         }
     }
 
-    /**
-     * Inserts a menu section into the composer's menu.
-     */
-    public void insert_menu_section(GLib.MenuModel section) {
-        var menu = this.more_options_button.menu_model as GLib.Menu;
-        if (menu != null) {
-            menu.insert_section(0, null, section);
-        }
-    }
-
-    /** Adds an action bar to the composer. */
-    public void add_action_bar(Gtk.ActionBar to_add) {
-        this.action_bar_box.pack_start(to_add);
-        this.action_bar_box.reorder_child(to_add, 0);
-    }
-
     /** Overrides the draft folder as a destination for saving. */
     public async void set_save_to_override(Geary.Folder? save_to)
         throws GLib.Error {
@@ -1230,7 +1075,7 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
             // show full fields.
             if (this.bcc_entry.is_modified ||
                 this.reply_to_entry.is_modified) {
-                this.editor_actions.change_action_state(
+                this.actions.change_action_state(
                     ACTION_SHOW_EXTENDED_HEADERS, true
                 );
             }
@@ -1254,69 +1099,16 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
         } else {
             // Need to grab the focus after the content has finished
             // loading otherwise the text caret will not be visible.
-            if (this.editor.is_content_loaded) {
-                this.editor.grab_focus();
+            if (this.editor.body.is_content_loaded) {
+                this.editor.body.grab_focus();
             } else {
-                this.editor.content_loaded.connect(() => {
-                        this.editor.grab_focus();
+                this.editor.body.content_loaded.connect(() => {
+                        this.editor.body.grab_focus();
                     });
             }
         }
     }
 
-    // Initializes all actions and adds them to the action group
-    private void initialize_actions() {
-        // Composer actions
-        this.composer_actions.add_action_entries(COMPOSER_ACTIONS, this);
-        // Main actions use the window prefix so they override main
-        // window actions. But for some reason, we can't use the same
-        // prefix for the headerbar.
-        insert_action_group(Action.Window.GROUP_NAME, this.composer_actions);
-        this.header.insert_action_group("cmh", this.composer_actions);
-
-        // Editor actions - scoped to the editor only.
-        this.editor_actions.add_action_entries(EDITOR_ACTIONS, this);
-        this.editor_container.insert_action_group(
-            Action.Edit.GROUP_NAME, this.editor_actions
-        );
-
-        GLib.SimpleActionGroup[] composer_action_entries_users = {
-            this.editor_actions, this.composer_actions
-        };
-        foreach (var entries_users in composer_action_entries_users) {
-            entries_users.change_action_state(
-                ACTION_SHOW_EXTENDED_HEADERS, false
-            );
-            entries_users.change_action_state(
-                ACTION_TEXT_FORMAT,
-                this.config.compose_as_html ? "html" : "plain"
-            );
-        }
-
-        this.composer_actions.change_action_state(
-            ACTION_SHOW_FORMATTING,
-            this.config.formatting_toolbar_visible
-        );
-
-        get_action(Action.Edit.UNDO).set_enabled(false);
-        get_action(Action.Edit.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.Edit.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
-        );
-    }
-
     private bool update_from_address(Geary.RFC822.MailboxAddresses? referred_addresses) {
         if (referred_addresses != null) {
             var senders = this.sender_context.account.information.sender_mailboxes;
@@ -1332,8 +1124,9 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
     }
 
     private void on_content_loaded() {
+        this.update_signature.begin(null);
         if (this.can_delete_quote) {
-            this.editor.selection_changed.connect(
+            this.editor.body.selection_changed.connect(
                 () => {
                     this.can_delete_quote = false;
                 }
@@ -1366,7 +1159,7 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
         var window = get_toplevel() as Gtk.Window;
         if (window != null) {
             Gtk.Widget? last_focused = window.get_focus();
-            if (last_focused == this.editor ||
+            if (last_focused == this.editor.body ||
                 (last_focused is Gtk.Entry && last_focused.is_ancestor(this))) {
                 this.focused_input_widget = last_focused;
             }
@@ -1459,13 +1252,13 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
         email.img_src_prefix = ClientWebView.INTERNAL_URL_PREFIX;
 
         try {
-            email.body_text = yield this.editor.get_text();
+            email.body_text = yield this.editor.body.get_text();
             if (for_draft) {
                 // Must save HTML even if in plain text mode since we
                 // need it to restore body/sig/reply state
-                email.body_html = yield this.editor.get_html_for_draft();
-            } else if (this.editor.is_rich_text) {
-                email.body_html = yield this.editor.get_html();
+                email.body_html = yield this.editor.body.get_html_for_draft();
+            } else if (this.editor.body.is_rich_text) {
+                email.body_html = yield this.editor.body.get_html();
             }
         } catch (Error error) {
             debug("Error getting composer message body: %s", error.message);
@@ -1494,7 +1287,7 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
 
         // Always use reply styling, since forward styling doesn't
         // work for inline quotes
-        this.editor.insert_html(
+        this.editor.body.insert_html(
             Util.Email.quote_email_for_reply(
                 referred,
                 to_quote,
@@ -1615,7 +1408,7 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
         update_attachments_view();
         update_pending_attachments(this.pending_include, true);
 
-        this.editor.load_html(
+        this.editor.body.load_html(
             body,
             quote,
             this.top_posting,
@@ -1635,7 +1428,9 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
         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);
         }
@@ -1648,7 +1443,7 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
         } 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(
                        string.join(
                            "|",
                            ATTACHMENT_KEYWORDS,
@@ -1679,7 +1474,7 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
         set_enabled(false);
 
         try {
-            yield this.editor.clean_content();
+            yield this.editor.body.clean_content();
             yield this.application.send_composed_email(this);
             yield close_draft_manager(DISCARD);
 
@@ -1925,7 +1720,7 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
                         if (content_id != null) {
                             Geary.Memory.FileBuffer file_buffer = new Geary.Memory.FileBuffer(file, true);
                             this.cid_files[content_id] = file_buffer;
-                            this.editor.add_internal_resource(
+                            this.editor.body.add_internal_resource(
                                 content_id, file_buffer
                             );
                         } else {
@@ -1962,8 +1757,8 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
             }
         }
 
-        this.new_message_attach_button.visible = !manual_enabled;
-        this.conversation_attach_buttons.visible = manual_enabled;
+        this.editor.new_message_attach_button.visible = !manual_enabled;
+        this.editor.conversation_attach_buttons.visible = manual_enabled;
 
         return have_added;
     }
@@ -2031,7 +1826,7 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
         }
 
         this.inline_files[unique_contentid] = target;
-        this.editor.add_internal_resource(
+        this.editor.body.add_internal_resource(
             unique_contentid, target
         );
     }
@@ -2097,6 +1892,81 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
         draft_changed();
     }
 
+    /**
+     * Handle a pasted image, adding it as an inline attachment
+     */
+    private void paste_image() {
+        // The slow operations here are creating the PNG and, to a lesser extent,
+        // requesting the image from the clipboard
+        this.editor.start_background_work_pulse();
+
+        get_clipboard(Gdk.SELECTION_CLIPBOARD).request_image((clipboard, pixbuf) => {
+            if (pixbuf != null) {
+                MemoryOutputStream os = new MemoryOutputStream(null);
+                pixbuf.save_to_stream_async.begin(os, "png", null, (obj, res) => {
+                    try {
+                        pixbuf.save_to_stream_async.end(res);
+                        os.close();
+
+                        Geary.Memory.ByteBuffer byte_buffer = new 
Geary.Memory.ByteBuffer.from_memory_output_stream(os);
+
+                        GLib.DateTime time_now = new GLib.DateTime.now();
+                        string filename = PASTED_IMAGE_FILENAME_TEMPLATE.printf(time_now.hash());
+
+                        string unique_filename;
+                        add_inline_part(byte_buffer, filename, out unique_filename);
+                        this.editor.body.insert_image(
+                            ClientWebView.INTERNAL_URL_PREFIX + unique_filename
+                        );
+                    } catch (Error error) {
+                        this.application.report_problem(
+                            new Geary.ProblemReport(error)
+                        );
+                    }
+
+                    this.editor.stop_background_work_pulse();
+                });
+            } else {
+                warning("Failed to get image from clipboard");
+                this.editor.stop_background_work_pulse();
+            }
+        });
+    }
+
+    /**
+     * Handle prompting for an inserting images as inline attachments
+     */
+    private void insert_image() {
+        AttachmentDialog dialog = new AttachmentDialog(
+            this.container.top_window, this.config
+        );
+        Gtk.FileFilter filter = new Gtk.FileFilter();
+        // Translators: This is the name of the file chooser filter
+        // when inserting an image in the composer.
+        filter.set_name(_("Images"));
+        filter.add_mime_type("image/*");
+        dialog.add_filter(filter);
+        if (dialog.run() == Gtk.ResponseType.ACCEPT) {
+            dialog.hide();
+            foreach (File file in dialog.get_files()) {
+                try {
+                    check_attachment_file(file);
+                    Geary.Memory.FileBuffer file_buffer = new Geary.Memory.FileBuffer(file, true);
+                    string path = file.get_path();
+                    string unique_filename;
+                    add_inline_part(file_buffer, path, out unique_filename);
+                    this.editor.body.insert_image(
+                        ClientWebView.INTERNAL_URL_PREFIX + unique_filename
+                    );
+                } catch (Error err) {
+                    attachment_failed(err.message);
+                    break;
+                }
+            }
+        }
+        dialog.destroy();
+    }
+
     private bool check_send_on_return(Gdk.EventKey event) {
         bool ret = Gdk.EVENT_PROPAGATE;
         switch (Gdk.keyval_name(event.keyval)) {
@@ -2106,7 +1976,7 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
                 // the Enter leaking through to the controls, but only
                 // send if send is available
                 if ((event.state & Gdk.ModifierType.CONTROL_MASK) != 0) {
-                    this.composer_actions.activate_action(ACTION_SEND, null);
+                    this.actions.activate_action(ACTION_SEND, null);
                     ret = Gdk.EVENT_STOP;
                 }
             break;
@@ -2161,151 +2031,31 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
         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) {
-        if (this.container.get_focus() == this.editor)
-            this.editor.cut_clipboard();
-        else if (this.container.get_focus() is Gtk.Editable)
-            ((Gtk.Editable) this.container.get_focus()).cut_clipboard();
+        var editable = this.container.get_focus() as Gtk.Editable;
+        if (editable != null) {
+            editable.cut_clipboard();
+        }
     }
 
     private void on_copy(SimpleAction action, Variant? param) {
-        if (this.container.get_focus() == this.editor)
-            this.editor.copy_clipboard();
-        else if (this.container.get_focus() is Gtk.Editable)
-            ((Gtk.Editable) this.container.get_focus()).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();
+        var editable = this.container.get_focus() as Gtk.Editable;
+        if (editable != null) {
+            editable.copy_clipboard();
+        }
     }
 
     private void on_paste(SimpleAction action, Variant? param) {
-        if (this.container.get_focus() == this.editor) {
-            if (this.editor.is_rich_text) {
-                // Check for pasted image in clipboard
-                Gtk.Clipboard clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD);
-                bool has_image = clipboard.wait_is_image_available();
-                if (has_image) {
-                    paste_image();
-                } else {
-                    this.editor.paste_rich_text();
-                }
-            } else {
-                this.editor.paste_plain_text();
-            }
-        } else if (this.container.get_focus() is Gtk.Editable) {
-            ((Gtk.Editable) this.container.get_focus()).paste_clipboard();
+        var editable = this.container.get_focus() as Gtk.Editable;
+        if (editable != null) {
+            editable.paste_clipboard();
         }
     }
 
-    /**
-     * Handle a pasted image, adding it as an inline attachment
-     */
-    private void paste_image() {
-        // The slow operations here are creating the PNG and, to a lesser extent,
-        // requesting the image from the clipboard
-        this.show_background_work_timeout.start();
-
-        get_clipboard(Gdk.SELECTION_CLIPBOARD).request_image((clipboard, pixbuf) => {
-            if (pixbuf != null) {
-                MemoryOutputStream os = new MemoryOutputStream(null);
-                pixbuf.save_to_stream_async.begin(os, "png", null, (obj, res) => {
-                    try {
-                        pixbuf.save_to_stream_async.end(res);
-                        os.close();
-
-                        Geary.Memory.ByteBuffer byte_buffer = new 
Geary.Memory.ByteBuffer.from_memory_output_stream(os);
-
-                        GLib.DateTime time_now = new GLib.DateTime.now();
-                        string filename = PASTED_IMAGE_FILENAME_TEMPLATE.printf(time_now.hash());
-
-                        string unique_filename;
-                        add_inline_part(byte_buffer, filename, out unique_filename);
-                        this.editor.insert_image(
-                            ClientWebView.INTERNAL_URL_PREFIX + unique_filename
-                        );
-                    } catch (Error error) {
-                        this.application.report_problem(
-                            new Geary.ProblemReport(error)
-                        );
-                    }
-
-                    stop_background_work_pulse();
-                });
-            } else {
-                warning("Failed to get image from clipboard");
-                stop_background_work_pulse();
-            }
-        });
-    }
-
-    private void on_paste_without_formatting(SimpleAction action, Variant? param) {
-        if (this.container.get_focus() == this.editor)
-            this.editor.paste_plain_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_text_format(SimpleAction? action, Variant? new_state) {
-        bool compose_as_html = new_state.get_string() == "html";
-        action.set_state(new_state.get_string());
-
-        foreach (string html_action in HTML_ACTIONS)
-            get_action(html_action).set_enabled(compose_as_html);
-
-        update_cursor_actions();
-
-        var show_formatting = (SimpleAction) this.composer_actions.lookup_action(ACTION_SHOW_FORMATTING);
-        show_formatting.set_enabled(compose_as_html);
-        update_formatting_toolbar();
-
-        this.editor.set_rich_text(compose_as_html);
-
-        this.config.compose_as_html = compose_as_html;
-        this.more_options_button.popover.popdown();
-    }
-
     private void reparent_widget(Gtk.Widget child, Gtk.Container new_parent) {
         ((Gtk.Container) child.get_parent()).remove(child);
         new_parent.add(child);
@@ -2351,201 +2101,6 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
         }
     }
 
-    private void update_formatting_toolbar() {
-        var show_formatting = (SimpleAction) this.composer_actions.lookup_action(ACTION_SHOW_FORMATTING);
-        var text_format = (SimpleAction) this.composer_actions.lookup_action(ACTION_TEXT_FORMAT);
-        this.formatting.reveal_child = text_format.get_state().get_string() == "html" && 
show_formatting.get_state().get_boolean();
-    }
-
-    private void on_show_formatting(SimpleAction? action, Variant? new_state) {
-        bool show_formatting = new_state.get_boolean();
-        this.config.formatting_toolbar_visible = show_formatting;
-        action.set_state(new_state);
-
-        update_formatting_toolbar();
-    }
-
-    private void on_font_family(SimpleAction action, Variant? param) {
-        string font = param.get_string();
-        this.editor.execute_editing_command_with_argument(
-            "fontname", font
-        );
-        action.set_state(font);
-
-        this.font_button_stack.visible_child_name = font;
-        this.font_button.popover.popdown();
-    }
-
-    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());
-
-        this.font_size_button.popover.popdown();
-    }
-
-    private async void update_color_icon(Gdk.RGBA color) {
-        var theme = Gtk.IconTheme.get_default();
-        var icon = theme.lookup_icon("font-color-symbolic", 16, 0);
-        Gdk.RGBA fg_color = Util.Gtk.rgba(0, 0, 0, 1);
-        this.get_style_context().lookup_color("theme_fg_color", out fg_color);
-
-        try {
-            var pixbuf = yield icon.load_symbolic_async(fg_color, color, null, null, null);
-            this.font_color_icon.pixbuf = pixbuf;
-        } catch(Error e) {
-            warning("Could not load icon `font-color-symbolic`!");
-            this.font_color_icon.icon_name = "font-color-symbolic";
-        }
-    }
-
-    private void on_select_color() {
-        Gtk.ColorChooserDialog dialog = new Gtk.ColorChooserDialog(_("Select Color"),
-            this.container.top_window);
-        if (dialog.run() == Gtk.ResponseType.OK) {
-            var rgba = dialog.get_rgba();
-            this.editor.execute_editing_command_with_argument(
-                "forecolor", rgba.to_string()
-            );
-
-            this.update_color_icon.begin(rgba);
-        }
-        dialog.destroy();
-    }
-
-    private void on_indent(SimpleAction action, Variant? param) {
-        this.editor.indent_line();
-    }
-
-    private void on_olist(SimpleAction action, Variant? param) {
-        this.editor.insert_olist();
-    }
-
-    private void on_ulist(SimpleAction action, Variant? param) {
-        this.editor.insert_ulist();
-    }
-
-    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 ?? "";
-        this.message_overlay_label.set_visible(copy_link_enabled);
-        get_action(ACTION_COPY_LINK).set_enabled(copy_link_enabled);
-    }
-
-    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,
-            WebKit.ContextMenuAction.INSERT_EMOJI,
-        };
-
-        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.
-
-        Util.Gtk.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 (this.config.enable_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) {
-        Util.Gtk.menu_foreach(section, (label, name, target, section) => {
-                string simple_name = name;
-                if ("." in simple_name) {
-                    simple_name = simple_name.split(".")[1];
-                }
-
-                GLib.SimpleAction? action = get_action(simple_name);
-                if (action != null) {
-                    context_menu.append(
-                        new WebKit.ContextMenuItem.from_gaction(
-                            action, label, target
-                        )
-                    );
-                } else {
-                    warning("Unknown action: %s/%s", name, label);
-                }
-            });
-    }
-
-    private void on_select_dictionary(SimpleAction action, Variant? param) {
-        this.select_dictionary_button.toggled();
-    }
-
     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
@@ -2559,7 +2114,7 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
         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();
+                this.editor.body.delete_quoted_message();
                 return Gdk.EVENT_STOP;
             }
         }
@@ -2567,16 +2122,8 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
         return Gdk.EVENT_PROPAGATE;
     }
 
-    /**
-     * Helper method, returns a composer action.
-     * @param action_name - The name of the action (as found in action_entries)
-     */
-    public GLib.SimpleAction? get_action(string action_name) {
-        GLib.Action? action = this.composer_actions.lookup_action(action_name);
-        if (action == null) {
-            action = this.editor_actions.lookup_action(action_name);
-        }
-        return action as SimpleAction;
+    private GLib.SimpleAction? get_action(string action_name) {
+        return this.actions.lookup_action(action_name) as GLib.SimpleAction;
     }
 
     private bool add_account_emails_to_from_list(
@@ -2618,9 +2165,7 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
         } else {
             text = this.draft_status_text;
         }
-
-        this.info_label.set_text(text);
-        this.info_label.set_tooltip_text(text);
+        this.editor.set_info_label(text);
     }
 
     // Updates from combobox contents and visibility, returns true if
@@ -2740,7 +2285,7 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
             // doesn't create &nbsp;'s
             sig = "";
         }
-        this.editor.update_signature(Geary.HTML.smart_escape(sig));
+        this.editor.body.update_signature(Geary.HTML.smart_escape(sig));
     }
 
     private void update_subject_spell_checker() {
@@ -2787,40 +2332,6 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
         buffer.spell_checker = checker;
     }
 
-    private async LinkPopover new_link_popover(LinkPopover.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);
-        }
-        LinkPopover popover = new LinkPopover(type);
-        popover.set_link_url(url);
-        popover.closed.connect(() => {
-                this.editor.free_selection(selection_id);
-            });
-        popover.hide.connect(() => {
-                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(selection_id);
-            });
-        return popover;
-    }
-
-    private void on_command_state_changed(bool can_undo, bool can_redo) {
-        get_action(Action.Edit.UNDO).set_enabled(can_undo);
-        get_action(Action.Edit.REDO).set_enabled(can_redo);
-    }
-
-    private void on_editor_content_loaded() {
-        this.update_signature.begin(null);
-    }
-
     private void on_draft_id_changed() {
         this.saved_id = this.draft_manager.current_draft_id;
     }
@@ -2861,68 +2372,6 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
         detach();
     }
 
-    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.config.compose_as_html) {
-            Gdk.EventButton? button = (Gdk.EventButton) event;
-            Gdk.Rectangle location = Gdk.Rectangle();
-            location.x = (int) button.x;
-            location.y = (int) button.y;
-
-            this.new_link_popover.begin(
-                LinkPopover.Type.EXISTING_LINK, this.pointer_url,
-                (obj, res) => {
-                    LinkPopover popover = this.new_link_popover.end(res);
-                    popover.set_relative_to(this.editor);
-                    popover.set_pointing_to(location);
-                    popover.popup();
-                });
-        }
-        return Gdk.EVENT_PROPAGATE;
-    }
-
-    private void on_cursor_context_changed(WebView.EditContext context) {
-        this.cursor_url = context.is_link ? context.link_url : null;
-        update_cursor_actions();
-
-        this.editor_actions.change_action_state(
-            ACTION_FONT_FAMILY, context.font_family
-        );
-
-        this.update_color_icon.begin(context.font_color);
-
-        if (context.font_size < 11)
-            this.editor_actions.change_action_state(ACTION_FONT_SIZE, "small");
-        else if (context.font_size > 20)
-            this.editor_actions.change_action_state(ACTION_FONT_SIZE, "large");
-        else
-            this.editor_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.editor_actions.change_action_state(
-            ACTION_BOLD,
-            (mask & WebKit.EditorTypingAttributes.BOLD) == WebKit.EditorTypingAttributes.BOLD
-        );
-        this.editor_actions.change_action_state(
-            ACTION_ITALIC,
-            (mask & WebKit.EditorTypingAttributes.ITALIC) == WebKit.EditorTypingAttributes.ITALIC
-        );
-        this.editor_actions.change_action_state(
-            ACTION_UNDERLINE,
-            (mask & WebKit.EditorTypingAttributes.UNDERLINE) == WebKit.EditorTypingAttributes.UNDERLINE
-        );
-        this.editor_actions.change_action_state(
-            ACTION_STRIKETHROUGH,
-            (mask & WebKit.EditorTypingAttributes.STRIKETHROUGH) == 
WebKit.EditorTypingAttributes.STRIKETHROUGH
-        );
-    }
-
     private void on_add_attachment() {
         AttachmentDialog dialog = new AttachmentDialog(
             this.container.top_window, this.config
@@ -2949,76 +2398,6 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
         }
     }
 
-    private void on_insert_image(SimpleAction action, Variant? param) {
-        AttachmentDialog dialog = new AttachmentDialog(
-            this.container.top_window, this.config
-        );
-        Gtk.FileFilter filter = new Gtk.FileFilter();
-        // Translators: This is the name of the file chooser filter
-        // when inserting an image in the composer.
-        filter.set_name(_("Images"));
-        filter.add_mime_type("image/*");
-        dialog.add_filter(filter);
-        if (dialog.run() == Gtk.ResponseType.ACCEPT) {
-            dialog.hide();
-            foreach (File file in dialog.get_files()) {
-                try {
-                    check_attachment_file(file);
-                    Geary.Memory.FileBuffer file_buffer = new Geary.Memory.FileBuffer(file, true);
-                    string path = file.get_path();
-                    string unique_filename;
-                    add_inline_part(file_buffer, path, out unique_filename);
-                    this.editor.insert_image(
-                        ClientWebView.INTERNAL_URL_PREFIX + unique_filename
-                    );
-                } catch (Error err) {
-                    attachment_failed(err.message);
-                    break;
-                }
-            }
-        }
-        dialog.destroy();
-    }
-
-    private void on_insert_link(SimpleAction action, Variant? param) {
-        LinkPopover.Type type = LinkPopover.Type.NEW_LINK;
-        string url = "https://";;
-        if (this.cursor_url != null) {
-            type = LinkPopover.Type.EXISTING_LINK;
-            url = this.cursor_url;
-        }
-
-        this.new_link_popover.begin(type, url, (obj, res) => {
-                LinkPopover popover = this.new_link_popover.end(res);
-
-                var style = this.insert_link_button.get_style_context();
-
-                // 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);
-                        style.set_state(NORMAL);
-                    });
-
-                popover.set_relative_to(this.insert_link_button);
-                popover.popup();
-                style.set_state(ACTIVE);
-            });
-    }
-
-    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_close() {
         conditional_close(this.container is Window);
     }
@@ -3071,22 +2450,9 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
             return;
         }
 
-        this.editor.insert_image(
+        this.editor.body.insert_image(
             ClientWebView.INTERNAL_URL_PREFIX + unique_filename
         );
     }
 
-    /** Shows and starts pulsing the progress meter. */
-    private void on_background_work_timeout() {
-        this.background_progress.fraction = 0.0;
-        this.background_work_pulse.start();
-        this.background_progress.show();
-    }
-
-    /** Hides and stops pulsing the progress meter. */
-    private void stop_background_work_pulse() {
-        this.background_progress.hide();
-        this.background_work_pulse.reset();
-        this.show_background_work_timeout.reset();
-    }
 }
diff --git a/src/client/meson.build b/src/client/meson.build
index ca995fe51..088f4e47d 100644
--- a/src/client/meson.build
+++ b/src/client/meson.build
@@ -74,6 +74,7 @@ client_vala_sources = files(
   'composer/composer-application-interface.vala',
   'composer/composer-box.vala',
   'composer/composer-container.vala',
+  'composer/composer-editor.vala',
   'composer/composer-email-entry.vala',
   'composer/composer-embed.vala',
   'composer/composer-headerbar.vala',
diff --git a/ui/composer-menus.ui b/ui/composer-editor-menus.ui
similarity index 100%
rename from ui/composer-menus.ui
rename to ui/composer-editor-menus.ui
diff --git a/ui/composer-editor.ui b/ui/composer-editor.ui
new file mode 100644
index 000000000..ce56ec172
--- /dev/null
+++ b/ui/composer-editor.ui
@@ -0,0 +1,775 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk+" version="3.20"/>
+  <template class="ComposerEditor" parent="GtkGrid">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="orientation">vertical</property>
+    <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>
+                  </object>
+                  <packing>
+                    <property name="index">-1</property>
+                  </packing>
+                </child>
+                <child type="overlay">
+                  <object class="GtkLabel" id="message_overlay_label">
+                    <property name="can_focus">False</property>
+                    <property name="no_show_all">True</property>
+                    <property name="halign">start</property>
+                    <property name="valign">end</property>
+                    <property name="ellipsize">middle</property>
+                    <style>
+                      <class name="geary-overlay"/>
+                    </style>
+                  </object>
+                </child>
+                <child type="overlay">
+                  <object class="GtkProgressBar" id="background_progress">
+                    <property name="can_focus">False</property>
+                    <property name="valign">start</property>
+                    <style>
+                      <class name="osd"/>
+                      <class name="top"/>
+                    </style>
+                  </object>
+                  <packing>
+                    <property name="index">1</property>
+                  </packing>
+                </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>
+    </child>
+    <child>
+      <object class="GtkBox" id="action_bar_box">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="orientation">vertical</property>
+        <child>
+          <object class="GtkRevealer" id="formatting">
+            <property name="visible">True</property>
+            <property name="transition_type">slide-up</property>
+            <child>
+              <object class="GtkActionBar">
+                <property name="visible">True</property>
+                <child>
+                  <object class="ComponentsReflowBox" id="toolbar_box">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="spacing">6</property>
+                    <property name="row_spacing">6</property>
+                    <property name="hexpand">True</property>
+                    <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 text</property>
+                            <property name="action_name">edt.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 text</property>
+                            <property name="action_name">edt.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 text</property>
+                            <property name="action_name">edt.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 text</property>
+                            <property name="action_name">edt.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>
+                    </child>
+                    <child>
+                      <object class="GtkBox" id="list_buttons">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <child>
+                          <object class="GtkButton" id="ulist_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 bulleted list</property>
+                            <property name="action_name">edt.ulist</property>
+                            <property name="always_show_image">True</property>
+                            <child>
+                              <object class="GtkImage" id="ulist_image">
+                                <property name="visible">True</property>
+                                <property name="can_focus">False</property>
+                                <property name="pixel_size">16</property>
+                                <property name="icon_name">format-unordered-list-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="olist_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 numbered list</property>
+                            <property name="action_name">edt.olist</property>
+                            <property name="always_show_image">True</property>
+                            <child>
+                              <object class="GtkImage" id="olist_image">
+                                <property name="visible">True</property>
+                                <property name="can_focus">False</property>
+                                <property name="pixel_size">16</property>
+                                <property name="icon_name">format-ordered-list-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>
+                    </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">Indent or quote text</property>
+                            <property name="action_name">edt.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">Un-indent or unquote 
text</property>
+                            <property name="action_name">edt.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>
+                    </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 text formatting</property>
+                        <property name="action_name">edt.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>
+                    </child>
+                    <child>
+                      <object class="GtkMenuButton" id="font_button">
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="focus_on_click">False</property>
+                        <property name="menu_model">font_menu</property>
+                        <property name="tooltip_text" translatable="yes">Change font type</property>
+                        <property name="direction">up</property>
+                        <child>
+                          <object class="GtkBox">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                            <property name="orientation">horizontal</property>
+                            <child>
+                              <object class="GtkStack" id="font_button_stack">
+                                <property name="visible">True</property>
+                                <property name="can_focus">False</property>
+                                <child>
+                                  <object class="GtkLabel">
+                                    <property name="visible">True</property>
+                                    <property name="can_focus">False</property>
+                                    <property name="label" translatable="yes">Sans Serif</property>
+                                    <property name="halign">start</property>
+                                  </object>
+                                  <packing>
+                                    <property name="name">sans</property>
+                                  </packing>
+                                </child>
+                                <child>
+                                  <object class="GtkLabel">
+                                    <property name="visible">True</property>
+                                    <property name="can_focus">False</property>
+                                    <property name="label" translatable="yes">Serif</property>
+                                    <property name="halign">start</property>
+                                  </object>
+                                  <packing>
+                                    <property name="name">serif</property>
+                                  </packing>
+                                </child>
+                                <child>
+                                  <object class="GtkLabel">
+                                    <property name="visible">True</property>
+                                    <property name="can_focus">False</property>
+                                    <property name="label" translatable="yes">Fixed Width</property>
+                                    <property name="halign">start</property>
+                                  </object>
+                                  <packing>
+                                    <property name="name">monospace</property>
+                                  </packing>
+                                </child>
+                              </object>
+                            </child>
+                            <child>
+                              <object class="GtkImage">
+                                <property name="visible">True</property>
+                                <property name="can_focus">False</property>
+                                <property name="icon-name">pan-down</property>
+                              </object>
+                            </child>
+                          </object>
+                        </child>
+                      </object>
+                    </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="action_name">edt.color</property>
+                        <property name="tooltip_text" translatable="yes">Change font color</property>
+                        <child>
+                          <object class="GtkImage" id="font_color_icon">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkMenuButton" id="font_size_button">
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="focus_on_click">False</property>
+                        <property name="menu_model">font_size_menu</property>
+                        <property name="tooltip_text" translatable="yes">Change font size</property>
+                        <property name="direction">up</property>
+                        <child>
+                          <object class="GtkBox">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                            <property name="orientation">horizontal</property>
+                            <child>
+                              <object class="GtkImage">
+                                <property name="visible">True</property>
+                                <property name="can_focus">False</property>
+                                <property name="icon-name">font-size-symbolic</property>
+                              </object>
+                            </child>
+                            <child>
+                              <object class="GtkImage">
+                                <property name="visible">True</property>
+                                <property name="can_focus">False</property>
+                                <property name="icon-name">pan-down</property>
+                              </object>
+                            </child>
+                          </object>
+                        </child>
+                      </object>
+                    </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 text 
link</property>
+                            <property name="action_name">edt.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</property>
+                            <property name="action_name">edt.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>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkActionBar">
+            <property name="visible">True</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</property>
+                    <property name="action_name">edt.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</property>
+                    <property name="action_name">edt.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>
+            </child>
+            <child>
+              <object class="GtkButton" id="new_message_attach_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">Attach a file</property>
+                <property name="action_name">win.add-attachment</property>
+                <property name="always_show_image">True</property>
+                <child>
+                  <object class="GtkImage" id="new_message_attach_image">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="pixel_size">16</property>
+                    <property name="icon_name">mail-attachment-symbolic</property>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="position">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkBox" id="conversation_attach_buttons">
+                <property name="can_focus">False</property>
+                <child>
+                  <object class="GtkButton" id="conversation_attach_new_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">Attach a file</property>
+                    <property name="action_name">win.add-attachment</property>
+                    <property name="always_show_image">True</property>
+                    <child>
+                      <object class="GtkImage" id="conversation_attach_new_image">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="pixel_size">16</property>
+                        <property name="icon_name">mail-attachment-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="conversation_attach_original_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">Add original attachments</property>
+                    <property name="action_name">win.add-original-attachments</property>
+                    <property name="always_show_image">True</property>
+                    <child>
+                      <object class="GtkImage" id="conversation_attach_original_image">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="pixel_size">16</property>
+                        <property name="icon_name">edit-copy-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="position">2</property>
+              </packing>
+            </child>
+            <child type="center">
+              <object class="GtkLabel" id="info_label">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="ellipsize">end</property>
+                <property name="width_chars">6</property>
+                <property name="xalign">0</property>
+                <style>
+                  <class name="dim-label"/>
+                </style>
+              </object>
+            </child>
+            <child>
+              <object class="GtkMenuButton" id="more_options_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>
+                <property name="menu_model">more_options_menu</property>
+                <property name="tooltip_text" translatable="yes">More options</property>
+                <property name="direction">up</property>
+                <child>
+                  <object class="GtkImage">
+                    <property name="visible">True</property>
+                    <property name="icon_name">view-more-symbolic</property>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="pack_type">end</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkToggleButton" id="show_formatting_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>
+                <property name="action_name">edt.show-formatting</property>
+                <property name="tooltip_text" translatable="yes">Show formatting toolbar</property>
+                <child>
+                  <object class="GtkImage">
+                    <property name="visible">True</property>
+                    <property name="icon_name">format-toolbar-toggle-symbolic</property>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="pack_type">end</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkMenuButton" 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">edt.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">tools-check-spelling-symbolic</property>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="pack_type">end</property>
+              </packing>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+
+  <menu id="font_menu">
+    <section>
+      <item>
+        <attribute name="label" translatable="yes">S_ans Serif</attribute>
+        <attribute name="action">edt.font-family</attribute>
+        <attribute name="target">sans</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">S_erif</attribute>
+        <attribute name="action">edt.font-family</attribute>
+        <attribute name="target">serif</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">_Fixed Width</attribute>
+        <attribute name="action">edt.font-family</attribute>
+        <attribute name="target">monospace</attribute>
+      </item>
+    </section>
+  </menu>
+
+  <menu id="font_size_menu">
+    <section>
+      <item>
+        <attribute name="label" translatable="yes">_Small</attribute>
+        <attribute name="action">edt.font-size</attribute>
+        <attribute name="target">small</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">_Medium</attribute>
+        <attribute name="action">edt.font-size</attribute>
+        <attribute name="target">medium</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">Lar_ge</attribute>
+        <attribute name="action">edt.font-size</attribute>
+        <attribute name="target">large</attribute>
+      </item>
+    </section>
+  </menu>
+
+  <menu id="more_options_menu">
+    <section>
+      <item>
+        <attribute name="label" translatable="yes">_Rich Text</attribute>
+        <attribute name="action">edt.text-format</attribute>
+        <attribute name="target">html</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">_Plain Text</attribute>
+        <attribute name="action">edt.text-format</attribute>
+        <attribute name="target">plain</attribute>
+      </item>
+    </section>
+  </menu>
+
+</interface>
diff --git a/ui/composer-widget.ui b/ui/composer-widget.ui
index 68f6e53c3..dc7d4dbfe 100644
--- a/ui/composer-widget.ui
+++ b/ui/composer-widget.ui
@@ -483,746 +483,9 @@
         </child>
         <child>
           <object class="GtkGrid" id="editor_container">
+            <property name="orientation">vertical</property>
             <property name="visible">True</property>
             <property name="can_focus">False</property>
-            <property name="orientation">vertical</property>
-            <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="can_focus">False</property>
-                            <property name="no_show_all">True</property>
-                            <property name="halign">start</property>
-                            <property name="valign">end</property>
-                            <property name="ellipsize">middle</property>
-                            <style>
-                              <class name="geary-overlay"/>
-                            </style>
-                          </object>
-                        </child>
-                        <child type="overlay">
-                          <object class="GtkProgressBar" id="background_progress">
-                            <property name="can_focus">False</property>
-                            <property name="valign">start</property>
-                            <style>
-                              <class name="osd"/>
-                              <class name="top"/>
-                            </style>
-                          </object>
-                          <packing>
-                            <property name="index">1</property>
-                          </packing>
-                        </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>
-            </child>
-            <child>
-              <object class="GtkBox" id="action_bar_box">
-                <property name="visible">True</property>
-                <property name="can_focus">False</property>
-                <property name="orientation">vertical</property>
-                <child>
-                  <object class="GtkRevealer" id="formatting">
-                    <property name="visible">True</property>
-                    <property name="transition_type">slide-up</property>
-                    <child>
-                      <object class="GtkActionBar">
-                        <property name="visible">True</property>
-                        <child>
-                          <object class="ComponentsReflowBox" id="toolbar_box">
-                            <property name="visible">True</property>
-                            <property name="can_focus">False</property>
-                            <property name="spacing">6</property>
-                            <property name="row_spacing">6</property>
-                            <property name="hexpand">True</property>
-                            <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 text</property>
-                                    <property name="action_name">edt.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 text</property>
-                                    <property name="action_name">edt.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 
text</property>
-                                    <property name="action_name">edt.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 
text</property>
-                                    <property name="action_name">edt.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>
-                            </child>
-                            <child>
-                              <object class="GtkBox" id="list_buttons">
-                                <property name="visible">True</property>
-                                <property name="can_focus">False</property>
-                                <child>
-                                  <object class="GtkButton" id="ulist_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 bulleted 
list</property>
-                                    <property name="action_name">edt.ulist</property>
-                                    <property name="always_show_image">True</property>
-                                    <child>
-                                      <object class="GtkImage" id="ulist_image">
-                                        <property name="visible">True</property>
-                                        <property name="can_focus">False</property>
-                                        <property name="pixel_size">16</property>
-                                        <property name="icon_name">format-unordered-list-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="olist_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 numbered 
list</property>
-                                    <property name="action_name">edt.olist</property>
-                                    <property name="always_show_image">True</property>
-                                    <child>
-                                      <object class="GtkImage" id="olist_image">
-                                        <property name="visible">True</property>
-                                        <property name="can_focus">False</property>
-                                        <property name="pixel_size">16</property>
-                                        <property name="icon_name">format-ordered-list-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>
-                            </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">Indent or quote 
text</property>
-                                    <property name="action_name">edt.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">Un-indent or unquote 
text</property>
-                                    <property name="action_name">edt.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>
-                            </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 text 
formatting</property>
-                                <property name="action_name">edt.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>
-                            </child>
-                            <child>
-                              <object class="GtkMenuButton" id="font_button">
-                                <property name="visible">True</property>
-                                <property name="can_focus">True</property>
-                                <property name="focus_on_click">False</property>
-                                <property name="menu_model">font_menu</property>
-                                <property name="tooltip_text" translatable="yes">Change font type</property>
-                                <property name="direction">up</property>
-                                <child>
-                                  <object class="GtkBox">
-                                    <property name="visible">True</property>
-                                    <property name="can_focus">False</property>
-                                    <property name="orientation">horizontal</property>
-                                    <child>
-                                      <object class="GtkStack" id="font_button_stack">
-                                        <property name="visible">True</property>
-                                        <property name="can_focus">False</property>
-                                        <child>
-                                          <object class="GtkLabel">
-                                            <property name="visible">True</property>
-                                            <property name="can_focus">False</property>
-                                            <property name="label" translatable="yes">Sans Serif</property>
-                                            <property name="halign">start</property>
-                                          </object>
-                                          <packing>
-                                            <property name="name">sans</property>
-                                          </packing>
-                                        </child>
-                                        <child>
-                                          <object class="GtkLabel">
-                                            <property name="visible">True</property>
-                                            <property name="can_focus">False</property>
-                                            <property name="label" translatable="yes">Serif</property>
-                                            <property name="halign">start</property>
-                                          </object>
-                                          <packing>
-                                            <property name="name">serif</property>
-                                          </packing>
-                                        </child>
-                                        <child>
-                                          <object class="GtkLabel">
-                                            <property name="visible">True</property>
-                                            <property name="can_focus">False</property>
-                                            <property name="label" translatable="yes">Fixed Width</property>
-                                            <property name="halign">start</property>
-                                          </object>
-                                          <packing>
-                                            <property name="name">monospace</property>
-                                          </packing>
-                                        </child>
-                                      </object>
-                                    </child>
-                                    <child>
-                                      <object class="GtkImage">
-                                        <property name="visible">True</property>
-                                        <property name="can_focus">False</property>
-                                        <property name="icon-name">pan-down</property>
-                                      </object>
-                                    </child>
-                                  </object>
-                                </child>
-                              </object>
-                            </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="action_name">edt.color</property>
-                                <property name="tooltip_text" translatable="yes">Change font color</property>
-                                <child>
-                                  <object class="GtkImage" id="font_color_icon">
-                                    <property name="visible">True</property>
-                                    <property name="can_focus">False</property>
-                                  </object>
-                                </child>
-                              </object>
-                            </child>
-                            <child>
-                              <object class="GtkMenuButton" id="font_size_button">
-                                <property name="visible">True</property>
-                                <property name="can_focus">True</property>
-                                <property name="focus_on_click">False</property>
-                                <property name="menu_model">font_size_menu</property>
-                                <property name="tooltip_text" translatable="yes">Change font size</property>
-                                <property name="direction">up</property>
-                                <child>
-                                  <object class="GtkBox">
-                                    <property name="visible">True</property>
-                                    <property name="can_focus">False</property>
-                                    <property name="orientation">horizontal</property>
-                                    <child>
-                                      <object class="GtkImage">
-                                        <property name="visible">True</property>
-                                        <property name="can_focus">False</property>
-                                        <property name="icon-name">font-size-symbolic</property>
-                                      </object>
-                                    </child>
-                                    <child>
-                                      <object class="GtkImage">
-                                        <property name="visible">True</property>
-                                        <property name="can_focus">False</property>
-                                        <property name="icon-name">pan-down</property>
-                                      </object>
-                                    </child>
-                                  </object>
-                                </child>
-                              </object>
-                            </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 text 
link</property>
-                                    <property name="action_name">edt.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</property>
-                                    <property name="action_name">edt.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>
-                            </child>
-                          </object>
-                        </child>
-                      </object>
-                    </child>
-                  </object>
-                </child>
-                <child>
-                  <object class="GtkActionBar">
-                    <property name="visible">True</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</property>
-                            <property name="action_name">edt.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</property>
-                            <property name="action_name">edt.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>
-                    </child>
-                    <child>
-                      <object class="GtkButton" id="new_message_attach_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">Attach a file</property>
-                        <property name="action_name">win.add-attachment</property>
-                        <property name="always_show_image">True</property>
-                        <child>
-                          <object class="GtkImage" id="new_message_attach_image">
-                            <property name="visible">True</property>
-                            <property name="can_focus">False</property>
-                            <property name="pixel_size">16</property>
-                            <property name="icon_name">mail-attachment-symbolic</property>
-                          </object>
-                        </child>
-                      </object>
-                      <packing>
-                        <property name="position">1</property>
-                      </packing>
-                    </child>
-                    <child>
-                      <object class="GtkBox" id="conversation_attach_buttons">
-                        <property name="can_focus">False</property>
-                        <child>
-                          <object class="GtkButton" id="conversation_attach_new_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">Attach a file</property>
-                            <property name="action_name">win.add-attachment</property>
-                            <property name="always_show_image">True</property>
-                            <child>
-                              <object class="GtkImage" id="conversation_attach_new_image">
-                                <property name="visible">True</property>
-                                <property name="can_focus">False</property>
-                                <property name="pixel_size">16</property>
-                                <property name="icon_name">mail-attachment-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="conversation_attach_original_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">Add original 
attachments</property>
-                            <property name="action_name">win.add-original-attachments</property>
-                            <property name="always_show_image">True</property>
-                            <child>
-                              <object class="GtkImage" id="conversation_attach_original_image">
-                                <property name="visible">True</property>
-                                <property name="can_focus">False</property>
-                                <property name="pixel_size">16</property>
-                                <property name="icon_name">edit-copy-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="position">2</property>
-                      </packing>
-                    </child>
-                    <child type="center">
-                      <object class="GtkLabel" id="info_label">
-                        <property name="visible">True</property>
-                        <property name="can_focus">True</property>
-                        <property name="ellipsize">end</property>
-                        <property name="width_chars">6</property>
-                        <property name="xalign">0</property>
-                        <style>
-                          <class name="dim-label"/>
-                        </style>
-                      </object>
-                    </child>
-                    <child>
-                      <object class="GtkMenuButton" id="more_options_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>
-                        <property name="menu_model">more_options_menu</property>
-                        <property name="tooltip_text" translatable="yes">More options</property>
-                        <property name="direction">up</property>
-                        <child>
-                          <object class="GtkImage">
-                            <property name="visible">True</property>
-                            <property name="icon_name">view-more-symbolic</property>
-                          </object>
-                        </child>
-                      </object>
-                      <packing>
-                        <property name="pack_type">end</property>
-                      </packing>
-                    </child>
-                    <child>
-                      <object class="GtkToggleButton" id="show_formatting_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>
-                        <property name="action_name">win.show-formatting</property>
-                        <property name="tooltip_text" translatable="yes">Show formatting toolbar</property>
-                        <child>
-                          <object class="GtkImage">
-                            <property name="visible">True</property>
-                            <property name="icon_name">format-toolbar-toggle-symbolic</property>
-                          </object>
-                        </child>
-                      </object>
-                      <packing>
-                        <property name="pack_type">end</property>
-                      </packing>
-                    </child>
-                    <child>
-                      <object class="GtkMenuButton" 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">win.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">tools-check-spelling-symbolic</property>
-                          </object>
-                        </child>
-                      </object>
-                      <packing>
-                        <property name="pack_type">end</property>
-                      </packing>
-                    </child>
-                  </object>
-                </child>
-              </object>
-            </child>
           </object>
           <packing>
             <property name="expand">False</property>
@@ -1261,59 +524,4 @@
       <widget name="subject_label"/>
     </widgets>
   </object>
-
-  <menu id="font_menu">
-    <section>
-      <item>
-        <attribute name="label" translatable="yes">S_ans Serif</attribute>
-        <attribute name="action">edt.font-family</attribute>
-        <attribute name="target">sans</attribute>
-      </item>
-      <item>
-        <attribute name="label" translatable="yes">S_erif</attribute>
-        <attribute name="action">edt.font-family</attribute>
-        <attribute name="target">serif</attribute>
-      </item>
-      <item>
-        <attribute name="label" translatable="yes">_Fixed Width</attribute>
-        <attribute name="action">edt.font-family</attribute>
-        <attribute name="target">monospace</attribute>
-      </item>
-    </section>
-  </menu>
-
-  <menu id="font_size_menu">
-    <section>
-      <item>
-        <attribute name="label" translatable="yes">_Small</attribute>
-        <attribute name="action">edt.font-size</attribute>
-        <attribute name="target">small</attribute>
-      </item>
-      <item>
-        <attribute name="label" translatable="yes">_Medium</attribute>
-        <attribute name="action">edt.font-size</attribute>
-        <attribute name="target">medium</attribute>
-      </item>
-      <item>
-        <attribute name="label" translatable="yes">Lar_ge</attribute>
-        <attribute name="action">edt.font-size</attribute>
-        <attribute name="target">large</attribute>
-      </item>
-    </section>
-  </menu>
-
-  <menu id="more_options_menu">
-    <section>
-      <item>
-        <attribute name="label" translatable="yes">_Rich Text</attribute>
-        <attribute name="action">win.text-format</attribute>
-        <attribute name="target">html</attribute>
-      </item>
-      <item>
-        <attribute name="label" translatable="yes">_Plain Text</attribute>
-        <attribute name="action">win.text-format</attribute>
-        <attribute name="target">plain</attribute>
-      </item>
-    </section>
-  </menu>
 </interface>
diff --git a/ui/org.gnome.Geary.gresource.xml b/ui/org.gnome.Geary.gresource.xml
index f0d5b32c9..0cdca875c 100644
--- a/ui/org.gnome.Geary.gresource.xml
+++ b/ui/org.gnome.Geary.gresource.xml
@@ -20,9 +20,10 @@
     <file compressed="true" preprocess="xml-stripblanks">components-inspector-log-view.ui</file>
     <file compressed="true" preprocess="xml-stripblanks">components-inspector-system-view.ui</file>
     <file compressed="true" preprocess="xml-stripblanks">components-placeholder-pane.ui</file>
+    <file compressed="true" preprocess="xml-stripblanks">composer-editor.ui</file>
+    <file compressed="true" preprocess="xml-stripblanks">composer-editor-menus.ui</file>
     <file compressed="true" preprocess="xml-stripblanks">composer-headerbar.ui</file>
     <file compressed="true" preprocess="xml-stripblanks">composer-link-popover.ui</file>
-    <file compressed="true" preprocess="xml-stripblanks">composer-menus.ui</file>
     <file compressed="true" preprocess="xml-stripblanks">composer-widget.ui</file>
     <file compressed="true">composer-web-view.css</file>
     <file compressed="true">composer-web-view.js</file>


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]