入力メソッドはキー入力イベントをテキスト入力に変換する。
キーボードのキーが押されたときに、どのキー入力イベントが発生するかはmacOSの設定によって変更できる。 通常はキーに印字されている文字と一致するようにQwerty配列が用いられるが、Dvorak配列などのそれ以外の配列にも変更できる。
そのためmacOS標準の日本語入力では、どのキーボード配列を使うかを設定できる。
🐙ソースコード
💎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はこれらは英語入力用の配列として扱う。
もう1種類は、フランス語キーボードのようにùなどの特別な文字を入力できるキーボードである。これらは英語以外のキーボードとして扱う。
 
この2種類のキーボード配列を区別する方法は公開されていない。しかし、TSMInputSourcePropertyScriptCodeプロパティを取得すれば可能である。このプロパティは日本語入力の設定画面のコードから発見した。
let r = TISGetInputSourceProperty(inputSource, "TSMInputSourcePropertyScriptCode" as CFString) let n = unsafeBitCast(r, to: NSInteger.self) // 39なら英語 return n == 39
Mojaveで若干挙動が変更になった。詳細は🏜InputMethodKit for Mojave - みずぴー日記を参照すること。
💄設定画面
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) }
📝 入力メソッド
入力メソッドでキーボード配列を指定するには IMKTextInput
の overrideKeyboard(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()) } }