Getting key names for keyboard codes in Swift
Asked Answered
A

2

10

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?

Aden answered 31/1, 2016 at 5:28 Comment(3)
Did you see this: How to use UCKeyTranslate ? It looks quite similar.Gorgonian
@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.Aden
There's no need for modifierKeys magic number. Use modifierKeys = UInt32(((alphaLock) >> 8) & 0xFF) according to UCKeyTranslate docsPericardium
G
12

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)
}
Gorgonian answered 1/2, 2016 at 19:36 Comment(15)
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.Aden
Hi, let keyboardLayout = UnsafePointer<UCKeyboardLayout>(layoutData.bytes) doesn't work on Swift 3.0.Limpet
@MartinR I got this error 'init' is unavailable: use 'withMemoryRebound(to:capacity:_)' to temporarily view memory as another layout-compatible type.Limpet
@Vayn: There are lots of changes in Swift 3, in particular with pointers. Give me some time, I'll try to update the code....Gorgonian
Haha, I'm waiting for the update. It is too hard to understand pointers in Swift 3.Limpet
@Vayn: I have added the Swift 3 code. Can you check if it works correctly?Gorgonian
So I don't need to cast the type of layoutData to UCKeyboardLayout now, right?Limpet
@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<UCKeyboardLayout> as the first parameter. – Swift 3 pointer magic :)Gorgonian
@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<UCKeyboardLayout>!', so I need to cast $0 with UnsafePointer<UCKeyboardLayout>($0)Limpet
@Vayn: Let us continue the discussion in chatGorgonian
There's no need for modifierKeys magic number. Use modifierKeys = UInt32(((alphaLock) >> 8) & 0xFF) according to UCKeyTranslate docsPericardium
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?Clop
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".Neuroglia
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 - #3203129Neuroglia
@Chris: Thanks for the suggestion and information. That matches also the documented parameter name of the UCKeyTranslate function.Gorgonian
A
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?)

Aden answered 1/2, 2016 at 3:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.