9

I know others have asked similar questions, but I haven’t seen a definitive answer, and I’m still stuck. I’m trying to write a Swift function that takes a hardware-generated keyboard scan code, such as from an NSEvent, and returns the alpha-caps-locked name of the key, for the particular key arrangement (Dvorak, Qwerty, etc.) currently in effect in the OS (which might be different from the arrangement in effect when the code was generated).

It’s my understanding that the only way to do this is to invoke some very old Carbon functions, skirting a lot of the Swift’s extreme type-safety, something I don’t feel comfortable doing. Here is The Show So Far:

import  Cocoa
import  Carbon

func keyName (scanCode: UInt16) -> String?
  { let maxNameLength = 4,      modifierKeys: UInt32 = 0x00000004   //  Caps Lock (Carbon Era)

    let deadKeys      = UnsafeMutablePointer<UInt32>(bitPattern: 0x00000000),
        nameBuffer    = UnsafeMutablePointer<UniChar>.alloc(maxNameLength),
        nameLength    = UnsafeMutablePointer<Int>.alloc(1),
        keyboardType  = UInt32(LMGetKbdType())

    let source        = TISGetInputSourceProperty ( TISCopyCurrentKeyboardLayoutInputSource()
                                                        .takeRetainedValue(),
                                                    kTISPropertyUnicodeKeyLayoutData )

    let dataRef       = unsafeBitCast(source, CFDataRef.self)
    let dataBuffer    = CFDataGetBytePtr(dataRef)

    let keyboardLayout  = unsafeBitCast(dataBuffer, UnsafePointer <UCKeyboardLayout>.self)

    let osStatus  = UCKeyTranslate  (keyboardLayout, scanCode, UInt16(kUCKeyActionDown),
                        modifierKeys, keyboardType, UInt32(kUCKeyTranslateNoDeadKeysMask),
                        deadKeys, maxNameLength, nameLength, nameBuffer)
    switch  osStatus
      { case  0:    return  NSString (characters: nameBuffer, length: nameLength[0]) as String
        default:    NSLog (“Code: 0x%04X  Status: %+i", scanCode, osStatus);    return  nil   }
  }

It doesn’t crash, which at this point I almost consider a game achievement in itself, but neither does it work. UCKeyTranslate always returns a status of -50, which I understand means there’s a parameter wrong. I suspect “keyboardLayout,” as it is the most complicated to set up. Can anyone see the parameter problem? Or is there a more up-to-date framework for this sort of thing?

Jeff J
  • 139
  • 8
  • 1
    Did you see this: [How to use UCKeyTranslate](http://stackoverflow.com/questions/27735217/how-to-use-uckeytranslate) ? It looks quite similar. – Martin R Jan 31 '16 at 10:17
  • @Martin I did, and found it and Ken Thomases’ answer helpful, but in the final version, it looks like the author is trying to pass a pointer to a Swift Array in the last parameter, which is probably expecting something more like a pointer to an old C-style array. I’m very new to Swift, but that looks dangerous, and I can’t get Swift to accept it. And I can’t tell if anyone ever solved the problem of the returned -50 status value. – Jeff J Jan 31 '16 at 19:12
  • There's no need for `modifierKeys` magic number. Use `modifierKeys = UInt32(((alphaLock) >> 8) & 0xFF)` according to UCKeyTranslate docs – pointum Jan 02 '17 at 18:13

2 Answers2

11

As you already found out, you have to pass the address of a UInt32 variable as the deadKeyState argument. Allocating memory is one way to solve that problem, but you must not forget to free the memory eventually, otherwise the program will leak memory.

Another possible solution is to pass the address of a variable as an inout-argument with &:

var deadKeys : UInt32 = 0
// ...
let osStatus = UCKeyTranslate(..., &deadKeys, ...)

This is a bit shorter and simpler, and you don't need to release the memory. The same can be applied to nameBuffer and nameLength.

The unsafeBitCast() can be avoided by using the Unmanaged type, compare Swift: CFArray : get values as UTF Strings for a similar problem and more detailed explanations.

Also you can take advantage of the toll-free bridging between CFData and NSData.

Then your function could look like this (Swift 2):

import Carbon

func keyName(virtualKeyCode: UInt16) -> String?
{
    let maxNameLength = 4
    var nameBuffer = [UniChar](count : maxNameLength, repeatedValue: 0)
    var nameLength = 0

    let modifierKeys = UInt32(alphaLock >> 8) & 0xFF // Caps Lock
    var deadKeys : UInt32 = 0
    let keyboardType = UInt32(LMGetKbdType())
    
    let source = TISCopyCurrentKeyboardLayoutInputSource().takeRetainedValue()
    let ptr = TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData)
    let layoutData = Unmanaged<CFData>.fromOpaque(COpaquePointer(ptr)).takeUnretainedValue() as NSData
    let keyboardLayout = UnsafePointer<UCKeyboardLayout>(layoutData.bytes)
    
    let osStatus = UCKeyTranslate(keyboardLayout, virtualKeyCode, UInt16(kUCKeyActionDown),
        modifierKeys, keyboardType, UInt32(kUCKeyTranslateNoDeadKeysMask),
        &deadKeys, maxNameLength, &nameLength, &nameBuffer)
    guard osStatus == noErr else {
        NSLog("Code: 0x%04X  Status: %+i", virtualKeyCode, osStatus);
        return nil
    }
    
    return  String(utf16CodeUnits: nameBuffer, count: nameLength)
}

Update for Swift 3:

import Carbon

func keyName(virtualKeyCode: UInt16) -> String? {
    let maxNameLength = 4
    var nameBuffer = [UniChar](repeating: 0, count : maxNameLength)
    var nameLength = 0
    
    let modifierKeys = UInt32(alphaLock >> 8) & 0xFF // Caps Lock
    var deadKeys: UInt32 = 0
    let keyboardType = UInt32(LMGetKbdType())
    
    let source = TISCopyCurrentKeyboardLayoutInputSource().takeRetainedValue()
    guard let ptr = TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData) else {
        NSLog("Could not get keyboard layout data")
        return nil
    }
    let layoutData = Unmanaged<CFData>.fromOpaque(ptr).takeUnretainedValue() as Data
    let osStatus = layoutData.withUnsafeBytes {
        UCKeyTranslate($0, virtualKeyCode, UInt16(kUCKeyActionDown),
                       modifierKeys, keyboardType, UInt32(kUCKeyTranslateNoDeadKeysMask),
                       &deadKeys, maxNameLength, &nameLength, &nameBuffer)
    }
    guard osStatus == noErr else {
        NSLog("Code: 0x%04X  Status: %+i", virtualKeyCode, osStatus);
        return nil
    }
    
    return  String(utf16CodeUnits: nameBuffer, count: nameLength)
}

Update for Swift 4:

As of Swift 4, Data.withUnsafeBytes calls the closure with a UnsafeRawBufferPointer which has to be bound a pointer to UCKeyboardLayout:

import Carbon

func keyName(virtualKeyCode: UInt16) -> String? {
    let maxNameLength = 4
    var nameBuffer = [UniChar](repeating: 0, count : maxNameLength)
    var nameLength = 0

    let modifierKeys = UInt32(alphaLock >> 8) & 0xFF // Caps Lock
    var deadKeys: UInt32 = 0
    let keyboardType = UInt32(LMGetKbdType())

    let source = TISCopyCurrentKeyboardLayoutInputSource().takeRetainedValue()
    guard let ptr = TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData) else {
        NSLog("Could not get keyboard layout data")
        return nil
    }
    let layoutData = Unmanaged<CFData>.fromOpaque(ptr).takeUnretainedValue() as Data
    let osStatus = layoutData.withUnsafeBytes {
        UCKeyTranslate($0.bindMemory(to: UCKeyboardLayout.self).baseAddress, virtualKeyCode, UInt16(kUCKeyActionDown),
                       modifierKeys, keyboardType, UInt32(kUCKeyTranslateNoDeadKeysMask),
                       &deadKeys, maxNameLength, &nameLength, &nameBuffer)
    }
    guard osStatus == noErr else {
        NSLog("Code: 0x%04X  Status: %+i", virtualKeyCode, osStatus);
        return nil
    }

    return  String(utf16CodeUnits: nameBuffer, count: nameLength)
}
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • After some experimentation, I have to say I really prefer your way of doing things: it’s cleaner, the inout parameters are easier to see, and no playing games with type-safety. It’s also good to know I can use a Swift Array directly in the last parameter. That is really good to know. Many thanks. – Jeff J Feb 02 '16 at 06:19
  • Hi, `let keyboardLayout = UnsafePointer(layoutData.bytes)` doesn't work on Swift 3.0. – Vayn Nov 13 '16 at 12:37
  • @MartinR I got this error `'init' is unavailable: use 'withMemoryRebound(to:capacity:_)' to temporarily view memory as another layout-compatible type.` – Vayn Nov 13 '16 at 12:40
  • @Vayn: There are *lots* of changes in Swift 3, in particular with pointers. Give me some time, I'll try to update the code.... – Martin R Nov 13 '16 at 12:41
  • Haha, I'm waiting for the update. It is too hard to understand pointers in Swift 3. – Vayn Nov 13 '16 at 12:43
  • @Vayn: I have added the Swift 3 code. Can you check if it works correctly? – Martin R Nov 13 '16 at 12:52
  • So I don't need to cast the type of `layoutData` to `UCKeyboardLayout` now, right? – Vayn Nov 13 '16 at 13:02
  • 1
    @Vayn: `keyboardLayout` has become the `$0` in `layoutData.withUnsafeBytes {...}`. That is a generic method of `Data` and the type of `$0` is inferred automatically by the compiler from the signature of `UCKeyTranslate()` which expects a `UnsafePointer` as the first parameter. – Swift 3 pointer magic :) – Martin R Nov 13 '16 at 13:06
  • @MartinR If I set `$0` as first parameter, Xcode would give me an error: `Cannot convert value of type 'UnsafePointer<_>' to expected argument type 'UnsafePointer!'`, so I need to cast `$0` with `UnsafePointer($0)` – Vayn Nov 13 '16 at 15:48
  • @Vayn: Let us [continue the discussion in chat](http://chat.stackoverflow.com/rooms/127992/discussion-between-vayn-and-martin-r) – Martin R Nov 13 '16 at 15:58
  • There's no need for `modifierKeys` magic number. Use `modifierKeys = UInt32(((alphaLock) >> 8) & 0xFF)` according to UCKeyTranslate docs – pointum Jan 02 '17 at 18:13
  • While this is an excellent working answer, I’m a bit worried that it’s based on `Carbon`, which was deprecated in 2007. Is there any chance for a solution without `Carbon`? – ixany Feb 03 '19 at 10:49
  • 1
    Worth noting that the term "scan code" might get confusing here. On an ANSI keyboard for example, the scan code coming from the physical hardware for the "a" key is 0x04, whereas the virtual key code for "a" is 0x00. Passing scan codes directly to UCKeyTranslate will result in unexpected unicode strings being returned. Could be worth renaming the param name to "virtualKeyCode". – Chris Jul 17 '21 at 09:58
  • All that said, it's trivial to map the scan codes to virtual key codes, and one could do so before passing the mapped virtual key code onto keyName. i.e 0x04->0x00, etc. Full list of virtual key codes here - https://stackoverflow.com/questions/3202629/where-can-i-find-a-list-of-mac-virtual-key-codes – Chris Jul 17 '21 at 09:59
  • 1
    @Chris: Thanks for the suggestion and information. That matches also the documented parameter name of the UCKeyTranslate function. – Martin R Jul 17 '21 at 10:27
1

Ok, I believe I’ve found the problem. As odd as it feels to answer my own question, I understand that is the proper thing to do in cases like this.

The offending parameter seems to have been deadKeys. In the model code I was following, this was defined as a bit pattern. Although supposedly a pointer to something mutable, I’m not sure it really was that, because when I decided to redefine it to match the other two call-by-reference parameters of UCKeyTranslate, everything started working perfectly. The solution was to do an explicit .alloc, followed by an explicit zeroing of the referenced value. Here is my function updated:

func    keyName       ( scanCode: UInt16  )     ->  String?
  { let maxNameLength = 4,      modifierKeys: UInt32  = 0x00000004,     //  Caps Lock (Carbon Era Mask)
        nameBuffer    = UnsafeMutablePointer <UniChar>  .alloc (maxNameLength),
        nameLength    = UnsafeMutablePointer <Int>      .alloc (1),
        deadKeys      = UnsafeMutablePointer <UInt32>   .alloc (1);     deadKeys[0] = 0x00000000

    let source        = TISGetInputSourceProperty ( TISCopyCurrentKeyboardLayoutInputSource()
                                                        .takeRetainedValue(),
                                                    kTISPropertyUnicodeKeyLayoutData  ),

    keyboardLayout    = unsafeBitCast ( CFDataGetBytePtr (unsafeBitCast (source, CFDataRef.self)),
                                        UnsafePointer <UCKeyboardLayout>.self),
    keyboardType      = UInt32 (LMGetKbdType())

    let osStatus      = UCKeyTranslate (keyboardLayout, scanCode, UInt16 (kUCKeyActionDown),
                            modifierKeys, keyboardType, UInt32 (kUCKeyTranslateNoDeadKeysMask),
                            deadKeys, maxNameLength, nameLength, nameBuffer)
    switch  osStatus
      { case  0:    return  String.init (utf16CodeUnits: nameBuffer, count: nameLength[0])
        default:    NSLog ("Code: 0x%04X  Status: %+i", scanCode, osStatus);    return  nil   }
  }

There are a few other changes, pretty much cosmetic: I eliminated a couple of intermediate constants leading up to the definition of keyboardLayout. (The “BitCasts” are only to satisfy Swiftian type-safety: they don’t really do anything else that I can see.) But the real problem was the original definition of deadKeys. I hope this will be of some use to somebody, at least until there is a non-Carbon alternative. (Will that happen?)

Jeff J
  • 139
  • 8