While developing a 3d modelling application for the Windows platform, I got the idea of integrating filestream operations with the undo/redo feature. Like Jacob going to Egypt, it seemed like a good idea at the time, but later it created a lot of problems. Since that time, I've decided that this method, while having its merits, is cumbersome and error-prone. Hence I've written this.
Your application maintains a project consisting of multiple interrelated objects. You need an undo function, or else nobody will want to use your application—perhaps not even you. How do you implement it?
Simply build the ability to maintain object histories into each class. 'Tain't hard.
When an object is deleted by user editing, do not delete it from memory, but merely flag it so that commencing with that change, the object is no longer valid, move it to the deleted objects list, and adjust the other objects which refer to it accordingly.
For objects created during an edit, start out with a historical entry that signifies an invalid object.
When the user selects undo:
When the user selects redo:
When loading an old project from file, the program should call takeSnapshot(0) for each object. This will force each object to be stored as the oldest version of itself.
You will want to have a way to detect orphan objects, that is, objects which are created, then deleted from the project, and then the user undoes past the edit which created the object, and makes another edit. This last edit will cause all of the undone history to be deleted from memory, resulting in an object flagged as deleted, with only a null entry in its history list. These objects can be deleted from memory (and not just the project). I found that the best time to detect this is inside the takeSnap() call for each object; if the object is flagged as deleted, and the most recent entry is the original dummy entry, flag the object for deletion from memory. Then, in the function which removes deleted objects from the active object lists, check the delete-from-memory flag, and if it's set, delete the object from memory.
This code fragment gives an overall idea of the needed structure:
enum EntryType { ET_NUL, ET_CHG, ET_DEL }; struct ObjHistory { struct doubleLink link; unsigned int histVersion; enum EntryType type; struct ObjData data; }; |
The struct doubleLink link is for making the ObjHistory structure part of a doubly-linked list. I do this because you can remove objects from a doubly-linked list without going through the whole list (or even knowing where the list header is located), which makes encapsulation easier. I suspect that a singly-linked list would probaby work just as well, too.
type records whether the entry marks the time before an object's creation, one of the changes to the object at a given edit, or its deletion from the project.
ObjData is a structure containing all of the data that would be saved to file. It's a good idea to have a like structure in the object itself, so that you can easily copy the data back and forth. The copy in the object is the working copy, and receives all of the edits.
Your data object structures will look like this:
struct myObject { struct doubleLink link; struct doubleListHeader history; struct ObjData data; }; |
The link field is for linking the object in the project. The history field is for maintaining the list of historical objects.
As can be expected, the major difference is that you can use the various features of C++ to better manage your code. For instance, you can override the assignment operator (=), the comparison operator (== or !=), and probably the constructor and destructor for each ObjData type so that you can centralize the managment of dynamic data in that object.
Go back and read the first article on this. That stuff's insane! This second method, methinks, is definitely better. It makes archiving the responsibility of each object, so the functions that handle this on a project level are far, far simpler, while the object-level functions are not particularly more complicated.
Got comments?
Send them to John at evilsnack at hotmail dot com
Return to John's Freeloading Home Page