[geary/mjog/account-command-stacks: 16/25] Prevent the same application action re-executed multiple times



commit c601be47011d2efa71432e42d310edfd85f3ed2b
Author: Michael Gratton <mike vee net>
Date:   Thu Oct 31 16:13:37 2019 +1100

    Prevent the same application action re-executed multiple times
    
    Before Application.Controller executes an action, ensure it's not the
    same action as was execued just before. This stops e.g. issues if
    someone holds down Delete.
    
    Fixes #578

 src/client/application/application-command.vala    | 11 +++-
 src/client/application/application-controller.vala | 76 ++++++++++++++++++++++
 2 files changed, 84 insertions(+), 3 deletions(-)
---
diff --git a/src/client/application/application-command.vala b/src/client/application/application-command.vala
index d04d3141..ed831cb5 100644
--- a/src/client/application/application-command.vala
+++ b/src/client/application/application-command.vala
@@ -145,6 +145,11 @@ public abstract class Application.Command : GLib.Object {
         yield execute(cancellable);
     }
 
+    /** Determines if this command is equal to another. */
+    public virtual bool equal_to(Command other) {
+        return (this == other);
+    }
+
     /** Returns a string representation of the command for debugging. */
     public virtual string to_string() {
         return get_type().name();
@@ -366,7 +371,7 @@ public class Application.CommandStack : GLib.Object {
      * This calls {@link Command.execute} and if no error is thrown,
      * pushes the command onto the undo stack.
      */
-    public async void execute(Command target, GLib.Cancellable? cancellable)
+    public virtual async void execute(Command target, GLib.Cancellable? cancellable)
         throws GLib.Error {
         debug("Executing: %s", target.to_string());
         yield target.execute(cancellable);
@@ -389,7 +394,7 @@ public class Application.CommandStack : GLib.Object {
      * stack. If an error is thrown, the command is discarded and the
      * redo stack is emptied.
      */
-    public async void undo(GLib.Cancellable? cancellable)
+    public virtual async void undo(GLib.Cancellable? cancellable)
         throws GLib.Error {
         if (!this.undo_stack.is_empty) {
             Command target = this.undo_stack.poll_head();
@@ -423,7 +428,7 @@ public class Application.CommandStack : GLib.Object {
      * stack. If an error is thrown, the command is discarded and the
      * redo stack is emptied.
      */
-    public async void redo(GLib.Cancellable? cancellable)
+    public virtual async void redo(GLib.Cancellable? cancellable)
         throws GLib.Error {
         if (!this.redo_stack.is_empty) {
             Command target = this.redo_stack.poll_head();
diff --git a/src/client/application/application-controller.vala 
b/src/client/application/application-controller.vala
index 414ab5b6..6fcf7bbb 100644
--- a/src/client/application/application-controller.vala
+++ b/src/client/application/application-controller.vala
@@ -2106,6 +2106,35 @@ public class Application.Controller : Geary.BaseObject {
 internal class Application.ControllerCommandStack : CommandStack {
 
 
+    private EmailCommand? last_executed = null;
+
+
+    /** {@inheritDoc} */
+    public override async void execute(Command target,
+                                       GLib.Cancellable? cancellable)
+        throws GLib.Error {
+        // Guard against things like Delete being held down by only
+        // executing a command if it is different to the last one.
+        if (this.last_executed == null || !this.last_executed.equal_to(target)) {
+            this.last_executed = target as EmailCommand;
+            yield base.execute(target, cancellable);
+        }
+    }
+
+    /** {@inheritDoc} */
+    public override async void undo(GLib.Cancellable? cancellable)
+        throws GLib.Error {
+        this.last_executed = null;
+        yield base.undo(cancellable);
+    }
+
+    /** {@inheritDoc} */
+    public override async void redo(GLib.Cancellable? cancellable)
+        throws GLib.Error {
+        this.last_executed = null;
+        yield base.redo(cancellable);
+    }
+
     /**
      * Notifies the stack that one or more folders were removed.
      *
@@ -2213,6 +2242,29 @@ public abstract class Application.EmailCommand : Command {
     }
 
 
+    public override bool equal_to(Command other) {
+        if (this == other) {
+            return true;
+        }
+
+        if (this.get_type() != other.get_type()) {
+            return false;
+        }
+
+        EmailCommand? other_email = other as EmailCommand;
+        if (other_email == null) {
+            return false;
+        }
+
+        return (
+            this.location == other_email.location &&
+            this.conversations.size == other_email.conversations.size &&
+            this.email.size == other_email.email.size &&
+            this.conversations.contains_all(other_email.conversations) &&
+            this.email.contains_all(other_email.email)
+        );
+    }
+
     /**
      * Determines the command's response when a folder is removed.
      *
@@ -2330,6 +2382,24 @@ private class Application.MarkEmailCommand : TrivialCommand, EmailCommand {
         );
     }
 
+    public override bool equal_to(Command other) {
+        if (!base.equal_to(other)) {
+            return false;
+        }
+
+        MarkEmailCommand other_mark = (MarkEmailCommand) other;
+        return (
+            ((this.to_add == other_mark.to_add) ||
+             (this.to_add != null &&
+              other_mark.to_add != null &&
+              this.to_add.equal_to(other_mark.to_add))) &&
+            ((this.to_remove == other_mark.to_remove) ||
+             (this.to_remove != null &&
+              other_mark.to_remove != null &&
+              this.to_remove.equal_to(other_mark.to_remove)))
+        );
+    }
+
 }
 
 
@@ -2732,4 +2802,10 @@ private class Application.EmptyFolderCommand : Command {
         );
     }
 
+    /** Determines if this command is equal to another. */
+    public override bool equal_to(Command other) {
+        EmptyFolderCommand? other_type = other as EmptyFolderCommand;
+        return (other_type != null && this.target == other_type.target);
+    }
+
 }


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