objective-cmacoscocoaswiftcore-services

How to use UCKeyTranslate


Given a key-code for a key pressed without modifiers, I want to produce the result of pressing the shift+key. Example: For a standard US keyboard <shift>+<period> gives >.

The relevant function is UCKeytranslate, but I need a bit of help getting the details right. The snippet below is a full program ready to run in Xcode. The intent of the program is given <period> to produce the character >.

The result of the program is:

Keyboard: <TSMInputSource 0x10051a930> KB Layout: U.S. (id=0)
Layout: 0x0000000102802000
Status: -50
UnicodeString: 97
String: a
Done
Program ended with exit code: 0

The part that gets the layout seems to be working, but the status code reveals that something went wrong. But what?

import Foundation
import Cocoa
import Carbon
import AppKit

// The current text input source (read keyboard) has a layout in which
// we can lookup how key-codes are resolved.

// Get the a reference keyboard using the current layout.
var unmanagedKeyboard = TISCopyCurrentKeyboardLayoutInputSource()
var keyboard = unmanagedKeyboard.takeUnretainedValue() as TISInputSource
print("Keyboard: ") ; println(keyboard)

// Get the layout
var ptrLayout   = TISGetInputSourceProperty(keyboard, kTISPropertyUnicodeKeyLayoutData)
var layout = UnsafeMutablePointer<UCKeyboardLayout>(ptrLayout)
print("Layout: "); println(layout)

// Let's see what the result of pressing  <shift> and <period>  (hopefully the result is > )
var keycode             = UInt16(kVK_ANSI_Period)                           // Keycode for <period>
var keyaction           = UInt16(kUCKeyActionDisplay)                       // The user is requesting information for key display
var modifierKeyState    = UInt32(1 << 17)                                   // Shift
var keyboardType        = UInt32(LMGetKbdType())
var keyTranslateOptions = UInt32(1 << kUCKeyTranslateNoDeadKeysBit)
var deadKeyState        = UnsafeMutablePointer<UInt32>(bitPattern: 0)       // Is 0 the correct value?
var maxStringLength     = UniCharCount(4)                                   // uint32
var actualStringLength  = UnsafeMutablePointer<UniCharCount>.alloc(1)       //
actualStringLength[0]=16
var unicodeString       = UnsafeMutablePointer<UniChar>.alloc(255)
unicodeString[0] = 97 // a (this value is meant to be overwritten by UCKeyTranslate)
var str = NSString(characters: unicodeString, length: 1)
var result = UCKeyTranslate(layout, keycode, keyaction, modifierKeyState, keyboardType, keyTranslateOptions,
                            deadKeyState, maxStringLength, actualStringLength, unicodeString)

// Print the results
print("Status: "); println(result)
var unichar = unicodeString[0];
print("UnicodeString: "); println(String(unichar))
print("String: "); println(str)
println("Done")

EDIT

I have rewritten the snippet following the suggestions of Ken Thomases. A few tricks from: Graphite a Swift program using keycodes was also used.

import Foundation
import Cocoa
import Carbon
import AppKit

// The current text input source (read keyboard) has a layout in which
// we can lookup how key-codes are resolved.

// Get the a reference keyboard using the current layout.
let keyboard = TISCopyCurrentKeyboardInputSource().takeRetainedValue()
let rawLayoutData = TISGetInputSourceProperty(keyboard, kTISPropertyUnicodeKeyLayoutData)
print("Keyboard: ") ; println(keyboard)

// Get the layout
var layoutData      = unsafeBitCast(rawLayoutData, CFDataRef.self)
var layout: UnsafePointer<UCKeyboardLayout> = unsafeBitCast(CFDataGetBytePtr(layoutData), UnsafePointer<UCKeyboardLayout>.self)
print("Layout: "); println(layout)

print("KbdType "); println(LMGetKbdType()) // Sanity check (prints 44)

var keycode             = UInt16(kVK_ANSI_Period)                         // Keycode for a
var keyaction           = UInt16(kUCKeyActionDisplay)
var modifierKeyState    = UInt32(1 << 1)                                  // Shift
var keyboardType        = UInt32(LMGetKbdType())
var keyTranslateOptions = OptionBits(kUCKeyTranslateNoDeadKeysBit)
var deadKeyState        = UInt32(0)                                       // Is 0 the correct value?
var maxStringLength     = UniCharCount(4)                                 // uint32
var chars: [UniChar]    = [0,0,0,0]
var actualStringLength  = UniCharCount(1)
var result = UCKeyTranslate(layout, keycode, keyaction, modifierKeyState, keyboardType, keyTranslateOptions,
                            &deadKeyState, maxStringLength, &actualStringLength, &chars)
// Print the results
print("Status: "); println(result)
print("Out:"); println(UnicodeScalar(chars[0]))
println("Done")

Solution

  • For kTISPropertyUnicodeKeyLayoutData, TISGetInputSourceProperty() returns a CFDataRef. You need to get its bytes pointer and treat that as the pointer to UCKeyboardLayout. I don't think that's what you're doing with this line:

    var layout = UnsafeMutablePointer<UCKeyboardLayout>(ptrLayout)
    

    I don't really know Swift, but it would probably work as:

    var layout = UnsafePointer<UCKeyboardLayout>(CFDataGetBytePtr(ptrLayout))
    

    or maybe:

    var layout = CFDataGetBytePtr(ptrLayout) as UnsafePointer<UCKeyboardLayout>
    

    Also, kUCKeyActionDisplay is mostly useless. Its intent is to return the label of the key, but it doesn't even do that reliably. You probably want to use kUCKeyActionDown.

    For the modifiers, you want to use the Carbon-era shiftKey bit mask shifted right 8 bits (as shown in the documentation for UCKeyTranslate()). shiftKey is 1 << 9, so shiftKey >> 8 is 1 << 1.

    For the options, you should be able to use kUCKeyTranslateNoDeadKeysMask for simplicity. It's equivalent to 1 << kUCKeyTranslateNoDeadKeysBit.

    Yes, 0 is the proper value for deadKeyState for an initial keystroke or one where you don't want to apply any previous dead-key state.

    I'm not sure why you've commented the maxStringLength line with uint32. That type is completely unrelated to the maximum string length. maxStringLength is the maximum number of UTF-16 code units (what the APIs incorrectly call "characters") the UCKeyTranslate() should write to the provided buffer. It's basically the buffer size, measured in UniChars (not bytes). In your case it should be 255. Or, since you probably don't expect to get 255 "characters" from a single keystroke, you could reduce the size of the buffer and set maxStringLength to match whatever it is.

    Your handling of str is strange. You construct it from the unicodeString buffer before calling UCKeyTranslate(). Do you expect that string object's value to be changed because UCKeyTranslate() changes the contents of unicodeString? It does not. If it did, that would be a very bad bug in NSString. You should construct the NSString from the buffer after UCKeyTranslate() has successfully populated that buffer. Of course, you should pass actualStringLength as the length parameter when constructing the NSString.