Undo framework
Met.3D uses Qt to implement undoable user actions. The details how that works is provided by Qt.
This page describes how user actions can be made undoable using the undo framework in Met.3D.
Undo command
Each action a user might do, should to be implemented in an Undo command. An Undo command encapsulates a single action from the user.
When the user triggers an action from the UI, it should instantiate an Undo command for that action. The command has to be pushed on the Undo stack which executes it. It is NOT needed to manually execute the action, the Undo command does that already. When the command is dropped from the Undo stack, either because it is full, cleared, or the command is obsolete, it will be deleted.
Each Undo command is implemented as a class inheriting from QUndoCommand.
It has to implement the redo() member function to execute the action the user triggered.
It also has to implement the undo() member function to revert that action.
Additionally, everytime these function are called, it should be checked if the command is still valid.
It might be, that the program state changed in some way that makes undoing or redoing the command impossible.
Currently, many undo commands are part of actions executed on some object, such as MActor.
To make it clear, that the command corresponds to the Actor class, we implement the undo commands as nested classes.
For example, the undo command to add an actor is implemented as class MActor::CreateCommand.
An example undo command is the following, which creates an actor.
1 class MActor : public QObject
2 {
3 // Actor class header
4 ...
5
6 // Undo commands
7 public:
8 /**
9 * Undo command used for actor creation.
10 */
11 class CreateCommand : public QUndoCommand
12 {
13 public:
14 CreateCommand(QString actorType, QString actorName, const QVector<MSceneControl *>& scenes = {});
15 CreateCommand(QString actorConfig,
16 QVector<MSceneControl *> scenes,
17 Qt::KeyboardModifiers mods = {});
18
19 void undo() override;
20 void redo() override;
21
22 MActor *getCreatedActor() const { return createdActor; }
23
24 private:
25 /**
26 * The type of actor to create.
27 */
28 QString actorType;
29 /**
30 * The name of the actor to create.
31 */
32 QString actorName;
33 /**
34 * The path to the actor config that is used to create the actor from.
35 */
36 QString actorConfig;
37 /**
38 * The scenes to add the actor to.
39 */
40 QVector<MSceneControl *> scenes;
41 /**
42 * Keyboard modifiers used when creating the actor.
43 */
44 Qt::KeyboardModifiers mods;
45 /**
46 * The created actor.
47 */
48 MActor *createdActor;
49 };
50 }
Undo stack
Met.3D uses a global undo stack in form of the singleton MUndoStack.
All undo commands can be pushed and executed by calling MUndoStack::run().
The undo stack will be cleared when switching sessions.
Merging commands
QUndoCommand::id() can be implemented in derived classes to return an ID for an undo command.
When an undo command is pushed onto the undo stack, and the top command shares the same ID as the newly pushed command, the stack will attempt to merge both commands into one.
This calls QUndoCommand::mergeWith(), which can be implemented in derived classes to merge two undo commands.
The existing command should incorporate the state of the newer command and return true if the merge was successful.
If the commands cannot be merged, mergeWith() should return false.
1bool mergeWith(const QUndoCommand *other) override
2{
3 // Check if both undo commands share the same type.
4 auto *cmd = dynamic_cast<const MyUndoCommand*>(other);
5
6 // Signal, that the merge is not possible as types mismatch.
7 if (cmd == nullptr) return false;
8
9 // Merge commands.
10 // In this case, we want to assume the up-to-date value of the newer undo command.
11 this->value = cmd->value;
12
13 // Signal that the merge was successful.
14 return true;
15}
The creator of the undo command should be able to configure the ID returned by id().
Ideally, it should be provided via the constructor of your undo command.
The ID has to be stored in your derived class and be returned by an overridden id() implementation.
This allows the creator to control merges of the undo commands.
For example, a drag from position 0 to 100 may generate commands for positions 10, 20, 30, etc.
All of these commands use the same ID, so they can be merged into one command representing the complete drag from 0 to 100.
A subsequent drag receives a different ID and therefore remains a separate undo operation.
By default, a “no-merge” id is returned by Qt, which has the value -1.