Programming, automation, algorithms, macOS, and more.

Deciphering an NSEvent

Since I need to handle key equivalents myself, I need to mimic how the system does it.

After having spent some time investigating this, it seems to be impossible to do with the info provided by NSEvent, but I ended up creating a heuristic which works fairly well (but isn’t perfect).

First let me briefly describe the problem. If you look in e.g. the menu of Xcode, you’ll find that shift left is bound to ⌘[. NSEvent gives us 3 informations:

  1. The modifiers pressed.
  2. The string resulting from the key press.
  3. The string (characters) ignoring modifier keys.

So what should we look at? Using the US keymap this doesn’t matter, since the string with and without modifiers is [. And we should of course check the modifiers for the ⌘ modifier key.

But if we’re using the Danish keymap (or one of the others where [ is not a primary key) the picture is a little different. The [ character is obtained by pressing ⌥8. So to shift left, we press ⌥⌘8.

In this case we’d use the string (with modifiers) and ignore that the ⌥ key is set among the modifiers. But had the user instead pressed ⌥⌘R then the string (with modifiers) would be ®, which is clearly not what we want. So in that case we actually want the string ignoring the modifiers, and then look at all the modifiers.

After looking at a lot of key events I found that the only way to resolve this is, to use the string with modifiers, only if that string is an ASCII character. Additionally unset the ⌥ key from the modifiers, if we go for the string with modifiers and that string is different from the string ignoring the modifiers.

For your information, here is a table showing the information given when typing 8 on US and DK keymap with the miscellaneous modifiers:

Key Press US   DK  
8 8 8 8 8
⇧8 * * ( (
⌥8 8 [ 8
⌥⇧8 ° * { (
⌘8 8 8 8 8
⇧⌘8 8 * 8 (
⌥⌘8 8 [ 8
⌥⇧⌘8 ° * { (

There are however more problems, I’m ignoring here that ⌃ is another modifier which can also change the string (with modifiers) and that both ⌥ and ⌃ can be set, cause it gets worse.

There’s a special Dvorak - Qwerty hybrid keymap. It works by letting the normal layout be Dvorak, but if you hold down the ⌘ key, the layout changes to Qwerty. That would mean that the string with modifiers should be different from that without modifiers, when the ⌘ key is pressed, and both strings should be ASCII, which sort of ruins our ⌥ heuristic from above.

It turns out that we don’t have to worry about it, cause NSEvent is simply not up to handling this hybrid keymap. On Dvorak the S key gives an O, so if we try to press the (Qwerty) S with the miscellaneous modifiers, we should get variants of O when ⌘ is not pressed, otherwise we should get variants of S, but here is what we actually get (the second table shows how the A key behaves on plain Qwerty / US):

Key Press DQ   Key Press US  
S o o A a a
⇧S O O ⇧A A A
⌥S ø o ⌥A å a
⌥⇧S Ø O ⌥⇧A Å A
⌘S s o ⌘A a a
⇧⌘S s O ⇧⌘A a A
⌥⌘S ß o ⌥⌘A å a
⌥⇧⌘S Í O ⌥⇧⌘A Å A

From these tables it’s clear that NSEvent is not up to the job of deciphering a key event to the “menu description” of that event (notice that by menu key I do not mean that the ⌘ modifier should necessary be pressed, e.g. I also need this for the general Cocoa key bindings system).

There are actual several additional problems I have neglected to mention, since I actually just wanted to put online the heuristic I ended up writing. So without further ado, here’s the actual code:

// given anEvent (NSEvent*) figure out what key 
// and modifiers we actually want to look at, 
// to compare it with a menu key description

unsigned int quals = [anEvent modifierFlags];

NSString* str = [anEvent characters];
NSString* strWithout = [anEvent charactersIgnoringModifiers];

unichar ch = [str length] ? [str characterAtIndex:0] : 0;
unichar without = [strWithout length] ? [strWithout characterAtIndex:0] : 0;

if(!(quals & NSNumericPadKeyMask))
   if((quals & NSControlKeyMask))
      ch = without;
   else if(quals & NSAlternateKeyMask)
      if(0x20 < ch && ch < 0x7f && ch != without)
            quals &= ~NSAlternateKeyMask;
      else  ch = without;
   else if((quals & (NSCommandKeyMask | NSShiftKeyMask)) == (NSCommandKeyMask | NSShiftKeyMask))
      ch = without;

   if((0x20 < ch && ch < 0x7f) || ch == 0x19)
      quals &= ~NSShiftKeyMask;

// the resulting values
key = ch;
modifiers = quals & (NSNumericPadKeyMask | NSShiftKeyMask | NSControlKeyMask | NSAlternateKeyMask | NSCommandKeyMask);

This works fairly well with Western keymaps, it probably fails miserably with non-Western keymaps. And for the Dvorak - Qwerty hybrid, it only handles these when ⌘ is the only modifier set (but look at the table above, there’s really no way to handle more).

{{ numberOfCommentsTitle }}

{{ submitComment.success }}

Error Posting Comment

{{ submitComment.error }}