[geary/wip/730682-refine-convo-list: 3/11] Implement single-conversation actions.



commit 42a635d8a3fd527571ce79630bbb2c5680f7c803
Author: Michael James Gratton <mike vee net>
Date:   Wed Jan 3 19:57:29 2018 +1100

    Implement single-conversation actions.
    
    This enables both context menus and per-conversatio flagging in the
    conversation list.
    
    * src/client/components/main-window.vala (MainWindow): Split conversation
      actions up into single-conversation actions and possibly-multiple
      conversation highlighted actions, update existing uses of highlighted
      action names. Add implementation for all single-conversations
      actions. Hook up to new ConversationList context menu signal, query to
      determine policy and show an appropriately updated context menu when
      the query comes back.
    
    * src/client/conversation-list/conversation-list-item.vala
      (ConversationListItem): Update star/unstar buttons actio target based
      on the item's conversation.
    
    * src/client/conversation-list/conversation-list.vala (ConversationList):
      Load the default context menu from resource, look for context-menu
      clicks and fire signal so get it shown as appropriate.

 po/POTFILES.in                                     |    1 +
 src/client/components/main-window.vala             |  381 +++++++++++++++++---
 .../conversation-list/conversation-list-item.vala  |   21 ++
 .../conversation-list/conversation-list.vala       |   32 ++
 src/engine/util/util-collection.vala               |    8 +
 ui/CMakeLists.txt                                  |    1 +
 ui/conversation-action-bar.ui                      |   18 +-
 ui/conversation-list-menus.ui                      |   46 +++
 8 files changed, 454 insertions(+), 54 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 366745e..c2a8947 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -410,6 +410,7 @@ ui/conversation-email.ui
 ui/conversation-email-attachment-view.ui
 ui/conversation-email-menus.ui
 ui/conversation-list-item.ui
+ui/conversation-list-menus.ui
 ui/conversation-message-menus.ui
 ui/conversation-message.ui
 ui/conversation-viewer.ui
