仅在 NSStatusBarButton 右键单击​​时显示 NSMenu?

Show NSMenu only on NSStatusBarButton right click?

我有以下代码:(可以复制粘贴到新的 macOS 项目)

import Cocoa
import SwiftUI

@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 = ""

        // Setting action
        statusBarItem.button?.action = #selector(self.statusBarButtonClicked(sender:))
        statusBarItem.button?.sendAction(on: [.leftMouseUp])

        let statusBarMenu = NSMenu(title: "Status Bar Menu")
        statusBarMenu.addItem(
            withTitle: "Order an apple",
            action: #selector(AppDelegate.orderAnApple),
            keyEquivalent: "")

        statusBarMenu.addItem(
            withTitle: "Cancel apple order",
            action: #selector(AppDelegate.cancelAppleOrder),
            keyEquivalent: "")

        // Setting menu
        statusBarItem.menu = statusBarMenu
    }

    @objc func statusBarButtonClicked(sender: NSStatusBarButton) {
        let event = NSApp.currentEvent!

        if event.type ==  NSEvent.EventType.rightMouseUp {
            print("Right click!")
        } else {
            print("Left click!")
        }
    }

    @objc func orderAnApple() {
        print("Ordering a apple!")
    }


    @objc func cancelAppleOrder() {
        print("Canceling your order :(")
    }

}

实际行为:菜单在左键和右键单击时均打开,未触发 statusBarButtonClicked。
删除此行后:

statusBarItem.menu = statusBarMenu

左键单击时触发 statusBarButtonClicked,菜单未显示(如预期)

期望的行为:右键单击菜单打开,左键单击菜单不打开,操作被触发。
我如何实现它?

编辑

在@red_menace 评论的帮助下,我设法实现了期望的行为:

import Cocoa
import SwiftUI

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
    var statusBarItem: NSStatusItem!
    var menu: NSMenu!

    func applicationDidFinishLaunching(_ aNotification: Notification) {

        let statusBar = NSStatusBar.system
        statusBarItem = statusBar.statusItem(
            withLength: NSStatusItem.squareLength)
        statusBarItem.button?.title = ""

        // Setting action
        statusBarItem.button?.action = #selector(self.statusBarButtonClicked(sender:))
        statusBarItem.button?.sendAction(on: [.leftMouseUp, .rightMouseUp])

        let statusBarMenu = NSMenu(title: "Status Bar Menu")
        statusBarMenu.addItem(
            withTitle: "Order an apple",
            action: #selector(AppDelegate.orderAnApple),
            keyEquivalent: "")

        statusBarMenu.addItem(
            withTitle: "Cancel apple order",
            action: #selector(AppDelegate.cancelAppleOrder),
            keyEquivalent: "")

        // Setting menu
        menu = statusBarMenu
    }

    @objc func statusBarButtonClicked(sender: NSStatusBarButton) {
        let event = NSApp.currentEvent!

        if event.type ==  NSEvent.EventType.rightMouseUp {
            statusBarItem.popUpMenu(menu)
        } else {
            print("Left click!")
        }
    }

    @objc func orderAnApple() {
        print("Ordering a apple!")
    }


    @objc func cancelAppleOrder() {
        print("Canceling your order :(")
    }

}

但是 Xcode 说 openMenu func 在 10.14 中被弃用并告诉我 Use the menu property instead。有没有办法用新的 API 实现所需的行为?

这是可能的方法。可能会有更准确的菜单位置计算,包括考虑 userInterfaceLayoutDirection 的可能差异,但想法保持不变 - 将可能的事件置于手动控制之下,并自行决定对每个事件执行的操作。

