|  | Home · Examples

# Undo Framework Example

Code:

- UndoFramework.java

This example shows how to implement undo/redo functionality with the Qt undo framework.
In the Qt undo framework, all actions that the user performs are implemented in classes that inherit QUndoCommand. An undo command class knows how to both redo() - or just do the first time - and undo() an action. For each action the user performs, a command is placed on a QUndoStack. Since the stack contains all commands executed (stacked in chronological order) on the document, it can roll the state of the document backwards and forwards by undoing and redoing its commands. See the overview document for a high-level introduction to the undo framework.

The undo example implements a simple diagram application. It is possible to add and delete items, which are either box or rectangular shaped, and move the items by dragging them with the mouse. The undo stack is shown in a QUndoView, which is a list in which the commands are shown as list items. Undo and redo are available through the edit menu. The user can also select a command from the undo view.

We use the graphics view framework to implement the diagram. We only treat the related code briefly as the framework has examples of its own (e.g., the Diagram Scene Example).

The example consists of the following classes:

- `UndoFramework` inherits QMainWindow and arranges the example's widgets. It creates the commands based on user input and keeps them on the command stack.
- `AddCommand` adds an item to the scene.
- `DeleteCommand` deletes an item from the scene.
- `When` an item is moved the MoveCommand keeps record of the start and stop positions of the move, and it moves the item according to these when `redo()` and `undo()` is called.
- `DiagramScene` inherits QGraphicsScene and emits signals for the `MoveComands` when an item is moved.
- `DiagramItem` inherits QGraphicsPolygonItem and represents an item in the diagram.

