[geary/wip/730682-refine-convo-list] Implement actions for selected conversations in the conversation list.



commit 2a30b12f896ea19a5a72f6762521d1f4b1f322e6
Author: Michael James Gratton <mike vee net>
Date:   Wed Dec 27 00:38:33 2017 +1030

    Implement actions for selected conversations in the conversation list.

 src/client/application/geary-controller.vala       |  154 ++++++++++---
 src/client/components/main-window.vala             |  257 +++++++++++++++++++-
 .../conversation-list/conversation-list.vala       |   38 +++-
 3 files changed, 416 insertions(+), 33 deletions(-)
---
diff --git a/src/client/application/geary-controller.vala b/src/client/application/geary-controller.vala
index 0b327dc..4317efa 100644
--- a/src/client/application/geary-controller.vala
+++ b/src/client/application/geary-controller.vala
@@ -1,6 +1,6 @@
 /*
  * Copyright 2016 Software Freedom Conservancy Inc.
- * Copyright 2016, 2017 Michael Gratton <mike vee net>
+ * Copyright 2016-2017 Michael Gratton <mike vee net>
  *
  * This software is licensed under the GNU Lesser General Public License
  * (version 2.1 or later). See the COPYING file in this distribution.
@@ -1155,7 +1155,121 @@ public class GearyController : Geary.BaseObject {
             message("Error enumerating accounts: %s", e.message);
         }
     }
-    
+
+    /**
+     * Adds and removes flags from a set of emails.
+     */
+    internal async void mark_email(Gee.Collection<Geary.EmailIdentifier> ids,
+                                   Geary.EmailFlags? flags_to_add,
+                                   Geary.EmailFlags? flags_to_remove)
+        throws Error {
+        if (ids.size > 0) {
+            Geary.App.EmailStore store =
+                this.email_stores.get(this.current_folder.account);
+            yield store.mark_email_async(
+                ids, flags_to_add, flags_to_remove, this.cancellable_folder
+            );
+        }
+    }
+
+    /**
+     * Moves a set of conversations to a special folder.
+     */
+    internal async void move_conversations(Gee.Collection<Geary.App.Conversation> targets,
+                                           Geary.SpecialFolderType type)
+        throws Error {
+        Gee.List<Geary.EmailIdentifier> ids = get_ids_in_folder(targets);
+        if (type == Geary.SpecialFolderType.ARCHIVE) {
+            Geary.FolderSupport.Archive? archivable =
+                this.current_folder as Geary.FolderSupport.Archive;
+            if (archivable == null) {
+                throw new Geary.ImapError.NOT_SUPPORTED(
+                    "Folder %s doesn't support archive",
+                    this.current_folder.to_string()
+                );
+            }
+            save_revokable(
+                yield archivable.archive_email_async(
+                    ids, this.cancellable_folder
+                ),
+                _("Undo archive (Ctrl+Z)")
+            );
+        } else {
+            Geary.Folder dest = this.current_account.get_special_folder(type);
+            Geary.FolderSupport.Move? movable =
+                this.current_folder as Geary.FolderSupport.Move;
+            if (movable == null) {
+                throw new Geary.ImapError.NOT_SUPPORTED(
+                    "Folder %s doesn't support moving",
+                    this.current_folder.to_string()
+                );
+            }
+            string tooltip = "";
+            switch (type) {
+            case Geary.SpecialFolderType.INBOX:
+                tooltip = _("Undo restore (Ctrl+Z)");
+                break;
+            case Geary.SpecialFolderType.TRASH:
+                tooltip = _("Undo trash (Ctrl+Z)");
+                break;
+            case Geary.SpecialFolderType.SPAM:
+                tooltip = _("Undo junk (Ctrl+Z)");
+                break;
+            default:
+                tooltip = _("Undo move (Ctrl+Z)");
+                break;
+            }
+            save_revokable(
+                yield movable.move_email_async(
+                    ids, dest.path, this.cancellable_folder
+                ),
+                tooltip
+            );
+        }
+    }
+
+    /**
+     * Restores a set of messages to their original location.
+     */
+    internal async void restore_conversations(Gee.Collection<Geary.App.Conversation> targets)
+        throws Error {
+        yield move_conversations(targets, Geary.SpecialFolderType.INBOX);
+    }
+
+    /**
+     * Permanently deletes a set of conversations, without prompting.
+     */
+    internal async void delete_conversations(Gee.Collection<Geary.App.Conversation> targets)
+        throws Error {
+        Gee.List<Geary.EmailIdentifier> ids = get_ids_in_folder(targets);
+        Geary.FolderSupport.Remove? removable =
+            this.current_folder as Geary.FolderSupport.Remove;
+        if (removable == null) {
+            throw new Geary.ImapError.NOT_SUPPORTED(
+                "Folder %s doesn't support deletion",
+                this.current_folder.to_string()
+            );
+        }
+
+        yield removable.remove_email_async(ids, this.cancellable_folder);
+    }
+
+    /**
+     * Returns conversation's email ids that are in-folder.
+     */
+    private Gee.List<Geary.EmailIdentifier> get_ids_in_folder(Gee.Collection<Geary.App.Conversation> 
targets) {
+        Gee.LinkedList<Geary.EmailIdentifier> ids =
+            new Gee.LinkedList<Geary.EmailIdentifier>();
+        foreach (Geary.App.Conversation convo in targets) {
+            foreach (Geary.Email email in
+                     convo.get_emails(Geary.App.Conversation.Ordering.NONE,
+                                      Geary.App.Conversation.Location.ANYWHERE)) {
+                ids.add(email.id);
+            }
+        }
+        return ids;
+    }
+
     /**
      * Returns true if we've attempted to open all accounts at this point.
      */
