[geary/wip/730682-refine-convo-list] Implement copy and move from ConversationActionBar.



commit 130f4ea2b54d032bf8d6ae83de1a176b5d54b7bb
Author: Michael James Gratton <mike vee net>
Date:   Thu Dec 28 12:10:21 2017 +1100

    Implement copy and move from ConversationActionBar.
    
    * src/client/components/main-window.vala (MainWindow): Add actions for
      both showing and activating copy and move ops from the action bar. Add
      some uitl methods for converting a folder path to a GVariant and back
      so it can be passed via an action param. Add action handlers for the
      new ops.
    
    * src/client/components/conversation-action-bar.vala
      (ConversationActionBar): Make move and copy menu buttons public so they
      can be messaed with. Hook up and update them with the account when it
      changes.
    
    * src/client/application/geary-controller.vala (GearyController): Add
      internal copy and move functions for MainWindow. Rename existing
      move_conversations() method to move_conversations_special() to make way
      for "plain" move op, update call sites.
    
    * src/engine/api/geary-account.vala (Account): Add factory method for
      constructing native FolderPath instances, so the MainWindow can
      deserialise paths serialised as a Variant.
    
    * src/engine/app/app-email-store.vala (EmailStore): Add support for
      moving a set of emails across folders.

 po/POTFILES.in                                     |    2 +
 src/CMakeLists.txt                                 |    1 +
 src/client/application/geary-controller.vala       |   40 ++++++++-
 src/client/components/conversation-action-bar.vala |   77 ++++++++++++++---
 src/client/components/main-window.vala             |   89 ++++++++++++++++++--
 src/engine/api/geary-account.vala                  |   46 +++++++----
 src/engine/app/app-email-store.vala                |   14 +++-
 src/engine/app/email-store/app-move-operation.vala |   32 +++++++
 .../imap-engine/imap-engine-generic-account.vala   |   14 +++-
 ui/conversation-action-bar.ui                      |    2 +
 10 files changed, 272 insertions(+), 45 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 11bd439..366745e 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -161,6 +161,7 @@ src/engine/app/email-store/app-copy-operation.vala
 src/engine/app/email-store/app-fetch-operation.vala
 src/engine/app/email-store/app-list-operation.vala
 src/engine/app/email-store/app-mark-operation.vala
+src/engine/app/email-store/app-move-operation.vala
 src/engine/common/common-message-data.vala
 src/engine/db/db-connection.vala
 src/engine/db/db-context.vala
@@ -404,6 +405,7 @@ ui/composer-headerbar.ui
 ui/composer-link-popover.ui
 ui/composer-menus.ui
 ui/composer-widget.ui
+ui/conversation-action-bar.ui
 ui/conversation-email.ui
 ui/conversation-email-attachment-view.ui
 ui/conversation-email-menus.ui
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index fb88f9b..fca0df0 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -68,6 +68,7 @@ engine/app/email-store/app-copy-operation.vala
 engine/app/email-store/app-fetch-operation.vala
 engine/app/email-store/app-list-operation.vala
 engine/app/email-store/app-mark-operation.vala
+engine/app/email-store/app-move-operation.vala
 
 engine/common/common-message-data.vala
 