diff --git a/src/client/components/main-window.vala b/src/client/components/main-window.vala
index 121b33a..745ad5a 100644
--- a/src/client/components/main-window.vala
+++ b/src/client/components/main-window.vala
@@ -10,17 +10,27 @@
 public class MainWindow : Gtk.ApplicationWindow {
 
 
-    public const string ACTION_ARCHIVE = "conversation-archive";
-    public const string ACTION_COPY = "conversation-copy";
-    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_MOVE = "conversation-move";
-    public const string ACTION_RESTORE = "conversation-restore";
-    public const string ACTION_TRASH = "conversation-trash";
+    public const string ACTION_CONVERSATION_ARCHIVE = "conversation-archive";
+    public const string ACTION_CONVERSATION_DELETE = "conversation-delete";
+    public const string ACTION_CONVERSATION_JUNK = "conversation-junk";
+    public const string ACTION_CONVERSATION_MARK_READ = "conversation-mark-read";
+    public const string ACTION_CONVERSATION_MARK_STARRED = "conversation-mark-starred";
+    public const string ACTION_CONVERSATION_MARK_UNREAD = "conversation-mark-unread";
+    public const string ACTION_CONVERSATION_MARK_UNSTARRED = "conversation-mark-unstarred";
+    public const string ACTION_CONVERSATION_RESTORE = "conversation-restore";
+    public const string ACTION_CONVERSATION_TRASH = "conversation-trash";
+
+    public const string ACTION_HIGHLIGHTED_ARCHIVE = "highlighted-archive";
+    public const string ACTION_HIGHLIGHTED_COPY = "highlighted-copy";
+    public const string ACTION_HIGHLIGHTED_DELETE = "highlighted-delete";
+    public const string ACTION_HIGHLIGHTED_JUNK = "highlighted-junk";
+    public const string ACTION_HIGHLIGHTED_MARK_READ = "highlighted-mark-read";
+    public const string ACTION_HIGHLIGHTED_MARK_STARRED = "highlighted-mark-starred";
+    public const string ACTION_HIGHLIGHTED_MARK_UNREAD = "highlighted-mark-unread";
+    public const string ACTION_HIGHLIGHTED_MARK_UNSTARRED = "highlighted-mark-unstarred";
+    public const string ACTION_HIGHLIGHTED_MOVE = "highlighted-move";
+    public const string ACTION_HIGHLIGHTED_RESTORE = "highlighted-restore";
+    public const string ACTION_HIGHLIGHTED_TRASH = "highlighted-trash";
 
     public const string ACTION_SHOW_COPY = "show-copy";
     public const string ACTION_SHOW_MOVE = "show-move";
@@ -33,17 +43,33 @@ public class MainWindow : Gtk.ApplicationWindow {
     private const int STATUS_BAR_HEIGHT = 18;
 
     private const ActionEntry[] action_entries = {
-        { ACTION_ARCHIVE,        on_conversation_archive        },
-        { ACTION_COPY,           on_conversation_copy, "as"     },
-        { 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_MOVE,           on_conversation_move, "as"     },
-        { ACTION_RESTORE,        on_conversation_restore        },
-        { ACTION_TRASH,          on_conversation_trash          },
+        // XXX Using "(yxx)" as the param type here is sketch since we
+        // don't know in advance what type is used to serialise
+        // ConversationListItem.id, since its type (EmailIdentifier)
+        // is abstract
+        { ACTION_CONVERSATION_ARCHIVE,        on_conversation_archive,        "(yxx)" },
+        { ACTION_CONVERSATION_MARK_READ,      on_conversation_mark_read,      "(yxx)" },
+        { ACTION_CONVERSATION_MARK_STARRED,   on_conversation_mark_starred,   "(yxx)" },
+        { ACTION_CONVERSATION_MARK_UNSTARRED, on_conversation_mark_unstarred, "(yxx)" },
+        { ACTION_CONVERSATION_MARK_UNREAD,    on_conversation_mark_unread,    "(yxx)" },
+        { ACTION_CONVERSATION_DELETE,         on_conversation_delete,         "(yxx)" },
+        { ACTION_CONVERSATION_JUNK,           on_conversation_junk,           "(yxx)" },
+        { ACTION_CONVERSATION_RESTORE,        on_conversation_restore,        "(yxx)" },
+        { ACTION_CONVERSATION_TRASH,          on_conversation_trash,          "(yxx)" },
+
+        { ACTION_HIGHLIGHTED_ARCHIVE,        on_highlighted_archive            },
+        { ACTION_HIGHLIGHTED_COPY,           on_highlighted_copy,
+                                                 Geary.FolderPath.VARIANT_TYPE },
+        { ACTION_HIGHLIGHTED_DELETE,         on_highlighted_delete             },
+        { ACTION_HIGHLIGHTED_JUNK,           on_highlighted_junk               },
+        { ACTION_HIGHLIGHTED_MARK_READ,      on_highlighted_mark_read          },
+        { ACTION_HIGHLIGHTED_MARK_STARRED,   on_highlighted_mark_starred       },
+        { ACTION_HIGHLIGHTED_MARK_UNREAD,    on_highlighted_mark_unread        },
+        { ACTION_HIGHLIGHTED_MARK_UNSTARRED, on_highlighted_mark_unstarred     },
+        { ACTION_HIGHLIGHTED_MOVE,           on_highlighted_move,
+                                                 Geary.FolderPath.VARIANT_TYPE },
+        { ACTION_HIGHLIGHTED_RESTORE,        on_highlighted_restore            },
+        { ACTION_HIGHLIGHTED_TRASH,          on_highlighted_trash              },
 
         { ACTION_SHOW_COPY },
         { ACTION_SHOW_MOVE },
@@ -187,6 +213,7 @@ public class MainWindow : Gtk.ApplicationWindow {
         Object(application: application);
 
         this.conversation_list = new ConversationList(application.config);
+        this.conversation_list.context_menu_requested.connect(on_context_menu_requested);
         this.conversation_list.conversation_selection_changed.connect(on_conversation_selection_changed);
         this.conversation_list.conversation_activated.connect(on_conversation_activated);
         this.conversation_list.items_marked.connect(on_conversation_items_marked);
@@ -416,6 +443,23 @@ public class MainWindow : Gtk.ApplicationWindow {
     }
 
     /**
+     * Returns the conversation from an email id action param, if valid.
+     */
+    private Geary.App.Conversation? variant_to_conversation(Variant? param) {
+        Geary.App.Conversation? target = null;
+        if (param != null) {
+            try {
+                Geary.EmailIdentifier id =
+                    this.current_folder.account.to_email_identifier(param);
+                target = this.current_conversations.get_conversation_for_email(id);
+            } catch (Geary.EngineError err) {
+                debug("Error getting action email id parameter: %s", err.message);
+            }
+        }
+        return target;
+    }
+
+    /**
      * Returns email ids from all highlighted conversations, if any.
      */
     private Gee.List<Geary.EmailIdentifier> get_highlighted_email() {
@@ -524,40 +568,40 @@ public class MainWindow : Gtk.ApplicationWindow {
         FolderActionPolicy policy = this.highlighted_policy ?? this.folder_policy;
         bool has_highlighted = this.conversation_list.has_highlighted_conversations;
 
-        get_action(ACTION_ARCHIVE).set_enabled(
+        get_action(ACTION_HIGHLIGHTED_ARCHIVE).set_enabled(
             has_highlighted && policy.can_archive
         );
-        get_action(ACTION_DELETE).set_enabled(
+        get_action(ACTION_HIGHLIGHTED_DELETE).set_enabled(
             has_highlighted && policy.can_delete
         );
-        get_action(ACTION_JUNK).set_enabled(
+        get_action(ACTION_HIGHLIGHTED_JUNK).set_enabled(
             has_highlighted && policy.can_junk
         );
-        get_action(ACTION_RESTORE).set_enabled(
+        get_action(ACTION_HIGHLIGHTED_RESTORE).set_enabled(
             has_highlighted && policy.can_restore
         );
-        get_action(ACTION_TRASH).set_enabled(
+        get_action(ACTION_HIGHLIGHTED_TRASH).set_enabled(
             has_highlighted && policy.can_trash
         );
 
-        get_action(ACTION_COPY).set_enabled(
+        get_action(ACTION_HIGHLIGHTED_COPY).set_enabled(
             has_highlighted && policy.can_copy
         );
         get_action(ACTION_SHOW_COPY).set_enabled(
             has_highlighted && policy.can_copy
         );
 
-        get_action(ACTION_MOVE).set_enabled(
+        get_action(ACTION_HIGHLIGHTED_MOVE).set_enabled(
             has_highlighted && policy.can_move
         );
         get_action(ACTION_SHOW_MOVE).set_enabled(
             has_highlighted && policy.can_move
         );
 
-        SimpleAction mark_read = get_action(ACTION_MARK_READ);
-        SimpleAction mark_unread = get_action(ACTION_MARK_UNREAD);
-        SimpleAction mark_starred = get_action(ACTION_MARK_STARRED);
-        SimpleAction mark_unstarred = get_action(ACTION_MARK_UNSTARRED);
+        SimpleAction mark_read = get_action(ACTION_HIGHLIGHTED_MARK_READ);
+        SimpleAction mark_unread = get_action(ACTION_HIGHLIGHTED_MARK_UNREAD);
+        SimpleAction mark_starred = get_action(ACTION_HIGHLIGHTED_MARK_STARRED);
+        SimpleAction mark_unstarred = get_action(ACTION_HIGHLIGHTED_MARK_UNSTARRED);
         if (has_highlighted && policy.can_mark) {
             bool has_read = false;
             bool has_unread = false;
@@ -588,6 +632,75 @@ public class MainWindow : Gtk.ApplicationWindow {
         }
     }
 
+    private void show_conversation_context_menu(Menu menu,
+                                                ConversationListItem target,
+                                                FolderActionPolicy policy) {
+
+        Geary.App.Conversation conversation = target.conversation;
+        Variant action_target = target.id.to_variant();
+        Menu target_menu = new Menu();
+        for (int i = 0; i < menu.get_n_items(); i++) {
+            Menu? existing = (Menu) menu.get_item_link(i, Menu.LINK_SECTION);
+            if (existing != null) {
+                Menu updated = (Menu) new Menu();
+                GtkUtil.menu_foreach(
+                    existing, (label, action_name, target) => {
+                        bool enabled = false;
+                        // Remove "win." prefix before checking
+                        switch (action_name.substring(4, action_name.length - 4)) {
+                        case ACTION_CONVERSATION_ARCHIVE:
+                            enabled = policy.can_archive;
+                            break;
+
+                        case ACTION_CONVERSATION_DELETE:
+                            enabled = policy.can_delete;
+                            break;
+
+                        case ACTION_CONVERSATION_JUNK:
+                            enabled = policy.can_junk;
+                            break;
+
+                        case ACTION_CONVERSATION_MARK_READ:
+                            enabled = policy.can_mark && conversation.is_unread();
+                            break;
+
+                        case ACTION_CONVERSATION_MARK_STARRED:
+                            enabled = policy.can_mark && !conversation.is_flagged();
+                            break;
+
+                        case ACTION_CONVERSATION_MARK_UNREAD:
+                            enabled = policy.can_mark && !conversation.is_unread();
+                            break;
+
+                        case ACTION_CONVERSATION_MARK_UNSTARRED:
+                            enabled = policy.can_mark && conversation.is_flagged();
+                            break;
+
+                        case ACTION_CONVERSATION_RESTORE:
+                            enabled = policy.can_restore;
+                            break;
+
+                        case ACTION_CONVERSATION_TRASH:
+                            enabled = policy.can_trash;
+                            break;
+                        }
+
+                        if (enabled) {
+                            MenuItem item = new MenuItem(label, null);
+                            item.set_action_and_target_value(
+                                action_name, action_target
+                            );
+                            updated.append_item(item);
+                        }
+                    });
+                target_menu.append_section(null, updated);
+            }
+        }
+
+        Gtk.Widget popover = new Gtk.Popover.from_model(target, target_menu);
+        popover.show();
+    }
+
     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.
@@ -786,6 +899,25 @@ public class MainWindow : Gtk.ApplicationWindow {
             this.folder_paned, "position");
     }
 
+    private void on_context_menu_requested(Menu menu, ConversationListItem target) {
+        this.application.controller.query_supported_operations.begin(
+            target.conversation.get_email_ids(),
+            this.load_cancellable,
+            (obj, res) => {
+                Gee.Set<Type>? supported = null;
+                try {
+                    supported = this.application.controller.query_supported_operations.end(res);
+                } catch (Error err) {
+                    debug("Error querying supported actions: %s", err.message);
+                }
+                show_conversation_context_menu(
+                    menu,
+                    target,
+                    new FolderActionPolicy(this.current_folder, supported)
+                );
+            });
+    }
+
     private void on_conversation_selection_changed(Geary.App.Conversation? selection) {
         show_conversation(selection);
         query_supported_actions();
@@ -911,6 +1043,165 @@ public class MainWindow : Gtk.ApplicationWindow {
     }
 
     private void on_conversation_archive(Action action, Variant? param) {
+        Geary.App.Conversation? target = variant_to_conversation(param);
+        if (target != null) {
+            this.application.controller.move_conversations_special.begin(
+                Geary.Collection.new_unary_linked_list(target),
+                Geary.SpecialFolderType.ARCHIVE,
+                (obj, ret) => {
+                    try {
+                        this.application.controller.move_conversations_special.end(ret);
+                    } catch (Error err) {
+                        report_problem(action, param, err);
+                    }
+                }
+            );
+        }
+    }
+
+    private void on_conversation_delete(Action action, Variant? param) {
+        Geary.App.Conversation? target = variant_to_conversation(param);
+        if (target != null) {
+            this.application.controller.delete_conversations.begin(
+                Geary.Collection.new_unary_linked_list(target),
+                (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) {
+        Geary.App.Conversation? target = variant_to_conversation(param);
+        if (target != null) {
+            this.application.controller.move_conversations_special.begin(
+                Geary.Collection.new_unary_linked_list(target),
+                Geary.SpecialFolderType.SPAM,
+                (obj, ret) => {
+                    try {
+                        this.application.controller.move_conversations_special.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);
+
+        Geary.App.Conversation? target = variant_to_conversation(param);
+        if (target != null) {
+            this.application.controller.mark_email.begin(
+                target.get_email_ids(), null, flags,
+                (obj, ret) => {
+                    try {
+                        this.application.controller.mark_email.end(ret);
+                    } catch (Error err) {
+                        report_problem(action, param, err);
+                    }
+                }
+            );
+        }
+    }
+
+    private void on_conversation_mark_starred(Action action, Variant? param) {
+        Geary.EmailFlags flags = new Geary.EmailFlags();
+        flags.add(Geary.EmailFlags.FLAGGED);
+
+        Geary.App.Conversation? target = variant_to_conversation(param);
+        if (target != null) {
+            this.application.controller.mark_email.begin(
+                target.get_email_ids(), 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_unread(Action action, Variant? param) {
+        Geary.EmailFlags flags = new Geary.EmailFlags();
+        flags.add(Geary.EmailFlags.UNREAD);
+
+        Geary.App.Conversation? target = variant_to_conversation(param);
+        if (target != null) {
+            this.application.controller.mark_email.begin(
+                target.get_email_ids(), 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);
+
+        Geary.App.Conversation? target = variant_to_conversation(param);
+        if (target != null) {
+            this.application.controller.mark_email.begin(
+                target.get_email_ids(), 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) {
+        Geary.App.Conversation? target = variant_to_conversation(param);
+        if (target != null) {
+            this.application.controller.restore_conversations.begin(
+                Geary.Collection.new_unary_linked_list(target),
+                (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) {
+        Geary.App.Conversation? target = variant_to_conversation(param);
+        if (target != null) {
+            this.application.controller.move_conversations_special.begin(
+                Geary.Collection.new_unary_linked_list(target),
+                Geary.SpecialFolderType.TRASH,
+                (obj, ret) => {
+                    try {
+                        this.application.controller.move_conversations_special.end(ret);
+                    } catch (Error err) {
+                        report_problem(action, param, err);
+                    }
+                }
+            );
+        }
+    }
+
+    private void on_highlighted_archive(Action action, Variant? param) {
         this.application.controller.move_conversations_special.begin(
             this.conversation_list.get_highlighted_conversations(),
             Geary.SpecialFolderType.ARCHIVE,
@@ -924,7 +1215,7 @@ public class MainWindow : Gtk.ApplicationWindow {
         );
     }
 
-    private void on_conversation_copy(Action action, Variant? param) {
+    private void on_highlighted_copy(Action action, Variant? param) {
         Geary.FolderPath? destination = null;
         if (param != null) {
             try {
@@ -950,7 +1241,7 @@ public class MainWindow : Gtk.ApplicationWindow {
         }
     }
 
-    private void on_conversation_delete(Action action, Variant? param) {
+    private void on_highlighted_delete(Action action, Variant? param) {
         if (confirm_delete()) {
             this.application.controller.delete_conversations.begin(
                 this.conversation_list.get_highlighted_conversations(),
@@ -965,7 +1256,7 @@ public class MainWindow : Gtk.ApplicationWindow {
         }
     }
 
-    private void on_conversation_junk(Action action, Variant? param) {
+    private void on_highlighted_junk(Action action, Variant? param) {
         this.application.controller.move_conversations_special.begin(
             this.conversation_list.get_highlighted_conversations(),
             Geary.SpecialFolderType.SPAM,
@@ -979,7 +1270,7 @@ public class MainWindow : Gtk.ApplicationWindow {
         );
     }
 
-    private void on_conversation_mark_read(Action action, Variant? param) {
+    private void on_highlighted_mark_read(Action action, Variant? param) {
         Geary.EmailFlags flags = new Geary.EmailFlags();
         flags.add(Geary.EmailFlags.UNREAD);
 
@@ -1009,7 +1300,7 @@ public class MainWindow : Gtk.ApplicationWindow {
         }
     }
 
-    private void on_conversation_mark_unread(Action action, Variant? param) {
+    private void on_highlighted_mark_unread(Action action, Variant? param) {
         Geary.EmailFlags flags = new Geary.EmailFlags();
         flags.add(Geary.EmailFlags.UNREAD);
 
@@ -1039,7 +1330,7 @@ public class MainWindow : Gtk.ApplicationWindow {
         }
     }
 
-    private void on_conversation_mark_starred(Action action, Variant? param) {
+    private void on_highlighted_mark_starred(Action action, Variant? param) {
         Geary.EmailFlags flags = new Geary.EmailFlags();
         flags.add(Geary.EmailFlags.FLAGGED);
         this.application.controller.mark_email.begin(
@@ -1054,7 +1345,7 @@ public class MainWindow : Gtk.ApplicationWindow {
         );
     }
 
-    private void on_conversation_mark_unstarred(Action action, Variant? param) {
+    private void on_highlighted_mark_unstarred(Action action, Variant? param) {
         Geary.EmailFlags flags = new Geary.EmailFlags();
         flags.add(Geary.EmailFlags.FLAGGED);
         this.application.controller.mark_email.begin(
@@ -1069,7 +1360,7 @@ public class MainWindow : Gtk.ApplicationWindow {
         );
     }
 
-    private void on_conversation_move(Action action, Variant? param) {
+    private void on_highlighted_move(Action action, Variant? param) {
         Geary.FolderPath? destination = null;
         if (param != null) {
             try {
@@ -1095,7 +1386,7 @@ public class MainWindow : Gtk.ApplicationWindow {
         }
     }
 
-    private void on_conversation_restore(Action action, Variant? param) {
+    private void on_highlighted_restore(Action action, Variant? param) {
         this.application.controller.restore_conversations.begin(
             this.conversation_list.get_highlighted_conversations(),
             (obj, ret) => {
@@ -1108,7 +1399,7 @@ public class MainWindow : Gtk.ApplicationWindow {
         );
     }
 
-    private void on_conversation_trash(Action action, Variant? param) {
+    private void on_highlighted_trash(Action action, Variant? param) {
         this.application.controller.move_conversations_special.begin(
             this.conversation_list.get_highlighted_conversations(),
             Geary.SpecialFolderType.TRASH,
@@ -1123,11 +1414,11 @@ public class MainWindow : Gtk.ApplicationWindow {
     }
 
     public void on_copy_folder(Geary.Folder target) {
-        get_action(ACTION_COPY).activate(target.path.to_variant());
+        get_action(ACTION_HIGHLIGHTED_COPY).activate(target.path.to_variant());
     }
 
     public void on_move_folder(Geary.Folder target) {
-        get_action(ACTION_MOVE).activate(target.path.to_variant());
+        get_action(ACTION_HIGHLIGHTED_MOVE).activate(target.path.to_variant());
     }
 
     private void on_selection_mode_enabled() {
diff --git a/src/client/conversation-list/conversation-list-item.vala 
b/src/client/conversation-list/conversation-list-item.vala
index 52de137..e9c855f 100644
--- a/src/client/conversation-list/conversation-list-item.vala
+++ b/src/client/conversation-list/conversation-list-item.vala
@@ -79,6 +79,16 @@ public class ConversationListItem : Gtk.ListBoxRow {
     /** Determines if this row is marked for selection mode */
     public bool is_marked { get; private set; default = false; }
 
+    /*
+     * An email id that can be used to look up this conversation.
+     *
+     * This is only guaranteed to remain stable while the conversation
+     * remains trimmed, after that it may have changed. Thus it should
+     * only be used for transient values such as context menu action
+     * targets.
+     */
+    public Geary.EmailIdentifier id { get; private set; }
+
 
     [GtkChild]
     private Gtk.Button star_button;
@@ -256,6 +266,17 @@ public class ConversationListItem : Gtk.ListBoxRow {
         if (count <= 1) {
             this.count.hide();
         }
+
+        // This must be done every time the conversation is trimmed
+        Geary.Email? email = this.conversation.get_earliest_recv_email(
+            Geary.App.Conversation.Location.ANYWHERE
+        );
+        if (email != null) {
+            Variant target = email.id.to_variant();
+            this.star_button.set_action_target_value(target);
+            this.unstar_button.set_action_target_value(target);
+            this.id = email.id;
+        }
     }
 
     private string get_participants_markup() {
diff --git a/src/client/conversation-list/conversation-list.vala 
b/src/client/conversation-list/conversation-list.vala
index c84923c..d9178ab 100644
--- a/src/client/conversation-list/conversation-list.vala
+++ b/src/client/conversation-list/conversation-list.vala
@@ -46,6 +46,7 @@ public class ConversationList : Gtk.ListBox {
     }
 
     private Configuration config;
+    private Menu context_menu;
     private int selected_index = -1;
     private bool selection_frozen = false;
     private Gee.Map<Geary.App.Conversation,ConversationListItem> marked =
@@ -74,6 +75,15 @@ public class ConversationList : Gtk.ListBox {
     }
 
     /**
+     * Fired when the user requested a context menu for an item.
+     *
+     * The application should set targets for the given menu model and
+     * selectively hide unwanted actions before displaying the popup
+     * on the item.
+     */
+    public signal void context_menu_requested(Menu menu, ConversationListItem target);
+
+    /**
      * Fired when a list item was targeted with a selection gesture.
      *
      * Selection gestures include Ctrl-click or Shift-click on the
@@ -104,6 +114,11 @@ public class ConversationList : Gtk.ListBox {
                 selection_changed();
             });
         this.show.connect(on_show);
+
+        Gtk.Builder builder = new Gtk.Builder.from_resource(
+            "/org/gnome/Geary/conversation-list-menus.ui"
+        );
+        this.context_menu = (Menu) builder.get_object("context_menu");
     }
 
     /**
@@ -203,6 +218,23 @@ public class ConversationList : Gtk.ListBox {
 
     public override bool button_press_event(Gdk.EventButton event) {
         bool ret = Gdk.EVENT_PROPAGATE;
+        if (event.button == 3) {
+            ConversationListItem? clicked =
+                get_row_at_y((int) event.y) as ConversationListItem;
+            if (clicked != null) {
+                context_menu_requested(this.context_menu, clicked);
+                ret = Gdk.EVENT_STOP;
+            }
+        }
+
+        if (ret == Gdk.EVENT_PROPAGATE) {
+            ret = base.button_press_event(event);
+        }
+        return ret;
+    }
+
+    public override bool button_release_event(Gdk.EventButton event) {
+        bool ret = Gdk.EVENT_PROPAGATE;
         if (event.button == 1) {
             ConversationListItem? clicked =
                 get_row_at_y((int) event.y) as ConversationListItem;
diff --git a/src/engine/util/util-collection.vala b/src/engine/util/util-collection.vala
index 6859569..7fae553 100644
--- a/src/engine/util/util-collection.vala
+++ b/src/engine/util/util-collection.vala
@@ -12,6 +12,14 @@ public inline bool is_empty(Gee.Collection? c) {
     return c == null || c.size == 0;
 }
 
+/** Returns a linked list containing one element. */
+public Gee.LinkedList<G> new_unary_linked_list<G>(G element,
+                                            owned Gee.EqualDataFunc<G>? equal_func = null) {
+    Gee.LinkedList<G> list = new Gee.LinkedList<G>(equal_func);
+    list.add(element);
+    return list;
+}
+
 // A substitute for ArrayList<G>.wrap() for compatibility with older versions of Gee.
 public Gee.ArrayList<G> array_list_wrap<G>(G[] a, owned Gee.EqualDataFunc<G>? equal_func = null) {
     Gee.ArrayList<G> list = new Gee.ArrayList<G>(equal_func);
diff --git a/ui/CMakeLists.txt b/ui/CMakeLists.txt
index 5164978..b2d9645 100644
--- a/ui/CMakeLists.txt
+++ b/ui/CMakeLists.txt
@@ -17,6 +17,7 @@ set(RESOURCE_LIST
   STRIPBLANKS "conversation-email-attachment-view.ui"
   STRIPBLANKS "conversation-email-menus.ui"
   STRIPBLANKS "conversation-list-item.ui"
+  STRIPBLANKS "conversation-list-menus.ui"
   STRIPBLANKS "conversation-message.ui"
   STRIPBLANKS "conversation-message-menus.ui"
   STRIPBLANKS "conversation-viewer.ui"
diff --git a/ui/conversation-action-bar.ui b/ui/conversation-action-bar.ui
index 0cf111d..9ba0a79 100644
--- a/ui/conversation-action-bar.ui
+++ b/ui/conversation-action-bar.ui
@@ -15,7 +15,7 @@
             <property name="visible">True</property>
             <property name="can_focus">True</property>
             <property name="receives_default">True</property>
-            <property name="action_name">win.conversation-mark-read</property>
+            <property name="action_name">win.highlighted-mark-read</property>
             <child>
               <object class="GtkImage">
                 <property name="visible">True</property>
@@ -33,7 +33,7 @@
           <object class="GtkButton" id="mark_unread_action">
             <property name="can_focus">True</property>
             <property name="receives_default">True</property>
-            <property name="action_name">win.conversation-mark-unread</property>
+            <property name="action_name">win.highlighted-mark-unread</property>
             <child>
               <object class="GtkImage">
                 <property name="visible">True</property>
@@ -52,7 +52,7 @@
             <property name="visible">True</property>
             <property name="can_focus">True</property>
             <property name="receives_default">True</property>
-            <property name="action_name">win.conversation-mark-starred</property>
+            <property name="action_name">win.highlighted-mark-starred</property>
             <child>
               <object class="GtkImage">
                 <property name="visible">True</property>
@@ -70,7 +70,7 @@
           <object class="GtkButton" id="mark_unstarred_action">
             <property name="can_focus">True</property>
             <property name="receives_default">True</property>
-            <property name="action_name">win.conversation-mark-unstarred</property>
+            <property name="action_name">win.highlighted-mark-unstarred</property>
             <child>
               <object class="GtkImage">
                 <property name="visible">True</property>
@@ -102,7 +102,7 @@
             <property name="visible">True</property>
             <property name="can_focus">True</property>
             <property name="receives_default">True</property>
-            <property name="action_name">win.conversation-archive</property>
+            <property name="action_name">win.highlighted-archive</property>
           </object>
           <packing>
             <property name="left_attach">0</property>
@@ -115,7 +115,7 @@
             <property name="visible">True</property>
             <property name="can_focus">True</property>
             <property name="receives_default">True</property>
-            <property name="action_name">win.conversation-restore</property>
+            <property name="action_name">win.highlighted-restore</property>
           </object>
           <packing>
             <property name="left_attach">1</property>
@@ -179,7 +179,7 @@
             <property name="visible">True</property>
             <property name="can_focus">True</property>
             <property name="receives_default">True</property>
-            <property name="action_name">win.conversation-junk</property>
+            <property name="action_name">win.highlighted-junk</property>
             <child>
               <object class="GtkImage">
                 <property name="visible">True</property>
@@ -198,7 +198,7 @@
             <property name="visible">True</property>
             <property name="can_focus">True</property>
             <property name="receives_default">True</property>
-            <property name="action_name">win.conversation-trash</property>
+            <property name="action_name">win.highlighted-trash</property>
             <child>
               <object class="GtkImage">
                 <property name="visible">True</property>
@@ -219,7 +219,7 @@
             <property name="receives_default">True</property>
             <property name="halign">end</property>
             <property name="hexpand">True</property>
-            <property name="action_name">win.conversation-delete</property>
+            <property name="action_name">win.highlighted-delete</property>
             <child>
               <object class="GtkImage">
                 <property name="visible">True</property>
diff --git a/ui/conversation-list-menus.ui b/ui/conversation-list-menus.ui
new file mode 100644
index 0000000..ffdc6f3
--- /dev/null
+++ b/ui/conversation-list-menus.ui
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<interface>
+  <menu id="context_menu">
+    <section id="context_menu_mark">
+      <item>
+        <attribute name="label" translatable="yes">Mark _Read</attribute>
+        <attribute name="action">win.conversation-mark-read</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">Un-mark _Read</attribute>
+        <attribute name="action">win.conversation-mark-unread</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">Mark _Starred</attribute>
+        <attribute name="action">win.conversation-mark-starred</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">Un-mark _Starred</attribute>
+        <attribute name="action">win.conversation-mark-unstarred</attribute>
+      </item>
+    </section>
+    <section id="context_menu_actions">
+      <item>
+        <attribute name="label" translatable="yes">_Archive</attribute>
+        <attribute name="action">win.conversation-archive</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">R_estore</attribute>
+        <attribute name="action">win.conversation-restore</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">_Junk</attribute>
+        <attribute name="action">win.conversation-junk</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">_Trash</attribute>
+        <attribute name="action">win.conversation-trash</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">_Delete</attribute>
+        <attribute name="action">win.conversation-delete</attribute>
+      </item>
+    </section>
+  </menu>
+</interface>



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