@@ -1568,14 +1682,6 @@ public class GearyController : Geary.BaseObject {
         return ids;
     }
 
-    private void mark_email(Gee.Collection<Geary.EmailIdentifier> ids,
-        Geary.EmailFlags? flags_to_add, Geary.EmailFlags? flags_to_remove) {
-        if (ids.size > 0) {
-            email_stores.get(current_folder.account).mark_email_async.begin(
-                ids, flags_to_add, flags_to_remove, cancellable_folder);
-        }
-    }
-
     private void on_show_mark_menu() {
         Geary.App.Conversation conversation =
             this.main_window.conversation_list.selected;
@@ -1645,7 +1751,7 @@ public class GearyController : Geary.BaseObject {
 
     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);
+        mark_email.begin(emails, flags_to_add, flags_to_remove);
     }
 
     private void on_mark_as_read(SimpleAction action) {
@@ -1653,7 +1759,7 @@ public class GearyController : Geary.BaseObject {
         flags.add(Geary.EmailFlags.UNREAD);
 
         Gee.ArrayList<Geary.EmailIdentifier> ids = get_selected_email_ids(false);
-        mark_email(ids, null, flags);
+        mark_email.begin(ids, null, flags);
 
         ConversationListBox? list =
             main_window.conversation_viewer.current_list;
@@ -1668,7 +1774,7 @@ public class GearyController : Geary.BaseObject {
         flags.add(Geary.EmailFlags.UNREAD);
 
         Gee.ArrayList<Geary.EmailIdentifier> ids = get_selected_email_ids(true);
-        mark_email(ids, flags, null);
+        mark_email.begin(ids, flags, null);
 
         ConversationListBox? list =
             main_window.conversation_viewer.current_list;
@@ -1681,21 +1787,21 @@ public class GearyController : Geary.BaseObject {
     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);
+        mark_email.begin(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);
+        mark_email.begin(get_selected_email_ids(false), null, flags);
     }
 
     private void on_show_move_menu(SimpleAction? action) {
-        this.main_window.main_toolbar.copy_conversation_button.clicked();
+        this.main_window.main_toolbar.copy_conversation_button.activate();
     }
 
     private void on_show_copy_menu(SimpleAction? action) {
-        this.main_window.main_toolbar.move_conversation_button.clicked();
+        this.main_window.main_toolbar.move_conversation_button.activate();
     }
 
     private async void mark_as_spam_toggle_async(Cancellable? cancellable) {
@@ -2274,17 +2380,7 @@ public class GearyController : Geary.BaseObject {
             && !current_folder.properties.is_local_only && current_account != null
             && (current_folder as Geary.FolderSupport.Move) != null);
     }
-    
-    public bool confirm_delete(int num_messages) {
-        main_window.present();
-        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 archive_or_delete_selection_async(bool archive, bool trash,
         Cancellable? cancellable) throws Error {
         if (!can_switch_conversation_view())
@@ -2346,7 +2442,7 @@ public class GearyController : Geary.BaseObject {
         if (supports_remove == null) {
             debug("Folder %s doesn't support remove", current_folder.to_string());
         } else {
-            if (confirm_delete(ids.size))
+            if (this.main_window.confirm_delete())
                 yield supports_remove.remove_email_async(ids, cancellable);
             else
                 last_deleted_conversation = null;
diff --git a/src/client/components/main-window.vala b/src/client/components/main-window.vala
index 8d8c318..97f0e09 100644
--- a/src/client/components/main-window.vala
+++ b/src/client/components/main-window.vala
@@ -10,15 +10,34 @@
 public class MainWindow : Gtk.ApplicationWindow {
 
 
+    public const string ACTION_ARCHIVE = "conversation-archive";
+    public const string ACTION_DELETE = "conversation-delete";
+    public const string ACTION_JUNK = "conversation-junk";
+    public const string ACTION_MARK_READ = "conversation-mark-read";
+    public const string ACTION_MARK_STARRED = "conversation-mark-starred";
+    public const string ACTION_MARK_UNREAD = "conversation-mark-unread";
+    public const string ACTION_MARK_UNSTARRED = "conversation-mark-unstarred";
+    public const string ACTION_RESTORE = "conversation-restore";
+    public const string ACTION_TRASH = "conversation-trash";
+
     public const string ACTION_SELECTION_MODE_DISABLE = "selection-mode-disable";
     public const string ACTION_SELECTION_MODE_ENABLE = "selection-mode-enable";
 
-
     private const int STATUS_BAR_HEIGHT = 18;
 
     private const ActionEntry[] action_entries = {
-        {ACTION_SELECTION_MODE_DISABLE, on_selection_mode_disabled },
-        {ACTION_SELECTION_MODE_ENABLE, on_selection_mode_enabled }
+        { ACTION_ARCHIVE,        on_conversation_archive        },
+        { ACTION_DELETE,         on_conversation_delete         },
+        { ACTION_JUNK,           on_conversation_junk           },
+        { ACTION_MARK_READ,      on_conversation_mark_read      },
+        { ACTION_MARK_STARRED,   on_conversation_mark_starred   },
+        { ACTION_MARK_UNREAD,    on_conversation_mark_unread    },
+        { ACTION_MARK_UNSTARRED, on_conversation_mark_unstarred },
+        { ACTION_RESTORE,        on_conversation_restore        },
+        { ACTION_TRASH,          on_conversation_trash          },
+
+        { ACTION_SELECTION_MODE_DISABLE, on_selection_mode_disabled },
+        { ACTION_SELECTION_MODE_ENABLE,  on_selection_mode_enabled  }
     };
 
 
@@ -298,6 +317,51 @@ public class MainWindow : Gtk.ApplicationWindow {
         return handled;
     }
 
+    /**
+     * Prompts the user to confirm deleting conversations.
+     */
+    internal bool confirm_delete() {
+        present();
+        ConfirmationDialog dialog = new ConfirmationDialog(
+            this,
+            _("Do you want to permanently delete conversation messages in this folder?"),
+            null,
+            _("Delete"),
+            "destructive-action"
+        );
+        return (dialog.run() == Gtk.ResponseType.OK);
+    }
+
+    /**
+     * Returns email ids from all highlighted conversations, if any.
+     */
+    private Gee.List<Geary.EmailIdentifier> get_highlighted_email() {
+        Gee.LinkedList<Geary.EmailIdentifier> ids =
+            new Gee.LinkedList<Geary.EmailIdentifier>();
+        foreach (Geary.App.Conversation convo in
+                 this.conversation_list.get_highlighted_conversations()) {
+            ids.add_all(convo.get_email_ids());
+        }
+        return ids;
+    }
+
+    /**
+     * Returns id of most latest email in all highlighted conversations.
+     */
+    private Gee.List<Geary.EmailIdentifier> get_highlighted_latest_email() {
+        Gee.LinkedList<Geary.EmailIdentifier> ids =
+            new Gee.LinkedList<Geary.EmailIdentifier>();
+        foreach (Geary.App.Conversation convo in
+                 this.conversation_list.get_highlighted_conversations()) {
+            Geary.Email? latest = convo.get_latest_sent_email(
+                Geary.App.Conversation.Location.IN_FOLDER_OUT_OF_FOLDER);
+            if (latest != null) {
+                ids.add(latest.id);
+            }
+        }
+        return ids;
+    }
+
     private void setup_actions() {
         add_action_entries(action_entries, this);
         add_window_accelerators(ACTION_SELECTION_MODE_DISABLE, { "Escape", });
@@ -337,6 +401,16 @@ public class MainWindow : Gtk.ApplicationWindow {
             this.main_toolbar.folder = this.current_folder.get_display_name();
     }
 
+    private void update_conversation_actions(Geary.App.Conversation? target) {
+        bool is_unread = target.is_unread();
+        get_action(ACTION_MARK_READ).set_enabled(is_unread);
+        get_action(ACTION_MARK_UNREAD).set_enabled(!is_unread);
+
+        bool is_starred = target.is_flagged();
+        get_action(ACTION_MARK_UNSTARRED).set_enabled(is_starred);
+        get_action(ACTION_MARK_STARRED).set_enabled(!is_starred);
+    }
+
     private inline void check_shift_event(Gdk.EventKey event) {
         // FIXME: it's possible the user will press two shift keys.  We want
         // the shift key to report as released when they release ALL of them.
@@ -397,6 +471,13 @@ public class MainWindow : Gtk.ApplicationWindow {
         this.conversation_viewer.show_none_selected();
     }
 
+    private void report_problem(Action action, Variant? param, Error err) {
+        // XXX
+        debug("Client problem reported: %s: %s",
+              action.get_name(),
+              err.message);
+    }
+
     private void on_conversation_monitor_changed() {
         this.conversation_list.freeze_selection();
         ConversationListModel? old_model = this.conversation_list.model;
@@ -412,6 +493,7 @@ public class MainWindow : Gtk.ApplicationWindow {
             old_monitor.scan_completed.disconnect(on_conversation_count_changed);
             old_monitor.conversations_added.disconnect(on_conversation_count_changed);
             old_monitor.conversations_removed.disconnect(on_conversation_count_changed);
+            old_monitor.email_flags_changed.disconnect(on_conversation_flags_changed);
         }
 
         Geary.App.ConversationMonitor? new_monitor =
@@ -430,6 +512,7 @@ public class MainWindow : Gtk.ApplicationWindow {
             new_monitor.scan_completed.connect(on_conversation_count_changed);
             new_monitor.conversations_added.connect(on_conversation_count_changed);
             new_monitor.conversations_removed.connect(on_conversation_count_changed);
+            new_monitor.email_flags_changed.connect(on_conversation_flags_changed);
         }
         this.conversation_list.thaw_selection();
     }
@@ -514,6 +597,7 @@ public class MainWindow : Gtk.ApplicationWindow {
 
     private void on_conversation_selection_changed(Geary.App.Conversation? selection) {
         show_conversation(selection);
+        update_conversation_actions(selection);
     }
 
     private void on_conversation_activated(Geary.App.Conversation activated) {
@@ -586,6 +670,12 @@ public class MainWindow : Gtk.ApplicationWindow {
         }
     }
 
+    private void on_conversation_flags_changed(Geary.App.Conversation changed) {
+        if (this.conversation_list.selected == changed) {
+            update_conversation_actions(changed);
+        }
+    }
+
     [GtkCallback]
     private bool on_delete_event() {
         if (this.application.is_background_service) {
@@ -619,6 +709,167 @@ public class MainWindow : Gtk.ApplicationWindow {
         }
     }
 
+    private void on_conversation_archive(Action action, Variant? param) {
+        this.application.controller.move_conversations.begin(
+            this.conversation_list.get_highlighted_conversations(),
+            Geary.SpecialFolderType.ARCHIVE,
+            (obj, ret) => {
+                try {
+                    this.application.controller.move_conversations.end(ret);
+                } catch (Error err) {
+                    report_problem(action, param, err);
+                }
+            }
+        );
+    }
+
+    private void on_conversation_delete(Action action, Variant? param) {
+        if (confirm_delete()) {
+            this.application.controller.delete_conversations.begin(
+                this.conversation_list.get_highlighted_conversations(),
+                (obj, ret) => {
+                    try {
+                        this.application.controller.delete_conversations.end(ret);
+                    } catch (Error err) {
+                        report_problem(action, param, err);
+                    }
+                }
+            );
+        }
+    }
+
+    private void on_conversation_junk(Action action, Variant? param) {
+        this.application.controller.move_conversations.begin(
+            this.conversation_list.get_highlighted_conversations(),
+            Geary.SpecialFolderType.SPAM,
+            (obj, ret) => {
+                try {
+                    this.application.controller.move_conversations.end(ret);
+                } catch (Error err) {
+                    report_problem(action, param, err);
+                }
+            }
+        );
+    }
+
+    private void on_conversation_mark_read(Action action, Variant? param) {
+        Geary.EmailFlags flags = new Geary.EmailFlags();
+        flags.add(Geary.EmailFlags.UNREAD);
+
+        Gee.List<Geary.EmailIdentifier> ids = get_highlighted_email();
+        ConversationListBox? list = this.conversation_viewer.current_list;
+        this.application.controller.mark_email.begin(
+            ids, null, flags,
+            (obj, ret) => {
+                try {
+                    this.application.controller.mark_email.end(ret);
+                } catch (Error err) {
+                    report_problem(action, param, err);
+                    // undo the manual marking
+                    if (list != null) {
+                        foreach (Geary.EmailIdentifier id in ids) {
+                            list.mark_manual_unread(id);
+                        }
+                    }
+                }
+            }
+        );
+
+        if (list != null) {
+            foreach (Geary.EmailIdentifier id in ids) {
+                list.mark_manual_read(id);
+            }
+        }
+    }
+
+    private void on_conversation_mark_unread(Action action, Variant? param) {
+        Geary.EmailFlags flags = new Geary.EmailFlags();
+        flags.add(Geary.EmailFlags.UNREAD);
+
+        Gee.List<Geary.EmailIdentifier> ids = get_highlighted_latest_email();
+        ConversationListBox? list = this.conversation_viewer.current_list;
+        this.application.controller.mark_email.begin(
+            ids, flags, null,
+            (obj, ret) => {
+                try {
+                    this.application.controller.mark_email.end(ret);
+                } catch (Error err) {
+                    report_problem(action, param, err);
+                    // undo the manual marking
+                    if (list != null) {
+                        foreach (Geary.EmailIdentifier id in ids) {
+                            list.mark_manual_read(id);
+                        }
+                    }
+                }
+            }
+        );
+
+        if (list != null) {
+            foreach (Geary.EmailIdentifier id in ids) {
+                list.mark_manual_unread(id);
+            }
+        }
+    }
+
+    private void on_conversation_mark_starred(Action action, Variant? param) {
+        Geary.EmailFlags flags = new Geary.EmailFlags();
+        flags.add(Geary.EmailFlags.FLAGGED);
+        this.application.controller.mark_email.begin(
+            get_highlighted_latest_email(), flags, null,
+            (obj, ret) => {
+                try {
+                    this.application.controller.mark_email.end(ret);
+                } catch (Error err) {
+                    report_problem(action, param, err);
+                }
+            }
+        );
+    }
+
+    private void on_conversation_mark_unstarred(Action action, Variant? param) {
+        Geary.EmailFlags flags = new Geary.EmailFlags();
+        flags.add(Geary.EmailFlags.FLAGGED);
+        this.application.controller.mark_email.begin(
+            get_highlighted_email(), null, flags,
+            (obj, ret) => {
+                try {
+                    this.application.controller.mark_email.end(ret);
+                } catch (Error err) {
+                    report_problem(action, param, err);
+                }
+            }
+        );
+    }
+
+    private void on_conversation_restore(Action action, Variant? param) {
+        this.application.controller.restore_conversations.begin(
+            this.conversation_list.get_highlighted_conversations(),
+            (obj, ret) => {
+                try {
+                    this.application.controller.restore_conversations.end(ret);
+                } catch (Error err) {
+                    report_problem(action, param, err);
+                }
+            }
+        );
+    }
+
+    private void on_conversation_trash(Action action, Variant? param) {
+        this.application.controller.move_conversations.begin(
+            this.conversation_list.get_highlighted_conversations(),
+            Geary.SpecialFolderType.TRASH,
+            (obj, ret) => {
+                try {
+                    this.application.controller.move_conversations.end(ret);
+                } catch (Error err) {
+                    report_problem(action, param, err);
+                }
+            }
+        );
+    }
+
+
     private void on_selection_mode_enabled() {
         set_selection_mode_enabled(true);
     }
diff --git a/src/client/conversation-list/conversation-list.vala 
b/src/client/conversation-list/conversation-list.vala
index 8c2f410..02139fc 100644
--- a/src/client/conversation-list/conversation-list.vala
+++ b/src/client/conversation-list/conversation-list.vala
@@ -12,7 +12,9 @@
  * This class uses the GtkListBox's selection system for selecting and
  * displaying individual conversations, and supports the GNOME3 HIG
  * selection mode pattern for to allow multiple conversations to be
- * marked, independent of the list's selection.
+ * marked, independent of the list's selection. These conversations
+ * are referred to `selected` and `marked`, respectively, or
+ * `highlighted` if referring to either.
  */
 public class ConversationList : Gtk.ListBox {
 
@@ -34,6 +36,15 @@ public class ConversationList : Gtk.ListBox {
     /** Determines if selection mode is enabled for the list. */
     public bool is_selection_mode_enabled { get; private set; default = false; }
 
+    /** Determines if the list has selected or marked conversations. */
+    public bool has_highlighted_conversations {
+        get {
+            return this.is_selection_mode_enabled
+                ? !this.marked.is_empty
+                : this.selected != null;
+        }
+    }
+
     private Configuration config;
     private int selected_index = -1;
     private bool selection_frozen = false;
@@ -96,6 +107,31 @@ public class ConversationList : Gtk.ListBox {
     }
 
     /**
+     * Returns current selected or marked conversations, if any.
+     */
+    public bool is_highlighted(Geary.App.Conversation target) {
+        return this.is_selection_mode_enabled
+            ? this.marked.has_key(target)
+            : this.selected == target;
+    }
+
+    /**
+     * Returns current selected or marked conversations, if any.
+     */
+    public Gee.Collection<Geary.App.Conversation> get_highlighted_conversations() {
+        Gee.Collection<Geary.App.Conversation>? highlighted = null;
+        if (this.is_selection_mode_enabled) {
+            highlighted = this.get_marked_items();
+        } else {
+            highlighted = new Gee.LinkedList<Geary.App.Conversation>();
+            if (this.selected != null) {
+                highlighted.add(this.selected);
+            }
+        }
+        return highlighted;
+    }
+
+    /**
      * Returns a read-only collection of currently marked items.
      *
      * This is distinct to the conversations marked via the list's


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