使用热键切换NSStatusItem的菜单打开/关闭 - 代码执行排队/阻止



我正在编辑这个问题,因为我认为我可能过度简化了状态项菜单打开的方式。对于这样一个简单的函数来说,它复杂得可笑!

我的状态项支持左键和右键操作。用户可以更改每次点击类型所发生的事情。此外,由于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内部事件调度程序(它在菜单未打开时工作),但它并不能解决问题,因为前面提到的NSEvents没有首先被触发(直到菜单关闭)。

我有根据的猜测是,这是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中运行,但这对于解决任何问题都不是特别有用。

最新更新