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?

Foi útil?

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.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top