[geary/mjog/account-command-stacks: 33/77] Move GAction related code from app Controller to MainWindow



commit 8e9f00295e918888f72d5853f13e4048ae8d1a35
Author: Michael Gratton <mike vee net>
Date:   Sat Oct 5 10:27:02 2019 +1000

    Move GAction related code from app Controller to MainWindow
    
    This moves a large nummber of the main window's concerns to the main
    window, decouping a large number of dependencies from the controller to
    the main window, and enables managing action and UI state per-window.

 src/client/application/application-controller.vala | 717 ++-------------------
 src/client/application/geary-application.vala      |   1 +
 src/client/components/main-toolbar.vala            |   4 +-
 src/client/components/main-window.vala             | 301 ++++++++-
 .../conversation-list/conversation-list-view.vala  |  19 +-
 5 files changed, 349 insertions(+), 693 deletions(-)
---
diff --git a/src/client/application/application-controller.vala 
b/src/client/application/application-controller.vala
index afeb4b4a..2b070eeb 100644
--- a/src/client/application/application-controller.vala
+++ b/src/client/application/application-controller.vala
@@ -14,32 +14,6 @@
  */
 public class Application.Controller : Geary.BaseObject {
 
-    // Named actions.
-    public const string ACTION_REPLY_TO_MESSAGE = "reply-to-message";
-    public const string ACTION_REPLY_ALL_MESSAGE = "reply-all-message";
-    public const string ACTION_FORWARD_MESSAGE = "forward-message";
-    public const string ACTION_ARCHIVE_CONVERSATION = "archive-conv";
-    public const string ACTION_TRASH_CONVERSATION = "trash-conv";
-    public const string ACTION_DELETE_CONVERSATION = "delete-conv";
-    public const string ACTION_EMPTY_SPAM = "empty-spam";
-    public const string ACTION_EMPTY_TRASH = "empty-trash";
-    public const string ACTION_FIND_IN_CONVERSATION = "conv-find";
-    public const string ACTION_ZOOM = "zoom";
-    public const string ACTION_SHOW_MARK_MENU = "mark-message-menu";
-    public const string ACTION_MARK_AS_READ = "mark-message-read";
-    public const string ACTION_MARK_AS_UNREAD = "mark-message-unread";
-    public const string ACTION_MARK_AS_STARRED = "mark-message-starred";
-    public const string ACTION_MARK_AS_UNSTARRED = "mark-message-unstarred";
-    public const string ACTION_MARK_AS_SPAM = "mark-message-spam";
-    public const string ACTION_MARK_AS_NOT_SPAM = "mark-message-not-spam";
-    public const string ACTION_COPY_MENU = "show-copy-menu";
-    public const string ACTION_MOVE_MENU = "show-move-menu";
-    public const string ACTION_SEARCH = "search-conv";
-    public const string ACTION_CONVERSATION_LIST = "focus-conv-list";
-    public const string ACTION_TOGGLE_SEARCH = "toggle-search";
-    public const string ACTION_TOGGLE_FIND = "toggle-find";
-    public const string ACTION_CONVERSATION_UP = "up-conversation";
-    public const string ACTION_CONVERSATION_DOWN = "down-conversation";
 
     // Properties
     public const string PROP_SELECTED_CONVERSATIONS ="selected-conversations";
@@ -166,48 +140,9 @@ public class Application.Controller : Geary.BaseObject {
     private Geary.Folder? previous_non_search_folder = null;
     private Gee.List<string?> pending_mailtos = new Gee.ArrayList<string>();
 
-    private uint operation_count = 0;
-    private Geary.Revokable? revokable = null;
-
-    // Store the description for the revokable for tooltip display.
-    // This was previously stored within the context of undo button of the main toolbar.
-    private string revokable_description { get; set; }
-
     // List of windows we're waiting to close before Geary closes.
     private Gee.List<ComposerWidget> waiting_to_close = new Gee.ArrayList<ComposerWidget>();
 
-    private const ActionEntry[] win_action_entries = {
-        {GearyApplication.ACTION_CLOSE, on_close                       },
-        {GearyApplication.ACTION_UNDO,  on_revoke                      },
-
-        {ACTION_CONVERSATION_LIST,     on_conversation_list            },
-        {ACTION_FIND_IN_CONVERSATION,  on_find_in_conversation_action  },
-        {ACTION_SEARCH,                on_search_activated             },
-        {ACTION_EMPTY_SPAM,            on_empty_spam                   },
-        {ACTION_EMPTY_TRASH,           on_empty_trash                  },
-        // Message actions
-        {ACTION_REPLY_TO_MESSAGE,      on_reply_to_message_action   },
-        {ACTION_REPLY_ALL_MESSAGE,     on_reply_all_message_action  },
-        {ACTION_FORWARD_MESSAGE,       on_forward_message_action    },
-        {ACTION_ARCHIVE_CONVERSATION,  on_archive_conversation      },
-        {ACTION_TRASH_CONVERSATION,    on_trash_conversation        },
-        {ACTION_DELETE_CONVERSATION,   on_delete_conversation       },
-        {ACTION_COPY_MENU,             on_show_copy_menu            },
-        {ACTION_MOVE_MENU,             on_show_move_menu            },
-        {ACTION_CONVERSATION_UP,       on_conversation_up           },
-        {ACTION_CONVERSATION_DOWN,     on_conversation_down         },
-        // Message marking actions
-        {ACTION_SHOW_MARK_MENU,     on_show_mark_menu           },
-        {ACTION_MARK_AS_READ,       on_mark_as_read             },
-        {ACTION_MARK_AS_UNREAD,     on_mark_as_unread           },
-        {ACTION_MARK_AS_STARRED,    on_mark_as_starred          },
-        {ACTION_MARK_AS_UNSTARRED,  on_mark_as_unstarred        },
-        {ACTION_MARK_AS_SPAM,       on_mark_as_spam_toggle      },
-        {ACTION_MARK_AS_NOT_SPAM,   on_mark_as_spam_toggle      },
-        // Message viewer
-        {ACTION_ZOOM,  on_zoom,  "s"  },
-    };
-
 
     /**
      * Constructs a new instance of the controller.
@@ -277,8 +212,6 @@ public class Application.Controller : Geary.BaseObject {
         main_window.retry_service_problem.connect(on_retry_service_problem);
         main_window.notify["has-toplevel-focus"].connect(on_has_toplevel_focus);
 
-        setup_actions();
-
         enable_message_buttons(false);
 
         engine.account_available.connect(on_account_available);
@@ -286,13 +219,8 @@ public class Application.Controller : Geary.BaseObject {
         // Connect to various UI signals.
         main_window.conversation_list_view.conversations_selected.connect(on_conversations_selected);
         main_window.conversation_list_view.conversation_activated.connect(on_conversation_activated);
-        main_window.conversation_list_view.mark_conversations.connect(on_mark_conversations);
         
main_window.conversation_list_view.visible_conversations_changed.connect(on_visible_conversations_changed);
         main_window.folder_list.folder_selected.connect(on_folder_selected);
-        main_window.folder_list.copy_conversation.connect(on_copy_conversation);
-        main_window.folder_list.move_conversation.connect(on_move_conversation);
-        main_window.main_toolbar.copy_folder_menu.folder_selected.connect(on_copy_conversation);
-        main_window.main_toolbar.move_folder_menu.folder_selected.connect(on_move_conversation);
         main_window.search_bar.search_text_changed.connect((text) => { do_search(text); });
         main_window.conversation_viewer.conversation_added.connect(
             on_conversation_view_added
@@ -303,9 +231,6 @@ public class Application.Controller : Geary.BaseObject {
 
         this.main_window.conversation_list_view.grab_focus();
 
-        // initialize revokable
-        save_revokable(null, null);
-
         // Migrate configuration if necessary.
         try {
             Migrate.xdg_config_dir(this.application.get_user_data_directory(),
@@ -382,13 +307,8 @@ public class Application.Controller : Geary.BaseObject {
         // Disconnect from various UI signals.
         this.main_window.conversation_list_view.conversations_selected.disconnect(on_conversations_selected);
         this.main_window.conversation_list_view.conversation_activated.disconnect(on_conversation_activated);
-        this.main_window.conversation_list_view.mark_conversations.disconnect(on_mark_conversations);
         
this.main_window.conversation_list_view.visible_conversations_changed.disconnect(on_visible_conversations_changed);
         this.main_window.folder_list.folder_selected.disconnect(on_folder_selected);
-        this.main_window.folder_list.copy_conversation.disconnect(on_copy_conversation);
-        this.main_window.folder_list.move_conversation.disconnect(on_move_conversation);
-        this.main_window.main_toolbar.copy_folder_menu.folder_selected.disconnect(on_copy_conversation);
-        this.main_window.main_toolbar.move_folder_menu.folder_selected.disconnect(on_move_conversation);
         this.main_window.conversation_viewer.conversation_added.disconnect(
             on_conversation_view_added
         );
@@ -401,9 +321,6 @@ public class Application.Controller : Geary.BaseObject {
         // be freed up
         this.plugin_manager.notifications.clear_folders();
 
-        // drop the Revokable, which will commit it if necessary
-        save_revokable(null, null);
-
         this.cancellable_open_account.cancel();
 
         // Create an array of known accounts so the loops below do not
@@ -554,50 +471,6 @@ public class Application.Controller : Geary.BaseObject {
         }
     }
 
-    private void setup_actions() {
-        this.main_window.add_action_entries(win_action_entries, this);
-
-        // Marking actions
-        //
-        // Unmark is the primary action
-        add_window_accelerators(ACTION_MARK_AS_READ, { "<Ctrl><Shift>U", "<Shift>I" });
-        add_window_accelerators(ACTION_MARK_AS_UNREAD, { "<Ctrl>U", "<Shift>U" });
-        // Ephy uses Ctrl+D for bookmarking
-        add_window_accelerators(ACTION_MARK_AS_STARRED, { "<Ctrl>D", "S" });
-        add_window_accelerators(ACTION_MARK_AS_UNSTARRED, { "<Ctrl><Shift>D", "D" });
-        add_window_accelerators(ACTION_MARK_AS_SPAM, { "<Ctrl>J", "exclam" }); // Exclamation mark (!)
-
-        // Replying & forwarding
-        add_window_accelerators(ACTION_REPLY_TO_MESSAGE, { "<Ctrl>R", "R" });
-        add_window_accelerators(ACTION_REPLY_ALL_MESSAGE, { "<Ctrl><Shift>R", "<Shift>R" });
-        add_window_accelerators(ACTION_FORWARD_MESSAGE, { "<Ctrl>L", "F" });
-
-        // Moving & labelling
-        add_window_accelerators(ACTION_COPY_MENU, { "<Ctrl>L", "L" });
-        add_window_accelerators(ACTION_MOVE_MENU, { "<Ctrl>M", "M" });
-        add_window_accelerators(ACTION_ARCHIVE_CONVERSATION, { "<Ctrl>K", "A", "Y" });
-        add_window_accelerators(ACTION_TRASH_CONVERSATION, { "Delete", "BackSpace" });
-        add_window_accelerators(ACTION_DELETE_CONVERSATION, { "<Shift>Delete", "<Shift>BackSpace" });
-
-        // Find & search
-        add_window_accelerators(ACTION_FIND_IN_CONVERSATION, { "<Ctrl>F", "slash" });
-        add_window_accelerators(ACTION_SEARCH, { "<Ctrl>S" });
-
-        // Zoom
-        add_window_accelerators(ACTION_ZOOM+("('in')"), { "<Ctrl>equal", "<Ctrl>plus" });
-        add_window_accelerators(ACTION_ZOOM+("('out')"), { "<Ctrl>minus" });
-        add_window_accelerators(ACTION_ZOOM+("('normal')"), { "<Ctrl>0" });
-
-        // Navigation
-        add_window_accelerators(ACTION_CONVERSATION_LIST, { "<Ctrl>B" });
-        add_window_accelerators(ACTION_CONVERSATION_UP, { "<Ctrl>bracketleft", "K" });
-        add_window_accelerators(ACTION_CONVERSATION_DOWN, { "<Ctrl>bracketright", "J" });
-    }
-
-    private void add_window_accelerators(string action, string[] accelerators, Variant? param = null) {
-        this.application.set_accels_for_action("win."+action, accelerators);
-    }
-
     private void open_account(Geary.Account account) {
         account.information.authentication_failure.connect(
             on_authentication_failure
@@ -1041,7 +914,7 @@ public class Application.Controller : Geary.BaseObject {
             this.main_window.folder_selected(null, null);
         } else if (folder != this.current_folder) {
             this.main_window.conversation_viewer.show_loading();
-            get_window_action(ACTION_FIND_IN_CONVERSATION).set_enabled(false);
+            get_window_action(MainWindow.ACTION_FIND_IN_CONVERSATION).set_enabled(false);
             enable_message_buttons(false);
 
             // To prevent the user from selecting folders too quickly,
@@ -1087,9 +960,6 @@ public class Application.Controller : Geary.BaseObject {
         // reenter.
         int mutex_token = yield select_folder_mutex.claim_async(cancellable_folder);
 
-        // clear Revokable, as Undo is only available while a folder is selected
-        save_revokable(null, null);
-
         // re-enable copy/move to the last selected folder
         if (current_folder != null) {
             main_window.main_toolbar.copy_folder_menu.enable_disable_folder(current_folder, true);
@@ -1146,7 +1016,7 @@ public class Application.Controller : Geary.BaseObject {
 
     private void on_conversations_selected(Gee.Set<Geary.App.Conversation> selected) {
         this.selected_conversations = selected;
-        get_window_action(ACTION_FIND_IN_CONVERSATION).set_enabled(false);
+        get_window_action(MainWindow.ACTION_FIND_IN_CONVERSATION).set_enabled(false);
         ConversationViewer viewer = this.main_window.conversation_viewer;
         if (this.current_folder != null && !this.main_window.has_composer) {
             switch(selected.size) {
@@ -1181,7 +1051,7 @@ public class Application.Controller : Geary.BaseObject {
                                 viewer.load_conversation.end(ret);
                                 enable_message_buttons(true);
                                 get_window_action(
-                                    ACTION_FIND_IN_CONVERSATION
+                                    MainWindow.ACTION_FIND_IN_CONVERSATION
                                 ).set_enabled(true);
                             } catch (GLib.IOError.CANCELLED err) {
                                 // All good
@@ -1469,56 +1339,6 @@ public class Application.Controller : Geary.BaseObject {
         );
     }
 
-    private void mark_email(Gee.Collection<Geary.EmailIdentifier> ids,
-        Geary.EmailFlags? flags_to_add, Geary.EmailFlags? flags_to_remove) {
-        if (ids.size > 0) {
-            Geary.App.EmailStore? store = get_email_store_for_folder(current_folder);
-            if (store != null) {
-                store.mark_email_async.begin(
-                    ids, flags_to_add, flags_to_remove, cancellable_folder
-                );
-            }
-        }
-    }
-
-    private void on_show_mark_menu() {
-        bool unread_selected = false;
-        bool read_selected = false;
-        bool starred_selected = false;
-        bool unstarred_selected = false;
-        foreach (Geary.App.Conversation conversation in selected_conversations) {
-            if (conversation.is_unread())
-                unread_selected = true;
-
-            // Only check the messages that "Mark as Unread" would mark, so we
-            // don't add the menu option and have it not do anything.
-            //
-            // Sort by Date: field to correspond with ConversationViewer ordering
-            Geary.Email? latest = conversation.get_latest_sent_email(
-                Geary.App.Conversation.Location.IN_FOLDER_OUT_OF_FOLDER);
-            if (latest != null && latest.email_flags != null
-                && !latest.email_flags.contains(Geary.EmailFlags.UNREAD))
-                read_selected = true;
-
-            if (conversation.is_flagged()) {
-                starred_selected = true;
-            } else {
-                unstarred_selected = true;
-            }
-        }
-        get_window_action(ACTION_MARK_AS_READ).set_enabled(unread_selected);
-        get_window_action(ACTION_MARK_AS_UNREAD).set_enabled(read_selected);
-        get_window_action(ACTION_MARK_AS_STARRED).set_enabled(unstarred_selected);
-        get_window_action(ACTION_MARK_AS_UNSTARRED).set_enabled(starred_selected);
-
-        bool in_spam_folder = current_folder.special_folder_type == Geary.SpecialFolderType.SPAM;
-        get_window_action(ACTION_MARK_AS_NOT_SPAM).set_enabled(in_spam_folder);
-        // If we're in Drafts/Outbox, we also shouldn't set a message as SPAM.
-        get_window_action(ACTION_MARK_AS_SPAM).set_enabled(!in_spam_folder &&
-            current_folder.special_folder_type != Geary.SpecialFolderType.DRAFTS &&
-            current_folder.special_folder_type != Geary.SpecialFolderType.OUTBOX);
-    }
-
     private void on_visible_conversations_changed(Gee.Set<Geary.App.Conversation> visible) {
         clear_new_messages("on_visible_conversations_changed", visible);
     }
@@ -1558,139 +1378,9 @@ public class Application.Controller : Geary.BaseObject {
         }
     }
 
-    private void on_mark_conversations(Gee.Collection<Geary.App.Conversation> conversations,
-        Geary.EmailFlags? flags_to_add, Geary.EmailFlags? flags_to_remove,
-        bool latest_only = false) {
-        mark_email(get_conversation_email_ids(conversations, latest_only),
-            flags_to_add, flags_to_remove);
-    }
-
-    private void on_conversation_viewer_mark_emails(Gee.Collection<Geary.EmailIdentifier> emails,
-        Geary.EmailFlags? flags_to_add, Geary.EmailFlags? flags_to_remove) {
-        mark_email(emails, flags_to_add, flags_to_remove);
-    }
-
-    private void on_mark_as_read(SimpleAction action) {
-        Geary.EmailFlags flags = new Geary.EmailFlags();
-        flags.add(Geary.EmailFlags.UNREAD);
-
-        Gee.Collection<Geary.EmailIdentifier> ids = get_selected_email_ids(false);
-        mark_email(ids, null, flags);
-
-        ConversationListBox? list =
-            main_window.conversation_viewer.current_list;
-        if (list != null) {
-            foreach (Geary.EmailIdentifier id in ids)
-                list.mark_manual_read(id);
-        }
-    }
-
-    private void on_mark_as_unread(SimpleAction action) {
-        Geary.EmailFlags flags = new Geary.EmailFlags();
-        flags.add(Geary.EmailFlags.UNREAD);
-
-        Gee.Collection<Geary.EmailIdentifier> ids = get_selected_email_ids(true);
-        mark_email(ids, flags, null);
-
-        ConversationListBox? list =
-            main_window.conversation_viewer.current_list;
-        if (list != null) {
-            foreach (Geary.EmailIdentifier id in ids)
-                list.mark_manual_unread(id);
-        }
-    }
-
-    private void on_mark_as_starred(SimpleAction action) {
-        Geary.EmailFlags flags = new Geary.EmailFlags();
-        flags.add(Geary.EmailFlags.FLAGGED);
-        mark_email(get_selected_email_ids(true), flags, null);
-    }
-
-    private void on_mark_as_unstarred(SimpleAction action) {
-        Geary.EmailFlags flags = new Geary.EmailFlags();
-        flags.add(Geary.EmailFlags.FLAGGED);
-        mark_email(get_selected_email_ids(false), null, flags);
-    }
-
-    private void on_show_move_menu(SimpleAction? action) {
-        this.main_window.main_toolbar.move_message_button.clicked();
-    }
-
-    private void on_show_copy_menu(SimpleAction? action) {
-        this.main_window.main_toolbar.copy_message_button.clicked();
-    }
-
-    private async void mark_as_spam_toggle_async(Cancellable? cancellable) {
-        Geary.Folder? destination_folder = null;
-        if (current_folder.special_folder_type != Geary.SpecialFolderType.SPAM) {
-            // Move to spam folder.
-            try {
-                destination_folder = yield current_account.get_required_special_folder_async(
-                    Geary.SpecialFolderType.SPAM, cancellable);
-            } catch (Error e) {
-                debug("Error getting spam folder: %s", e.message);
-            }
-        } else {
-            // Move out of spam folder, back to inbox.
-            destination_folder = current_account.get_special_folder(Geary.SpecialFolderType.INBOX);
-        }
-
-        if (destination_folder != null)
-            on_move_conversation(destination_folder);
-    }
-
-    private void on_mark_as_spam_toggle(SimpleAction action) {
-        mark_as_spam_toggle_async.begin(null);
-    }
-
-    private void copy_email(Gee.Collection<Geary.EmailIdentifier> ids,
-        Geary.FolderPath destination) {
-        if (ids.size > 0) {
-            Geary.App.EmailStore? store = get_email_store_for_folder(current_folder);
-            if (store != null) {
-                store.copy_email_async.begin(
-                    ids, destination, cancellable_folder
-                );
-            }
-        }
-    }
-
-    private void on_copy_conversation(Geary.Folder destination) {
-        copy_email(get_selected_email_ids(false), destination.path);
-    }
-
-    private void on_move_conversation(Geary.Folder destination) {
-        // Nothing to do if nothing selected.
-        if (selected_conversations == null || selected_conversations.size == 0)
-            return;
-
-        Gee.Collection<Geary.EmailIdentifier> ids = get_selected_email_ids(false);
-        if (ids.size == 0)
-            return;
-
-        selection_operation_started();
-
-        Geary.FolderSupport.Move? supports_move = current_folder as Geary.FolderSupport.Move;
-        if (supports_move != null)
-            move_conversation_async.begin(
-                supports_move, ids, destination.path, cancellable_folder,
-                (obj, ret) => {
-                    move_conversation_async.end(ret);
-                    selection_operation_finished();
-                });
-    }
-
-    private async void move_conversation_async(Geary.FolderSupport.Move source_folder,
-                                               Gee.Collection<Geary.EmailIdentifier> ids,
-                                               Geary.FolderPath destination,
-                                               Cancellable? cancellable) {
-        try {
-            save_revokable(yield source_folder.move_email_async(ids, destination, cancellable),
-                ngettext("Moved %d message to %s", "Moved %d messages to %s", ids.size).printf(ids.size, 
destination.to_string()));
-        } catch (Error err) {
-            debug("%s: Unable to move %d emails: %s", source_folder.to_string(), ids.size,
-                err.message);
-        }
+    private void on_conversation_viewer_mark_emails(Gee.Collection<Geary.EmailIdentifier> email,
+                                                    Geary.EmailFlags? to_add,
+                                                    Geary.EmailFlags? to_remove) {
     }
 
     private void on_attachments_activated(Gee.Collection<Geary.Attachment> attachments) {
@@ -1952,29 +1642,6 @@ public class Application.Controller : Geary.BaseObject {
         return true;
     }
 
-    // View contains the email from whose menu this reply or forward
-    // was triggered.  If null, this was triggered from the headerbar
-    // or shortcut.
-    private void create_reply_forward_widget(ComposerWidget.ComposeType compose_type,
-                                             owned ConversationEmail? email_view) {
-        if (email_view == null) {
-            ConversationListBox? list_view =
-                main_window.conversation_viewer.current_list;
-            if (list_view != null) {
-                email_view = list_view.get_reply_target();
-            }
-        }
-
-        if (email_view != null) {
-            email_view.get_selection_for_quoting.begin((obj, res) => {
-                    string? quote = email_view.get_selection_for_quoting.end(res);
-                    create_compose_widget(compose_type, email_view.email, quote);
-                });
-        } else {
-            create_compose_widget(compose_type, email_view.email, null);
-        }
-    }
-
     /**
      * Creates a composer widget.
      *
@@ -2110,323 +1777,12 @@ public class Application.Controller : Geary.BaseObject {
         }
     }
 
-    private void on_close() {
-        this.main_window.close();
-    }
-
-    private void on_reply_to_message(ConversationEmail target_view) {
-        create_reply_forward_widget(ComposerWidget.ComposeType.REPLY, target_view);
-    }
-
-    private void on_reply_to_message_action(SimpleAction action) {
-        create_reply_forward_widget(ComposerWidget.ComposeType.REPLY, null);
-    }
-
-    private void on_reply_all_message(ConversationEmail target_view) {
-        create_reply_forward_widget(ComposerWidget.ComposeType.REPLY_ALL, target_view);
-    }
-
-    private void on_reply_all_message_action(SimpleAction action) {
-        create_reply_forward_widget(ComposerWidget.ComposeType.REPLY_ALL, null);
-    }
-
-    private void on_forward_message(ConversationEmail target_view) {
-        create_reply_forward_widget(ComposerWidget.ComposeType.FORWARD, target_view);
-    }
-
-    private void on_forward_message_action(SimpleAction action) {
-        create_reply_forward_widget(ComposerWidget.ComposeType.FORWARD, null);
-    }
-
-    private void on_find_in_conversation_action(SimpleAction action) {
-        this.main_window.conversation_viewer.enable_find();
-    }
-
-    private void on_search_activated(SimpleAction action) {
-        this.main_window.show_search_bar();
-    }
-
-    private void on_archive_conversation(SimpleAction action) {
-        archive_or_delete_selection_async.begin(true, false, cancellable_folder,
-            on_archive_or_delete_selection_finished);
-    }
-
-    private void on_trash_conversation(SimpleAction action) {
-        archive_or_delete_selection_async.begin(false, true, cancellable_folder,
-            on_archive_or_delete_selection_finished);
-    }
-
-    private void on_delete_conversation(SimpleAction action) {
-        archive_or_delete_selection_async.begin(false, false, cancellable_folder,
-            on_archive_or_delete_selection_finished);
-    }
-
-    private void on_empty_spam(SimpleAction action) {
-        on_empty_trash_or_spam(Geary.SpecialFolderType.SPAM);
-    }
-
-    private void on_empty_trash(SimpleAction action) {
-        on_empty_trash_or_spam(Geary.SpecialFolderType.TRASH);
-    }
-
-    private void on_empty_trash_or_spam(Geary.SpecialFolderType special_folder_type) {
-        // Account must be in place, must have the specified special folder type, and that folder
-        // must support Empty in order for this command to proceed
-        if (current_account == null)
-            return;
-
-        Geary.Folder? folder = current_account.get_special_folder(special_folder_type);
-        if (folder == null)
-            return;
-
-        Geary.FolderSupport.Empty? emptyable = folder as Geary.FolderSupport.Empty;
-        if (emptyable == null) {
-            debug("%s: Special folder %s (%s) does not support emptying", current_account.to_string(),
-                folder.path.to_string(), special_folder_type.to_string());
-
-            return;
-        }
-
-        ConfirmationDialog dialog = new ConfirmationDialog(main_window,
-            _("Empty all email from your %s folder?").printf(special_folder_type.get_display_name()),
-            _("This removes the email from Geary and your email server.")
-                + "  <b>" + _("This cannot be undone.") + "</b>",
-            _("Empty %s").printf(special_folder_type.get_display_name()), "destructive-action");
-        dialog.use_secondary_markup(true);
-        dialog.set_focus_response(Gtk.ResponseType.CANCEL);
-
-        if (dialog.run() == Gtk.ResponseType.OK)
-            empty_folder_async.begin(emptyable, cancellable_folder);
-    }
-
-    private async void empty_folder_async(Geary.FolderSupport.Empty emptyable, Cancellable? cancellable) {
-        try {
-            yield do_empty_folder_async(emptyable, cancellable);
-        } catch (Error err) {
-            // don't report to user if cancelled
-            if (err is IOError.CANCELLED)
-                return;
-
-            ErrorDialog dialog = new ErrorDialog(main_window,
-                _("Error emptying %s").printf(emptyable.get_display_name()), err.message);
-            dialog.run();
-        }
-    }
-
-    private async void do_empty_folder_async(Geary.FolderSupport.Empty emptyable, Cancellable? cancellable)
-        throws Error {
-        bool open = false;
-        try {
-            yield emptyable.open_async(Geary.Folder.OpenFlags.NO_DELAY, cancellable);
-            open = true;
-            yield emptyable.empty_folder_async(cancellable);
-        } finally {
-            if (open) {
-                try {
-                    yield emptyable.close_async(null);
-                } catch (Error err) {
-                    // ignored
-                }
-            }
-        }
-    }
-
     private bool current_folder_supports_trash() {
         return (current_folder != null && current_folder.special_folder_type != Geary.SpecialFolderType.TRASH
             && !current_folder.properties.is_local_only && current_account != null
             && (current_folder as Geary.FolderSupport.Move) != null);
     }
 
-    private bool confirm_delete(int num_messages) {
-        ConfirmationDialog dialog = new ConfirmationDialog(main_window, ngettext(
-            "Do you want to permanently delete this message?",
-            "Do you want to permanently delete these messages?", num_messages),
-            null, _("Delete"), "destructive-action");
-
-        return (dialog.run() == Gtk.ResponseType.OK);
-    }
-
-    private async void trash_messages_async(Gee.Collection<Geary.EmailIdentifier> ids, Cancellable? 
cancellable)
-            throws Error {
-        debug("Trashing selected messages");
-
-        Geary.FolderSupport.Move? supports_move = current_folder as Geary.FolderSupport.Move;
-        if (current_folder_supports_trash() && supports_move != null) {
-            Geary.FolderPath trash_path = (yield current_account.get_required_special_folder_async(
-                Geary.SpecialFolderType.TRASH, cancellable)).path;
-            save_revokable(yield supports_move.move_email_async(ids, trash_path, cancellable),
-                ngettext("Trashed %d message", "Trashed %d messages", ids.size).printf(ids.size));
-        } else {
-            debug("Folder %s doesn't support move or account %s doesn't have a trash folder",
-                current_folder.to_string(), current_account.to_string());
-        }
-    }
-
-    private async void delete_messages_async(Gee.Collection<Geary.EmailIdentifier> ids, Cancellable? 
cancellable)
-            throws Error {
-        debug("Deleting selected messages");
-
-        Geary.FolderSupport.Remove? supports_remove = current_folder as Geary.FolderSupport.Remove;
-        if (supports_remove != null) {
-            if (confirm_delete(ids.size)) {
-                yield supports_remove.remove_email_async(ids, cancellable);
-            } else {
-                last_deleted_conversation = null;
-            }
-        } else {
-            debug("Folder %s doesn't support remove", current_folder.to_string());
-        }
-    }
-
-    private async void archive_or_delete_selection_async(bool archive, bool trash,
-        Cancellable? cancellable) throws Error {
-        ConversationListBox list_view =
-            main_window.conversation_viewer.current_list;
-        if (list_view != null &&
-            list_view.conversation == last_deleted_conversation) {
-            debug("Not archiving/trashing/deleting; viewed conversation is last deleted conversation");
-            return;
-        }
-
-        selection_operation_started();
-
-        last_deleted_conversation = selected_conversations.size > 0
-            ? Geary.traverse<Geary.App.Conversation>(selected_conversations).first() : null;
-
-        Gee.Collection<Geary.EmailIdentifier> ids = get_selected_email_ids(false);
-        if (archive) {
-            debug("Archiving selected messages");
-
-            Geary.FolderSupport.Archive? supports_archive = current_folder as Geary.FolderSupport.Archive;
-            if (supports_archive == null) {
-                debug("Folder %s doesn't support archive", current_folder.to_string());
-            } else {
-                save_revokable(yield supports_archive.archive_email_async(ids, cancellable),
-                    ngettext("Archived %d message", "Archived %d messages", ids.size).printf(ids.size));
-            }
-
-            return;
-        }
-
-        if (trash) {
-            yield trash_messages_async(ids, cancellable);
-        } else {
-            yield delete_messages_async(ids, cancellable);
-        }
-    }
-
-    private void on_archive_or_delete_selection_finished(Object? source, AsyncResult result) {
-        try {
-            archive_or_delete_selection_async.end(result);
-        } catch (Error e) {
-            debug("Unable to archive/trash/delete messages: %s", e.message);
-        }
-        selection_operation_finished();
-    }
-
-    private void save_revokable(Geary.Revokable? new_revokable, string? description) {
-        // disconnect old revokable & blindly commit it
-        if (revokable != null) {
-            revokable.notify[Geary.Revokable.PROP_VALID].disconnect(on_revokable_valid_changed);
-            revokable.notify[Geary.Revokable.PROP_IN_PROCESS].disconnect(update_revokable_action);
-            revokable.committed.disconnect(on_revokable_committed);
-
-            revokable.commit_async.begin();
-        }
-
-        // store new revokable
-        this.revokable = new_revokable;
-        this.revokable_description = description;
-
-        // connect to new revokable
-        if (revokable != null) {
-            revokable.notify[Geary.Revokable.PROP_VALID].connect(on_revokable_valid_changed);
-            revokable.notify[Geary.Revokable.PROP_IN_PROCESS].connect(update_revokable_action);
-            revokable.committed.connect(on_revokable_committed);
-        }
-
-        if (this.main_window != null) {
-            if (this.revokable != null && this.revokable_description != null) {
-                Components.InAppNotification ian =
-                    new Components.InAppNotification(this.revokable_description);
-                ian.set_button(_("Undo"), "win." + GearyApplication.ACTION_UNDO);
-                this.main_window.add_notification(ian);
-            }
-            update_revokable_action();
-        }
-    }
-
-    private void update_revokable_action() {
-        get_window_action(GearyApplication.ACTION_UNDO).set_enabled(
-            this.revokable != null &&
-            this.revokable.valid &&
-            !this.revokable.in_process
-        );
-    }
-
-    private void on_revokable_valid_changed() {
-        // remove revokable if it goes invalid
-        if (revokable != null && !revokable.valid)
-            save_revokable(null, null);
-    }
-
-    private void on_revokable_committed(Geary.Revokable? committed_revokable) {
-        if (committed_revokable == null)
-            return;
-
-        save_revokable(committed_revokable, this.revokable_description);
-    }
-
-    private void on_revoke() {
-        if (revokable != null && revokable.valid)
-            revokable.revoke_async.begin(null, on_revoke_completed);
-    }
-
-    private void on_revoke_completed(Object? object, AsyncResult result) {
-        // Don't use the "revokable" instance because it might have gone null before this callback
-        // was reached
-        Geary.Revokable? origin = object as Geary.Revokable;
-        if (origin == null)
-            return;
-
-        try {
-            origin.revoke_async.end(result);
-        } catch (Error err) {
-            debug("Unable to revoke operation: %s", err.message);
-        }
-    }
-
-    private void selection_operation_started() {
-        this.operation_count += 1;
-        if (this.operation_count == 1) {
-            this.main_window.conversation_list_view.set_changing_selection(true);
-        }
-    }
-
-    private void selection_operation_finished() {
-        this.operation_count -= 1;
-        if (this.operation_count == 0) {
-            this.main_window.conversation_list_view.set_changing_selection(false);
-        }
-    }
-
-    private void on_zoom(SimpleAction action, Variant? parameter) {
-        ConversationListBox? view = main_window.conversation_viewer.current_list;
-        if (view != null && parameter != null) {
-            string zoom_action = parameter.get_string();
-            if (zoom_action == "in")
-                view.zoom_in();
-            else if (zoom_action == "out")
-                view.zoom_out();
-            else
-                view.zoom_reset();
-        }
-    }
-
-    private void on_conversation_list() {
-        this.main_window.conversation_list_view.grab_focus();
-    }
-
     private void on_sent(Geary.Account account, Geary.RFC822.Message sent) {
         // Translators: The label for an in-app notification. The
         // string substitution is a list of recipients of the email.
@@ -2478,17 +1834,9 @@ public class Application.Controller : Geary.BaseObject {
     }
 
     private void on_trash_message(ConversationEmail target_view) {
-        Gee.Collection<Geary.EmailIdentifier> ids =
-            new Gee.ArrayList<Geary.EmailIdentifier>();
-        ids.add(target_view.email.id);
-        trash_messages_async.begin(ids, cancellable_folder);
     }
 
     private void on_delete_message(ConversationEmail target_view) {
-        Gee.Collection<Geary.EmailIdentifier> ids =
-            new Gee.ArrayList<Geary.EmailIdentifier>();
-        ids.add(target_view.email.id);
-        delete_messages_async.begin(ids, cancellable_folder);
     }
 
     private void on_view_source(ConversationEmail email_view) {
@@ -2526,15 +1874,15 @@ public class Application.Controller : Geary.BaseObject {
         main_window.main_toolbar.selected_conversations = this.selected_conversations.size;
 
         // Single message only buttons.
-        get_window_action(ACTION_REPLY_TO_MESSAGE).set_enabled(false);
-        get_window_action(ACTION_REPLY_ALL_MESSAGE).set_enabled(false);
-        get_window_action(ACTION_FORWARD_MESSAGE).set_enabled(false);
+        get_window_action(MainWindow.ACTION_REPLY_TO_MESSAGE).set_enabled(false);
+        get_window_action(MainWindow.ACTION_REPLY_ALL_MESSAGE).set_enabled(false);
+        get_window_action(MainWindow.ACTION_FORWARD_MESSAGE).set_enabled(false);
 
         // Mutliple message buttons.
-        get_window_action(ACTION_MOVE_MENU).set_enabled(current_folder is Geary.FolderSupport.Move);
-        get_window_action(ACTION_ARCHIVE_CONVERSATION).set_enabled(current_folder is 
Geary.FolderSupport.Archive);
-        get_window_action(ACTION_TRASH_CONVERSATION).set_enabled(current_folder_supports_trash());
-        get_window_action(ACTION_DELETE_CONVERSATION).set_enabled(current_folder is 
Geary.FolderSupport.Remove);
+        get_window_action(MainWindow.ACTION_MOVE_MENU).set_enabled(current_folder is 
Geary.FolderSupport.Move);
+        get_window_action(MainWindow.ACTION_ARCHIVE_CONVERSATION).set_enabled(current_folder is 
Geary.FolderSupport.Archive);
+        get_window_action(MainWindow.ACTION_TRASH_CONVERSATION).set_enabled(current_folder_supports_trash());
+        get_window_action(MainWindow.ACTION_DELETE_CONVERSATION).set_enabled(current_folder is 
Geary.FolderSupport.Remove);
 
         cancel_context_dependent_buttons();
         enable_context_dependent_buttons_async.begin(true, cancellable_context_dependent_buttons);
@@ -2549,13 +1897,13 @@ public class Application.Controller : Geary.BaseObject {
         if (current_folder != null && current_folder.special_folder_type == Geary.SpecialFolderType.DRAFTS)
             respond_sensitive = false;
 
-        get_window_action(ACTION_REPLY_TO_MESSAGE).set_enabled(respond_sensitive);
-        get_window_action(ACTION_REPLY_ALL_MESSAGE).set_enabled(respond_sensitive);
-        get_window_action(ACTION_FORWARD_MESSAGE).set_enabled(respond_sensitive);
-        get_window_action(ACTION_MOVE_MENU).set_enabled(sensitive && (current_folder is 
Geary.FolderSupport.Move));
-        get_window_action(ACTION_ARCHIVE_CONVERSATION).set_enabled(sensitive && (current_folder is 
Geary.FolderSupport.Archive));
-        get_window_action(ACTION_TRASH_CONVERSATION).set_enabled(sensitive && 
current_folder_supports_trash());
-        get_window_action(ACTION_DELETE_CONVERSATION).set_enabled(sensitive && (current_folder is 
Geary.FolderSupport.Remove));
+        get_window_action(MainWindow.ACTION_REPLY_TO_MESSAGE).set_enabled(respond_sensitive);
+        get_window_action(MainWindow.ACTION_REPLY_ALL_MESSAGE).set_enabled(respond_sensitive);
+        get_window_action(MainWindow.ACTION_FORWARD_MESSAGE).set_enabled(respond_sensitive);
+        get_window_action(MainWindow.ACTION_MOVE_MENU).set_enabled(sensitive && (current_folder is 
Geary.FolderSupport.Move));
+        get_window_action(MainWindow.ACTION_ARCHIVE_CONVERSATION).set_enabled(sensitive && (current_folder 
is Geary.FolderSupport.Archive));
+        get_window_action(MainWindow.ACTION_TRASH_CONVERSATION).set_enabled(sensitive && 
current_folder_supports_trash());
+        get_window_action(MainWindow.ACTION_DELETE_CONVERSATION).set_enabled(sensitive && (current_folder is 
Geary.FolderSupport.Remove));
 
         cancel_context_dependent_buttons();
         enable_context_dependent_buttons_async.begin(sensitive, cancellable_context_dependent_buttons);
@@ -2584,8 +1932,8 @@ public class Application.Controller : Geary.BaseObject {
         if (selected_operations != null)
             supported_operations.add_all(selected_operations.get_values());
 
-        get_window_action(ACTION_SHOW_MARK_MENU).set_enabled(sensitive && (typeof(Geary.FolderSupport.Mark) 
in supported_operations));
-        get_window_action(ACTION_COPY_MENU).set_enabled(sensitive && 
(supported_operations.contains(typeof(Geary.FolderSupport.Copy))));
+        get_window_action(MainWindow.ACTION_SHOW_MARK_MENU).set_enabled(sensitive && 
(typeof(Geary.FolderSupport.Mark) in supported_operations));
+        get_window_action(MainWindow.ACTION_COPY_MENU).set_enabled(sensitive && 
(supported_operations.contains(typeof(Geary.FolderSupport.Copy))));
     }
 
     // Returns a list of composer windows for an account, or null if none.
@@ -2842,12 +2190,25 @@ public class Application.Controller : Geary.BaseObject {
         );
     }
 
-    private void on_conversation_up() {
-        this.main_window.conversation_list_view.scroll(Gtk.ScrollType.STEP_UP);
+    private void on_reply_to_message(ConversationEmail target_view) {
+        target_view.get_selection_for_quoting.begin((obj, res) => {
+                string? quote = target_view.get_selection_for_quoting.end(res);
+                create_compose_widget(REPLY, target_view.email, quote);
+            });
+    }
+
+    private void on_reply_all_message(ConversationEmail target_view) {
+        target_view.get_selection_for_quoting.begin((obj, res) => {
+                string? quote = target_view.get_selection_for_quoting.end(res);
+                create_compose_widget(REPLY_ALL, target_view.email, quote);
+            });
     }
 
-    private void on_conversation_down() {
-        this.main_window.conversation_list_view.scroll(Gtk.ScrollType.STEP_DOWN);
+    private void on_forward_message(ConversationEmail target_view) {
+        target_view.get_selection_for_quoting.begin((obj, res) => {
+                string? quote = target_view.get_selection_for_quoting.end(res);
+                create_compose_widget(FORWARD, target_view.email, quote);
+            });
     }
 
     private void on_save_attachments(Gee.Collection<Geary.Attachment> attachments) {
diff --git a/src/client/application/geary-application.vala b/src/client/application/geary-application.vala
index 5746ad0c..3fe307cb 100644
--- a/src/client/application/geary-application.vala
+++ b/src/client/application/geary-application.vala
@@ -446,6 +446,7 @@ public class GearyApplication : Gtk.Application {
         add_window_accelerators(ACTION_REDO, { "<Ctrl><Shift>Z" });
         add_window_accelerators(ACTION_UNDO, { "<Ctrl>Z" });
 
+        MainWindow.add_window_accelerators(this);
         ComposerWidget.add_window_accelerators(this);
         Components.Inspector.add_window_accelerators(this);
 
diff --git a/src/client/components/main-toolbar.vala b/src/client/components/main-toolbar.vala
index 8896f1df..8d40bf7e 100644
--- a/src/client/components/main-toolbar.vala
+++ b/src/client/components/main-toolbar.vala
@@ -157,7 +157,7 @@ public class MainToolbar : Gtk.Box {
         );
 
         if (this.show_trash_button) {
-            this.trash_delete_button.action_name = "win."+Application.Controller.ACTION_TRASH_CONVERSATION;
+            this.trash_delete_button.action_name = "win."+MainWindow.ACTION_TRASH_CONVERSATION;
             this.trash_delete_button.image = trash_image;
             this.trash_delete_button.tooltip_text = ngettext(
                 "Move conversation to Trash",
@@ -165,7 +165,7 @@ public class MainToolbar : Gtk.Box {
                 this.selected_conversations
             );
         } else {
-            this.trash_delete_button.action_name = "win."+Application.Controller.ACTION_DELETE_CONVERSATION;
+            this.trash_delete_button.action_name = "win."+MainWindow.ACTION_DELETE_CONVERSATION;
             this.trash_delete_button.image = delete_image;
             this.trash_delete_button.tooltip_text = ngettext(
                 "Delete conversation",
diff --git a/src/client/components/main-window.vala b/src/client/components/main-window.vala
index 9ee97cda..2d034cbb 100644
--- a/src/client/components/main-window.vala
+++ b/src/client/components/main-window.vala
@@ -10,16 +10,165 @@
 public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface {
 
 
+    // Named actions.
+    public const string ACTION_ARCHIVE_CONVERSATION = "archive-conv";
+    public const string ACTION_CONVERSATION_DOWN = "down-conversation";
+    public const string ACTION_CONVERSATION_LIST = "focus-conv-list";
+    public const string ACTION_CONVERSATION_UP = "up-conversation";
+    public const string ACTION_COPY_MENU = "show-copy-menu";
+    public const string ACTION_DELETE_CONVERSATION = "delete-conv";
+    public const string ACTION_EMPTY_SPAM = "empty-spam";
+    public const string ACTION_EMPTY_TRASH = "empty-trash";
+    public const string ACTION_FIND_IN_CONVERSATION = "conv-find";
+    public const string ACTION_FORWARD_MESSAGE = "forward-message";
+    public const string ACTION_MARK_AS_READ = "mark-message-read";
+    public const string ACTION_MARK_AS_STARRED = "mark-message-starred";
+    public const string ACTION_MARK_AS_UNREAD = "mark-message-unread";
+    public const string ACTION_MARK_AS_UNSTARRED = "mark-message-unstarred";
+    public const string ACTION_MOVE_MENU = "show-move-menu";
+    public const string ACTION_REPLY_ALL_MESSAGE = "reply-all-message";
+    public const string ACTION_REPLY_TO_MESSAGE = "reply-to-message";
+    public const string ACTION_SEARCH = "search-conv";
+    public const string ACTION_SHOW_MARK_MENU = "mark-message-menu";
+    public const string ACTION_TOGGLE_FIND = "toggle-find";
+    public const string ACTION_TOGGLE_SEARCH = "toggle-search";
+    public const string ACTION_TOGGLE_SPAM = "toggle-message-spam";
+    public const string ACTION_TRASH_CONVERSATION = "trash-conv";
+    public const string ACTION_ZOOM = "zoom";
+
     private const int STATUS_BAR_HEIGHT = 18;
     private const int UPDATE_UI_INTERVAL = 60;
     private const int MIN_CONVERSATION_COUNT = 50;
 
+    private const ActionEntry[] win_action_entries = {
+        {GearyApplication.ACTION_CLOSE, on_close },
+        {GearyApplication.ACTION_UNDO,  on_undo },
+        {GearyApplication.ACTION_REDO,  on_redo },
+
+        {ACTION_CONVERSATION_LIST,     on_conversation_list            },
+        {ACTION_FIND_IN_CONVERSATION,  on_find_in_conversation_action  },
+        {ACTION_SEARCH,                on_search_activated             },
+        {ACTION_EMPTY_SPAM,            on_empty_spam                   },
+        {ACTION_EMPTY_TRASH,           on_empty_trash                  },
+        // Message actions
+        {ACTION_REPLY_TO_MESSAGE,      on_reply_to_message     },
+        {ACTION_REPLY_ALL_MESSAGE,     on_reply_all_message    },
+        {ACTION_FORWARD_MESSAGE,       on_forward_message      },
+        {ACTION_ARCHIVE_CONVERSATION,  on_archive_conversation },
+        {ACTION_TRASH_CONVERSATION,    on_trash_conversation   },
+        {ACTION_DELETE_CONVERSATION,   on_delete_conversation  },
+        {ACTION_COPY_MENU,             on_show_copy_menu       },
+        {ACTION_MOVE_MENU,             on_show_move_menu       },
+        {ACTION_CONVERSATION_UP,       on_conversation_up      },
+        {ACTION_CONVERSATION_DOWN,     on_conversation_down    },
+        // Message marking actions
+        {ACTION_SHOW_MARK_MENU,     on_show_mark_menu           },
+        {ACTION_MARK_AS_READ,       on_mark_as_read             },
+        {ACTION_MARK_AS_UNREAD,     on_mark_as_unread           },
+        {ACTION_MARK_AS_STARRED,    on_mark_as_starred          },
+        {ACTION_MARK_AS_UNSTARRED,  on_mark_as_unstarred        },
+        {ACTION_TOGGLE_SPAM,        on_mark_as_spam_toggle      },
+        // Message viewer
+        {ACTION_ZOOM,  on_zoom,  "s"  },
+    };
+
+
+    public static void add_window_accelerators(GearyApplication owner) {
+        // Marking actions
+        //
+        // Unmark is the primary action
+        owner.add_window_accelerators(
+            ACTION_MARK_AS_READ, { "<Ctrl><Shift>U", "<Shift>I" }
+        );
+        owner.add_window_accelerators(
+            ACTION_MARK_AS_UNREAD, { "<Ctrl>U", "<Shift>U" }
+        );
+        // Ephy uses Ctrl+D for bookmarking
+        owner.add_window_accelerators(
+            ACTION_MARK_AS_STARRED, { "<Ctrl>D", "S" }
+        );
+        owner.add_window_accelerators(
+            ACTION_MARK_AS_UNSTARRED, { "<Ctrl><Shift>D", "D" }
+        );
+        owner.add_window_accelerators(
+            ACTION_TOGGLE_SPAM, { "<Ctrl>J", "exclam" } // Exclamation mark (!)
+        );
+
+        // Replying & forwarding
+        owner.add_window_accelerators(
+            ACTION_REPLY_TO_MESSAGE, { "<Ctrl>R", "R" }
+        );
+        owner.add_window_accelerators(
+            ACTION_REPLY_ALL_MESSAGE, { "<Ctrl><Shift>R", "<Shift>R" }
+        );
+        owner.add_window_accelerators(
+            ACTION_FORWARD_MESSAGE, { "<Ctrl>L", "F" }
+        );
+
+        // Moving & labelling
+        owner.add_window_accelerators(
+            ACTION_COPY_MENU, { "<Ctrl>L", "L" }
+        );
+        owner.add_window_accelerators(
+            ACTION_MOVE_MENU, { "<Ctrl>M", "M" }
+        );
+        owner.add_window_accelerators(
+            ACTION_ARCHIVE_CONVERSATION, { "<Ctrl>K", "A", "Y" }
+        );
+        owner.add_window_accelerators(
+            ACTION_TRASH_CONVERSATION, { "Delete", "BackSpace" }
+        );
+        owner.add_window_accelerators(
+            ACTION_DELETE_CONVERSATION, { "<Shift>Delete", "<Shift>BackSpace" }
+        );
+
+        // Find & search
+        owner.add_window_accelerators(
+            ACTION_FIND_IN_CONVERSATION, { "<Ctrl>F", "slash" }
+        );
+        owner.add_window_accelerators(
+            ACTION_SEARCH, { "<Ctrl>S" }
+        );
+
+        // Zoom
+        owner.add_window_accelerators(
+            ACTION_ZOOM+("('in')"), { "<Ctrl>equal", "<Ctrl>plus" }
+        );
+        owner.add_window_accelerators(
+            ACTION_ZOOM+("('out')"), { "<Ctrl>minus" }
+        );
+        owner.add_window_accelerators(
+            ACTION_ZOOM+("('normal')"), { "<Ctrl>0" }
+        );
+
+        // Navigation
+        owner.add_window_accelerators(
+            ACTION_CONVERSATION_LIST, { "<Ctrl>B" }
+        );
+        owner.add_window_accelerators(
+            ACTION_CONVERSATION_UP, { "<Ctrl>bracketleft", "K" }
+        );
+        owner.add_window_accelerators(
+            ACTION_CONVERSATION_DOWN, { "<Ctrl>bracketright", "J" }
+        );
+    }
+
 
     public new GearyApplication application {
         get { return (GearyApplication) base.get_application(); }
         set { base.set_application(value); }
     }
 
+    /** Currently selected account, null if none selected */
+    public Geary.Account? current_account {
+        owned get {
+            Geary.Account? account = null;
+            if (this.current_folder != null) {
+                account = this.current_folder.account;
+            }
+            return account;
+        }
+    }
     /** Currently selected folder, null if none selected */
     public Geary.Folder? current_folder { get; private set; default = null; }
 
@@ -110,14 +259,15 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface {
 
         load_config(application.config);
         restore_saved_window_state();
-
-        this.application.engine.account_available.connect(on_account_available);
-        this.application.engine.account_unavailable.connect(on_account_unavailable);
+        add_action_entries(win_action_entries, this);
 
         set_styling();
         setup_layout(application.config);
         on_change_orientation();
 
+        this.application.engine.account_available.connect(on_account_available);
+        this.application.engine.account_unavailable.connect(on_account_unavailable);
+
         this.update_ui_timeout = new Geary.TimeoutManager.seconds(
             UPDATE_UI_INTERVAL, on_update_ui_timeout
         );
@@ -235,7 +385,7 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface {
             new ComposerWindow(composer, this.application);
         } else {
             this.conversation_viewer.do_compose(composer);
-            get_action(Application.Controller.ACTION_FIND_IN_CONVERSATION).set_enabled(false);
+            get_action(ACTION_FIND_IN_CONVERSATION).set_enabled(false);
         }
     }
 
@@ -382,6 +532,8 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface {
         );
 
         this.main_toolbar = new MainToolbar(config);
+        this.main_toolbar.move_folder_menu.folder_selected.connect(on_move_conversation);
+        this.main_toolbar.copy_folder_menu.folder_selected.connect(on_copy_conversation);
         this.main_toolbar.bind_property("search-open", this.search_bar, "search-mode-enabled",
             BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL);
         this.main_toolbar.bind_property("find-open", this.conversation_viewer.conversation_find_bar,
@@ -405,10 +557,15 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface {
 
         // Search bar
         this.search_bar_box.pack_start(this.search_bar, false, false, 0);
+
         // Folder list
         this.folder_list_scrolled.add(this.folder_list);
+        this.folder_list.move_conversation.connect(on_move_conversation);
+        this.folder_list.copy_conversation.connect(on_copy_conversation);
+
         // Conversation list
         this.conversation_list_scrolled.add(this.conversation_list_view);
+
         // Conversation viewer
         this.conversations_paned.pack2(this.conversation_viewer, true, true);
 
@@ -873,4 +1030,140 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface {
         update_ui();
     }
 
+    // Action callbacks
+
+    private void on_undo() {
+    }
+
+    private void on_redo() {
+    }
+
+    private void on_close() {
+        close();
+    }
+
+    private void on_conversation_list() {
+        this.conversation_list_view.grab_focus();
+    }
+
+    private void on_find_in_conversation_action() {
+        this.conversation_viewer.enable_find();
+    }
+
+    private void on_search_activated() {
+        show_search_bar();
+    }
+
+    private void on_zoom(SimpleAction action, Variant? parameter) {
+        ConversationListBox? view = this.conversation_viewer.current_list;
+        if (view != null && parameter != null) {
+            string zoom_action = parameter.get_string();
+            if (zoom_action == "in")
+                view.zoom_in();
+            else if (zoom_action == "out")
+                view.zoom_out();
+            else
+                view.zoom_reset();
+        }
+    }
+
+    private void on_reply_to_message() {
+    }
+
+    private void on_reply_all_message() {
+    }
+
+    private void on_forward_message() {
+    }
+
+    private void on_show_copy_menu() {
+        this.main_toolbar.copy_message_button.clicked();
+    }
+
+    private void on_show_move_menu() {
+        this.main_toolbar.move_message_button.clicked();
+    }
+
+    private void on_conversation_up() {
+        this.conversation_list_view.scroll(Gtk.ScrollType.STEP_UP);
+    }
+
+    private void on_conversation_down() {
+        this.conversation_list_view.scroll(Gtk.ScrollType.STEP_DOWN);
+    }
+
+    private void on_show_mark_menu() {
+        bool unread_selected = false;
+        bool read_selected = false;
+        bool starred_selected = false;
+        bool unstarred_selected = false;
+        foreach (Geary.App.Conversation conversation in
+                 this.conversation_list_view.get_selected_conversations()) {
+            if (conversation.is_unread())
+                unread_selected = true;
+
+            // Only check the messages that "Mark as Unread" would mark, so we
+            // don't add the menu option and have it not do anything.
+            //
+            // Sort by Date: field to correspond with ConversationViewer ordering
+            Geary.Email? latest = conversation.get_latest_sent_email(
+                Geary.App.Conversation.Location.IN_FOLDER_OUT_OF_FOLDER);
+            if (latest != null && latest.email_flags != null
+                && !latest.email_flags.contains(Geary.EmailFlags.UNREAD))
+                read_selected = true;
+
+            if (conversation.is_flagged()) {
+                starred_selected = true;
+            } else {
+                unstarred_selected = true;
+            }
+        }
+        get_action(ACTION_MARK_AS_READ).set_enabled(unread_selected);
+        get_action(ACTION_MARK_AS_UNREAD).set_enabled(read_selected);
+        get_action(ACTION_MARK_AS_STARRED).set_enabled(unstarred_selected);
+        get_action(ACTION_MARK_AS_UNSTARRED).set_enabled(starred_selected);
+
+        // If we're in Drafts/Outbox, we also shouldn't set a message as SPAM.
+        bool in_spam_folder = current_folder.special_folder_type == Geary.SpecialFolderType.SPAM;
+        get_action(ACTION_TOGGLE_SPAM).set_enabled(!in_spam_folder &&
+            current_folder.special_folder_type != Geary.SpecialFolderType.DRAFTS &&
+            current_folder.special_folder_type != Geary.SpecialFolderType.OUTBOX);
+    }
+
+    private void on_mark_as_read() {
+    }
+
+    private void on_mark_as_unread() {
+    }
+
+    private void on_mark_as_starred() {
+    }
+
+    private void on_mark_as_unstarred() {
+    }
+
+    private void on_mark_as_spam_toggle() {
+    }
+
+    private void on_move_conversation(Geary.Folder destination) {
+    }
+
+    private void on_copy_conversation(Geary.Folder destination) {
+    }
+
+    private void on_archive_conversation() {
+    }
+
+    private void on_trash_conversation() {
+    }
+
+    private void on_delete_conversation() {
+    }
+
+    private void on_empty_spam() {
+    }
+
+    private void on_empty_trash() {
+    }
+
 }
diff --git a/src/client/conversation-list/conversation-list-view.vala 
b/src/client/conversation-list/conversation-list-view.vala
index 6a1dedf3..4ab74dad 100644
--- a/src/client/conversation-list/conversation-list-view.vala
+++ b/src/client/conversation-list/conversation-list-view.vala
@@ -338,7 +338,7 @@ public class ConversationListView : Gtk.TreeView, Geary.BaseInterface {
                         "Move conversations to _Trash",
                         this.selected.size
                     ),
-                    "win." + Application.Controller.ACTION_ARCHIVE_CONVERSATION
+                    "win." + MainWindow.ACTION_ARCHIVE_CONVERSATION
                 );
             } else {
                 context_menu_model.append(
@@ -348,25 +348,25 @@ public class ConversationListView : Gtk.TreeView, Geary.BaseInterface {
                         "_Delete conversations",
                         this.selected.size
                     ),
-                    "win." + Application.Controller.ACTION_DELETE_CONVERSATION
+                    "win." + MainWindow.ACTION_DELETE_CONVERSATION
                 );
             }
 
             if (conversation.is_unread())
-                context_menu_model.append(_("Mark as _Read"), 
"win."+Application.Controller.ACTION_MARK_AS_READ);
+                context_menu_model.append(_("Mark as _Read"), "win."+MainWindow.ACTION_MARK_AS_READ);
 
             if (conversation.has_any_read_message())
-                context_menu_model.append(_("Mark as _Unread"), 
"win."+Application.Controller.ACTION_MARK_AS_UNREAD);
+                context_menu_model.append(_("Mark as _Unread"), "win."+MainWindow.ACTION_MARK_AS_UNREAD);
 
             if (conversation.is_flagged())
-                context_menu_model.append(_("U_nstar"), 
"win."+Application.Controller.ACTION_MARK_AS_UNSTARRED);
+                context_menu_model.append(_("U_nstar"), "win."+MainWindow.ACTION_MARK_AS_UNSTARRED);
             else
-                context_menu_model.append(_("_Star"), "win."+Application.Controller.ACTION_MARK_AS_STARRED);
+                context_menu_model.append(_("_Star"), "win."+MainWindow.ACTION_MARK_AS_STARRED);
 
             Menu actions_section = new Menu();
-            actions_section.append(_("_Reply"), "win."+Application.Controller.ACTION_REPLY_TO_MESSAGE);
-            actions_section.append(_("R_eply All"), "win."+Application.Controller.ACTION_REPLY_ALL_MESSAGE);
-            actions_section.append(_("_Forward"), "win."+Application.Controller.ACTION_FORWARD_MESSAGE);
+            actions_section.append(_("_Reply"), "win."+MainWindow.ACTION_REPLY_TO_MESSAGE);
+            actions_section.append(_("R_eply All"), "win."+MainWindow.ACTION_REPLY_ALL_MESSAGE);
+            actions_section.append(_("_Forward"), "win."+MainWindow.ACTION_FORWARD_MESSAGE);
             context_menu_model.append_section(null, actions_section);
 
             // Use a popover rather than a regular context menu since
@@ -577,4 +577,5 @@ public class ConversationListView : Gtk.TreeView, Geary.BaseInterface {
         return Gdk.EVENT_PROPAGATE;
 
     }
+
 }


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