diff --git a/src/client/application/geary-controller.vala b/src/client/application/geary-controller.vala
index 578eb7d..6d25c6f 100644
--- a/src/client/application/geary-controller.vala
+++ b/src/client/application/geary-controller.vala
@@ -1154,8 +1154,25 @@ public class GearyController : Geary.BaseObject {
     /**
      * Moves a set of conversations to a special folder.
      */
-    internal async void move_conversations(Gee.Collection<Geary.App.Conversation> targets,
-                                           Geary.SpecialFolderType type)
+    internal async void copy_conversations(Gee.Collection<Geary.App.Conversation> targets,
+                                           Geary.FolderPath destination)
+        throws Error {
+        Gee.List<Geary.EmailIdentifier> ids = get_ids_in_folder(targets);
+        yield this.email_stores.get(this.current_account).copy_email_async(
+            ids, destination, this.cancellable_folder
+        );
+    }
+
+    /**
+     * Moves a set of conversations to a special folder.
+     *
+     * This does some extra work when moving conversations to specific
+     * special folders like archive and trash, and hence should be
+     * used in preference to {@link move_conversation} for special
+     * folders.
+     */
+    internal async void move_conversations_special(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) {
@@ -1174,7 +1191,6 @@ public class GearyController : Geary.BaseObject {
                 _("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) {
@@ -1183,6 +1199,7 @@ public class GearyController : Geary.BaseObject {
                     this.current_folder.to_string()
                 );
             }
+            Geary.Folder dest = this.current_account.get_special_folder(type);
             string tooltip = "";
             switch (type) {
             case Geary.SpecialFolderType.INBOX:
@@ -1199,6 +1216,9 @@ public class GearyController : Geary.BaseObject {
                 break;
             }
             save_revokable(
+                // XXX really should be using
+                // EmailStore.move_email_async here, but it doesn't
+                // return a revokable :<
                 yield movable.move_email_async(
                     ids, dest.path, this.cancellable_folder
                 ),
@@ -1208,11 +1228,23 @@ public class GearyController : Geary.BaseObject {
     }
 
     /**
+     * Moves a set of conversations to a special folder.
+     */
+    internal async void move_conversations(Gee.Collection<Geary.App.Conversation> targets,
+                                           Geary.FolderPath destination)
+        throws Error {
+        Gee.List<Geary.EmailIdentifier> ids = get_ids_in_folder(targets);
+        yield this.email_stores.get(this.current_account).move_email_async(
+            ids, destination, this.cancellable_folder
+        );
+    }
+
+    /**
      * 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);
+        yield move_conversations_special(targets, Geary.SpecialFolderType.INBOX);
     }
 
     /**
diff --git a/src/client/components/conversation-action-bar.vala 
b/src/client/components/conversation-action-bar.vala
index 9ceaba6..c3e1d92 100644
--- a/src/client/components/conversation-action-bar.vala
+++ b/src/client/components/conversation-action-bar.vala
@@ -11,14 +11,22 @@
 [GtkTemplate (ui = "/org/gnome/Geary/conversation-action-bar.ui")]
 public class ConversationActionBar : Gtk.ActionBar {
 
-    private Geary.Account? account = null;
+    /** The folder popover for copying/labelling conversations. */
+    public FolderPopover copy_folder_menu {
+        get; set; default = new FolderPopover();
+    }
+
+    /** The folder popover for moving conversations. */
+    public FolderPopover move_folder_menu {
+        get; set; default = new FolderPopover();
+    }
+
+    private Geary.Account? owner = null;
+    private Geary.Folder? location = null;
 
     private bool has_archive = false;
     private bool has_trash = false;
 
-    private FolderPopover copy_folder_menu = new FolderPopover();
-    private FolderPopover move_folder_menu = new FolderPopover();
-
     [GtkChild]
     private Gtk.Grid flag_actions;
     [GtkChild]
@@ -57,16 +65,25 @@ public class ConversationActionBar : Gtk.ActionBar {
     }
 
     public void set_account(Geary.Account account) {
-        if (this.account != null) {
-            this.account.folders_special_type.disconnect(on_special_folder_changed);
+        if (this.owner != null) {
+            this.owner.folders_available_unavailable.disconnect(on_folders_changed);
+            this.owner.folders_special_type.disconnect(on_special_folder_changed);
         }
 
-        this.account = account;
-        this.account.folders_special_type.connect(on_special_folder_changed);
+        this.owner = account;
+        this.owner.folders_available_unavailable.connect(on_folders_changed);
+        this.owner.folders_special_type.connect(on_special_folder_changed);
         update_account();
     }
 
     public void update_location(Geary.Folder location) {
+        if (this.location != null) {
+            this.copy_folder_menu.enable_disable_folder(this.location, true);
+            this.move_folder_menu.enable_disable_folder(this.location, true);
+        }
+
+        this.location = location;
+
         Gtk.Button? primary_action = null;
         bool show_flag_actions = false;
         bool show_folder_actions = false;
@@ -136,7 +153,9 @@ public class ConversationActionBar : Gtk.ActionBar {
         this.archive_action.set_visible(primary_action == this.archive_action);
         this.restore_action.set_visible(primary_action == this.restore_action);
         this.copy_action.set_visible(show_folder_actions);
+        this.copy_folder_menu.enable_disable_folder(location, false);
         this.move_action.set_visible(show_folder_actions);
+        this.move_folder_menu.enable_disable_folder(location, false);
 
         if (show_trash && !this.has_trash) {
             show_trash = false;
@@ -153,17 +172,30 @@ public class ConversationActionBar : Gtk.ActionBar {
     private void update_account() {
         try {
             this.has_archive = (
-                this.account.get_special_folder(Geary.SpecialFolderType.ARCHIVE) != null
+                this.owner.get_special_folder(Geary.SpecialFolderType.ARCHIVE) != null
             );
         } catch (Error err) {
-            debug("Could not get Archive for account: %s", this.account.to_string());
+            debug("Could not get Archive for account: %s", this.owner.to_string());
         }
         try {
             this.has_trash = (
-                this.account.get_special_folder(Geary.SpecialFolderType.TRASH) != null
+                this.owner.get_special_folder(Geary.SpecialFolderType.TRASH) != null
             );
         } catch (Error err) {
-            debug("Could not get Trash for account: %s", this.account.to_string());
+            debug("Could not get Trash for account: %s", this.owner.to_string());
+        }
+
+        this.copy_folder_menu.clear();
+        this.move_folder_menu.clear();
+        try {
+            foreach (Geary.Folder f in this.owner.list_folders()) {
+                this.copy_folder_menu.add_folder(f);
+                this.move_folder_menu.add_folder(f);
+            }
+        } catch (Error err) {
+            debug("Could not list folders for %s: %s",
+                  this.owner.to_string(),
+                  err.message);
         }
     }
 
@@ -184,6 +216,27 @@ public class ConversationActionBar : Gtk.ActionBar {
         secondary.set_visible(!show_primary);
     }
 
+    private void on_folders_changed(Gee.List<Geary.Folder>? available,
+                                    Gee.List<Geary.Folder>? unavailable) {
+        if (available != null) {
+            foreach (Geary.Folder folder in available) {
+                if (!this.copy_folder_menu.has_folder(folder))
+                    this.copy_folder_menu.add_folder(folder);
+                if (!this.move_folder_menu.has_folder(folder))
+                    this.move_folder_menu.add_folder(folder);
+            }
+        }
+
+        if (unavailable != null) {
+            foreach (Geary.Folder folder in unavailable) {
+                if (this.copy_folder_menu.has_folder(folder))
+                    this.copy_folder_menu.remove_folder(folder);
+                if (this.move_folder_menu.has_folder(folder))
+                    this.move_folder_menu.remove_folder(folder);
+            }
+        }
+    }
+
     private void on_special_folder_changed() {
         update_account();
     }
diff --git a/src/client/components/main-window.vala b/src/client/components/main-window.vala
index afc9d7a..3d329c0 100644
--- a/src/client/components/main-window.vala
+++ b/src/client/components/main-window.vala
@@ -11,15 +11,20 @@ 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_SHOW_COPY = "show-copy";
+    public const string ACTION_SHOW_MOVE = "show-move";
+
     public const string ACTION_SELECTION_MODE_DISABLE = "selection-mode-disable";
     public const string ACTION_SELECTION_MODE_ENABLE = "selection-mode-enable";
 
@@ -29,15 +34,20 @@ public class MainWindow : Gtk.ApplicationWindow {
 
     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          },
 
+        { ACTION_SHOW_COPY },
+        { ACTION_SHOW_MOVE },
+
         { ACTION_SELECTION_MODE_DISABLE, on_selection_mode_disabled },
         { ACTION_SELECTION_MODE_ENABLE,  on_selection_mode_enabled  }
     };
@@ -118,6 +128,8 @@ public class MainWindow : Gtk.ApplicationWindow {
         this.conversation_list.visible_conversations_changed.connect(on_visible_conversations_changed);
         this.conversation_list.load_more.connect(on_load_more);
 
+        this.conversation_list_actions.copy_folder_menu.folder_selected.connect(on_copy_folder);
+        this.conversation_list_actions.move_folder_menu.folder_selected.connect(on_move_folder);
         this.conversation_list_grid.add(this.conversation_list_actions);
 
         load_config(application.config);
@@ -541,17 +553,33 @@ public class MainWindow : Gtk.ApplicationWindow {
         this.conversation_viewer.show_none_selected();
     }
 
-    private void report_problem(Action action, Variant? param, Error err) {
+    private void report_problem(Action action, Variant? param, Error? err = null) {
         // XXX
         debug("Client problem reported: %s: %s",
               action.get_name(),
-              err.message);
+              err != null ? err.message : "no error reported");
     }
 
     private inline SimpleAction get_action(string name) {
         return (SimpleAction) lookup_action(name);
     }
 
+    private Geary.FolderPath? variant_to_path(Variant? strv) {
+        Geary.FolderPath? path = null;
+        if (this.current_folder != null &&
+            strv != null &&
+            strv.get_type_string() == "as") {
+            path = this.current_folder.account.new_folder_path(
+                new Gee.ArrayList<string>.wrap(strv.get_strv())
+            );
+        }
+        return path;
+    }
+
+    private Variant path_to_variant(Geary.FolderPath path) {
+        return new Variant.strv(path.as_list().to_array());
+    }
+
     private void on_folder_selected(Geary.Folder? folder) {
         if (folder != null) {
             update_folder(folder);
@@ -730,12 +758,12 @@ public class MainWindow : Gtk.ApplicationWindow {
     }
 
     private void on_conversation_archive(Action action, Variant? param) {
-        this.application.controller.move_conversations.begin(
+        this.application.controller.move_conversations_special.begin(
             this.conversation_list.get_highlighted_conversations(),
             Geary.SpecialFolderType.ARCHIVE,
             (obj, ret) => {
                 try {
-                    this.application.controller.move_conversations.end(ret);
+                    this.application.controller.move_conversations_special.end(ret);
                 } catch (Error err) {
                     report_problem(action, param, err);
                 }
@@ -743,6 +771,25 @@ public class MainWindow : Gtk.ApplicationWindow {
         );
     }
 
+    private void on_conversation_copy(Action action, Variant? param) {
+        Geary.FolderPath? destination = variant_to_path(param);
+        if (path != null) {
+            this.application.controller.copy_conversations.begin(
+                this.conversation_list.get_highlighted_conversations(),
+                destination,
+                (obj, ret) => {
+                    try {
+                        this.application.controller.copy_conversations.end(ret);
+                    } catch (Error err) {
+                        report_problem(action, param, err);
+                    }
+                }
+            );
+        } else {
+            report_problem(action, param);
+        }
+    }
+
     private void on_conversation_delete(Action action, Variant? param) {
         if (confirm_delete()) {
             this.application.controller.delete_conversations.begin(
@@ -759,12 +806,12 @@ public class MainWindow : Gtk.ApplicationWindow {
     }
 
     private void on_conversation_junk(Action action, Variant? param) {
-        this.application.controller.move_conversations.begin(
+        this.application.controller.move_conversations_special.begin(
             this.conversation_list.get_highlighted_conversations(),
             Geary.SpecialFolderType.SPAM,
             (obj, ret) => {
                 try {
-                    this.application.controller.move_conversations.end(ret);
+                    this.application.controller.move_conversations_special.end(ret);
                 } catch (Error err) {
                     report_problem(action, param, err);
                 }
@@ -862,6 +909,25 @@ public class MainWindow : Gtk.ApplicationWindow {
         );
     }
 
+    private void on_conversation_move(Action action, Variant? param) {
+        Geary.FolderPath? destination = variant_to_path(param);
+        if (path != null) {
+            this.application.controller.move_conversations.begin(
+                this.conversation_list.get_highlighted_conversations(),
+                destination,
+                (obj, ret) => {
+                    try {
+                        this.application.controller.move_conversations.end(ret);
+                    } catch (Error err) {
+                        report_problem(action, param, err);
+                    }
+                }
+            );
+        } else {
+            report_problem(action, param);
+        }
+    }
+
     private void on_conversation_restore(Action action, Variant? param) {
         this.application.controller.restore_conversations.begin(
             this.conversation_list.get_highlighted_conversations(),
@@ -876,12 +942,12 @@ public class MainWindow : Gtk.ApplicationWindow {
     }
 
     private void on_conversation_trash(Action action, Variant? param) {
-        this.application.controller.move_conversations.begin(
+        this.application.controller.move_conversations_special.begin(
             this.conversation_list.get_highlighted_conversations(),
             Geary.SpecialFolderType.TRASH,
             (obj, ret) => {
                 try {
-                    this.application.controller.move_conversations.end(ret);
+                    this.application.controller.move_conversations_special.end(ret);
                 } catch (Error err) {
                     report_problem(action, param, err);
                 }
@@ -889,6 +955,13 @@ public class MainWindow : Gtk.ApplicationWindow {
         );
     }
 
+    public void on_copy_folder(Geary.Folder target) {
+        get_action(ACTION_COPY).activate(path_to_variant(target.path));
+    }
+
+    public void on_move_folder(Geary.Folder target) {
+        get_action(ACTION_MOVE).activate(path_to_variant(target.path));
+    }
 
     private void on_selection_mode_enabled() {
         set_selection_mode_enabled(true);
diff --git a/src/engine/api/geary-account.vala b/src/engine/api/geary-account.vala
index a7fd343..6773ed9 100644
--- a/src/engine/api/geary-account.vala
+++ b/src/engine/api/geary-account.vala
@@ -127,22 +127,6 @@ public abstract class Geary.Account : BaseObject {
     }
 
     /**
-     * A utility method to sort a Gee.Collection of {@link Folder}s by their {@link FolderPath}s
-     * to ensure they comport with {@link folders_available_unavailable} and
-     * {@link folders_added_removed} signals' contracts.
-     */
-    protected Gee.List<Geary.Folder> sort_by_path(Gee.Collection<Geary.Folder> folders) {
-        Gee.TreeSet<Geary.Folder> sorted = new Gee.TreeSet<Geary.Folder>(folder_path_comparator);
-        sorted.add_all(folders);
-        
-        return Collection.to_array_list<Geary.Folder>(sorted);
-    }
-    
-    private int folder_path_comparator(Geary.Folder a, Geary.Folder b) {
-        return a.path.compare_to(b.path);
-    }
-    
-    /**
      * Opens the {@link Account} and makes it and its {@link Folder}s available for use.
      *
      * @throws EngineError.CORRUPT if the local store is corrupt or unusable
@@ -223,7 +207,19 @@ public abstract class Geary.Account : BaseObject {
      * list_matching_folders().
      */
     public abstract Gee.Collection<Geary.Folder> list_folders() throws Error;
-    
+
+    /**
+     * Returns a path for a list of folder names.
+     *
+     * This is useful for converting a string representation of a
+     * folder path back into an actual instance of a folder path. This
+     * does not guarantee that the folder represented by the path will
+     * exist.
+     *
+     * {@see FolderPath.as_list}
+     */
+    public abstract FolderPath new_folder_path(Gee.List<string> name_list);
+
     /**
      * Gets a perpetually update-to-date collection of autocompletion contacts.
      */
@@ -434,4 +430,20 @@ public abstract class Geary.Account : BaseObject {
         report_problem(new ServiceProblemReport(type, this.information, service_type, err));
     }
 
+    /**
+     * A utility method to sort a Gee.Collection of {@link Folder}s by their {@link FolderPath}s
+     * to ensure they comport with {@link folders_available_unavailable} and
+     * {@link folders_added_removed} signals' contracts.
+     */
+    protected Gee.List<Geary.Folder> sort_by_path(Gee.Collection<Geary.Folder> folders) {
+        Gee.TreeSet<Geary.Folder> sorted = new Gee.TreeSet<Geary.Folder>(folder_path_comparator);
+        sorted.add_all(folders);
+
+        return Collection.to_array_list<Geary.Folder>(sorted);
+    }
+
+    private int folder_path_comparator(Geary.Folder a, Geary.Folder b) {
+        return a.path.compare_to(b.path);
+    }
+
 }
diff --git a/src/engine/app/app-email-store.vala b/src/engine/app/app-email-store.vala
index 4390dfa..f74ede3 100644
--- a/src/engine/app/app-email-store.vala
+++ b/src/engine/app/app-email-store.vala
@@ -97,7 +97,7 @@ public class Geary.App.EmailStore : BaseObject {
         yield do_folder_operation_async(new Geary.App.MarkOperation(flags_to_add, flags_to_remove),
             emails, cancellable);
     }
-    
+
     /**
      * Copies any set of EmailIdentifiers as if they were all in one
      * Geary.FolderSupport.Copy folder.
@@ -107,7 +107,17 @@ public class Geary.App.EmailStore : BaseObject {
         yield do_folder_operation_async(new Geary.App.CopyOperation(destination),
             emails, cancellable);
     }
-    
+
+    /**
+     * Moves any set of EmailIdentifiers as if they were all in one
+     * Geary.FolderSupport.Move folder.
+     */
+    public async void move_email_async(Gee.Collection<Geary.EmailIdentifier> emails,
+        Geary.FolderPath destination, Cancellable? cancellable = null) throws Error {
+        yield do_folder_operation_async(new Geary.App.MoveOperation(destination),
+            emails, cancellable);
+    }
+
     private async Gee.HashMap<Geary.FolderPath, Geary.Folder> get_folder_instances_async(
         Gee.Collection<Geary.FolderPath> paths, Cancellable? cancellable) throws Error {
         Gee.HashMap<Geary.FolderPath, Geary.Folder> folders
diff --git a/src/engine/app/email-store/app-move-operation.vala 
b/src/engine/app/email-store/app-move-operation.vala
new file mode 100644
index 0000000..63f70bb
--- /dev/null
+++ b/src/engine/app/email-store/app-move-operation.vala
@@ -0,0 +1,32 @@
+/* Copyright 2016 Software Freedom Conservancy Inc.
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later).  See the COPYING file in this distribution.
+ */
+
+private class Geary.App.MoveOperation : Geary.App.AsyncFolderOperation {
+
+    public override Type folder_type {
+        get { return typeof(Geary.FolderSupport.Move); }
+    }
+
+    public Geary.FolderPath destination;
+
+    public MoveOperation(Geary.FolderPath destination) {
+        this.destination = destination;
+    }
+
+    public override async Gee.Collection<Geary.EmailIdentifier>
+        execute_async(Geary.Folder folder,
+                      Gee.Collection<Geary.EmailIdentifier> ids,
+                      Cancellable? cancellable)
+        throws Error {
+        Geary.FolderSupport.Move? move = folder as Geary.FolderSupport.Move;
+        assert(move != null);
+
+        Gee.List<Geary.EmailIdentifier> list =
+            Geary.Collection.to_array_list<Geary.EmailIdentifier>(ids);
+        yield move.move_email_async(list, destination, cancellable);
+        return ids;
+    }
+}
diff --git a/src/engine/imap-engine/imap-engine-generic-account.vala 
b/src/engine/imap-engine/imap-engine-generic-account.vala
index 2ac8e73..6872e0a 100644
--- a/src/engine/imap-engine/imap-engine-generic-account.vala
+++ b/src/engine/imap-engine/imap-engine-generic-account.vala
@@ -367,10 +367,20 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
         Gee.HashSet<Geary.Folder> all_folders = new Gee.HashSet<Geary.Folder>();
         all_folders.add_all(folder_map.values);
         all_folders.add_all(local_only.values);
-        
+
         return all_folders;
     }
-    
+
+    public override FolderPath new_folder_path(Gee.List<string> name_list) {
+        Gee.Iterator<string> names = name_list.iterator();
+        names.next();
+        Geary.FolderPath path = new Imap.FolderRoot(names.get());
+        while (names.next()) {
+            path = path.get_child(names.get());
+        }
+        return path;
+    }
+
     private void reschedule_unseen_update(Geary.Folder folder) {
         if (!folder_map.has_key(folder.path))
             return;
diff --git a/ui/conversation-action-bar.ui b/ui/conversation-action-bar.ui
index c84cdc6..0cf111d 100644
--- a/ui/conversation-action-bar.ui
+++ b/ui/conversation-action-bar.ui
@@ -127,6 +127,7 @@
             <property name="visible">True</property>
             <property name="can_focus">True</property>
             <property name="receives_default">True</property>
+            <property name="action_name">win.show-copy</property>
             <child>
               <object class="GtkImage">
                 <property name="visible">True</property>
@@ -145,6 +146,7 @@
             <property name="visible">True</property>
             <property name="can_focus">True</property>
             <property name="receives_default">True</property>
+            <property name="action_name">win.show-move</property>
             <child>
               <object class="GtkImage">
                 <property name="visible">True</property>


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