NSMenu.popUp(positioning:at:in:) doesn't enable menu items when opened inside modal window

In my app I use NSMenu.popUp(positioning:at:in:) for displaying a menu in response to the user clicking a button.

But it seems that when the menu is opened inside a modal window, all the menu items are always disabled.

Using NSMenu.popUpContextMenu(_:with:for:) instead works. What's the reason and what's the difference between the two methods? According to the documentation, one is for opening "popup menus" and the other for opening "context menus", but I cannot see an explanation of the difference between the two.

@main
class AppDelegate: NSObject, NSApplicationDelegate {

    func applicationDidFinishLaunching(_ aNotification: Notification) {
        let window = NSWindow(contentViewController: ViewController())
        NSApp.runModal(for: window)
    }

}

class ViewController: NSViewController {

    override func loadView() {
        let button = NSButton(title: "Click", target: self, action: #selector(click(_:)))
        view = NSView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
        view.addSubview(button)
    }
    
    @objc func click(_ sender: Any?) {
        let menu = NSMenu(title: "")
        menu.addItem(withTitle: "asdf", action: #selector(asdf(_:)), keyEquivalent: "")
        menu.addItem(withTitle: "bla", action: nil, keyEquivalent: "")
        menu.items[0].target = self
        menu.items[1].target = self
//        NSMenu.popUpContextMenu(menu, with: NSApp.currentEvent!, for: view) // this works
        menu.popUp(positioning: nil, at: .zero, in: view) // this doesn't work
    }
    
    @IBAction func asdf(_ sender: Any) {
        print(0)
    }

}

Replies

When a menu is displayed, AppKit performs menu item validation, which allows the menu item target, or some other object in the responder chain, to determine if the item should be enabled.

However, if the app is in a modal state, AppKit skips over item validation and always disables the item. This is meant to provide automatic disabling of main menu items while a modal window is the main window.

That behavior is less useful for popup and context menus, though. So AppKit has a special case for context menus; if the view that presents the menu is contained in the modal window, then AppKit still allows the menu item to be validated. This special case does not apply when the menu is presented via popUpPositioningAt. So that explains the difference in observed behavior between the two methods of presenting a menu.

I agree that this is an obscure corner of AppKit, and difficult to understand and debug.

There is a workaround, though. If you implement the method -(BOOL)worksWhenModal to return YES on the target object for your menu item, AppKit will allow the menu item to be validated, and enabled if the target implements the item's action.

I think you may have reported this issue with Bug Reporter as FB9190141?

This problem has been previously noticed and reported, so I'm going to mark your feedback item as a duplicate of an older report. I'm also going to investigate whether we can fix this in AppKit.

However, if the app is in a modal state, AppKit skips over item validation and always disables the item.

Thanks for your feedback. Indeed, it seems that all menu items targeting a window or view controller are automatically disabled, while menu items targeting the app delegate can still be enabled, as well as the standard copy, paste, etc. I think the correct behaviour should be to still enable menu items targeting the modal window itself.

I think you may have reported this issue with Bug Reporter as FB9190141?

Yes, I opened FB9190141 2.5 years ago but got no response, which is why I decided to ask here. It would be great if you could investigate it.

Also to everyone else who might be tempted to use NSMenu.popUpContextMenu(_:with:for:): provide a made-up NSEvent and don't rely on NSApp.currentEvent being non-nil. When using VoiceOver for instance, it's nil.

If you implement the method -(BOOL)worksWhenModal to return YES on the target object for your menu item, AppKit will allow the menu item to be validated, and enabled if the target implements the item's action.

Still doesn't seem to work for the sample code I provided. I had to leave the menu item's target to nil to make it work, but then using a normal window was enough, without the need to implement worksWhenModal.

This is in fact the workaround I just found. If I leave the menu item's target to nil and make sure that the view of the view controller or a subview is first responder, e.g. with

    override func viewDidAppear() {
        view.window?.makeFirstResponder(view)
    }

then the menu item is enabled. In my case it doesn't work though, since the view controller is a child of another view controller and it's not guaranteed that the view of the view controller showing the menu (and implementing the menu item actions) is first responder (as the parent can have other views that can be first responders). As soon as I set the menu item's target to self, the menu items are disabled again.