[geary/wip/721828-undo-2: 7/8] Allow undoing a move even after email was moved (committed) to dest



commit 1c0b5f211a86e6eb6f816d3ea50fadb0a73c6d81
Author: Jim Nelson <jim yorba org>
Date:   Tue Feb 3 14:50:48 2015 -0800

    Allow undoing a move even after email was moved (committed) to dest

 src/CMakeLists.txt                                 |    1 +
 src/client/application/geary-controller.vala       |   29 ++++++++-
 src/engine/api/geary-revokable.vala                |   30 +++++++++-
 .../imap-engine-revokable-committed-move.vala      |   64 ++++++++++++++++++++
 .../imap-engine/imap-engine-revokable-move.vala    |   23 ++++++-
 .../replay-ops/imap-engine-move-email-commit.vala  |    8 ++-
 6 files changed, 145 insertions(+), 10 deletions(-)
---
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index f1d7928..80a715a 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -193,6 +193,7 @@ engine/imap-engine/imap-engine-minimal-folder.vala
 engine/imap-engine/imap-engine-replay-operation.vala
 engine/imap-engine/imap-engine-replay-queue.vala
 engine/imap-engine/imap-engine-revokable-move.vala
+engine/imap-engine/imap-engine-revokable-committed-move.vala
 engine/imap-engine/imap-engine-send-replay-operation.vala
 engine/imap-engine/gmail/imap-engine-gmail-account.vala
 engine/imap-engine/gmail/imap-engine-gmail-all-mail-folder.vala
