Cocoa Programming for Mac OS X - Aaron Hillegass [49]
NSInvocation
As you might imagine, it is handy to be able to package up a message (including the selector, the receiver, and all arguments) as an object that can be invoked at your leisure. Such an object is an instance of NSInvocation.
One exceedingly convenient use for invocations is in message forwarding. When an object is sent a message that it does not understand, before raising an exception, the message-sending system checks whether the object has implemented the following method:
- (void)forwardInvocation:(NSInvocation *)x
If the object has such a method, the message sent is packed up as an NSInvocation and forwardInvocation: is called.
How the NSUndoManager Works
Suppose that the user opens a new RaiseMan document and makes three edits:
1. Inserts a new record
2. Changes the name from New Employee to Rex Fido
3. Changes the raise from 0 to 20
As each edit is performed, your controller will add an invocation that would undo that edit to the undo stack. For the sake of simplifying the prose, let’s say, “The inverse of the edit gets added to the undo stack.”
Figure 9.1 shows what the undo stack would look like after these three edits.
Figure 9.1. The Undo Stack
If the user now chooses the Undo menu item, the first invocation is taken off the stack and invoked. This would change the person’s raise back to zero. If the user chooses the Undo menu item again, the person’s name would change back to New Employee.
Each time an item is popped off the undo stack and invoked, the inverse of the undo operation must be added to the redo stack. Thus, after undoing the two operations described, the undo and redo stacks should look like Figure 9.2.
Figure 9.2. The Revised Undo Stack
The undo manager is quite clever: When the user is doing edits, the undo invocations go onto the undo stack. When the user is undoing edits, the undo invocations go onto the redo stack. When the user is redoing edits, the undo invocations go onto the undo stack. These tasks are handled automatically for you; your only job is to give the undo manager the inverse invocations that need to be added.
Now suppose that you are writing a method called makeItHotter and that the inverse of this method is called makeItColder. Here is how you would enable the undo:
- (void)makeItHotter
{
temperature = temperature + 10;
[[undoManager prepareWithInvocationTarget:self] makeItColder];
[self showTheChangesToTheTemperature];
}
As you might guess, the prepareWithInvocationTarget: method notes the target and returns the undo manager itself. Then, the undo manager cleverly overrides forwardInvocation: such that it adds the invocation for makeItColder: to the undo stack.
To complete the example, you would implement makeItColder:
- (void)makeItColder
{
temperature = temperature - 10;
[[undoManager prepareWithInvocationTarget:self] makeItHotter];
[self showTheChangesToTheTemperature];
}
Note that we have again registered the inverse with the undo manager. If makeItColder is invoked as a result of an undo, this inverse will go onto the redo stack.
The invocations on either stack are grouped. By default, all invocations added to a stack during a single event are grouped together. Thus, if one user action causes changes in several objects, all the changes are undone by a single click of the Undo menu item.
The undo manager can also change the label on the Undo and Redo menu items. For example, “Undo Insert” is more descriptive than just “Undo.” To set the label, use the following code:
[undoManager setActionName:@"Insert"];
How do you get an undo manager? You can create one explicitly, but note that each instance of NSDocument already has its own undo manager.
Adding Undo to RaiseMan
Let’s