Entries from 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?
- It doesn’t load the keys to replace from a localizable dictionary. (like PTHotKeysLib does)
- 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.
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.- 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.
An Introduction
Loosing my computer seems like a petty trauma. Like a strange distortion of sensory deprivation. It depresses me how reliant on this hyper-connected world I have become. It seems to infiltrate even my thoughts. The computer has replaced memory & reason. Unfortunately in my case it’s all self inflicted. From Tuesday my trusty scuffed Macbook Pro will be packed off in a box, to return at some undefined point in the future with a less broken screen. So not the most auspicious start to this blogery.
My names Harry Jordan, and amongst other things I’m a designer and a programmer. I have a thing for User Interfaces and prefer to write Mac applications using Cocoa/Objective-C, although I dabble in all sorts. I share my time between Leicester, where I’m close to not being a student at De Montfort University, and my real heartlands, which are in deepest darkest Wales.