Key equivalent matching for non-US English keyboard layouts

Based on these Swift docs and this forum thread, keyboard shortcuts / key equivalents are expected to be declared based on a US English layout.

As an application developer, you may then rely on the auto localization provided by allowsAutomaticKeyEquivalentLocalization, (for menus) or localize your key equivalents manually (menus and other controls with key equivalents).

But how does AppKit handle non localized key equivalents when faced with a non-US English keyboard layout? In particular, with a Hebrew layout active, the C key unmodified produces "ב", but when modified with ⌘ produces "c".

Does AppKit compare the key equivalent ⌘c to the modified or un-modified char of the event, or both? Or is there more to this logic? E.g. does it also try to match the incoming event against a US-English layout, even if not active at the moment?

The use-case here is implementing performKeyEquivalent for a custom control, where the documentation says:

You should extract the characters for a key equivalent using the NSEvent method charactersIgnoringModifiers.

So would simply comparing the event modifiers to the key equvialent modifers, and the event charactersIgnoringModifiers to the key equivalent give similar behavior to AppKit's own logic, e.g. in [NSEvent _matchesKeyEquivalent:modifierMask:]?

Based on the observed behavior when pressing ⌘c with a Hebrew layout active, it does trigger an NSButton with a key equivalent of ⌘c, which doesn't seem to match the documented behavior of using the unmodified chars ("ב") as basis.

Post not yet marked as solved Up vote post of torarnv Down vote post of torarnv
926 views

Replies

On non-Latin keyboard layouts, the ⌘ layer switches the keyboard to a Latin/English-like layout. E.g. pressing ⌘ switches ב into C.

You can observe this behavior in Accessibility Keyboard.

This non-Latin layout ⌘ switch does all the work here. For non-Latin layouts, keyboard localization (menu-only) would be only used here for some special characters like ⌘[ or to switch arrow keys based on the UI directionality.

I encourage you to add input methods like Hebrew in System Settings › Keyboard and run tests within your app to make sure events are caught.

Also, based on the shortcut you want, catching via the key code instead of the key character may be best.

  • Please see additional replies below, thank you.

Add a Comment

Thank you for your reply. I've observed the switch to a Latin/English-like layout (using Accessibility Keyboard) when pressing ⌘ for the Hebrew layout.

The question is how an implementation of performKeyEquivalent would correctly account for and support this?

Based on the performKeyEquivalent documentation here, and the Handling Key Events documentation here, the implementation should check the unmodified characters of the incoming events:

You should extract the characters for a key equivalent using the NSEvent method charactersIgnoringModifiers.

The implementation should extract the characters for a key equivalent from the passed-in NSEvent object using the charactersIgnoringModifiers method and then examine them to determine if they are a key equivalent it recognizes.

Which naively would be something like:

- (BOOL)performKeyEquivalent:(NSEvent *)event
{
    if (event.modifierFlags & NSEventModifierFlagCommand) {
        if ([event.charactersIgnoringModifiers isEqualToString:@"c"])
            return YES;
    }
    return NO;
}

But this implementation would fail to catch the Latin layer of the Hebrew layout.

Assuming the documentation is incomplete, a more robust implementation would perhaps be something like:

- (BOOL)performKeyEquivalent:(NSEvent *)event
{
    if (event.modifierFlags & NSEventModifierFlagCommand) {
        if ([event.charactersIgnoringModifiers isEqualToString:@"c"])
            return YES;
        if ([event.characters isEqualToString:@"c"])
            return YES;
    }
    return NO;
}

But is that correct? Are there cases where comparing against both the modified and unmodified characters will fail or produce unexpected matches? Should the behavior be different based on properties of the keyboard layout?

[NSEvent _matchesKeyEquivalent:modifierMask:] seems to handle this correctly, but contains references to text input APIs like TISCopyCurrentKeyboardInputSource, TSMGetInputSourceProperty, and TSMGetDeadKeyState, which seems to indicate the naive approach above is not sufficient to match AppKit's own behavior.

The goal would be for the custom control to respond to performKeyEquivalent the same way an NSButton or NSMenuItem would in terms of which events are considered matches and which are not. In particular, for a generic control that allows user defined key equivalents, so without relying on specifics of any particular key equivalent.

  • Interestingly, [NSEvent _matchesKeyEquivalent:modifierMask:] returns YES for a real key event:

    NSEvent: type=KeyDown loc=(21.2695,-129.457) time=78998.2 flags=0x100108 win=0x13227f2c0 winNum=2566 ctxt=0x0 chars="c" unmodchars="ב" repeat=0 keyCode=8

    But NO for a synthesized event via [NSEvent keyEventWithType]

    NSEvent: type=KeyDown loc=(0,0) time=0.0 flags=0x100000 win=0x0 winNum=0 ctxt=0x0 chars="c" unmodchars="ב" repeat=0 keyCode=8
  • The flags, location, time, window, and winNumber might affect this, or perhaps [NSEvent _matchesKeyEquivalent:modifierMask:] also looks at state outside of the arguments?

  • The reason for the synthetic event not behaving the same as the real key event was that the synthetic event didn't have an eventRef, causing _matchesKeyEquivalent to bail out early. Calling syntheticEvent.eventRef (https://developer.apple.com/documentation/appkit/nsevent/1525143-eventref?language=objc) forces NSEvent to lazily create the eventRef.

Add a Comment

To test things further, I've created a custom keyboard layout (attached), with the following mappings:

+----------+--------------+--------------------+----------+
| Key code | Physical key | No modifiers layer |  ⌘ layer |
+----------+--------------+--------------------+----------+
|     7    |       x      |          e         |     c    |
+----------+--------------+--------------------+----------+
|     8    |       c      |          a         |     b    |
+----------+--------------+--------------------+----------+
|     9    |       v      |          c         |     d    |
+----------+--------------+--------------------+----------+

Testing with an NSButton with .keyEquivalent = @"c" and .keyEquivalentModifierMask = NSEventModifierFlagCommand, I'm observing:

  • Pressing the c key alone, or with ⌘, does not trigger the button, so the logic in NSButton does not seem to be based on the key code of the event (8)
  • Pressing the x key alone does not trigger the button, but pressing ⌘x does, so the logic does seems to match on NSEvent.characters
  • Pressing the v key alone does not trigger the button, nor does pressing ⌘v, so the logic does not to match on NSEvent.charactersIgnoringModifiers

The behavior observed for [NSEvent _matchesKeyEquivalent:modifierMask:] seems to be exactly the same.

This would indicate that a "correct and robust" implementation is:

- (BOOL)performKeyEquivalent:(NSEvent *)event
{
    if (event.modifierFlags & self.keyEquivalentModifierMask 
        && [event.characters isEqualToString:self.keyEquivalent])
        return YES;

    return NO;
}

It this the case? Are the more things to consider? Thanks!