[geary/wip/714104-refine-account-dialog] Add a user command abstraction and manager for handling undo/redo.



commit e2d97019cf38f260323f91d9011ea65bc5d4e040
Author: Michael James Gratton <mike vee net>
Date:   Wed Jun 13 17:31:46 2018 +1000

    Add a user command abstraction and manager for handling undo/redo.

 po/POTFILES.in                                  |   1 +
 src/CMakeLists.txt                              |   1 +
 src/client/application/application-command.vala | 251 ++++++++++++++++++++++++
 src/client/meson.build                          |   1 +
 4 files changed, 254 insertions(+)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 40367ee8..ec71d50a 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -28,6 +28,7 @@ src/client/accounts/goa-service-information.vala
 src/client/accounts/local-service-information.vala
 src/client/accounts/login-dialog.vala
 src/client/application/autostart-manager.vala
+src/client/application/application-command.vala
 src/client/application/geary-application.vala
 src/client/application/geary-args.vala
 src/client/application/geary-controller.vala
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 6e98a498..b2987f65 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -328,6 +328,7 @@ ${CMAKE_BINARY_DIR}/geary-version.vala
 
 set(CLIENT_SRC
 client/application/autostart-manager.vala
+client/application/application-command.vala
 client/application/geary-application.vala
 client/application/geary-args.vala
 client/application/geary-config.vala
diff --git a/src/client/application/application-command.vala b/src/client/application/application-command.vala
new file mode 100644
index 00000000..533370c0
--- /dev/null
+++ b/src/client/application/application-command.vala
@@ -0,0 +1,251 @@
+/*
+ * Copyright 2018 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.
+ */
+
+
+/**
+ * A generic application user command with undo and redo support.
+ */
+public abstract class Application.Command : GLib.Object {
+
+
+    /**
+     * A human-readable label describing the effect of calling {@link undo}.
+     *
+     * This can be used in a user interface, perhaps as a tooltip for
+     * an Undo button, to indicate what will happen if the command is
+     * un-done. For example, "Conversation restored from Trash".
+     */
+    public string? undo_label { get; protected set; default = null; }
+
+    /**
+     * A human-readable label describing the effect of calling {@link redo}.
+     *
+     * This can be used in a user interface, perhaps as a tooltip for
+     * a Redo button, to indicate what will happen if the command is
+     * re-done. For example, "Conversation restored from Trash".
+     */
+    public string? redo_label { get; protected set; default = null; }
+
+    /**
+     * A human-readable label describing the result of calling {@link execute}.
+     *
+     * This can be used in a user interface to indicate the effects of
+     * the action just executed. For example, "Conversation moved to
+     * Trash".
+     *
+     * Since the effects of re-doing a command should be identical to
+     * that of executing it, this string can also be used to describe
+     * the effects of {@link redo}.
+     */
+    public string? executed_label { get; protected set; default = null; }
+
+    /**
+     * A human-readable label describing the result of calling {@link undo}.
+     *
+     * This can be used in a user interface to indicate the effects of
+     * the action just executed. For example, "Conversation restored
+     * from Trash".
+     */
+    public string? undone_label { get; protected set; default = null; }
+
+
+    /**
+     * Called by {@link CommandStack} to execute the command.
+     *
+     * Applications should not call this method directly, rather pass
+     * it to {@link CommandStack.execute}.
+     *
+     * Command implementations should apply the user command when this
+     * method is called. It will be called at most once when used sole
+     * with the command stack.
+     */
+    public abstract async void execute(GLib.Cancellable? cancellable)
+        throws GLib.Error;
+
+    /**
+     * Called by {@link CommandStack} to undo the executed command.
+     *
+     * Applications should not call this method directly, rather they
+     * should call {@link CommandStack.undo} so that it is managed
+     * correctly.
+     *
+     * Command implementations should reverse the user command carried
+     * out by the call to {@link execute}. It will be called zero or
+     * more times, but only ever after a call to either {@link
+     * execute} or {@link redo} when used sole with the command stack.
+     */
+    public abstract async void undo(GLib.Cancellable? cancellable)
+        throws GLib.Error;
+        
+    /**
+     * Called by {@link CommandStack} to redo the executed command.
+     *
+     * Applications should not call this method directly, rather they
+     * should call {@link CommandStack.redo} so that it is managed
+     * correctly.
+     *
+     * Command implementations should re-apply a user command that has
+     * been un-done by a call to {@link undo}. By default, this method
+     * simply calls {@link execute}, but implementations with more
+     * complex requirements can override this. It will called zero or
+     * more times, but only ever after a call to {@link undo} when
+     * used sole with the command stack.
+     */
+    public virtual async void redo(GLib.Cancellable? cancellable)
+        throws GLib.Error {
+        yield execute(cancellable);
+    }
+        
+}
+
+
+/**
+ * A stack of executed application commands.
+ *
+ * The command stack manages calling the {@link Command.execute},
+ * {@link Command.undo}, and {@link Command.redo} methods on an
+ * application's user commands. It enforces the strict ordering of
+ * calls to those methods so that if a command is well implemented,
+ * then the application will be in the same state after executing and
+ * re-doing a command, and the application will return to the original
+ * state after being undone, both for individual commands and between
+ * after a number of commands have been executed.
+ *
+ * Applications should call {@link execute} to execute a command,
+ * which will push it on to an undo stack after executing it. The
+ * command at the top of the stack can be undone by calling {@link
+ * undo}, which undoes the command, pops it from the undo stack and
+ * pushes it on the redo stack. If a new command is executed when the
+ * redo stack is non-empty, it will be emptied first.
+ */
+public class Application.CommandStack : GLib.Object {
+
+
+    // The can_undo and can_redo are automatic properties so
+    // applications can get notified when they change.
+
+    /** Determines if there are any commands able to be un-done. */
+    public bool can_undo { get; private set; }
+
+    /** Determines if there are any commands available to be re-done. */
+    public bool can_redo { get; private set; }
+
+
+    private Gee.LinkedList<Command> undo_stack = new Gee.LinkedList<Command>();
+    private Gee.LinkedList<Command> redo_stack = new Gee.LinkedList<Command>();
+
+
+    /** Fired when a command is first executed */
+    public signal void executed(Command command);
+
+    /** Fired when a command is un-done */
+    public signal void undone(Command command);
+
+    /** Fired when a command is re-executed */
+    public signal void redone(Command command);
+
+
+    /**
+     * Executes an command and pushes it onto the undo stack.
+     *
+     * 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)
+        throws GLib.Error {
+        yield target.execute(cancellable);
+
+        this.undo_stack.insert(0, target);
+        this.can_undo = true;
+
+        this.redo_stack.clear();
+        this.can_redo = false;
+
+        executed(target);
+    }
+
+    /**
+     * Pops a command off the undo stack and un-does is.
+     *
+     * This calls {@link Command.undo} on the topmost command on the
+     * undo stack and if no error is thrown, pushes it on the redo
+     * stack. If an error is thrown, the command is discarded and the
+     * redo stack is emptied.
+     */
+    public async void undo(GLib.Cancellable? cancellable)
+        throws GLib.Error {
+        if (!this.undo_stack.is_empty) {
+            Command target = this.undo_stack.remove_at(0);
+
+            if (this.undo_stack.is_empty) {
+                this.can_undo = false;
+            }
+
+            try {
+                yield target.undo(cancellable);
+            } catch (Error err) {
+                this.redo_stack.clear();
+                this.can_redo = false;
+                throw err;
+            }
+
+            this.redo_stack.insert(0, target);
+            this.can_redo = true;
+            undone(target);
+        }
+    }
+
+    /**
+     * Pops a command off the redo stack and re-applies it.
+     *
+     * This calls {@link Command.redo} on the topmost command on the
+     * redo stack and if no error is thrown, pushes it on the undo
+     * stack. If an error is thrown, the command is discarded and the
+     * redo stack is emptied.
+     */
+    public async void redo(GLib.Cancellable? cancellable)
+        throws GLib.Error {
+        if (!this.redo_stack.is_empty) {
+            Command target = this.redo_stack.remove_at(0);
+
+            if (this.redo_stack.is_empty) {
+                this.can_redo = false;
+            }
+
+            try {
+                yield target.redo(cancellable);
+            } catch (Error err) {
+                this.redo_stack.clear();
+                this.can_redo = false;
+                throw err;
+            }
+
+            this.undo_stack.insert(0, target);
+            this.can_undo = true;
+            redone(target);
+        }
+    }
+
+    /** Returns the command at the top of the undo stack, if any. */
+    public Command? peek_undo() {
+        return this.undo_stack.is_empty ? null : this.undo_stack[0];
+    }
+
+    /** Returns the command at the top of the redo stack, if any. */
+    public Command? peek_redo() {
+        return this.redo_stack.is_empty ? null : this.redo_stack[0];
+    }
+
+    /** Clears all commands from both the undo and redo stacks. */
+    public void clear() {
+        this.undo_stack.clear();
+        this.can_undo = false;
+        this.redo_stack.clear();
+        this.can_redo = false;
+    }
+
+}
diff --git a/src/client/meson.build b/src/client/meson.build
index 4ee9feb3..52c9f68e 100644
--- a/src/client/meson.build
+++ b/src/client/meson.build
@@ -1,6 +1,7 @@
 # Geary client
 geary_client_vala_sources = files(
   'application/autostart-manager.vala',
+  'application/application-command.vala',
   'application/geary-application.vala',
   'application/geary-args.vala',
   'application/geary-config.vala',


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