A more complex Undo/Redo example

By: | Post date: July 2, 2007 | Comments: No Comments
Posted in categories: Cocoa
Tags:

One of the issues I was having with MagicBrush-Photo concerned improper behavior of undo/redo functionality.

Undo/Redo is fully described in the Apple documentation and there are lots of samples available, usually highlighting what developers “get for free” when they follow standard practices in Cocoa development. Of course, since these samples are “selling” Cocoa development, they are often not the most complex examples of behavior. For example, it will be a simple setting of a value/property on an object either directly or using bindings.

Naturally, it was one of these more complicated scenarios I was running up against.

Within MagicBrush-Photo, the application of a brush effect to an image (or layer) is not as simple as setting a property. The edit/brush cycle begins with the mouseDown event, and ends with the mouseUp event. So the functionality to undo is any changes to the image or layer from mouseDown to mouseUp. Following the documentation, I got that working pretty easily. Here is some code to show how it works….

The document’s (IBAction)mouseDown:(id)sender method calls tailored methods based on the layer type being edited. Here is the method for an image layer:

– (void)mouseDownImageLayer:(NSEvent*)theEvent;
{
NSDictionary *undoState = [NSDictionary dictionaryWithObjectsAndKeys:
[self activeLayer], @”activeLayer”,
[[self activeLayer] getLayerImage], @”layerImage”,
nil];
[[self undoManager] registerUndoWithTarget:self selector:@selector(undoDrag:) object:undoState];
[[self undoManager] setActionName:NSLocalizedString(@”Brush Stroke”, @”undo (brush stroke)”)];
[_mask setValue:[[GWBrushesController sharedBrushesController] imageForBrush] forKey:@”inputImage”];
[self mouseDragged:theEvent];
}

This code takes the current layer image and “snaps” a copy to register with the NSUndoManager for the document. It also sets what selector to use to perform the undo operation when invoked.

And here is the undoDrag method:

– (void)undoDrag:(id)undoState
{
id theLayer = [undoState valueForKey:@”activeLayer”];
CIImage *theImage = [undoState valueForKey:@”layerImage”];
[theLayer setLayerImage:theImage];
[self refresh];
}

This code is straight forward and works for undoing a brush stroke.

My issue came with redo, which according to the documentation should “just happen:”

NSUndoManager is a general-purpose recorder of operations for undo and redo. You register an undo operation by specifying the object that is changing (or the owner of that object), along with a method to invoke to revert its state, and the arguments for that method. NSUndoManager groups all operations within a single cycle of the run loop, so that performing an undo reverts all changes that occurred during the loop. Also, when performing undo an NSUndoManager saves the operations reverted so that you can redo the undos.”

Unfortunately, that was not the case. Instead, the “Redo” item in the edit menu would remain grayed out and unavailable after an Undo was performed.

So what was causing my strange behavior? It took a discussion with an Apple Engineer at WWDC to determine. Of course, it only took them five minutes to identify the issue 😉

Since I am directly invoking the undo manager and managing the undo stack for each document, I have to register the redo action from within the method referenced by the undo selector. So, my undoDrag method needs to becomes:

– (void)undoDrag:(id)undoState
{
id theLayer = [undoState valueForKey:@”activeLayer”];
CIImage *theImage = [undoState valueForKey:@”layerImage”];
NSDictionary *redoState = [NSDictionary dictionaryWithObjectsAndKeys:
[self activeLayer], @”activeLayer”,
[[self activeLayer] getLayerImage], @”layerImage”,
nil];

[[self undoManager] registerUndoWithTarget:self
selector:@selector(undoDrag:)
object:redoState];

[[self undoManager] setActionName:NSLocalizedString(@”Brush Stroke”, @”redo (brush stroke)”)];
[theLayer setLayerImage:theImage];
[self refresh];
}

In hindsight, the solution seems very obvious. However, I know from pouring over the documents and second guessing myself to no end, it was not that simple. That’s why I’m posting it here, so some other poor coder might save themselves the anguish of beating their head against the wall.

I hope it helps someone.