I’m implementing a Java app that includes an Undo/Redo stack. I’ve noticed that some apps (such as TextEdit on Mac OS X) let you choose “Undo Typing” from the Edit Menu after typing some text. I’d like to implement that sort of thing into my app as well, but I’m having a really hard time finding guidelines about how it should behave.
With some trial and error, my best guess about how TextEdit’s Undo Typing behaves is:
- When the user types a new character (or types the delete key), merge it into the previous Undo Typing item if one is at the top of the Undo stack, unless one of the following situations occurs
- Always create a new Undo Typing item after the user continues to type after at least 15 seconds of inactivity
- Always create a new Undo Typing item after the user is typing for an extended period of time and some condition is met (couldn’t figure out if this was time based or character count based).
- Always create a new Undo Typing item when any text is selected and then deleted or overwritten (selecting text, not making a change, then returning to the original insertion point and continuing to type does not trigger this)
In practice, Apple’s strategy seems to work (at least it works for me when I type), but as noted by the last point, I haven’t really been able to figure out the rules. Also, it seems like other programs follow different rules, such as Microsoft Word. Google has not turned up a defined list of rules for any implementation of Undo Typing and I haven’t come across any best practices for how it should behave. So how should it behave? Or is it just up to the whims of the programmer?
EDIT: Just to clarify, I’m not interested in implementation details right now. I’m especially curious as to whether or not an authoritative reference (e.g. best practices or user interface document) exists describing this or a description of how it is implemented across multiple products.
8
If you’re looking for an authoritative source, I think the best Mac-related material will be found in the Undo Architecture document from Apple.
I don’t think you’re going to find a list of rules about when you should or shouldn’t coalesce undo events, though. What feels right for one application won’t necessarily make sense for another one. For example, coalescing keystrokes makes sense in a text editor because the user will probably see typing a paragraph as a single action and not as 539 separate actions, and also because you don’t want the user to have to undo 539 times just to get to the point they were at before they typed that paragraph. But what about move operations on a shape in a drawing program? Or sequential adjustments to a fill color? You could make a good case for these being coalesced or not, depending on the nature of your program.
Always create a new Undo Typing item after the user is typing for an
extended period of time and some condition is met (couldn’t figure out
if this was time based or character count based).
It’s based on autosave. Lucky for you, the TextEdit source code is available and well commented. I think that if you take a look at it, you’ll get a better idea of what’s going on and why. For example:
- (void)saveToURL:(NSURL *)absoluteURL ofType:(NSString *)typeName forSaveOperation:(NSSaveOperationType)saveOperation completionHandler:(void (^)(NSError *error))handler {
// Note that we do the breakUndoCoalescing call even during autosave, which
// means the user's undo of long typing will take them back to the last spot an
// autosave occured. This might seem confusing, and a more elaborate solution may
// be possible (cause an autosave without having to breakUndoCoalescing), but since
// this change is coming late in Leopard, we decided to go with the lower risk fix.
[[self windowControllers] makeObjectsPerformSelector:@selector(breakUndoCoalescing)];
...
I know you said you’re not interested in implementation details yet, but looking at the way that Apple implemented TextEdit can inform the decisions you make for your own application.
1
on keydown -> timer representing your idle starts
on keydown/timer-running -> reset timer
on keydown/no timer-running -> readjust cell blocks to prepare for a new preserved state as position changes
idle-timer runs down -> Establish a new undo state
I wouldn’t track keypress identities. I’d split into cellular blocks of text (by character count) that allow you to track position by offsets from the nearest cell’s beginning positions so you don’t have to save the entire state of a tolstoy novel every time an idle timer runs down. Readjusting those offsets when cells before other cells get edited is the tricky part.