Add a user command abstraction and manager for handling undo/redo.
This commit is contained in:
parent
688040663a
commit
6e1ff62b60
3 changed files with 253 additions and 0 deletions
|
|
@ -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
|
||||
|
|
|
|||
251
src/client/application/application-command.vala
Normal file
251
src/client/application/application-command.vala
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue