Setting up undoManager

This is an OS X app ; using Swift 2.2 in XCode 7.3.


I have a windowController with a view aView


I am trying to implement undoin a func, but the undoManager remains nil.


    func changeColor(sender: NSMenuItem) {
        undoManager?.registerUndoWithTarget(self, selector: #selector(remove(_:)), object: aView)  //
        undoManager?.setActionName("colored")
        Swift.print(undoManager)    // always get nil
}

The function for undo / redo :

    func add(v: NSView) {
        undoManager?.registerUndoWithTarget(self, selector: #selector(remove(_:)), object: v) /
    }

    func removeCircle(v: NSView) {
        undoManager?.prepareWithInvocationTarget(self).add(v) /
    }


I have declared the view as firstResponder in windowDidLoad


        self.aView.becomeFirstResponder()    


In changeColor, undoManager remains nil.


Where should I initialize it ?

Answered by QuinceyMorris in 198326022

Did you change the class of the window controller in the storyboard from NSWindowController to WindowController? Or are you creating the UI programmatically?


Also, make sure you declare WindowController as conforming to NSWindowDelegate.

Which undoManager remains nil? If it's an instance property you've declared, it's not going to have a value unless you give it one.


If your application is NSDocument-based, each document will create its own undo manager. If not, you'll have to create the undo manager yourself. See the NSWindowDelegate method "windowWillReturnUndoManager(_:)".

That was the "general" undo manager. But my app is not doc based, so....


I added in the windowController class MyController :


    private var myundoManager : NSUndoManager? = NSUndoManager()

    func windowWillReturnUndoManager(window: NSWindow) -> NSUndoManager? {
        return myundoManager
    }

    func add(v: MyController) { // controller, not its window. Is that correct ?
        myundoManager?.registerUndoWithTarget(self.window!, selector: #selector(remove(_:)), object: v)
    }

    func remove(v: MyController) {
        myundoManager?.prepareWithInvocationTarget(self.window!).add(v) 
    }

and in the func in MyController to be undoable :


        myundoManager?.registerUndoWithTarget(self.window!, selector: #selector(remove(_:)), object: self)
        myundoManager?.setActionName("color it")


in windowDidLoad :

        self.window?.becomeFirstResponder()


But the undo menu remains unchanged.

I tested

myundoManager!.undoActionName

It is correct, as well as myundoManager!.undoMenuItemTitle

SO THE QUESTION: how to connect myundoManager!.undoMenuItemTitle to the main menu bar undo item ? Should I just declare an IBOutlet ?

That's what I did, it works, but that seems bizarre to me. In such a case, what is myundoManager!.undoMenuItemTitle used for ?


I wonder if my registration is correct.

myundoManager?.registerUndoWithTarget(self.window!, selector: #selector(remove(_:)), object: self)

self is MyController ; I tried with

myundoManager?.registerUndoWithTarget(self, selector: #selector(remove(_:)), object: self)

doesn't work either.


Should I declare the delegate of myundoManager ?

I don't know what a "general" undo manager is. What class was your original "undoManager" a property of? It probably doesn't matter, but I'm not sure there isn't something going on outside the code you've shown us.


Since your various action method reference "myundoManager" directly, it's not surprising that you can see evidence of undo actions in that object. The issue is that the "myundoManager" object is apparently unknown to the responder mechanisms, which normally should be able to query the undo manager from the window without any additional code (apart from the "windowWillReturnUndoManager(_:)" delegate method).


The most likely explanation for this is that you haven't set the window controller as the delegate of the window. This does not happen by default in IB, you have to make the connection yourself. If that's not the problem, I'm not sure what's wrong, but you should at least verify that "windowWillReturnUndoManager(_:)" is being called, and is returning a non-nil value.

Thanks for help, I'm nearly there.


By general, I mean that myundoManager is for a class, not for the whole app. But I can probably make it a global var.


It now works OK, the menu is updated with:

        let appDelegate = NSApplication.sharedApplication().delegate as! AppDelegate
        appDelegate.appController.redoMenuItem.title = myundoManager!.redoMenuItemTitle
        appDelegate.appController.undoMenuItem.title = myundoManager!.undoMenuItemTitle


I have still one pending question : I want to test if there is a pending undo or redo, to disable undo menuItem,


I have 2 IBOutlets in AppController :

    @IBOutlet weak var undoMenuItem                     : NSMenuItem!     
    @IBOutlet weak var redoMenuItem                     : NSMenuItem!


To disable these items, I use:


        if myundoManager!.canUndo {
            appDelegate.appController.undoMenuItem.title = myundoManager!.undoMenuItemTitle
        } else {
            appDelegate.appController.undoMenuItem.title = "Undo"
        }
        appDelegate.appController.undoMenuItem.enabled = myundoManager!.canUndo


Just after instanciation of myundoManeger, canUndo is effectively false

myundoManager!.canUndo is true after the action // normal


But after executing the undo through the menu, it remains true, even if there is nothing more to undo. In fact, if I call the undo menu item, nothing occurs

I thought the undo / redo counter was set automatically, Is there something to do to enable it ?

It is generally incorrect to set the undo/redo menu item titles directly. That's because the "Undo" or "Redo" text is typically localized by the system, and you should not interfere with that. Setting the action names should be enough for the menu items to show "Undo <action-name>" or "Redo <action-name>" as appropriate.


I don't remember, in the case of a non-document-based app, whether you're required to handling the disabling of the menu items yourself. Possibly this isn't automatic, in which case you have to intervene at appropriate times. (Monitoring one or more of the NSUndoManager notifications might be a good start.) However, you have to be careful because your custom undo manager doesn't necessarily "own" the menu items all the time. For example, if the first responder is a text field, there is a separate undo manager maintained by the text field that allows typing to be undone, and you don't want to mess with this. A very delicate hand is needed for manual behavior customizations.

I am not very comfortable either with all thoise manipulations.


I tried to set the title with a mere :


myundoManager?.setActionName("some text")


I hoped that would add the text to the menus, nothing happens.


I'll continue searching, thanks for your help.

Have you by any chance changed the action method selectors associated with the undo/redo menu items, or possibly implemented action methods of the standard names? I can't think of another reason why it should ignore your action names, unless for some reason it's using an undo manager other than the one you created.

Actions are still undo: and redo:, and no other method uses these names.


And what is surprising is that it is partially working

- undo: / redo: do effectively work.

- the menu titles enabled status don't change automatically but can be changed manually

- canUndo and canRedo properties are not set, but isRedoing or isUndoing do work


Could the problem be with firstResponder ?

Should I set it to the controller and not to its window. Or to something else ?


I wonder if I need to set the undoManager, and to whom, as described in this very old post


http : / / www.cocoabuilder.com/archive/cocoa/109927-nsundomanager-in-non-nsdocument-cocoa-applications-unable-to-change-undo-menu-item-name.html

I just tried it in a new project, and everything works fine for me. I added a menu item wired up to FirstResponder.doFromMenu, and a button in the window wired up to FirstResponder.doFromButton.


Here's the code for the window controller:


class WindowController: NSWindowController, NSWindowDelegate
{
  static let windowUndoManager = UndoManager ()

  func windowWillReturnUndoManager(_ window: NSWindow) -> UndoManager?
  {
       return WindowController.windowUndoManager
  }

  @IBAction func doFromMenu (_ sender: Any)
  {
       WindowController.windowUndoManager.registerUndo (withTarget: self, selector: #selector (menuDo), object: nil)
       WindowController.windowUndoManager.setActionName ("Do From Menu")
  }

  func menuDo () { }

}


and the view controller:


class ViewController: NSViewController
{
  @IBAction func doFromButton (_ sender: Any)
  {
       WindowController.windowUndoManager.registerUndo (withTarget: self, selector: #selector (buttonDo), object: nil)
       WindowController.windowUndoManager.setActionName ("Do From Button")
  }

  func buttonDo () { }

}


There is literally no other code. The various methods are called at the expected times, and the Undo/Redo menus contain the correct text (with the action names when appropriate), and are disabled when appropriate.

Hi Quincey,

I understand that menu of view are two different tests cases.

I try to repeat exactly this with the first case, but cannot get it work. I must miss something. Sorry for being a bit slow on this.


This is what I do:

I create the project.


I create a class WindowController : NSWindowController, no xib, with the code you provided (I adatpted to Swift 2.2, but it's the same)


I add a menu Item testMenu in Edit menu of menu bar

In mainMenu.xib, I control drag from testMenu to FirstResponder and select doFromMenu:

question: how should I do this programmatically, not in IB ?


In IB IBAction doFromMenu is not connected to anything (dot on left remains white); is it normal ?


I run : but testMenu remains disabled, so I cannot test.


Should I declare a windowController instance in AppController ?

Accepted Answer

Did you change the class of the window controller in the storyboard from NSWindowController to WindowController? Or are you creating the UI programmatically?


Also, make sure you declare WindowController as conforming to NSWindowDelegate.

I do not use a storyboard. So, which WindowController ? In fact, I had no windowController in this test case, just the window created in Main.xib whose file owner is NSApplication.


Yse, WindowController conforms to NSWindowDelegate.


So, I create a Window.xib, makes its owner WindowController. But problem remains : Test undo menu remains disabled because .


OK. I TRIED WITH STORYBOARD.


I now get the menu enabled.


But undo and redo menu do not change. In fact, they also remain disabled (unless I uncheck Auto Enables Items ifor Edit menu). However, IBAction is called, I see the trace on the console.

Here is the code adapted for Swift2.2

    @IBAction func doFromMyMenu (sender: AnyObject)
    {
        WindowController.windowUndoManager.registerUndoWithTarget (self, selector: #selector (menuDo), object: nil)
        WindowController.windowUndoManager.setActionName ("Do From Menu")
        Swift.print("Do From Menu")
    }


BUT I remember now : In my app, I have translated the titles of undo and redo: seems unlikely it causes the problem ? Their FirstResponder is still undo: and redo:

IT WORKS in my app.


I had to autoenable the Edit menu !!!!!

Setting up undoManager
 
 
Q