UPDATE: You may consider studying more advanced techniques described in second “undo” article: Designing Undo for large-scale projects. Going deep.
~~~~~
When you implement user interfaces, you should always be ready that your customers will ask very simple question: “Hey, where is undo in my application?”. Sometimes it makes developers totally puzzled :) because they haven’t even considered it. Then developers can find that they have to apply to project so much code changes and in so much places, that definitely will reflect stability of project, bring new bugs etc.
So, what can be done there? Well, good to have it planned from beginning, but usually it does not happen, especially if several people worked on the project, some of them left project, some of them working only on command-line interface, etc.
It is possible that you need to implement undo in project which grown up to 100K lines of code and of course the primary question is how to do it smooth and transparent without affecting much code. I had similar experience and would like to share some tricks about it.
Let’s consider some project which compound of multiple components and one of them is model which used by several views and even used through filter/sorting proxy models. Usually developers quickly goes to cover everything than users can do in views, so they subclass views and start working hard on covering all actions there. Well, even if such idea works, this is not good, considering complexity of your project, and especially if there are multiple components and they are using each other, so they should now about undo actions of every other component in project. Are you using different views of same models? – well, you have to implement same undo few times in different view, oh wait, there is also filter/sorting model in middle – you have to code more, etc
You can get feeling that with implementing undo you started some avalanche in code of new dependencies, new apis, new methods, hundred of undo commands etc… Even more – you have to teach other developers how to apply undo or document it all very well.
Let’s try another approach – we can try to cover one single component without affecting API, and make it totally transparent for other classes in project.
Imaging that we have some model:
class Model : public QAbstractItemModel { Q_OBJECT public: Model(QObject * parent); virtual ~Model() // model reimpl: int rowCount(const QModelIndex &parent = QModelIndex()) const; int columnCount(const QModelIndex &parent = QModelIndex()) const; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; virtual bool setData(const QModelIndex& index, const QVariant & value, int role = Qt::EditRole); // you api virtual void add(Item *); virtual void remove(Item *); };
There are 3 points where your model can be changed: add(), remove() and setData(). It is ideal to apply undo only in these 3 points and make it automatic. What means by “automatic”? – our code should know when apply undo and when not.
Remember that our project may also be used in command line mode. It makes sense not to mix model logic code with undo code, but we are not going to implement it in view, so let’s subclass our model and implement all Gui stuff and Undo there:
class ModelGui : public Model { Q_OBJECT public: ModelGui(QObject * parent); virtual ~ModelGui() QUndoStack * undoStack() const; void setUndoStack(QUndoStack *); // reimplemented for undo: virtual bool setData(const QModelIndex& index, const QVariant & value, int role = Qt::EditRole); virtual void add(Item *); virtual void remove(Item *); private: class AddCmd; class RemoveCmd; class ChangeCmd; };
We subclassed Model to reimplement add(), remove(), setData(), also we declared undo commands in “private” section (but we will define them in cpp to make them hidden). Now we need to implement add() where we cover it by undo command. Same time undo AddCmd should access “logic” add() method of superclass – we don’t need a recursion, so some kind of flag is needed, also it makes sense to make the flag kind of “global” to have control of covering actions by undo and in case of macro commands etc. Good trick to use “property” of qobject, also good to use as the qobject – the undo stack itself.
Let’s show implementation to make it clear:
... class ModelGui::AddCmd : public QUndoCommand { public: AddCmd(ModelGui * m, Item * newItem): model(m), item(newItem) {} void redo() { bool cover = model->undoStack()->property('cover').toBool(); model->undoStack()->setProperty('cover', false); model->Model::add(item); model->undoStack()->setProperty('cover', cover); } void undo() { bool cover = model->undoStack()->property('cover').toBool(); model->undoStack()->setProperty('cover', false); model->Model::remove(item); model->undoStack()->setProperty('cover', cover); } private: ModelGui * model; Item * item; }; void ModelGui::add(Item * newItem) { bool cover = undoStack()->property('cover').toBool(); if (cover) { undoStack()->beginMacro("Add Item"); undoStack()->push(new AddCmd(this, newItem)); undoStack()->endMacro(); } else Model::add(newItem); } ...
What we have accomplished here?
- There is single place where undo applied
- Implementation of undo commands are hidden
- Other classes or components don’t even need to know about undo in the model
- You can use multiple views and sorting/filtering proxy models without worry about undo
- If there is also command-line use of project, you just exclude ModelGui.h/cpp
- If other component/module cast for add(), it is covered by undo automatically
- If other undo command cast for add() from inside, it is not covered by undo – no recursion
- The project can be as complex as possible, you only need to worry about using “cover”
3 Comments
This is the closest I have ever seen to someone talking about Qt Undo/Redo in the context of a large application with a variety of data views, etc. The Qt docs for undo/redo explain the concept, but the example simply doesn’t scale. I feel the same about their examples for Model-View, which focus on tree and table views rather than a large application that will have custom forms, inhomogeneous and hierarchical data, etc. And this is coming from someone that thinks the Qt is one of the best documented developer APIs out there.
I was hoping that you would elaborate on the potential recursion issue and your ‘cover’ property? How/When does the ‘cover’ property get initialized? A brief state-style description of how ‘cover’ is protecting us from a recursion situation (and the harm that would create?) would be really appreciated.
Scott, there’s another more complete implementation of this here: http://doc.qt.nokia.com/qq/qq25-undo.html
That example is cleaner in that it uses ProxyModels to hide the Undo/Redo system and Path proxies to safely map QModelIndexes across major model changes.
In regard to the ‘cover’ property uses here, it strikes me as an over-engineered hack. There is no need for this. In my case, I define a custom user role which distinguishes actual dataChanges from dataChanges that should push commands.
Also this example misuses the being/end macro API for no benefit.
danny: I may recommend to read more advanced examples developing those ideas, just next article: http://lynxline.com/designing-undo-for-large-scale-projects-going-deep/
One Trackback
[…] logic to have them transparently cover by undo. That’s something that I described in Undo in complex Qt projects but maybe not deep […]