Hide/Show menu item in application's main menu by pressing Option key
Pergunta
I want to add a menu item into the application's main menu that will be used quite rare. I want it to be hidden by default and show it only when user hold down Option key. How do i do this?
It seems that I should handle flagsChanged:
, but it is NSResponder
's method and NSMenu
does not inherits from NSResponder
? I tried it inside main window controller, and it works when I press Option key before I click on menu. The following use case doe not work: click on menu item (there is no item), press option key — my item should appear, release option key — item should disappear.
I've also tried NSEvent's addLocalMonitorForEventsMatchingMask:handler:
and addGlobalMonitorForEventsMatchingMask:handler:
for NSFlagsChangedMask
but when option key pressed while main menu is open neither local or global handlers are not fired.
How can I do this?
Solução
Add the following to applicationDidFinishLaunching.
// Dynamically update QCServer menu when option key is pressed
NSMenu *submenu = [[[NSApp mainMenu] itemWithTitle:@"QCServer"] submenu];
NSTimer *t = [NSTimer timerWithTimeInterval:0.1 target:self selector:@selector(updateMenu:) userInfo:submenu repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:t forMode:NSEventTrackingRunLoopMode];
then add
- (void)updateMenu:(NSTimer *)t {
static NSMenuItem *menuItem = nil;
static BOOL isShowing = YES;
// Get global modifier key flag, [[NSApp currentEvent] modifierFlags] doesn't update while menus are down
CGEventRef event = CGEventCreate (NULL);
CGEventFlags flags = CGEventGetFlags (event);
BOOL optionKeyIsPressed = (flags & kCGEventFlagMaskAlternate) == kCGEventFlagMaskAlternate;
CFRelease(event);
NSMenu *menu = [t userInfo];
if (!menuItem) {
// View Batch Jobs...
menuItem = [menu itemAtIndex:6];
[menuItem retain];
}
if (!isShowing && optionKeyIsPressed) {
[menu insertItem:menuItem atIndex:6];
[menuItem setEnabled:YES];
isShowing = YES;
} else if (isShowing && !optionKeyIsPressed) {
[menu removeItem:menuItem];
isShowing = NO;
}
NSLog(@"optionKeyIsPressed %d", optionKeyIsPressed);
}
The timer only fires while controls are being tracked so it's not too much of a performance hit.
Outras dicas
When constructing the menu include the optional item and mark it as hidden. Then set your class instance as the menu's delegate and add a run loop observer while the menu is open to control the optional item's hidden state.
@implementation AppController {
CFRunLoopObserverRef _menuObserver;
}
- (void)updateMenu {
BOOL hideOptionalMenuItems = ([NSEvent modifierFlags] & NSAlternateKeyMask) != NSAlternateKeyMask;
[self.optionalMenuItem setHidden:hideOptionalMenuItems];
}
- (void)menuWillOpen:(NSMenu *)menu {
[self updateMenu];
if (_menuObserver == NULL) {
_menuObserver = CFRunLoopObserverCreateWithHandler(NULL, kCFRunLoopBeforeWaiting, true, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
[self updateMenu];
});
CFRunLoopAddObserver(CFRunLoopGetCurrent(), _menuObserver, kCFRunLoopCommonModes);
}
}
- (void)menuDidClose:(NSMenu *)menu {
if (_menuObserver != NULL) {
CFRunLoopObserverInvalidate(_menuObserver);
CFRelease(_menuObserver);
_menuObserver = NULL;
}
}
The best way you can achieve this is by using two menu items, the first menu item uses a custom view of height 0, and is disabled, then right under it is an "alternate" item. (You will have to set this item's keyEquivalentModifierMask
to NSAlternateKeyMask
) With this arrangement, when you press the option key, NSMenu will automatically replace the zero-height menu item with the alternate item which will have the effect of making a menu item magically appear.
No need for timers, updates or flag change notifications.
This functionality is described in the documentation here: Managing Alternates
Since the NSMenuDelegate
method menuNeedsUpdate:
is called before display, it's possible to override it, check if [NSEvent modifierFlags]
has the alternate bit set, and use that to show/hide your secret menu items.
Here's an example, copied from Reveal Functionality with Key Modifiers, which covers exactly this topic:
#pragma NSMenu delegate methods
- (void) menuNeedsUpdate: (NSMenu *)menu
{
NSLog(@"menuNeedsUpdate: %@", menu);
NSUInteger flags = ([NSEvent modifierFlags] & NSDeviceIndependentModifierFlagsMask);
// We negate the value below because, if flags == NSAlternateKeyMask is TRUE, that
// means the user has the Option key held, and wants to see the secret menu, so we
// need shoudHideSecretMenu to be FALSE, so we just negate the value.
BOOL shoudHideSecretMenu = !(flags == NSAlternateKeyMask);
NSLog(@"Flags: 0x%lx (0x%x), shoudHideSecretMenu = %d", flags, NSAlternateKeyMask, shoudHideSecretMenu);
[secretMenuItem setHidden:shoudHideSecretMenu];
}
There's some complex answers here but it's actually very simple:
Create 2 menuitems. The first is the default with whatever keyEquivalent and title you want. The second is what will be shown when the modifier key is down - again with separate keyEquivalent and title. On the second menuitem, enable 'Alternate' and everything else will happen automatically.
The required modifier is detected by comparing the 2 keyEquivalent values.