使用热键切换 NSStatusItem 的菜单 Open/Closed - 代码执行 Queued/Blocked

Toggle NSStatusItem's Menu Open/Closed Using Hot Key - Code Execution Queued/Blocked

我正在编辑这个问题,因为我认为我可能过于简化了状态项菜单的打开方式。这么简单的功能,复杂的离谱!

我的状态项支持左键和右键单击操作。用户可以更改每次点击类型发生的情况。此外,由于 a macOS bug,当有 2 个或更多 screens/displays 连接并且它们垂直排列时,我必须做一些额外的特殊工作。

我正在使用 MASShortcut to open an NSStatusItem's menu via a system-wide hot key ("⌘ ⌥ M", let's say), and I'm finding that once the menu has been opened, it's not possible to close it with a hot key. I'm trying to toggle the menu from closed to open and vice versa. When the menu is open, code execution is blocked, however. Are there any ways around this? I found this question 这似乎是一个类似的问题,但遗憾的是没有找到答案。

在此先感谢您的帮助!

更新:Sample Project Demonstrating Issue


当用户执行指定的热键显示状态项菜单时,运行如下:

[[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]];

    }
}

我可以证实你的观察

I'm trying to toggle the menu from closed to open and vice versa. When the menu is open, code execution is blocked

原因是 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 中运行,但这对解决任何问题都不是特别有用。

我最终通过以编程方式将 NSMenuItem 的 keyEquivalent 分配为与 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];
}