Re: How to deal with property and Undo?
Re: How to deal with property and Undo?
- Subject: Re: How to deal with property and Undo?
- From: Graham Cox <email@hidden>
- Date: Wed, 14 May 2008 10:19:53 +1000
This is quite straightforward - it's covered by Aaron Hillegasse in
his excellent book. I use this technique and it works beautifully.
I've also wrapped up the common functionality into a class that
"undoable observables" can derive from, and once I had that, each
class that wants undo just needs to do 2 things:
1. publish a list of those properties that it wants to be undoable
(this is just a list of strings).
2. Return a user-readable string for the undo action name for each
undoable property. The undo manager sets this as the undo action name
- this is not required to make this actually work but you probably
don't want the Undo menu just to say "Undo", but instead tell you what
it will undo.
So, when an object wants to start having its properties be undoable,
the controller uses the list of properties it publishes and starts
observing each one. Every change to any property ends up calling -
observeValueForKeyPath:ofObject:change:context: which converts the
change to an undo task using the undo manager's usual -
prepareWithInvocationTarget: method. The cunning thing is that the
target is the controller, and the method invoked is some private
method you declare. On invoking Undo, this private method then
reroutes the undo change back to the original object and property-
setting method. It completely unifies undo for all KVO-compliant
properties for any number of subsidiary objects - in my case I have
many different ones, all with many different properties. Undo "just
works" for all of them, once I provide the list of properties to the
controller.
Here's part of the code in the controller. The "cunning private
method" is ultra-simple:
- (void) changeKeyPath:(NSString*) keypath ofObject:(id) object
toValue:(id) value
{
if([value isEqual:[NSNull null]])
value = nil;
[object setValue:value forKeyPath:keypath];
}
- (void) observeValueForKeyPath:(NSString*) keypath ofObject:(id)
object change:(NSDictionary*) change context:(void*) context
{
#pragma unused(context)
NSKeyValueChange ch = [[change objectForKey:NSKeyValueChangeKindKey]
intValue];
BOOL wasChanged = NO;
if ( ch == NSKeyValueChangeSetting )
{
if(![[change objectForKey:NSKeyValueChangeOldKey] isEqual:[change
objectForKey:NSKeyValueChangeNewKey]])
{
[[[self undoManager] prepareWithInvocationTarget:self]
changeKeyPath:keypath
ofObject:object
toValue:[change objectForKey:NSKeyValueChangeOldKey]];
wasChanged = YES;
}
}
else if ( ch == NSKeyValueChangeInsertion || ch ==
NSKeyValueChangeRemoval )
{
// Cocoa has a bug (at least on 10.4) where array insertion/deletion
changes don't properly record the old array.
// GCObserveableObject gives us a workaround
NSArray* old = [object oldArrayValueForKeyPath:keypath];
[[[self undoManager] prepareWithInvocationTarget:self]
changeKeyPath:keypath
ofObject:object
toValue:old];
wasChanged = YES;
}
if ( wasChanged && !([[self undoManager] isUndoing] || [[self
undoManager] isRedoing]))
{
if([object
respondsToSelector:@selector(actionNameForKeyPath:changeKind:)])
[[self undoManager] setActionName:[object
actionNameForKeyPath:keypath changeKind:ch]];
else
[[self undoManager] setActionName:[GCObservableObject
actionNameForKeyPath:keypath objClass:[object class]]];
}
}
On 14 May 2008, at 1:10 am, Mike Abdullah wrote:
In which case you need to set up KVO of the properties in a
controller object of some kind and use that to register the undos.
Of course, Core Data does this all automatically :)
_______________________________________________
Cocoa-dev mailing list (email@hidden)
Please do not post admin requests or moderator comments to the list.
Contact the moderators at cocoa-dev-admins(at)lists.apple.com
Help/Unsubscribe/Update your Subscription:
This email sent to email@hidden