I have the following code: (can be copy-pasted to New macOS project)
import Cocoa
import SwiftUI
@NSApplicationMain
class AppDelegate: NSObject, NSAppl
I'm using this code on Catalina. Rather than performClick
like some of the other answers suggest, I had to manually position the popup. This works for me with external monitors.
func applicationDidFinishLaunching(_ aNotification: Notification) {
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
statusItem.button?.action = #selector(onClick)
statusItem.button?.sendAction(on: [.leftMouseUp, .rightMouseUp])
menu = NSMenu()
menu.addItem(NSMenuItem(title: "Quit", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q"))
menu.delegate = self
}
@objc func onClick(sender: NSStatusItem) {
let event = NSApp.currentEvent!
if event.type == NSEvent.EventType.rightMouseUp {
// right click, show quit menu
statusItem.menu = menu;
menu.popUp(positioning: nil,
at: NSPoint(x: 0, y: statusItem.statusBar!.thickness),
in: statusItem.button)
} else {
// main click
}
}
@objc func menuDidClose(_ menu: NSMenu) {
// remove menu when closed so we can override left click behavior
statusItem.menu = nil
}
I'm using this code on macOS Catalina. 10.15.2. ( Xcode 11.3).
On left click It trigger action.
On right click it show menu.
//HEADER FILE
#import <Foundation/Foundation.h>
#import <Cocoa/Cocoa.h>
NS_ASSUME_NONNULL_BEGIN
@protocol MOSMainStatusBarDelegate
- (void) menuBarControllerStatusChanged: (BOOL) active;
@end
@interface MOSMainStatusBar : NSObject
@property (strong) NSMenu *menu;
@property (strong, nonatomic) NSImage *image;
@property (unsafe_unretained, nonatomic) id<MOSMainStatusBarDelegate> delegate;
- (instancetype) initWithImage: (NSImage *) image menu: (NSMenu *) menu;
- (NSStatusBarButton *) statusItemView;
- (void) showStatusItem;
- (void) hideStatusItem;
@end
//IMPLEMANTION FILE.
#import "MOSMainStatusBar.h"
@interface MOSMainStatusBar ()
@property (strong, nonatomic) NSStatusItem *statusItem;
@end
@implementation MOSMainStatusBar
- (instancetype) initWithImage: (NSImage *) image menu: (NSMenu *) menu {
self = [super init];
if (self) {
self.image = image;
self.menu = menu;
}
return self;
}
- (void) setImage: (NSImage *) image {
_image = image;
self.statusItem.button.image = image;
}
- (NSStatusBarButton *) statusItemView {
return self.statusItem.button;
}
- (void) showStatusItem {
if (!self.statusItem) {
self.statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength];
[self initStatusItem10];
}
}
- (void) hideStatusItem {
if (self.statusItem) {
[[NSStatusBar systemStatusBar] removeStatusItem:self.statusItem];
self.statusItem = nil;
}
}
- (void) initStatusItem10 {
self.statusItem.button.image = self.image;
self.statusItem.button.imageScaling = NSImageScaleAxesIndependently;
self.statusItem.button.appearsDisabled = NO;
self.statusItem.button.target = self;
self.statusItem.button.action = @selector(leftClick10:);
__unsafe_unretained MOSMainStatusBar *weakSelf = self;
[NSEvent addLocalMonitorForEventsMatchingMask:
(NSEventMaskRightMouseDown | NSEventModifierFlagOption | NSEventMaskLeftMouseDown) handler:^(NSEvent *incomingEvent) {
if (incomingEvent.type == NSEventTypeLeftMouseDown) {
weakSelf.statusItem.menu = nil;
}
if (incomingEvent.type == NSEventTypeRightMouseDown || [incomingEvent modifierFlags] & NSEventModifierFlagOption) {
weakSelf.statusItem.menu = weakSelf.menu;
}
return incomingEvent;
}];
}
- (void)leftClick10:(id)sender {
[self.delegate menuBarControllerStatusChanged:YES];
}
Here is possible approach. There might be more accurate calculations for menu position, including taking into account possible differences of userInterfaceLayoutDirection
, but the idea remains the same - take possible events under manual control and make own decision about what to do on each event.
Important places commented in code. (Tested on Xcode 11.2, macOS 10.15)
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var statusBarItem: NSStatusItem!
func applicationDidFinishLaunching(_ aNotification: Notification) {
let statusBar = NSStatusBar.system
statusBarItem = statusBar.statusItem(
withLength: NSStatusItem.squareLength)
statusBarItem.button?.title = "
The usual way a to show a menu is to assign a menu to the status item, where it will be shown when the status item button is clicked. Since popUpMenu
is deprecated, another way is needed to show the menu under different conditions. If you want the right click to use an actual status item menu instead of just showing a contextual menu at the status item location, the status item menu property can be kept nil until you want to show it.
I've adapted your code to keep the statusBarItem
and statusBarMenu
references separate, only adding the menu to the status item in the clicked action method. In the action method, once the menu is added, a normal click is performed on the status button to drop the menu. Since the status item will then always show its menu when the button is clicked, an NSMenuDelegate method is added to set the menu property to nil
when the menu is closed, restoring the original operation:
import Cocoa
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
// keep status item and menu separate
var statusBarItem: NSStatusItem!
var statusBarMenu: NSMenu!
func applicationDidFinishLaunching(_ aNotification: Notification) {
let statusBar = NSStatusBar.system
statusBarItem = statusBar.statusItem(withLength: NSStatusItem.squareLength)
statusBarItem.button?.title = "