-
Notifications
You must be signed in to change notification settings - Fork 39
How to use CommandManager Commands
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
Using a CommandManager with a custom class is simple. Just create a CommandManager
instance 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.
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.
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>());
// 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));
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
...
};
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
...
};
❤️ Pepr3D 2018-2019 · Home