Mac Native Main Menu keyboard handling


#1

I've just been looking into the keyboard handling for main menus. Using the standard juce menus - when navigating to a menu item and invoking a command using the keyboard - as in using arrow keys to move around and then hit enter to invoke - the command does get triggered as expected. On Mac - when using native menu bars -  this does not seem to be the case. Instead it propagates the key press through to the currently focused window which can trigger some unexpected results!

I can reproduce the odd behaviour in the “Tabs and Widgets” section of the Juce demo app. The standard Juce menus perform as expected, hitting enter always triggers the correct command. But if you then select “Use native menu bar” from the Look and Feel menu and then hit enter on any of the native menu bar items it just triggers the “Show Popup Menu” button.

I’m don’t think this should be the default behaviour for the native menu bar - but the code doing this in the juce_mac_MainMenu.mm file looks like it must be there for a reason…

static void menuItemInvoked (id self, SEL, NSMenuItem* item)
{
  JuceMainMenuHandler* const owner = getIvar<JuceMainMenuHandler*> (self, "owner");

  if ([[item representedObject] isKindOfClass: [NSArray class]])
  {
    // If the menu is being triggered by a keypress, the OS will have picked it up before we had a chance to offer it to
    // our own components, which may have wanted to intercept it. So, rather than dispatching directly, we'll feed it back
    // into the focused component and let it trigger the menu item indirectly.
    NSEvent* e = [NSApp currentEvent];
    if ([e type] == NSKeyDown || [e type] == NSKeyUp)
    {
      if (juce::Component* focused = juce::Component::getCurrentlyFocusedComponent())
      {
        if (juce::NSViewComponentPeer* peer = dynamic_cast <juce::NSViewComponentPeer*> (focused->getPeer()))
        {
          if ([e type] == NSKeyDown)
            peer->redirectKeyDown (e);
          else
            peer->redirectKeyUp (e);
          return;
        }
      }
    }

    NSArray* info = (NSArray*) [item representedObject];
    owner->invoke ((int) [item tag],
                   (ApplicationCommandManager*) (pointer_sized_int)
                     [((NSNumber*) [info objectAtIndex: 0]) unsignedLongLongValue],
                   (int) [((NSNumber*) [info objectAtIndex: 1]) intValue]);
  }
}

Am I missing something? Do I have to trigger the command elsewhere? I would think that if a menu is currently open, key presses should not be passed to other components...

Thanks in advance for any pointers!


#2

Bump!


#3

This is on the tip of the master branch.


#4

Hi rewolf,

Thank you for reporting this. I've looking into this and it seems as if  JUCE is ignoring if the key stroke was actually consumed by the focused component (the return value of redirectKeyDown in the code that you posted above is note being observed). The following change fixes it for me if no component consumes the event:

static void menuItemInvoked (id self, SEL, NSMenuItem* item)
{
    JuceMainMenuHandler* const owner = getIvar<JuceMainMenuHandler*> (self, "owner");
    if ([[item representedObject] isKindOfClass: [NSArray class]])
    {
        // If the menu is being triggered by a keypress, the OS will have picked it up before we had a chance to offer it to
        // our own components, which may have wanted to intercept it. So, rather than dispatching directly, we'll feed it back
        // into the focused component and let it trigger the menu item indirectly.
        NSEvent* e = [NSApp currentEvent];
        if ([e type] == NSKeyDown || [e type] == NSKeyUp)
        {
            if (juce::Component* focused = juce::Component::getCurrentlyFocusedComponent())
            {
                if (juce::NSViewComponentPeer* peer = dynamic_cast <juce::NSViewComponentPeer*> (focused->getPeer()))
                {
                    bool used = false;
                    if ([e type] == NSKeyDown)
                        used = peer->redirectKeyDown (e);
                    else
                        used = peer->redirectKeyUp (e);
                            
                    if (used)
                        return;
                }
            }
        }
        NSArray* info = (NSArray*) [item representedObject];
        owner->invoke ((int) [item tag],
                       (ApplicationCommandManager*) (pointer_sized_int)
                       [((NSNumber*) [info objectAtIndex: 0]) unsignedLongLongValue],
                       (int) [((NSNumber*) [info objectAtIndex: 1]) intValue]);
    }
}

however, I am a bit confused by Jules comment which seems to indicate that the focused component should indirectly invoke the menu bar item. I'll get back to you once I've confirmed what the correct method is to solve this.


#5

Hi Fabian,

Thanks for looking at this. I agree, those changes seem logical - it's pretty much what I had done here to work around the problem. It is the comment that through me the most...

Thanks,

Steve


#6

Hi Fabian, Jules, 

 

Are there any plans to fix this?

 

Thanks,

Steve