3

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!

Noah Nuebling
  • 219
  • 2
  • 11

2 Answers2

2

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.

rob mayoff
  • 375,296
  • 67
  • 796
  • 848
  • Hey Rob, thanks for your answer! This unfortunately also doesn't seem to work while the `NSMenu` is open. This is perfectly usable but it's not quite as nice as the 'Library' option in the Finder's 'Go' Menu which I'm trying to emulate. That one also reacts to user input while the `NSMenu` is open. – Noah Nuebling Mar 18 '21 at 20:19
2

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.

Noah Nuebling
  • 219
  • 2
  • 11
  • 1
    Clever! I was going to test using a hidden placeholder item instead of a zero-height item but didn't get around to it. Glad you figured something out. – rob mayoff Mar 19 '21 at 04:01
  • 1
    Actually this solution has one issue: when using arrow keys to navigate the menu, you can select the zero-height item. – Noah Nuebling Apr 11 '21 at 22:07
  • 1
    Maybe try setting the zero-height item's `enabled` to `NO`. – rob mayoff Apr 12 '21 at 02:33
  • Thanks for your input, that's a good idea! But it doesn't work in my case unfortunately. All menu items seem to get re-enabled when I open the menu. Not sure if this is a quirk of NSMenu or if it's something in my code doing that. – Noah Nuebling Apr 12 '21 at 13:38
  • Nevermind. After setting `autoenablesItems` to `NO` on the containing `NSPopUpButton`, this works great! Thanks again. – Noah Nuebling Apr 12 '21 at 14:04