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
Swift: Lazily Encapsulating Chains of Map, Filter, Flatmap
How Does Swift Memory Management Work
Search Multiple Words in One String in Swift
Swift and Objectmapper: Nsdate with Min Value
How to Return a Button from a Function in Swiftui
Ckcontainer.Discoverallidentities Always Fails
What Is the Markup Format for Documentation on the Parameters of a Block in Swift
Remove Programmatically Added Uiimageview
How to Import Modules Without an Xcode Project in Swift
How Does Let X Where X.Hassuffix("Pepper") Work
Swiftui: Using View Modifiers Between Different iOS Versions Without #Available
Com.Apple.Itunes Aedeterminepermissiontoautomatetarget Is Always Return -600
How to Use Key-Value Observing with Smart Keypaths in Swift 4
Xcode 8 Shell Script Invocation Error
Sending an Email from Your App with an Image Attached in Swift