## The UndoFramework class

    public class UndoFramework extends QMainWindow
    {
        private QAction deleteAction;
        private QAction addBoxAction;
        private QAction addTriangleAction;
        private QAction undoAction;
        private QAction redoAction;
        private QAction exitAction;
        private QAction aboutAction;

        private QMenu fileMenu;
        private QMenu editMenu;
        private QMenu itemMenu;
        private QMenu helpMenu;

        private DiagramScene diagramScene;
        private QUndoStack undoStack;
        private QUndoView undoView;

The `UndoFramework` class maintains the undo stack, i.e., it creates QUndoCommands and pushes and pops them from the stack when it receives the `triggered()` signal from `undoAction` and `redoAction`.

We will start with a look at the constructor:

        public UndoFramework()
        {
            undoStack = new QUndoStack();

            createActions();
            createMenus();

            undoStack.canRedoChanged.connect(redoAction, "setEnabled(boolean)");
            undoStack.canUndoChanged.connect(undoAction, "setEnabled(boolean)");

            createUndoView();

            diagramScene = new DiagramScene();
            diagramScene.setSceneRect(new QRectF(0, 0, 500, 500));

            diagramScene.itemMoved.connect(this, "itemMoved(UndoFramework$DiagramItem,QPointF)");

            setWindowTitle("Undo Framework");
            QGraphicsView view = new QGraphicsView(diagramScene);
            setCentralWidget(view);
            resize(700, 500);
        }

By connecting the undo stack's `canRedoChanged()` and canUndoChanged() signals to our undo and redo action's setEnabled() slot we make the actions disabled when the stack cannot undo and redo commands.

The rest of the constructor sets up the DiagramScene and QGraphicsView. Notice the syntax used for slots in inner classes.

Here is the `createUndoView()` method:

        private void createUndoView()
        {
            undoView = new QUndoView(undoStack);
            undoView.setWindowTitle(tr("Command List"));
            undoView.setAttribute(Qt.WidgetAttribute.WA_QuitOnClose, false);

            QDialog dialog = new QDialog(this);
            QVBoxLayout layout = new QVBoxLayout(dialog);
            layout.setContentsMargins(0, 0, 0, 0);
            layout.addWidget(undoView);
            dialog.show();
        }

The QUndoView is a widget that displays the text, which is set with the setText() method, for each QUndoCommand in the undo stack in a list.

Here is the `createActions()` method:

        private void createActions()
        {
            deleteAction = new QAction(tr("&Delete Item"), this);
            deleteAction.setShortcut(tr("Del"));
            deleteAction.triggered.connect(this, "deleteItem()");
    ...
            undoAction = new QAction(tr("&Undo"), this);
            undoAction.setShortcut(tr("Ctrl+Z"));
            undoAction.setEnabled(false);
            undoAction.triggered.connect(undoStack, "undo()");

            redoAction = new QAction(tr("&Redo"), this);
            List<QKeySequence> redoShortcuts = new LinkedList<QKeySequence>();
            redoShortcuts.add(new QKeySequence(tr("Ctrl+Y")));
            redoShortcuts.add(new QKeySequence(tr("Shift+Ctrl+Z")));
            redoAction.setShortcuts(redoShortcuts);
            redoAction.setEnabled(false);
            redoAction.triggered.connect(undoStack, "redo()");

The `createActions()` method sets up all the examples actions in the manner shown above. We can connect our `undoAction` and `redoAction` directly to the stack's `undo()` and `redo()` slots as we disable the actions when the stack cannot undo and redo. For the other actions we have implemented slots in the `UndoFramework` class.

Here is the `createMenues()` method:

        private void createMenus()
        {
    ...
            editMenu = menuBar().addMenu(tr("&Edit"));
            editMenu.addAction(undoAction);
            editMenu.addAction(redoAction);
            editMenu.addSeparator();
            editMenu.addAction(deleteAction);
            editMenu.aboutToShow.connect(this, "itemMenuAboutToShow()");
            editMenu.aboutToHide.connect(this, "itemMenuAboutToHide()");

    ...
        }

We have to use the QMenu `aboutToShow()` and `aboutToHide()` signals since we only want `deleteAction` to be enabled when we have selected an item. We also want the text() to be shown in the `undoAction` and `redoAction` menu items.

Here is the `itemMoved()` slot:

        public void itemMoved(DiagramItem movedItem, QPointF oldPosition)
        {
            undoStack.push(new MoveCommand(movedItem, oldPosition));
        }

We push a MoveCommand on the stack, which calls `redo()` on it.

Here is the `deleteItem()` slot:

        private void deleteItem()
        {
            if (diagramScene.selectedItems().isEmpty())
                return;

            QUndoCommand deleteCommand = new DeleteCommand(diagramScene);
            undoStack.push(deleteCommand);
        }

An item must be selected to be deleted. We need to check if it is selected as the `deleteAction` may be enabled even if an item is not selected. This can happen as we do not catch a signal or event when an item is selected.

Here is the `itemMenuAboutToShow()` and itemMenuAboutToHide() slots:

        private void itemMenuAboutToHide()
        {
            deleteAction.setEnabled(true);
        }

        private void itemMenuAboutToShow()
        {
            undoAction.setText(tr("Undo ") + undoStack.undoText());
            redoAction.setText(tr("Redo ") + undoStack.redoText());
            deleteAction.setEnabled(!diagramScene.selectedItems().isEmpty());
        }

We implement `itemMenuAboutToShow()` and `itemMenuAboutToHide()` to get a dynamic item menu. These slots are connected to the aboutToShow() and aboutToHide() signals. We need this to disable or enable the `deleteAction` and fill the `redoAction` and `undoAction` menu item with the text from the next QUndoCommand that will be redone or undone.

Here is the `addBox()` slot:

        private void addBox()
        {
            QUndoCommand addCommand = new AddCommand(DiagramType.Box, diagramScene);
            undoStack.push(addCommand);
        }

The `addBox()` method creates an AddCommand and pushes it on the undo stack.

Here is the `addTriangle()` sot:

        private void addTriangle()
        {
            QUndoCommand addCommand = new AddCommand(DiagramType.Triangle,
                                                     diagramScene);
            undoStack.push(addCommand);
        }

The `addTriangle()` method creates an AddCommand and pushes it on the undo stack.

Here is the implementation of `about()`:

        private void about()
        {
            QMessageBox.about(this, tr("About Undo"),
                              tr("The <b>Undo</b> example demonstrates how to " +
                              "use Qt's undo framework."));
        }

The about slot is triggered by the `aboutAction` and displays an about box for the example.

## AddCommand Class

        class AddCommand extends QUndoCommand
        {
            private DiagramItem myDiagramItem;
            private QGraphicsScene myGraphicsScene;
            private QPointF initialPosition;

The `AddCommand` class adds DiagramItem graphics items to the DiagramScene. We will explain the member variables as we stumble upon them in the implementation.

We start with the constructor:

            public AddCommand(DiagramType addType, QGraphicsScene scene)
            {
                myGraphicsScene = scene;
                myDiagramItem = new DiagramItem(addType);
                initialPosition = new QPointF((UndoFramework.itemCount * 15) % (int) scene.width(),
                                  (UndoFramework.itemCount * 15) % (int) scene.height());
                scene.update();
                ++UndoFramework.itemCount;
                setText(tr("Add " + UndoFramework.createCommandString(myDiagramItem, initialPosition)));
            }

We first create the DiagramItem to add to the DiagramScene. The setText() method let us set a QString that describes the command. We use this to get custom messages in the QUndoView and in the menu of the main window.

            public void redo()
            {
                myGraphicsScene.addItem(myDiagramItem);
                myDiagramItem.setPos(initialPosition);
                myGraphicsScene.clearSelection();
                myGraphicsScene.update();
            }

We set the position of the item as we do not do this in the constructor.

            public void undo()
            {
                myGraphicsScene.removeItem(myDiagramItem);
                myGraphicsScene.update();
            }

`undo()` removes the item from the scene.

## DeleteCommand Class Definition

        class DeleteCommand extends QUndoCommand
        {
            private DiagramItem myDiagramItem;
            private QGraphicsScene myGraphicsScene;

The DeleteCommand class implements the functionality to remove an item from the scene.

            public DeleteCommand(QGraphicsScene scene)
            {
                myGraphicsScene = scene;
                List<QGraphicsItemInterface> list = myGraphicsScene.selectedItems();
                list.get(0).setSelected(false);
                myDiagramItem = (DiagramItem) list.get(0);
                setText("Delete " + UndoFramework.createCommandString(myDiagramItem, myDiagramItem.pos()));
            }

We know that there must be one selected item as it is not possible to create a DeleteCommand unless the item to be deleted is selected and that only one item can be selected at any time. The item must be unselected if it is inserted back into the scene.

            public void undo()
            {
                myGraphicsScene.addItem(myDiagramItem);
                myGraphicsScene.update();
            }

The item is simply reinserted into the scene.

            public void redo()
            {
                myDiagramItem.setPos(newPos);
                setText(tr("Move " + UndoFramework.createCommandString(myDiagramItem, newPos)));
            }

The item is removed from the scene.

## MoveCommand Class

        class MoveCommand extends QUndoCommand
        {
            private DiagramItem myDiagramItem;
            private QPointF myOldPos;
            private QPointF newPos;

MoveCommand implements the command for moving items.

The constructor of MoveCommand looks like this:

            public MoveCommand(DiagramItem diagramItem, QPointF oldPos)
            {
                myDiagramItem = diagramItem;
                newPos = diagramItem.pos();
                myOldPos = oldPos;
            }

We save both the old and new positions for undo and redo respectively.

            public void undo()
            {
                myDiagramItem.setPos(myOldPos);
                myDiagramItem.scene().update();
                setText(tr("Move " + UndoFramework.createCommandString(myDiagramItem, newPos)));
           }

            @Override

            public void redo()
            {
                myDiagramItem.setPos(newPos);
                setText(tr("Move " + UndoFramework.createCommandString(myDiagramItem, newPos)));
            }

We simply set the items old position and update the scene.

            public void redo()
            {
                myGraphicsScene.addItem(myDiagramItem);
                myDiagramItem.setPos(initialPosition);
                myGraphicsScene.clearSelection();
                myGraphicsScene.update();
            }

We set the item to its new position.

            public boolean mergeWith(QUndoCommand other)
            {
                MoveCommand moveCommand = (MoveCommand) other;
                DiagramItem item = moveCommand.myDiagramItem;

                if (!myDiagramItem.equals(item))
                    return false;

                newPos = item.pos();
                setText(tr("Move " + UndoFramework.createCommandString(myDiagramItem, newPos)));

                return true;
            }

The mergeWith() is reimplemented to make consecutive moves of an item one MoveCommand, i.e, the item will be moved back to the start position of the first move.

Whenever a MoveCommand is created, this method is called to check if it should be merged with the previous command. It is the previous command object that is kept on the stack. The method returns true if the command is merged; otherwise false.

We first check whether it is the same item that has been moved twice, in which case we merge the commands. We update the position of the item so that it will take the last position in the move sequence when undone.

## DiagramScene Class

        class DiagramScene extends QGraphicsScene
        {
            public Signal2<DiagramItem,QPointF> itemMoved =
                new Signal2<DiagramItem,QPointF>();

            private DiagramItem movingItem;
            private QPointF oldPos;
    ...
        }

The DiagramScene implements the functionality to move a DiagramItem with the mouse. It emits a signal when a move is completed. This is caught by the `MainWindow`, which makes MoveCommands. We do not examine the implementation of DiagramScene as it only deals with graphics framework issues.

We declare a signal that is used to notify UndoFramework that an item has been moved in the scene.

We do not examine the DiagramScene class's implementation in detail, as it does not contain any functionality concerning the undo framework.

## The main() Function

`main()` The `main()` method of the program looks like this:

        public static void main(String args[])
        {
            QApplication.initialize(args);

            UndoFramework mainWindow = new UndoFramework();
            mainWindow.show();

            QApplication.exec();
        }

The main method creates the `MainWindow` and shows it as a top level window.