I have implemented the undo functionality for XML support in ee-xml and I am doing the same for the text editor.
Undo is a little bit interesting in application development, and it’s a good case study for F* development: in F*, we try to keep ‘features’ separate and self-contained.
An undo manager needs to coordinate actions triggered by several, separate features – by ‘features’, I mean ‘value increments’ and by ‘separate’, I mean, bundled in separate package, and written in such a way that deleting a feature package will remove a feature completely without otherwise threatening the integrity of an application.
Generic Undo Functionality
When I wrote the {undo} feature for XML, I see in retrospect that I planned on sharing code. This uses the following classes.
- UndoSetup. This is a feature setup class and is typical of F*. In this case, UndoSetup adds a couple of menu items in each new application frame (as an AppFrameCreated.Listener), and registers a listener on a Keyboard class.
- History. This manages a list of undo actions and provides the undo/redo actions. More interestingly, this implements ModelAction.Listener, which allows it accumulate UndoableAction items
- Undo, Redo and HistoryKeyHandler. These are just UI event handlers allowing the user to invoke History methods.
- All classes are bundled in the same package (except Keyboard, which belongs to an amorphous utilities package). notification interfaces are all bundled in an event package as notifications are normally made available to all actors in the system.
Undoing XML actions
All XML edits are created as UndoableAction instances, then applied, then a notification is generated via ModelAction. I packaged XML actions separately, which may be OK (several features may need to use the same actions). Where I’m less convinced is that these actions are really provided only so that they can be undoed, and the packaging doesn’t really reflect that. Here, following feature and value driven separation, I have two problems:
- The {undo} feature is generic. It doesn’t do anything and is meant to be shared by actual features (ie, undoing XML actions, text actions, image edits… ). Eventually I’ll want to resolve this. generic {undo} is a framework feature targetting developers, not an application feature targetting end users.
- The undo functionality for XML (the actual value increment) is not represented anywhere. XML-actions was designed as a utilities package. As such it is ill formed, because it generates application level notifications. In the meantime, it is really the basic xml edit feature, {xml-edit}, that supports the undo functionality by relying on XML actions and notifying ModelAction.
Undoing text actions
My first reflex was to hook a (swing) DocumentListener onto my text components to generate UndoableAction instances from DocumentEvent. However, the structure of document events in swing is somehow intimidating. I didn’t intend to spend so much time on this, and I spotted nearly simultaneously that I could register an UndoableEditListener.
Behind UndoableEditListener, there is a structure very similar to what I originally designed – but provided at API level, and slightly more complex. From UndoableEditListener, we get swing UndoableEdits intended to be operated upon by an UndoManager. As I already have my own undo manager (the History class) I wrap the UndoableEdits using an adapter class. I then notify my edits using the ModelAction dispatcher.
This works (almost) like a charm, especially considering that (unlike with XML, where I actually had to painstakingly define each action and it’s undoing) we don’t need to distinguish between edit types. Leaving a couple of open questions…
- Using an adapter worked fine to the extent that swing’s UndoableEdit and my UndoableAction share core functionality. However, the swing interface provides a die() method used to release resources (if any). In order to ensure that die() is invoked, I had to provide a secondary interface and do some typecasting from History. Obviously I had no intention to grow my existing UndoableAction interface. But then I still find myself with an additional interface (in this case, just a resource release interface, Heavy). Frustrating…
- When undoing text actions, we don’t generally expect or want to undo input character by character. UndoableEdit distinguishes between significant and non significant edits (and I’d rather if special symbol inputs/deletions counted as significant versus alphanumerical input) but… …whatever implementation is underlying the edits fired by swing’s PlainDocument is a quick affair that doesn’t distinguish between significant and non significant edits. Pity considering I actually spent the time to fit an edit accumulator in my implementation, based on the significant/non significant distinction…
Conclusion
Writing this article and implementing the undo functionality for text took 110 minutes.
Overall, I somehow prefer the implementation of undo for text. Unlike what I did for XML, the feature is completely self contained and advertises itself as such.
It may seem that not having undo for XML actions defined as an actual feature makes no difference. I think it makes a huge difference: I could get rid of undo for text by deleting the package and crossing one line in the features registry. In contrast, removing undo functionality for XML would require updating several classes within the {xml-edit} feature.
More importantly, undo functionality for text is easier to upgrade than it’s XML counterpart.
Source?
Well… you might as well ask, or maybe I’ll link bits and pieces later on (mind, the java tutorial does have an article about this).