我正在编辑这个问题,因为我认为我可能过度简化了状态项菜单打开的方式。对于这样一个简单的函数来说,它复杂得可笑!
我的状态项支持左键和右键操作。用户可以更改每次点击类型所发生的事情。此外,由于macOS的一个bug,当有2个或更多的屏幕/显示器连接并且它们垂直排列时,我必须做一些额外的特殊工作。
我正在使用MASShortcut通过系统范围的热键(比方说)打开NSStatusItem的菜单,我发现一旦打开菜单,就不可能用热键关闭它。我试图切换菜单从关闭到打开,反之亦然。但是,当菜单打开时,代码执行将被阻止。有什么办法可以解决这个问题吗?我发现了这个问题,似乎是一个类似的问题,但遗憾的是没有找到答案。
提前感谢您的帮助!
更新:示例项目演示问题
当用户按下指定的热键显示状态项菜单时,运行如下代码:
[[MASShortcutBinder sharedBinder] bindShortcutWithDefaultsKey: kShowMenuHotkey toAction: ^
{
if (!self.statusMenuOpen)
{
[self performSelector:@selector(showStatusMenu:) withObject:self afterDelay:0.01];
}
else
{
[self.statusMenu cancelTracking];
}
}];
下面是其他相关代码:
- (void) applicationDidFinishLaunching: (NSNotification *) aNotification
{
// CREATE AND CONFIGURE THE STATUS ITEM
self.statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength: NSVariableStatusItemLength];
[self.statusItem.button sendActionOn:(NSLeftMouseUpMask|NSRightMouseUpMask)];
[self.statusItem.button setAction: @selector(statusItemClicked:)];
self.statusMenu.delegate = self;
}
- (IBAction) statusItemClicked: (id) sender
{
// Logic exists here to determine if the status item click was a left or right click
// and whether the menu should show based on user prefs and click type
if (menuShouldShow)
{
[self performSelector:@selector(showStatusMenu:) withObject:self afterDelay:0.01];
}
}
- (IBAction) showStatusMenu: (id) sender
{
// macOS 10.15 introduced an issue with some status item menus not appearing
// properly when two or more screens/displays are arranged vertically
// Logic exists here to determine if this issue is present on the current system
if (@available(*, macOS 10.15))
{
if (verticalScreensIssuePresent)
{
[self performSelector:@selector(popUpStatusItemMenu) withObject:nil afterDelay:0.05];
}
else // vertical screens issues not present
{
// DISPLAY THE MENU NORMALLY
self.statusItem.menu = self.statusMenu;
[self.statusItem.button performClick:nil];
}
}
else // not macOS 10.15+
{
// DISPLAY THE MENU NORMALLY
self.statusItem.menu = self.statusMenu;
[self.statusItem.button performClick:nil];
}
}
- (void) popUpStatusItemMenu
{
// Logic exists here to determine how wide the menu is
// If the menu is too wide to fit on the right, display
// it on the left side of the status item
// menu is too wide for screen, need to open left side
if (pt.x + menuWidth >= NSMaxX(currentScreen.frame))
{
[self.statusMenu popUpMenuPositioningItem:[self.statusMenu itemAtIndex:0]
atLocation:CGPointMake((-menuWidth + self.statusItem.button.superview.frame.size.width), -5)
inView:[self.statusItem.button superview]];
}
else // not too wide
{
[self.statusMenu popUpMenuPositioningItem:[self.statusMenu itemAtIndex:0]
atLocation:CGPointMake(0, -5)
inView:[self.statusItem.button superview]];
}
}
我最终通过编程分配NSMenuItem的keyeequivalent来解决这个问题,使其与MASShortcut热键值相同。这允许用户使用相同的热键来执行不同的功能(关闭NSMenu)
设置热键时:
-(void) setupOpenCloseMenuHotKey
{
[[MASShortcutBinder sharedBinder] bindShortcutWithDefaultsKey: kShowMenuHotkey toAction: ^
{
// UNHIDES THE NEW "CLOSE MENU" MENU ITEM
self.closeMenuItem.hidden = NO;
// SET THE NEW "CLOSE MENU" MENU ITEM'S KEY EQUIVALENT TO BE THE SAME
// AS THE MASSHORTCUT VALUE
[self.closeMenuItem setKeyEquivalentModifierMask: self.showMenu.shortcutValue.modifierFlags];
[self.closeMenuItem setKeyEquivalent:self.showMenu.shortcutValue.keyCodeString];
self.showMenuTemp = [self.showMenu.shortcutValue copy];
self.showMenu.shortcutValue = nil;
dispatch_async(dispatch_get_main_queue(), ^{
[self performSelector:@selector(showStatusMenu:) withObject:self afterDelay:0.01];
});
}];
}
然后,当菜单关闭时:
- (void) menuDidClose : (NSMenu *) aMenu
{
// HIDE THE MENU ITEM FOR HOTKEY CLOSE MENU
self.closeMenuItem.hidden = YES;
self.showMenu.shortcutValue = [self.showMenuTemp copy];
self.showMenuTemp = nil;
[self setupOpenCloseMenuHotKey];
}
我可以证实你的观察
我试图切换菜单从关闭到打开,反之亦然。当菜单打开时,代码执行被阻塞
原因是NSMenu
打开时接管了应用程序的NSEvent
处理(它的内部__NSHLTBMenuEventProc
处理),而不是标准的[NSApplication run]
队列。
实际触发快捷方式处理的事件最终是NSEventTypeSystemDefined
与子类型6(9是下面的keyUp,在这里并不真正相关)。
当菜单打开时,这些NSEventTypeSystemDefined
根本不会被触发。一些机制延迟了它们的触发,直到菜单被取消,应用程序回到[NSApplication run]
队列。A尝试了许多技巧和技巧来绕过它,但无济于事。
MASShortcut使用旧的Carbon API来安装这个自定义事件处理程序。我能够将其插入到NSMenu内部事件调度程序(它在菜单未打开时工作),但它并不能解决问题,因为前面提到的NSEvent
s没有首先被触发(直到菜单关闭)。
我有根据的猜测是,这是MacOS的WindowServer,这是管理(因为它知道的东西,如控制键按在其他)。
不管怎样,我很高兴你找到了解决办法。
如果有人想调试这些事件(我想这是我能提供的最好的起点),这里是我使用的代码:
int main(int argc, const char * argv[]) {
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
}
Class clazz = NSApplication.class;
SEL selectorNextEventMatchingEventMask = NSSelectorFromString(@"_nextEventMatchingEventMask:untilDate:inMode:dequeue:");
Method method = class_getInstanceMethod(clazz, selectorNextEventMatchingEventMask);
const char *typesSelectorNextEventMatchingMask = method_getTypeEncoding(method);
IMP genuineSelectorNextEventMatchingMask = method_getImplementation(method);
IMP test = class_replaceMethod(clazz, selectorNextEventMatchingEventMask, imp_implementationWithBlock(^(__unsafe_unretained NSApplication* self, NSEventMask mask, NSDate* expiration, NSRunLoopMode mode, BOOL deqFlag) {
NSEvent* (*genuineSelectorNextEventMatchingMaskTyped)(id, SEL, NSEventMask, NSDate*, NSRunLoopMode, BOOL) = (void *)genuineSelectorNextEventMatchingMask;
NSEvent* event = genuineSelectorNextEventMatchingMaskTyped(self, selectorNextEventMatchingEventMask, mask, expiration, mode, deqFlag);
if (event.type == NSEventTypeSystemDefined) {
if (event.subtype == 6l) {
NSLog(@"⚪️ %@ %i %@", event, mask, mode);
}
else if (event.subtype == 9l) {
NSLog(@"⚪️⚪️ %@ %i %@", event, mask, mode);
}
else if (event.subtype == 7l) {
NSLog(@"🔵 UNKNOWN %@ %i %@", event, mask, mode);
}
else {
NSLog(@"🧩 %@ %i %@", event, mask, mode);
}
} else if (event == NULL && [mode isEqualToString:NSEventTrackingRunLoopMode]) {
//NSMenu "null" events happening here
NSLog(@"⚪️⚪️⚪️ %@ %i %@", event, mask, mode);
} else if (event == NULL) {
NSLog(@"⭐️ %@ %i %@", event, mask, mode);
} else {
NSLog(@"🎾 %@ %i %@", event, mask, mode);
}
return event;
}), typesSelectorNextEventMatchingMask);
return NSApplicationMain(argc, argv);
}
可以注意到NSMenu触发事件将在NSEventTrackingRunLoopMode
中运行,但这对于解决任何问题都不是特别有用。