251 lines
8.5 KiB
Vala
251 lines
8.5 KiB
Vala
/*
|
|
* 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;
|
|
}
|
|
|
|
}
|