Undo-menu action only reaches NSApplication when the action is not 'undo(_:)'

Question:

How can I make it so that the 'undo' menuItem actions reaches the NSApplication-subclass?


Context:

In my storyboard there is an NSMenuItem called 'Undo', it came with the template.

My app uses a subclass of NSApplication, and in this class I implemented

@objc func undo(_ sender: Any){}

I did not implement

override func validateMenuItem(_ menuItem: NSMenuItem) -> Bool


In the storyboard the menuItem's action is 'undo:', and the target is 'first responder'.


The problem I have is that the menuItem is never enabled, and when I implement 'validateMenuItem(_:)', it is called only for 'startDictation:' and 'orderFrontCharacterPalette:'.


However, when I change the method signature in my NSApplication subclass to:

@objc@IBActionfunc undo2(_ sender: Any)

and update the storyboard to use this action, this method DOES get called (note that it is called undo2)


My application is not document based, and the structure is:

WindowController -> Window -> ViewController -> TableView

Not sure it if this is relevant, but the viewController implements cut, copy, paste and delete, and as such has a validateMenuItem(_:) implementation.

Replies

You will have to review the concept of the "first responder" and how it works with the undo architecture. I have dived into this in the past, but it was so long ago that I can't give specific advice.

If this is a non-document based app, then I suspect you will have to implement the undo manager yourself. The undo manager comes for free with document based apps and mostly works out of the box.


https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/UndoArchitecture/UndoArchitecture.html#//apple_ref/doc/uid/10000010-SW1

My question pertains to the flow of action messages coming from NSMenuItem. Currently, in my 'Edit' menu, there are menuItems that send cut(_:), copy(_:), paste(_:), delete(_:) to the first responder, and these actions are 'caught' by the viewController.

In that same 'Edit' menu, there are 'Undo' and 'Redo'. Before discussing how to to use an NSUndoManager, I would like these actions to be 'caught' by my subclass of NSApplication. Weirdly enough, if the action is called something else then 'undo', like 'undo2', the actions DO make it to my NSApplication subclass. When the action is called 'undo', the action does NOT reach my NSApplication subclass.


It seems as though some object somewhere along the responder chain consumes this action. Although checking the responder chain, none of the classes (or superclasses thereof) implement 'undo(_:)'.

Yes, I am well aware. I studied in detail this documen, yet the undo/redo action messages don't seem to bubble up to NSApplication-subclass.

If I implement them in my viewController, and have the validateMenuItems-function returns 'true' for them, the ARE called in the viewController. Walking the nextresponder chain from their, it goes:

NSTableView -> NSClipView -> NSScrollView -> NSView -> NSView -> MyViewController -> MyNSWindow -> MyNSWindowController


See that there is no MYNSApplication at the end of the line?

No that documentation this more about event-management. You need to review this documentation about the Undo architecture. It's tricky. I think what you are doing with validateMenuItem is just hacking it and getting you part way there, but it is a dead end. You can't brute force it.

I tried this in a view controller:


        let target = NSApp.target(forAction: "undo:")


and examined the value of "target" in the debugger:


(lldb) po target
▿ Optional
  - some : NSWindow: 0x10070eef0


So the action is being sent to the window. That makes sense, because the window is responsible for checking whether there's an undo manager, and that's part of the menu item validation.