I've created an XML editor and I'm stuck at the last phase: adding undo/redo functionality.
I've only got to add undo/redo for when users add elements, attributes, or text to the JTree.
I'm still quite new at this but in school today I attempted (unsuccessfully) to create two stack object []'s called undo and redo and to add the actions performed into them.
For instance, I have:
Action AddElement() {
// some code
public void actionPerformed(ActionEvent e) {
performElementAction();
}
}
the performElementAction just actually adds an Element to the JTree.
I want to add a way to add this action performed to my undo stack. is there a simple way to just undo.push( the entire action performed) or something?
TL;DR: You can support undo and redo actions by implementing the Command and Memento patterns (Design Patterns - Gama et. al).
The Memento Pattern
This simple pattern allows you to save the states of an object. Simply wrap the object in a new class and whenever its state changes, update it.
public class Memento
{
MyObject myObject;
public MyObject getState()
{
return myObject;
}
public void setState(MyObject myObject)
{
this.myObject = myObject;
}
}
The Command Pattern
The Command pattern stores the original object (that we want to support undo/redo) and the memento object, which we need in case of an undo. Moreover, 2 methods are defined:
Code:
public abstract class Command
{
MyObject myObject;
Memento memento;
public abstract void execute();
public abstract void unExecute();
}
The define the logical "Actions" that extend Command (e.g. Insert):
public class InsertCharacterCommand extends Command
{
//members..
public InsertCharacterCommand()
{
//instantiate
}
@Override public void execute()
{
//create Memento before executing
//set new state
}
@Override public void unExecute()
{
this.myObject = memento.getState()l
}
}
Applying the patterns:
This last step defines the undo/redo behaviour. They core idea is to store a stack of commands that works as a history-list of the commands. To support redo, you can keep a secondary pointer whenever an undo command is applied. Note that whenever a new object is inserted, then all the commands after its current position get removed; that's achieved by the deleteElementsAfterPointer
method defined below:
private int undoRedoPointer = -1;
private Stack<Command> commandStack = new Stack<>();
private void insertCommand()
{
deleteElementsAfterPointer(undoRedoPointer);
Command command =
new InsertCharacterCommand();
command.execute();
commandStack.push(command);
undoRedoPointer++;
}
private void deleteElementsAfterPointer(int undoRedoPointer)
{
if(commandStack.size()<1)return;
for(int i = commandStack.size()-1; i > undoRedoPointer; i--)
{
commandStack.remove(i);
}
}
private void undo()
{
Command command = commandStack.get(undoRedoPointer);
command.unExecute();
undoRedoPointer--;
}
private void redo()
{
if(undoRedoPointer == commandStack.size() - 1)
return;
undoRedoPointer++;
Command command = commandStack.get(undoRedoPointer);
command.execute();
}
Conclusion:
What makes this design powerful is the fact that you can add as many commands as you like (by extending the Command
class) e.g., RemoveCommand
, UpdateCommand
and so on. Moreover, the same pattern is applicable to any type of object, making the design reusable and modifiable across different use cases.