みずぴー日記

人間の再起動ボタンはハワイのビーチにある

⌨️キーボード配列の取得

入力メソッドはキー入力イベントをテキスト入力に変換する。

キーボードのキーが押されたときに、どのキー入力イベントが発生するかはmacOSの設定によって変更できる。 通常はキーに印字されている文字と一致するようにQwerty配列が用いられるが、Dvorak配列などのそれ以外の配列にも変更できる。

そのためmacOS標準の日本語入力では、どのキーボード配列を使うかを設定できる。

f:id:mzp:20171124114611p:plain

🐙ソースコード

💎TISInputSource

利用可能なキーボードを取得する関数は、Carbon frameworkのHIToolboxで提供される。HIToolboxではキーボード配列や入力メソッドなどを総称して入力ソース(InputSource)と呼ぶ。

TISCreateInputSourceList は条件に合致したものを入力ソース取得する。 入力メソッドで使えるキーボードを取得するには種別がキーボード配列であり、ASCII文字を入力できる入力ソースを取得すればよい。

let conditions = CFDictionaryCreateMutable(nil, 2, nil, nil)
CFDictionaryAddValue(conditions,
                     unsafeBitCast(kTISPropertyInputSourceType, to: UnsafeRawPointer.self),
                     unsafeBitCast(kTISTypeKeyboardLayout, to: UnsafeRawPointer.self))
CFDictionaryAddValue(conditions,
                     unsafeBitCast(kTISPropertyInputSourceIsASCIICapable, to: UnsafeRawPointer.self),
                     unsafeBitCast(kCFBooleanTrue, to: UnsafeRawPointer.self))

guard let array = TISCreateInputSourceList(conditions, true) else {
       return nil
}
return array.takeRetainedValue() as? [TISInputSource]

取得したキーボード配列の属性はTISGetInputSourcePropertyで取得する。

// 入力ソースのIDを取得する
TISGetInputSourceProperty(inputSource, kTISPropertyInputSourceID)

// キーボード配列の名前を取得する
TISGetInputSourceProperty(inputSource, kTISPropertyLocalizedName)

🙊キーボード配列の種類

ASCII文字が入力可能なキーボード配列は2種類存在する。

1種類目はDvorak、ColemarkのようなQwertyと同様のキーを持ちながら配置のみが違うものである。macOSはこれらは英語入力用の配列として扱う。

f:id:mzp:20171124115052p:plain

もう1種類は、フランス語キーボードのようにùなどの特別な文字を入力できるキーボードである。これらは英語以外のキーボードとして扱う。

f:id:mzp:20171124115042p:plain

この2種類のキーボード配列を区別する方法は公開されていない。しかし、TSMInputSourcePropertyScriptCodeプロパティを取得すれば可能である。このプロパティは日本語入力の設定画面のコードから発見した。

let r = TISGetInputSourceProperty(inputSource, "TSMInputSourcePropertyScriptCode" as CFString)
let n = unsafeBitCast(r, to: NSInteger.self)

// 39なら英語
return n == 39

💄設定画面

HIToolboxのままだと、CoreFoundationの型が必要になるなど、扱いが面倒である。 そこで TISInputSource を拡張し、Swiftからも利用しやすくする。

extension TISInputSource {
    var localizedName: String {
        return unsafeBitCast(
            TISGetInputSourceProperty(self, kTISPropertyLocalizedName),
            to: NSString.self) as String
    }

    var inputSourceID: String {
        return unsafeBitCast(
            TISGetInputSourceProperty(self, kTISPropertyInputSourceID),
            to: NSString.self) as String
    }

    var scriptCode: Int? {
        let r = TISGetInputSourceProperty(self, "TSMInputSourcePropertyScriptCode" as CFString)
        let n = unsafeBitCast(r, to: NSInteger.self)
        return n
    }

    class func keyboardLayouts() -> [TISInputSource]? {
        let conditions = CFDictionaryCreateMutable(nil, 2, nil, nil)
        CFDictionaryAddValue(conditions,
                             unsafeBitCast(kTISPropertyInputSourceType, to: UnsafeRawPointer.self),
                             unsafeBitCast(kTISTypeKeyboardLayout, to: UnsafeRawPointer.self))
        CFDictionaryAddValue(conditions,
                             unsafeBitCast(kTISPropertyInputSourceIsASCIICapable, to: UnsafeRawPointer.self),
                             unsafeBitCast(kCFBooleanTrue, to: UnsafeRawPointer.self))

        guard let array = TISCreateInputSourceList(conditions, true) else {
            return nil
        }
        guard let keyboards = array.takeRetainedValue() as? [TISInputSource] else {
            return nil
        }
        return keyboards.sorted {
            $0.localizedName < $1.localizedName
        }
    }
}

入力メソッドの設定画面で作成した設定画面にキーボード配列選択のためのポップアップを追加する。

public class Preferences: NSPreferencePane {
    private let store: SettingStore = SettingStore()
    private lazy var keyboardLayouts: [TISInputSource]? = TISInputSource.keyboardLayouts()?.filter { $0.scriptCode == 39 }

    override public func mainViewDidLoad() {
        let keyboard = NSPopUpButton() ※ {
            for layout in keyboardLayouts ?? [] {
                $0.addItem(withTitle: layout.localizedName)
            }
        }
        …
    }
}

選択したキーボード配列をNSUserDefaultに保存するように変更する。さらに起動時に保存した内容を読み込むようにする。

keyboard.reactive.selectedIndexes.observeValues {
        if let layout = self.keyboardLayouts?[$0] {
                self.store.setKeyboardLayout(inputSourceID: layout.inputSourceID)
        }
}
if let index = keyboardLayouts?.index(where: { $0.inputSourceID == store.keyboardLayout() }) {
       keyboard.selectItem(at: index)
}

📝 入力メソッド

入力メソッドでキーボード配列を指定するには IMKTextInputoverrideKeyboard(withKeyboardNamed:) を用いる。

クライアントアプリケーションが切り替わったときに呼ばれる activateServer と入力モードが切り替わったときに呼ばれる setValue(_:, forTag:,client) でキーボード配列を指定する。

extension EmojiInputController /* IMKStateSetting*/ {
    override func activateServer(_ sender: Any) {
        NSLog("%@", "\(#function)((\(sender))")

        guard let client = sender as? IMKTextInput else {
            return
        }
        client.overrideKeyboard(withKeyboardNamed: SettingStore().keyboardLayout())
    }

    override func setValue(_ value: Any, forTag tag: Int, client sender: Any) {
        NSLog("%@", "\(#function)(\(value), forTag: \(tag))")
        guard let value = value as? NSString else {
            return
        }
        guard let sender = sender as? IMKTextInput else {
            return
        }
        sender.overrideKeyboard(withKeyboardNamed: SettingStore().keyboardLayout())
    }
}