How to create an NSMenu containing an NSMenuItem which only appears while holding a keyboard modifier key?

486 Views Asked by At

I'd like to create an NSMenu containing an NSMenuItem which is hidden by default, and only appears while the user is holding a keyboard modifier key.

Basically, I'm looking for the same behaviour as the 'Library' option in the Finder's 'Go' Menu:

Without holding Option (⌥): enter image description here

While holding Option (⌥): enter image description here


I already tried installing a key listener using [NSEvent addGlobalMonitorForEventsMatchingMask: handler:] to hide and unhide the NSMenuItem programmatically by setting it's hidden property. This kind of worked, but the problem is that the hiding/unhiding wouldn't work while the NSMenu was open. Apparently an NSMenu completely takes over the event processing loop while it's open, preventing the key listener from working.
I could probably use a CGEventTap to still receive events while the NSMenu is open, but that seems like complete overkill.

Another thing I discovered which does a similar thing to what I want is the 'alternate' mechanism of NSMenu. But I could only get it to switch out NSMenuItems, not hide/unhide them.

Any help would be greatly appreciated. Thanks!

2

There are 2 best solutions below

5
Noah Nuebling On BEST ANSWER

I found a solution that behaves perfectly!

  1. On the NSMenuItem you want hidable, set the alternate property to YES, and set the keyEquivalentModifierMask property to the keyboard modifiers which you want to unhide the item.

  2. In your NSMenu, right before the NSMenuItem which you want to be hideable, insert another NSMenuItem that has height 0.

    In Objc, you can create an NSMenuItem with height 0 like this:

    NSMenuItem *i = [[NSMenuItem alloc] init];
    i.view = [[NSView alloc] initWithFrame:NSZeroRect];
    

The hideable NSMenuItem will now be 'alternate' to the zero-height NSMenuItem preceding it. The zero-height item will display by default, but while you hold the keyboard modifier(s) you specified, the zero-height item will be swapped out with the hideable item. Because the zero-height item is invisible, this has the effect of unhiding the hideable item.

1
rob mayoff On

Let's say your option-only menu item's action is (in Swift) performOptionOnlyMenuItem(_:) and its target is your AppDelegate.

  • The first thing you need to do is make sure AppDelegate conforms to the NSMenuItemValidation protocol.

  • The second thing you need to do is implement the validateMenuItem(_:) method, and have it check whether the menu item sends the performOptionOnlyMenuItem(_:) action. If so, set the item's isHidden property based on whether the option key is currently pressed.

If you don't need to validate any other menu items, the code can look like this:

extension AppDelegate: NSMenuItemValidation {
    func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
        switch menuItem.action {
        case #selector(performOptionOnlyMenuItem(_:)):
            let flags = NSApp.currentEvent?.modifierFlags ?? []
            menuItem.isHidden = !flags.contains(.option)
            return true
        default:
            return true
        }
    }
}

If the action is sent to some other target, you need to implement the validation (including the protocol conformance) on that target. Each menu item is validated only by the item's target.