Inquisitive Cocoa

Unless otherwise stated all code is released under the MIT License
5th April ‘09

Key Code Translator

The Problem

Today’s problem is to translate the keyCodes that are generated by keyboard NSEvents into characters. Press a ‘q’ key, and the field in the preferences should display an ‘q’ instead of 12. There are simpler options. If you still have the event handily lying around then you could try calling [theEvent characters] or [theEvent charactersIgnoringModifiers]. Simple.. init.

Except to keep a users keyboard preferences I don’t want to have to be archive and unarchiving NSEvents every time I want to show a list of keyboard commands. Why go to all this effort when it’d be so easy to just save that single key code as a single integer. Things get even stickier if the user has changed keyboards since they last opened your application.

I want to map the physical location of the key that’s just been pressed, to a method, regardless of whether that key will print (in this completely contrived example) a ‘b’ or an ‘ß’. All I want to change is for the label that the user sees in the preferences window to match the current keyboard layout. As well as displaying normal characters, there are a set of special characters that want to be displayed specially, for example the space, tab, enter, arrow keys all need to be displayed in a human friendly fashion.

Existing Solutions

There are other options out there that currently solve this problem. There’s Shortcut Recorder (originally created Waffle Software but now with it’s own google code site, which has it’s own Leopard only branch, and a snazzy new IB3 palette. It looks pretty decent. With a nice simple drag & drop & hook up to the delegate, it’s easy to use too. Which is all well and good.. until you try and get it too compile against 10.5 frameworks. Eek!! The compiler bails. KLGetCurrentKeyboardLayout() and KLGetKeyboardLayoutProperty() are undefined. The documentation says that they’ve only been depreciated, but that doesn’t help when the compiler starts popping up errors. The solution is simple. Compile against the 32 bit, 10.4 framework.

Except that I want it to work the ‘right’ way, darn it. I want 64 bits. I want the future, not the hackery of a previous system, and it’d be kinda nice if it didn’t break at the first sight of Snow Leopard.

Updated 2009 – 12 – 22
Shortcut Recorder is now full 64bit friendly, and rather good. It does a lot more than my version, and does it better. For one thing it has a complete dictionary of replacement characters, and support for Padkey’s and similar niceties. I’m sure I’ll be using it in the future.

HotKey3 from Rogue Amoeba is available from their /source directory and with the application the 105SDK.patch file. Not being a command-line ace, I couldn’t even figure out how to apply the patch. Plus with 6 classes and over a thousand lines of code in its pre-patched state (I never did get the patch utility to work) PTKeyCode is overkill for my very basic needs. I could cut it down to two classes and only 379 lines of code, but still, 22 methods is excessive. It takes a concerted effort to trace back what’s going on. Simplicity is key to easy understanding, and I like to understand the code that I’m including in my applications.

My version is a category on NSString. One class method that takes a key code and any modifier flags (or zero if you want to ignore modifiers) and returns either the character, or a friendly name for the key that is defined in the mapOfNamesForUnicodeGlyphs struct in the header file.

+ (NSString *)stringForKeyCode:(unsigned short)keyCode withModifierFlags:(NSUInteger)modifierFlags
 
{
   TISInputSourceRef currentKeyboard = TISCopyCurrentKeyboardInputSource();
   CFDataRef uchr = (CFDataRef)TISGetInputSourceProperty(currentKeyboard, kTISPropertyUnicodeKeyLayoutData);
   const UCKeyboardLayout *keyboardLayout = (const UCKeyboardLayout*)CFDataGetBytePtr(uchr);
 
   if(keyboardLayout) {
      UInt32 deadKeyState = 0;
      UniCharCount maxStringLength = 255;
      UniCharCount actualStringLength = 0;
      UniChar unicodeString[maxStringLength];
 
      OSStatus status = UCKeyTranslate(keyboardLayout,
                               keyCode, kUCKeyActionDown, modifierFlags,
                               LMGetKbdType(), 0,
                               &deadKeyState,
                               maxStringLength,
                               &actualStringLength, unicodeString);
 
      if(status != noErr)
         NSLog(@"There was an %s error translating from the '%d' key code to a human readable string: %s",
                                 GetMacOSStatusErrorString(status), status, GetMacOSStatusCommentString(status));
      else if(actualStringLength > 0) {
         // Replace certain characters with user friendly names, e.g. Space, Enter, Tab etc.
         NSUInteger i = 0;
         while(i <= NumberOfUnicodeGlyphReplacements) {
            if(mapOfNamesForUnicodeGlyphs[i].glyph == unicodeString[0])
               return NSLocalizedString(([NSString stringWithFormat:@"%s", mapOfNamesForUnicodeGlyphs[i].name, nil]), @"Friendly Key Name");
 
            i++;
         }
 
         // NSLog(@"Unicode character as hexadecimal: %X", unicodeString[0]);
         return [NSString stringWithCharacters:unicodeString length:(NSInteger)actualStringLength];
      } else
         NSLog(@"Couldn't find a translation for the '%d' key code", keyCode);
   } else
      NSLog(@"Couldn't find a suitable keyboard layout from which to translate");
 
   return nil;
}

I’m not going to explain much at all. When you see it all in one place it’s hopefully not too hard to read what’s going on. The code is reasonably self documenting, although the mix of syntaxes complicates things. The difficult part was taking a series of leaps that the documentation completely fail to explain. For instance getting from a TISGetInputSourceRef to a UCKeyboardLayout is particularly tenuous.

The method itself is 41 lines of code in all, which is pretty neat considering. The source files are available here under the MIT license. It’d be nice to hear if you end up using this, maybe even an attribution in your About pane.

What it doesn’t do?

  1. It doesn’t load the keys to replace from a localizable dictionary. (like PTHotKeysLib does)
  2. It doesn’t (and could easily be modified to..) filter keyCodes before their translated, rather than after they’ve been converted to unicode characters. Useful for handling F keys.
  3. It doesn’t work with the delete/backspace key. This is particularly annoying, since I have no idea why. It should work. The hexadecimal characters match up. Maybe there is a better way to compare unicode characters than the simple ‘==’ I currently use. (See no. 2 for a easy solution)
    Now works as expected.
  4. It current only filters characters that are 1 unicode character long. In other languages, particularly asian alphabets, one key press can produce a series of unicode characters. Hopefully the value that UCKeyTranslate returns will be suitable to cover these cases.

Doubts

Bear in mind that I’m very definitely a Cocoa person and this problem is pushing at the limits of my comfort zone of C/Carbon. I think I’ve done right, but I still have doubts. Should I be retaining that CFDataRef? Why don’t the unicode character constants defined by Apple match up to the expected key presses?

I’m open to any corrections or suggestions.

Update
Fixed the link to the source files. Thanks Zac for the heads up.
Updated again, to use LMGetKbdType() instead of some other kludge that just happened to work at the time, and set deadKeyState to 0. Many thanks to Micheal Tyson for the fixes.

Cocoa, Objective-C, Source

3 Responses to “Key Code Translator”

  1. Michael Tyson

    Fantastic, thankyou – this is just what I was looking for!

    Building from Snow Leopard with the 10.5 SDK, I was getting “: kCGErrorIllegalArgument: CGEventSourceGetKeyboardType: invalid event source” errors; Looking up the docs for UCKeyTranslate, it’s suggested that “LMGetKbdType()” is adequate for getting the keyboardType parameter value. I’m using this instead of the CGEventSourceGetKeyboardType call, and everything’s working properly.

    I was also having some odd discrepancies between the debug and release builds, where the key code would not be identified correctly for the release build. It turns out ‘deadKeyState’ needs to be initialised to 0 before the UCKeyTranslate call. Doing so solves the problem.

    • Harry

      Your right. I’m not sure what I was thinking!
      The version that I’ve been using for my development has been using LMGetKbdType(), and I assumed the version up here was the same, but I’d have never have thought that setting deadKeyState to zero. Thank you! I’ve updated the files to match.

      Thanks for the heads up about the blogs comments system too, and for prompting me to do a bit of tinkering on the website.

  2. George

    uchr was null for me although currentKeyboard and kTISPropertyUnicodeKeyLayoutData were apparently OK. Any idea why that would happen? This is on 10.6.

Add your thoughts