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

问题描述

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

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

我正在使用 MASShortcut 通过系统范围的热键(例如“⌘ ⌥ M”)打开 NsstatusItem 的菜单,我发现一旦打开菜单,它就不是可以用热键关闭它。我正在尝试将菜单关闭切换到打开,反之亦然。但是,当菜单打开时,代码执行会被阻止。有没有办法解决这个问题?我发现 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]];

    }
}

解决方法

我可以确认你的观察

我正在尝试将菜单从关闭切换到打开,反之亦然。 当菜单打开时,代码执行被阻止

原因是 NSMenu 当打开时通过标准 NSEvent 队列接管应用程序的 __NSHLTBMenuEventProc 处理(它是内部 [NSApplication run] 处理)。

最终将实际触发快捷方式处理的事件是 NSEventTypeSystemDefined 子类型 6(9 是以下 keyUp,这里并不真正相关)。

打开菜单时根本不会触发那些 NSEventTypeSystemDefined。某些机制正在推迟它们的触发,直到取消菜单并且应用返回到 [NSApplication run] 队列。 A 尝试了很多技巧和技巧来规避这一点,但都无济于事。

MASShortcut 使用旧版 Carbon API 来安装此自定义事件处理程序。我能够将它插入 NSMenu 内部事件调度程序(它在菜单未打开时工作)但它没有解决问题,因为前面提到的 NSEvent 没有首先被触发(直到菜单消失)。

我有根据的猜测是 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,mask,expiration,mode,deqFlag);
        
        if (event.type == NSEventTypeSystemDefined) {
            if (event.subtype == 6l) {
                NSLog(@"⚪️ %@ %i %@",event,mode);
            }
            else if (event.subtype == 9l) {
                NSLog(@"⚪️⚪️ %@ %i %@",mode);
            }
            else if (event.subtype == 7l) {

                NSLog(@"? UNKNOWN %@ %i %@",mode);
            }
            else {
                NSLog(@"? %@ %i %@",mode);
            }
            
        } else if (event == NULL && [mode isEqualToString:NSEventTrackingRunLoopMode]) {
            //NSMenu "null" events happening here
            NSLog(@"⚪️⚪️⚪️ %@ %i %@",mode);
        } else if (event == NULL) {
            NSLog(@"⭐️ %@ %i %@",mode);
        } else {
            NSLog(@"? %@ %i %@",mode);
        }
        
        return event;
        
    }),typesSelectorNextEventMatchingMask);
    
    return NSApplicationMain(argc,argv);
}

可以注意到 NSMenu 触发的事件将在 NSEventTrackingRunLoopMode 中运行,但这对于解决任何问题都不是特别有用。

,

我最终通过以编程方式将 NSMenuItem 的 keyEquivalent 分配为与 MASShortcut 热键值相同的热键来解决此问题。这允许用户使用相同的热键来执行不同的功能(关闭 NSMenu。)

设置热键时:

{this.state.Book.filter(Book => Book.id >= 8 && Book.id <= 12).map(Book =>
                                <Link to={Book.path} className="hpsurah">
                                    <p className="hpid">{Book.id}</p>
                                    <div>
                                        <h2 className="hpsurahsq">{Book.surah}</h2>
                                        <h1 className="hpsurahen">{Book.surahsq}</h1>
                                        <h3 className="hpnumber">{Book.verses}</h3>
                                    </div>
                                </Link>)}

然后,当菜单关闭时:

-(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];
        });
    }];
}

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...