Swift: Nsstatusitem Menu Behaviour in 10.10 (E.G. Show Only on Right Mouse Click)

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

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.

NSStatusItem right click menu

NSStatusItem popUpStatusItemMenu: did the trick. I am calling it from my right click action and passing in the menu I want to show and it's showing it! This is not what I would have expected this function to do, but it's working.

Here's the important parts of what my code looks like:

- (void)showMenu{
// check if we are showing the highlighted state of the custom status item view
if(self.statusItemView.clicked){
// show the right click menu
[self.statusItem popUpStatusItemMenu:self.rightClickMenu];
}
}

// menu delegate method to unhighlight the custom status bar item view
- (void)menuDidClose:(NSMenu *)menu{
[self.statusItemView setHighlightState:NO];
}

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification{
// setup custom view that implements mouseDown: and rightMouseDown:
self.statusItemView = [[ISStatusItemView alloc] init];
self.statusItemView.image = [NSImage imageNamed:@"menu.png"];
self.statusItemView.alternateImage = [NSImage imageNamed:@"menu_alt.png"];
self.statusItemView.target = self;
self.statusItemView.action = @selector(mainAction);
self.statusItemView.rightAction = @selector(showMenu);

// set menu delegate
[self.rightClickMenu setDelegate:self];

// use the custom view in the status bar item
self.statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength];
[self.statusItem setView:self.statusItemView];
}

Here is the implementation for the custom view:

@implementation ISStatusItemView

@synthesize image = _image;
@synthesize alternateImage = _alternateImage;
@synthesize clicked = _clicked;
@synthesize action = _action;
@synthesize rightAction = _rightAction;
@synthesize target = _target;

- (void)setHighlightState:(BOOL)state{
if(self.clicked != state){
self.clicked = state;
[self setNeedsDisplay:YES];
}
}

- (void)drawImage:(NSImage *)aImage centeredInRect:(NSRect)aRect{
NSRect imageRect = NSMakeRect((CGFloat)round(aRect.size.width*0.5f-aImage.size.width*0.5f),
(CGFloat)round(aRect.size.height*0.5f-aImage.size.height*0.5f),
aImage.size.width,
aImage.size.height);
[aImage drawInRect:imageRect fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0f];
}

- (void)drawRect:(NSRect)rect{
if(self.clicked){
[[NSColor selectedMenuItemColor] set];
NSRectFill(rect);
if(self.alternateImage){
[self drawImage:self.alternateImage centeredInRect:rect];
}else if(self.image){
[self drawImage:self.image centeredInRect:rect];
}
}else if(self.image){
[self drawImage:self.image centeredInRect:rect];
}
}

- (void)mouseDown:(NSEvent *)theEvent{
[super mouseDown:theEvent];
[self setHighlightState:!self.clicked];
if ([theEvent modifierFlags] & NSCommandKeyMask){
[self.target performSelectorOnMainThread:self.rightAction withObject:nil waitUntilDone:NO];
}else{
[self.target performSelectorOnMainThread:self.action withObject:nil waitUntilDone:NO];
}
}

- (void)rightMouseDown:(NSEvent *)theEvent{
[super rightMouseDown:theEvent];
[self setHighlightState:!self.clicked];
[self.target performSelectorOnMainThread:self.rightAction withObject:nil waitUntilDone:NO];
}

- (void)dealloc{
self.target = nil;
self.action = nil;
self.rightAction = nil;
[super dealloc];
}

@end

Show menu for NSStatusItem with view

So I found a way of accomplishing this. You can use NSStatusItem'spopUpStatusItemMenu(menu: NSMenu) method to show the menu in the views mouseDown(event: NSEvent) method.

However this does not take care of highlighting the NSStatusItem. The simplest way I could find to do that is to make the view conform to NSMenuDelegate and set it as the menu delegate. Then you can override the menuWillOpen(menu: NSMenu) and menuDidClose(menu: NSMenu) methods in the following manner:

func menuWillOpen(menu: NSMenu) {
drawHighlight(true)
}

func menuDidClose(menu: NSMenu) {
drawHighlight(false)
}

func drawHighlight(highlight:Bool) {
let image = NSImage(size: self.frame.size)
image.lockFocus()
statusItem.drawStatusBarBackgroundInRect(self.bounds, withHighlight: highlight)
image.unlockFocus()
self.layer?.contents = image
}

refresh NSMenuItem on click/open of NSStatusItem

Keep a reference to the created NSMenuItem in your app delegate and update its state (assuming you use the item only in a single menu).

class AppDelegate: NSApplicationDelegate {

var fooMenuItem: NSMenuItem?

}

func createStatusBarItem() {
...
let enableDisableMenuItem = NSMenuItem(title: "Enabled", action: #selector(toggleAdvancedMouseHandlingObjc), keyEquivalent: "e")
self.fooMenuItem = enableDisableMenuItem
...
}

@objc func toggleAdvancedMouseHandlingObjc() {
if sHandler.isAdvancedMouseHandlingEnabled() {
sHandler.disableAdvancedMouseHandling()
} else {
sHandler.enableAdvancedMouseHandling()
}

self.fooMenuItem.state = sHandler.isAdvancedMouseHandlingEnabled() ? NSControl.StateValue.on : NSControl.StateValue.off
}

NSMenu and NSStatusItem action wont work together

"If the status item has a menu set, the action is not sent to the target when the status item is clicked; instead, the click causes the menu to appear."- apple dev NSStatusItem.action

Menu Bar Extra not opening on left click

If you use a custom view you are responsible to handle all events, drawing and the highlighting.

In the init(frame:) method of the view pass the NSStatusBar instance. Assign the menu to the view rather than to statusItem.

At least you have to override mouseDown

override func mouseDown(with theEvent: NSEvent) {
statusItem.popUpMenu(menu!)
needsDisplay = true
}

NSStatusItem shows only up if it is defined outside of my method

Because if you only declare the object inside the method and don't keep a reference to it elsewhere it will be scoped to the method. When the method finishes execution your object will be released and go away.
If you want it to live as long as the app runs, you would want to assign it to a property of the app delegate or another object that is going to live as long as the app.



Related Topics



Leave a reply



Submit