Chea Indian
Chea Indian

Reputation: 697

How do I implement a simple undo/redo for actions in java?

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?

Upvotes: 16

Views: 45866

Answers (4)

tobi delbruck
tobi delbruck

Reputation: 325

It's never so simple to implement Undo/Redo for things other than text documents. I try to summarize how it is done for a complex introspected GUI control panel here in case it might be helpful. There is a complete implementation as part of the introspected GUI controls of properties of event camera filters in FilterPanel and FilterFrame as part of jAER.

FilterPanel introspects an EventFilter class to find out the javabean properties and builds controls for these properties. Each property is wrapped in a GUI JPanel that allows users to control the property. The JPanel is a subclass of MyControl. MyControl has a setUndoableState method that starts and ends the property edit. These call stores the edit as a state on the UndoManager's stack. Any Components added to MyControl are automatically added to top level FilterFrame to get UndoableEditOccurred events.

The end result are Undo and Redo Actions at the level of FilterFrame that allow un/re doing the property changes:

enter image description here

Here is the code that automatically adds the MyControl subcomoonents to the UndoableEditSupport of the FilterFrame:

            addAncestorListener(new javax.swing.event.AncestorListener() {

            public void ancestorAdded(javax.swing.event.AncestorEvent evt) {
                if (addedUndoListener) {
                    return;
                }
                addedUndoListener = true;
                if (evt.getComponent() instanceof Container) {
                    Container anc = (Container) evt.getComponent();
                    while (anc != null && anc instanceof Container) {
                        if (anc instanceof UndoableEditListener) {
                            getEditSupport().addUndoableEditListener((UndoableEditListener) anc);
                            break;
                        }
                        anc = anc.getParent();
                    }
                }
            }

            public void ancestorMoved(javax.swing.event.AncestorEvent evt) {
            }

            public void ancestorRemoved(javax.swing.event.AncestorEvent evt) {
            }

        });

MyControl has a method setUndoableState(Object o) that starts and ends the user edit:

       /**
     * Subclasses should call super().setUndoableState()
     */
    public Object setUndoableState(Object o) {
        if (o == null) {
            log.warning("null object, will not set " + name);
            return null;
        }
        try {
            startEdit();
            write.invoke(getFilter(), o); // might call property change listeners
            Object ro = read.invoke(getFilter()); // constrain by writer
            setCurrentState(ro);
            setGuiState(o);
            return ro;
        } catch (IllegalAccessException | InvocationTargetException ex) {
            Logger.getLogger(FilterPanel.class.getName()).log(Level.SEVERE, null, ex);
        } finally {
            endEdit();
        }
        return null;
    }

FilterFrame has Actions for Undo and Redo

UndoManager undoManager = new UndoManager();
// undo/redo
UndoableEditSupport editSupport = new UndoableEditSupport();
UndoAction undoAction = new UndoAction();
RedoAction redoAction = new RedoAction();

private class UndoAction extends AbstractAction {

    public UndoAction() {
        putValue(NAME, "Undo");
        putValue(SHORT_DESCRIPTION, "Undo the last property change");
        putValue(SMALL_ICON, new javax.swing.ImageIcon(getClass().getResource("/net/sf/jaer/biasgen/undo.gif")));
        putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_Z, java.awt.event.InputEvent.CTRL_DOWN_MASK));
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        undo();
        putValue(SHORT_DESCRIPTION, undoManager.getUndoPresentationName());
    }

}

There is also a fixUndoRedo() method to update the action states depending on whether there are undoables. This method also updates the tooltips to show what would be undone.

    void fixUndoRedo() {
    final boolean canUndo = undoManager.canUndo(), canRedo = undoManager.canRedo();
    undoAction.setEnabled(canUndo);
    redoAction.setEnabled(canRedo);
    if (canUndo) {
        undoAction.putValue(AbstractAction.SHORT_DESCRIPTION, undoManager.getUndoPresentationName());
    }
    if (canRedo) {
        redoAction.putValue(AbstractAction.SHORT_DESCRIPTION, undoManager.getRedoPresentationName());
    }
}

The undo() method in FilterFrame is simple:

    void undo() {
    try {
        undoManager.undo();
    } catch (CannotUndoException e) {
        Toolkit.getDefaultToolkit().beep();
        log.warning(e.getMessage());
    } finally {
        fixUndoRedo();
    }
}

The cryptic business is the StateEdit that stores and restores the state. This class is part of Swing but not well-documented. MyControl implements StateEditable which means it has a storeState and restoreState method.

The whole business is pretty complicated to implement in my experience, but eventually can be made to work.

Upvotes: 0

Menelaos Kotsollaris
Menelaos Kotsollaris

Reputation: 5506

TL;DR: You can support undo and redo actions by implementing the Command (p.233) and Memento (p.283) patterns (Design Patterns - Gamma 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:

  1. execute: executes the command
  2. unExecute: removes the command

Code:

public abstract class Command
{
    MyObject myObject;
    Memento memento;
    
    public abstract void execute();
    
    public abstract void unExecute();
}

They 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 behavior. The 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.

Upvotes: 19

Ravindra babu
Ravindra babu

Reputation: 38910

You have to define undo(), redo() operations along with execute() in Command interface itself.

example:

interface Command {

    void execute() ;

    void undo() ;

    void redo() ;
}

Define a State in your ConcreteCommand class. Depending on current State after execute() method, you have to decide whether command should be added to Undo Stack or Redo Stack and take decision accordingly.

Have a look at this undo-redo command article for better understanding.

Upvotes: 1

nolegs
nolegs

Reputation: 608

I would try to create an Action class, with a AddElementAction class inheriting off Action. AddElementAction could have a Do() and Undo() method which would add/remove elements accordingly. You can then keep two stacks of Actions for undo/redo, and just call Do()/Undo() on the top element before popping it.

Upvotes: 0

Related Questions