Skip to content

How to use CommandManager Commands

Luis Sanchez edited this page Nov 13, 2018 · 4 revisions

Tutorial on CommandManager

CommandManager handles actions that happen to a target in a way that makes them undo-able. To allow that, all edits to the target must happen through execution of Commands

Setting up a CommandManager with a target

Using a CommandManager with a custom class is simple. Just create a CommandManagerinstance with your class as its target.

 // Your class that you want to change through the command manager.
class Geometry {...};
Geometry myGeometry{};

// Create CommandManager that will do edits to our Geometry instance as its target
CommandManager<Geometry> commandManager(myGeometry);

To allow the CommandManager to work with a target, it must be able to save and restore it's state. This is done via two methods:

class Geometry {
public:
    GeometryState saveState() const;
    void loadState(const GeometryState&);
};

Here GeometryState can be any type of your choice. As long as you are able to save and restore all the state you need, CommandManager doesn't care. Both of these methods must be implemented on the target to allow CommandManager to work.

Changing the target through commands

All changes that happen to the target must happen through the CommandManager via the use of Commands. To be able to do any changes at all, you must create your own commands:

// All your commands will derive the CommandBase
// specialized by the type of your target:
class MyCommand: public CommandBase<Geometry> {

    // Set a description for your command
    std::string_view getDescription() const override { return "My command description"; }

protected:
    // Will be called when CommandManager wants to apply this command to target
    void run(IntegerState& target) const override {

    // Do some change to the target
    // Your code goes here :)
    }
}

All changes to the target should happen only in the run() method. There is no need to store a pointer to the target or volatile data inside the command. The command should produce consistent results when being used a target with the same state.

Executing the commands

Every command instance must be executed exactly once. The recommended way to do so is to construct the command instance and immediately pass it to the CommandManager, which will handle the execution.

... 
// Creates an instance of MyCommand, 
// which will be immediately passed into commandManager and executed
commandManager.execute(std::make_unique<MyCommand>());

// Executing the command again requires creating a new instance
commandManager.execute(std::make_unique<MyCommand>());

// Both commands are now undoable
assert(commandManager.canUndo() == true);

// Undo the second command
commandManager.undo();

// Undo the first command
commandManager.undo();

// Both commands can be redone
assert(commandManager.canRedo() == true)

// Executing any other command will make us lose the possibility to redo
commandManager.execute(std::make_unique<MyCommand>());

Setting up your command data

// You can pass parameters to your command's constructor
commandManager.execute(std::make_unique<MyCommand>(1,2,3,4,5));

// If you need a more complicated process to set up your command:
auto myCommandPtr = std::make_unique<MyCommand>();
myCommandPtr->myData = 1;
myCommandPtr->moreData = 5;
myCommandPtr->mySetupFunction();

// Pass the pointer as an R-value
// This makes it possible for commandManager to take ownership
commandManager.execute(std::move(myCommandPtr));

Advanced usage

Creating snapshots after slow commands

Some commands might be more computationally expensive than others, causing unnecessary delay when the user tries to undo a following command. This can be avoided by making the CommandManager always create a snapshot of the target's state after the command. The snapshot will be used to continue from a newer state of the target.

class MySlowCommand: public CommandBase<Geometry> {
public:

    // Mark this command as a slow (thus requesting a snapshot after)
    // by passing a boolean parameter to the CommandBase constructor
    MySlowCommand():
        CommandBase(true) {}
    ...
    // Other CommandBase methods
    ...
};

Joining commands together

Sometimes there are lots of small changes to the target, that would ideally be grouped and undo-ed together. Command joining allows us to do that.

Command joining merges the changes of a new command to the changes of the previous one.

class MyJoinableCommand: public CommandBase<Geometry> {
// Identifier unique for each group of joinable commands
public:
    
    MyJoinableCommand():
        CommandBase(false, true /*join this command with the previous one */) {}

    bool joinCommand(const CommandBase& other) override {
        // Code that joins the other command into this one
        // The other command will still be executed, but will not be saved  

        // try dynamic_cast<> to get specialized pointer/ref

        return true; // return true on success
    }
    ...
    // Other CommandBase methods
    ...
};