Search code examples
objective-ccocoansmenunsstatusitem

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


I'm editing this question because I think I may have oversimplified the way in which my status item's menu opens. It's ridiculously complicated for such a simple function!

My status item supports both left and right click actions. The user can change what happens which each click type. Also, due to a macOS bug, I have to do some extra-special work when there are 2 or more screens/displays connected and they are arranged vertically.

I’m using 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 which seems like a similar issue, but sadly no answer was ever found.

Thanks in advance for any assistance!

UPDATE: Sample Project Demonstrating Issue


When the user executes the designated hot key to show the status item menu, the following runs:

[[MASShortcutBinder sharedBinder] bindShortcutWithDefaultsKey: kShowMenuHotkey toAction: ^
     {
         if (!self.statusMenuOpen)
         {
             [self performSelector:@selector(showStatusMenu:) withObject:self afterDelay:0.01];
         }
         else
         {
             [self.statusMenu cancelTracking];
         }
     }];

And here's the other relevant code:

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

    }
}

Solution

  • I ended up solving this issue by programmatically assigning an NSMenuItem's keyEquivalent to be the same hot key as the MASShortcut hot key value. This allows the user to use the same hot key to perform a different function (close the NSMenu.)

    When setting up the hot key:

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

    Then, when the menu closes:

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