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

This commit is contained in:
Michael James Gratton 2018-06-13 17:31:46 +10:00
parent 688040663a
commit 6e1ff62b60
3 changed files with 253 additions and 0 deletions

View file

@ -29,6 +29,7 @@ src/client/accounts/local-service-information.vala
src/client/accounts/login-dialog.vala
src/client/application/application-avatar-store.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

View file

@ -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;
}
}

View file

@ -2,6 +2,7 @@
geary_client_vala_sources = files(
'application/application-avatar-store.vala',
'application/autostart-manager.vala',
'application/application-command.vala',
'application/geary-application.vala',
'application/geary-args.vala',
'application/geary-config.vala',