diff --git a/src/client/application/geary-controller.vala b/src/client/application/geary-controller.vala
index 1a4e353..6424790 100644
--- a/src/client/application/geary-controller.vala
+++ b/src/client/application/geary-controller.vala
@@ -2519,19 +2519,31 @@ public class GearyController : Geary.BaseObject {
     
     private void save_revokable(Geary.Revokable? new_revokable, string? description) {
         // disconnect old revokable
-        if (revokable != null)
+        if (revokable != null) {
             revokable.notify[Geary.Revokable.PROP_VALID].disconnect(on_revokable_valid_changed);
+            revokable.notify[Geary.Revokable.PROP_IN_PROCESS].disconnect(update_revokable_action);
+            revokable.committed.disconnect(on_revokable_committed);
+        }
         
         // store new revokable
         revokable = new_revokable;
         
         // connect to new revokable
-        if (revokable != null)
+        if (revokable != null) {
             revokable.notify[Geary.Revokable.PROP_VALID].connect(on_revokable_valid_changed);
+            revokable.notify[Geary.Revokable.PROP_IN_PROCESS].connect(update_revokable_action);
+            revokable.committed.connect(on_revokable_committed);
+        }
         
         Gtk.Action undo_action = GearyApplication.instance.get_action(ACTION_UNDO);
-        undo_action.sensitive = revokable != null && revokable.valid;
         undo_action.tooltip = (revokable != null && description != null) ? description : _("Undo (Ctrl+Z)");
+        
+        update_revokable_action();
+    }
+    
+    private void update_revokable_action() {
+        Gtk.Action undo_action = GearyApplication.instance.get_action(ACTION_UNDO);
+        undo_action.sensitive = revokable != null && revokable.valid && !revokable.in_process;
     }
     
     private void on_revokable_valid_changed() {
@@ -2540,6 +2552,17 @@ public class GearyController : Geary.BaseObject {
             save_revokable(null, null);
     }
     
+    private void on_revokable_committed(Geary.Revokable? committed_revokable) {
+        if (committed_revokable == null)
+            return;
+        
+        debug("Committed Revokable issued another Revokable");
+        
+        // use existing description
+        Gtk.Action undo_action = GearyApplication.instance.get_action(ACTION_UNDO);
+        save_revokable(committed_revokable, undo_action.tooltip);
+    }
+    
     private void on_revoke() {
         if (revokable != null && revokable.valid)
             revokable.revoke_async.begin(null, on_revoke_completed);
diff --git a/src/engine/api/geary-revokable.vala b/src/engine/api/geary-revokable.vala
index 68707d3..c6bf35d 100644
--- a/src/engine/api/geary-revokable.vala
+++ b/src/engine/api/geary-revokable.vala
@@ -35,6 +35,22 @@ public abstract class Geary.Revokable : BaseObject {
     private uint commit_timeout_id = 0;
     
     /**
+     * Fired when the { link Revokable} has been revoked.
+     *
+     * { link valid} will stil be true when this is fired.
+     */
+    public signal void revoked();
+    
+    /**
+     * Fired when the { link Revokable} has been committed.
+     *
+     * Some Revokables will offer a new Revokable to allow revoking the committed state.
+     *
+     * { link valid} will stil be true when this is fired.
+     */
+    public signal void committed(Geary.Revokable? commit_revokable);
+    
+    /**
      * Create a { link Revokable} with optional parameters.
      *
      * If commit_timeout_sec is nonzero, Revokable will automatically call { link commit_async}
@@ -50,6 +66,14 @@ public abstract class Geary.Revokable : BaseObject {
             Source.remove(commit_timeout_id);
     }
     
+    protected virtual void notify_revoked() {
+        revoked();
+    }
+    
+    protected virtual void notify_committed(Geary.Revokable? commit_revokable) {
+        committed(commit_revokable);
+    }
+    
     /**
      * Revoke (undo) the operation.
      *
@@ -81,7 +105,8 @@ public abstract class Geary.Revokable : BaseObject {
      * ({ link in_process}, throwing the appropriate Error, etc.)  Child classes can override this
      * method and only worry about the revoke operation itself.
      *
-     * This call *must* set { link valid} before exiting.
+     * This call *must* set { link valid} before exiting.  It must also call { link notify_revoked}
+     * if successful.
      */
     protected abstract async void internal_revoke_async(Cancellable? cancellable) throws Error;
     
@@ -120,7 +145,8 @@ public abstract class Geary.Revokable : BaseObject {
      * ({ link in_process}, throwing the appropriate Error, etc.)  Child classes can override this
      * method and only worry about the revoke operation itself.
      *
-     * This call *must* set { link valid} before exiting.
+     * This call *must* set { link valid} before exiting.  It must also call { link notify_committed}
+     * if successful.
      */
     protected abstract async void internal_commit_async(Cancellable? cancellable) throws Error;
     
diff --git a/src/engine/imap-engine/imap-engine-revokable-committed-move.vala 
b/src/engine/imap-engine/imap-engine-revokable-committed-move.vala
new file mode 100644
index 0000000..864672f
--- /dev/null
+++ b/src/engine/imap-engine/imap-engine-revokable-committed-move.vala
@@ -0,0 +1,64 @@
+/* Copyright 2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later).  See the COPYING file in this distribution.
+ */
+
+/**
+ * A { link Geary.Revokable} for moving email back to its source after committed with
+ * { link RevokableMove}.
+ */
+
+private class Geary.ImapEngine.RevokableCommittedMove : Revokable {
+    private GenericAccount account;
+    private FolderPath source;
+    private FolderPath destination;
+    private Gee.Set<Imap.UID> destination_uids;
+    
+    public RevokableCommittedMove(GenericAccount account, FolderPath source, FolderPath destination,
+        Gee.Set<Imap.UID> destination_uids) {
+        this.account = account;
+        this.source = source;
+        this.destination = destination;
+        this.destination_uids = destination_uids;
+    }
+    
+    protected override async void internal_revoke_async(Cancellable? cancellable) throws Error {
+        Imap.Folder? detached_destination = null;
+        try {
+            // use a detached folder to quickly open, issue command, and leave, without full
+            // normalization that MinimalFolder requires
+            detached_destination = yield account.fetch_detached_folder_async(destination, cancellable);
+            
+            yield detached_destination.open_async(cancellable);
+            
+            foreach (Imap.MessageSet msg_set in Imap.MessageSet.uid_sparse(destination_uids)) {
+                // don't use Cancellable to try to make operations atomic
+                yield detached_destination.copy_email_async(msg_set, source, null);
+                yield detached_destination.remove_email_async(msg_set.to_list(), null);
+                
+                if (cancellable != null && cancellable.is_cancelled())
+                    throw new IOError.CANCELLED("Revoke cancelled");
+            }
+            
+            notify_revoked();
+        } finally {
+            if (detached_destination != null) {
+                try {
+                    yield detached_destination.close_async(cancellable);
+                } catch (Error err) {
+                    // ignored
+                }
+            }
+            
+            valid = false;
+        }
+    }
+    
+    protected override async void internal_commit_async(Cancellable? cancellable) throws Error {
+        // pretty simple: already committed, so done
+        notify_committed(null);
+        valid = false;
+    }
+}
+
diff --git a/src/engine/imap-engine/imap-engine-revokable-move.vala 
b/src/engine/imap-engine/imap-engine-revokable-move.vala
index b7c8add..80991ce 100644
--- a/src/engine/imap-engine/imap-engine-revokable-move.vala
+++ b/src/engine/imap-engine/imap-engine-revokable-move.vala
@@ -4,8 +4,17 @@
  * (version 2.1 or later).  See the COPYING file in this distribution.
  */
 
+/**
+ * A @{link Geary.Revokable} for { link MinimalFolder} move operations.
+ *
+ * This will delay executing the move until (a) the source Folder is closed or (b) a timeout passes.
+ * Even then, it will fire its "committed" signal with a { link RevokableCommittedMove} to allow
+ * the user to undo the operation, albeit taking more time to connect, open the destination folder,
+ * and move the mail back.
+ */
+
 private class Geary.ImapEngine.RevokableMove : Revokable {
-    private const int REVOKE_TIMEOUT_SEC = 60;
+    private const int COMMIT_TIMEOUT_SEC = 60;
     
     private GenericAccount account;
     private ImapEngine.MinimalFolder source;
@@ -14,7 +23,7 @@ private class Geary.ImapEngine.RevokableMove : Revokable {
     
     public RevokableMove(GenericAccount account, ImapEngine.MinimalFolder source, FolderPath destination,
         Gee.Set<ImapDB.EmailIdentifier> move_ids) {
-        base (REVOKE_TIMEOUT_SEC);
+        base (COMMIT_TIMEOUT_SEC);
         
         this.account = account;
         this.source = source;
@@ -49,6 +58,9 @@ private class Geary.ImapEngine.RevokableMove : Revokable {
         try {
             yield source.exec_op_async(new MoveEmailRevoke(source, move_ids, cancellable),
                 cancellable);
+            
+            // valid must still be true before firing
+            notify_revoked();
         } finally {
             valid = false;
         }
@@ -56,8 +68,11 @@ private class Geary.ImapEngine.RevokableMove : Revokable {
     
     protected override async void internal_commit_async(Cancellable? cancellable) throws Error {
         try {
-            yield source.exec_op_async(new MoveEmailCommit(source, move_ids, destination, cancellable),
-                cancellable);
+            MoveEmailCommit op = new MoveEmailCommit(source, move_ids, destination, cancellable);
+            yield source.exec_op_async(op, cancellable);
+            
+            // valid must still be true before firing
+            notify_committed(new RevokableCommittedMove(account, source.path, destination, 
op.destination_uids));
         } finally {
             valid = false;
         }
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-move-email-commit.vala 
b/src/engine/imap-engine/replay-ops/imap-engine-move-email-commit.vala
index fb71550..d0de4d5 100644
--- a/src/engine/imap-engine/replay-ops/imap-engine-move-email-commit.vala
+++ b/src/engine/imap-engine/replay-ops/imap-engine-move-email-commit.vala
@@ -9,6 +9,8 @@
  */
 
 private class Geary.ImapEngine.MoveEmailCommit : Geary.ImapEngine.SendReplayOperation {
+    public Gee.Set<Imap.UID> destination_uids = new Gee.HashSet<Imap.UID>();
+    
     private MinimalFolder engine;
     private Gee.List<ImapDB.EmailIdentifier> to_move = new Gee.ArrayList<ImapDB.EmailIdentifier>();
     private Geary.FolderPath destination;
@@ -58,7 +60,11 @@ private class Geary.ImapEngine.MoveEmailCommit : Geary.ImapEngine.SendReplayOper
             
             Imap.MessageSet msg_set = iter.get();
             
-            yield engine.remote_folder.copy_email_async(msg_set, destination, null);
+            Gee.Map<Imap.UID, Imap.UID>? map = yield engine.remote_folder.copy_email_async(msg_set,
+                destination, null);
+            if (map != null)
+                destination_uids.add_all(map.values);
+            
             yield engine.remote_folder.remove_email_async(msg_set.to_list(), null);
             
             // completed successfully, remove from list in case of retry


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