代码中注释了重要的地方。 (在 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 = ""

        // Setting action
        statusBarItem.button?.action = #selector(self.statusBarButtonClicked(sender:))
        statusBarItem.button?.sendAction(on: [.leftMouseUp, .rightMouseUp]) // << send action in both cases

        let statusBarMenu = NSMenu(title: "Status Bar Menu")
        statusBarMenu.addItem(
            withTitle: "Order an apple",
            action: #selector(AppDelegate.orderAnApple),
            keyEquivalent: "")

        statusBarMenu.addItem(
            withTitle: "Cancel apple order",
            action: #selector(AppDelegate.cancelAppleOrder),
            keyEquivalent: "")

        // Setting menu
        statusBarItem.button?.menu = statusBarMenu // << store menu in button, not item
    }

    @objc func statusBarButtonClicked(sender: NSStatusBarButton) {
        let event = NSApp.currentEvent!

        if event.type ==  NSEvent.EventType.rightMouseUp {
            print("Right click!")
            if let button = statusBarItem.button { // << pop up menu programmatically
                button.menu?.popUp(positioning: nil, at: CGPoint(x: -1, y: button.bounds.maxY + 5), in: button)
            }
        } else {
            print("Left click!")
        }
    }

    @objc func orderAnApple() {
        print("Ordering a apple!")
    }


    @objc func cancelAppleOrder() {
        print("Canceling your order :(")
    }

}

我在 macOS Catalina 上使用此代码。 10.15.2。 ( Xcode 11.3).
左键单击它触发动作。
右键单击它显示菜单。

//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];
}

显示菜单的常用方法是将菜单分配给状态项,单击状态项按钮时将显示该菜单。由于 popUpMenu 已被弃用,因此需要另一种方式来在不同条件下显示菜单。如果您希望右键单击以使用实际的状态项菜单而不是仅在状态项位置显示上下文菜单,则状态项菜单 属性 可以保持为零,直到您想要显示它。

我已经调整了您的代码以将 statusBarItemstatusBarMenu 引用分开,仅将菜单添加到单击操作方法中的状态项。在 action 方法中,一旦添加了菜单,就会对状态按钮执行正常的单击以删除菜单。由于当点击按钮时状态项将始终显示其菜单,因此添加了一个 NSMenuDelegate 方法以在菜单被点击时将菜单 属性 设置为 nil关闭,恢复原操作:

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 = ""

        statusBarItem.button?.action = #selector(self.statusBarButtonClicked(sender:))
        statusBarItem.button?.sendAction(on: [.leftMouseUp, .rightMouseUp])

        statusBarMenu = NSMenu(title: "Status Bar Menu")
        statusBarMenu.delegate = self
        statusBarMenu.addItem(
            withTitle: "Order an apple",
            action: #selector(AppDelegate.orderAnApple),
            keyEquivalent: "")
        statusBarMenu.addItem(
            withTitle: "Cancel apple order",
            action: #selector(AppDelegate.cancelAppleOrder),
            keyEquivalent: "")
    }

    @objc func statusBarButtonClicked(sender: NSStatusBarButton) {
        let event = NSApp.currentEvent!
        if event.type ==  NSEvent.EventType.rightMouseUp {
            print("Right click!")
            statusBarItem.menu = statusBarMenu // add menu to button...
            statusBarItem.button?.performClick(nil) // ...and click
        } else {
            print("Left click!")
        }
    }

    @objc func menuDidClose(_ menu: NSMenu) {
        statusBarItem.menu = nil // remove menu so button works as before
    }

    @objc func orderAnApple() {
        print("Ordering a apple!")
    }

    @objc func cancelAppleOrder() {
        print("Canceling your order :(")
    }

}

我在 Catalina 上使用此代码。而不是 performClick 像其他一些答案所暗示的那样,我不得不手动定位弹出窗口。这适用于我的外接显示器。

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
    }

从这里已经给出的很好的答案中学习,我想出了一个替代方案。

这种方法的优点是您只需设置 NSMenu 一次,而不必再设置或删除它。

正在设置 NSStatusBarButton

let status = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
status.button!.image = NSImage(named: "status")
status.button!.target = self
status.button!.action = #selector(triggerStatus)
status.button!.menu = /* your NSMenu */
status.button!.sendAction(on: [.leftMouseUp, .rightMouseUp])

接收动作

@objc private func triggerStatus(_ button: NSStatusBarButton) {
    guard let event = NSApp.currentEvent else { return }
        
    switch event.type {
    case .rightMouseUp:
        NSMenu.popUpContextMenu(button.menu!, with: event, for: button)
    case .leftMouseUp:
            /* show your Popup or any other action */
    default:
        break
    }
}