Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift: NSStatusItem menu behaviour in 10.10 (e.g. show only on right mouse click)

I am writing a simple status bar app in Swift, and attempting to use the new NSStatusItem API introduced in OS X 10.10.

The interface I'm aiming for is a simple left mouse click on the statusItem to toggle a core feature on and off, with a right mouse click (or control-click) option to show a settings menu. I have no need for a custom view or popover for this functionality.

By default, if a NSMenu is assigned to a NSStatusItem, it will display the menu on both a left and right click. I want to change the behaviour to only show the menu on a right click, or as a workaround, prevent the menu popping up on a left click

Previously, it seems that to get control of mouse events on a NSStatusItem, one had to set a custom view with overridden mouse events (see this related question).

In the new NSStatusItem API introduced in 10.10, methods for setting a custom view have been deprecated, and it looks like this behaviour is discouraged. According to @Taylor in this answer a few of the deprecated behaviours should be used through the NSStatusBarButton object returned by statusItemObject.button() but as of writing there is no documentation for NSStatusBarButton, and the object returned is read-only and cannot be replaced with a custom button with overridden mouse event handlers.

Is there a way to bring some level of control to whether an NSMenu attached to an NSStatusItem (or an NSStatusBarButton) is displayed with regard to mouse events?

like image 523
Kieran Avatar asked Nov 14 '14 06:11

Kieran


1 Answers

Here's the solution I came up with. It works fairly well, though there's one thing I'm not happy with: the status item stays highlighted after you choose an option in the right-click menu. The highlight goes away as soon as you interact with something else.

Also note that popUpStatusItemMenu: is "softly deprecated" as of OS X 10.10 (Yosemite), and will be formally deprecated in a future release. For now, it works and won't give you any warnings. Hopefully we'll have a fully supported way to do this before it's formally deprecated—I'd recommend filing a bug report if you agree.

First you'll need a few properties and an enum:

typedef NS_ENUM(NSUInteger,JUNStatusItemActionType) {
    JUNStatusItemActionNone,
    JUNStatusItemActionPrimary,
    JUNStatusItemActionSecondary
};

@property (nonatomic, strong) NSStatusItem *statusItem;
@property (nonatomic, strong) NSMenu *statusItemMenu;
@property (nonatomic) JUNStatusItemActionType statusItemAction;

Then at some point you'll want to set up the status item:

NSStatusItem *item = [[NSStatusBar systemStatusBar] statusItemWithLength:29.0];
NSStatusBarButton *button = item.button;
button.image = [NSImage imageNamed:@"Menu-Icon"];
button.target = self;
button.action = @selector(handleStatusItemAction:);
[button sendActionOn:(NSLeftMouseDownMask|NSRightMouseDownMask|NSLeftMouseUpMask|NSRightMouseUpMask)];
self.statusItem = item;

Then you just need to handle the actions sent by the status item button:

- (void)handleStatusItemAction:(id)sender {

    const NSUInteger buttonMask = [NSEvent pressedMouseButtons];
    BOOL primaryDown = ((buttonMask & (1 << 0)) != 0);
    BOOL secondaryDown = ((buttonMask & (1 << 1)) != 0);
    // Treat a control-click as a secondary click
    if (primaryDown && ([NSEvent modifierFlags] & NSControlKeyMask)) {
        primaryDown = NO;
        secondaryDown = YES;
    }

    if (primaryDown) {
        self.statusItemAction = JUNStatusItemActionPrimary;
    } else if (secondaryDown) {
        self.statusItemAction = JUNStatusItemActionSecondary;
        if (self.statusItemMenu == nil) {
            NSMenu *menu = [[NSMenu alloc] initWithTitle:@""];
            [menu addItemWithTitle:NSLocalizedString(@"Quit",nil) action:@selector(terminate:) keyEquivalent:@""];
            self.statusItemMenu = menu;
        }
        [self.statusItem popUpStatusItemMenu:self.statusItemMenu];
    } else {
        self.statusItemAction = JUNStatusItemActionNone;
        if (self.statusItemAction == JUNStatusItemActionPrimary) {
            // TODO: add whatever you like for the primary action here
        }
    }

}

So basically, handleStatusItemAction: is called on mouse down and mouse up for both mouse buttons. When a button is down, it keeps track of whether it should do the primary or secondary action. If it's a secondary action, that's handled immediately, since menus normally appear on mouse down. If it's a primary action, that's handled on mouse up.

like image 96
robotspacer Avatar answered Oct 19 '22 23:10

robotspacer