How can I make NSUndoManager's undo/redo action names work properly?

3.7k views Asked by At

I'm learning Cocoa, and I've gotten undo to work without much trouble. But the setActionName: method is puzzling me. Here's a simple example: a toy app whose windows contain a single text label and two buttons. Press the On button and the label reads 'On'. Press the Off button and the label changes to read 'Off'. Here are the two relevant methods (the only code I wrote for the app):

-(IBAction) turnOnLabel:(id)sender
{
    [[self undoManager] registerUndoWithTarget:self selector:@selector(turnOffLabel:) object:self];
    [[self undoManager] setActionName:@"Turn On Label"];
    [theLabel setStringValue:@"On"];
}

-(IBAction) turnOffLabel:(id)sender
{
    [[self undoManager] registerUndoWithTarget:self selector:@selector(turnOnLabel:) object:self];
    [[self undoManager] setActionName:@"Turn Off Label"];
    [theLabel setStringValue:@"Off"];
}

Here's what I expect:

  • I click the On button
  • The label changes to say 'On'
  • In the Edit menu is the item 'Undo Turn On Label'
  • I click that menu item
  • The label changes to say 'Off'
  • In the Edit menu is the item 'Redo Turn On Label'

In fact, all these things work as I expect apart from the last one. The item in the Edit menu reads 'Redo Turn Off Label', not 'Redo Turn On Label'. (When I click that menu item, the label does turn to On, as I'd expect, but this makes the menu item's name even more of a mystery.)

What am i misunderstanding, and how can I get these menu items to display the way I want them to?

3

There are 3 answers

1
Vassilis On BEST ANSWER

Remember: when you are redoing, your code must set an actionName for the Undo menu item.

When you are undoing or redoing, the actionName in the Redo menu item is set automatically.

setActionName: changes the Undo menu item only. The Redo menu item actionName is automated.

When you initially setActionName: when ![[self undoManager] isUndoing], this actionName goes to the Undo menu item. When then you choose to Undo ([[self undoManager] isUndoing] == YES, no actionNames are set by you) the undoManager automatically sets this actionName to the Redo menu item and the previous undo actionName to the Undo menu item. When you then choose to Redo, you still have to pass an actionName to go to the Undo menu item.

In other words: you have to set actionNames only when your code is not Undoing (but you must set when initially invoked or is redoing).

0
Gabriel Roth On

After looking at some sample code, I was able to fix this by adding a condition to the setActionName: calls in each method, like this:

if (![[self undoManager] isUndoing])
    [[self undoManager] setActionName:@"Turn On Label"];

I'll give the correct answer to whoever can explain why NSUndoManager needs me to do this.

1
Clarence On

Because your turnOnLabel: method can be invoked three possible ways:

1) when the control that the selected is associated with is performing its target/action sequence (i.e., the NSUndoManager isUndoing and isRedoing methods would return NO)

2) when you are performing an undo operation, and the turnOnLabel: method is actually being invoked by the NSUndoManager (i.e., isUndoing = YES and isRedoing = NO)

3) when you are performing a redo operation, and the turnOnLabel: method is actually being invoked by the NSUndoManager (i.e., isUndoing = NO and isRedoing = YES)

isUndoing  isRedoing    Action
-------------------------------------------------
   0           0        Turn On Label
   0           1        Turn On Label
   1           0        Turn Off Label
   1           1        <impossible state>