🍣入力メソッドを拡張し、テキストを入力し、Enterで確定できるようにした。
コード
https://github.com/mzp/EmojiIM/tree/marked
未確定文字列の挿入
未確定文字列は、入力セッションの一部としてマークされている文字列なのでmarked textと呼ばれる。
IMKTextInput
プロトコルの setMarkedText
で設定できる。例えば"あいうえ"を未確定文字列として表示したい場合は、以下のコードになる。
let notFound = NSRange(location: NSNotFound, length: NSNotFound) client.setMarkedText("あいうえ", selectionRange: notFound, replacementRange: notFound)
クリアしたい場合は第一引数に空文字列を渡す。nilを渡すとクラッシュする。
状態遷移
未確定文字列の入力した後、入力を確定するためには、入力メソッド内で状態遷移を管理する必要がある。
未確定文字列を持たない初期状態と、未確定文字列を持つ入力中状態があり、キー入力とEnterキーの入力で遷移するので 状態遷移図は以下のようになる。
ReactiveAutomaton
状態遷移を記述するためにReactiveAutomatonを用いる。
これはReactiveCocoaで状態遷移機械を書くためのライブラリである。アイデアは以下のスライドで解説されいる。
このライブラリを使い、入力メソッドの状態遷移および遷移時のアクションを定義する。
入力
ユーザの入力はテキスト入力とEnterの押下の2種類とする。 以下のenumで定義する。テキスト入力は入力された文字列をパラメータに持つようにする。
public enum UserInput { case input(text: String) case enter }
状態
入力メソッドの状態は「通常状態」と「入力中状態」の2種類とする。以下のenumで定義する。
public enum InputMethodState { case normal case composing }
遷移
ReactiveAutomatonのDSLを用いて状態遷移を定義する。
static func isInput(_ state: UserInput) -> Bool { switch state { case .input: return true default: return false } } let mappings: [ActionMapping<InputMethodState, UserInput>] = [ /* Input <|> fromState => toState */ /* --------------------------------*/ isInput <|> .normal => .composing, isInput <|> .composing => .composing, .enter <|> .composing => .normal ]
遷移が発生した際に実行するアクションを指定できるようにDSLを拡張する。 (ReactiveAutomaton+Action.swift) そして、遷移時のアクションで確定文字列、未確定文字列を更新する。
let (text, textObserver) = Signal<String, NoError>.pipe() let markedTextProperty = MutableProperty<String>("") let mappings: [ActionMapping<InputMethodState, UserInput>] = [ /* Input <|> fromState => toState <|> action */ /* -------------------------------------------*/ isInput <|> .normal => .composing <|> { switch $0 { case .input(text: let text): markedTextProperty.swap(text) default: () } }, isInput <|> .composing => .composing <|> { switch $0 { case .input(text: let text): markedTextProperty.modify { $0.append(text) } default: () } }, .enter <|> .composing => .normal <|> { _ in textObserver.send(value: markedTextProperty.value) markedTextProperty.swap("") } ]
この状態遷移をもとに、オートマトンを作る。
let (inputSignal, observer) = Signal<UserInput, NoError>.pipe() self.automaton = Automaton(state: .normal, input: inputSignal, mapping: reduce(mappings))
入力コントローラとの接続
イベントの処理
オートマトンと入力コントローラを接続するために、オートマトンにイベントを送るメソッドを作る。 入力コントローラの仕様にあわせ、状態遷移が発生した場合は真を、そうでない場合は偽を返すようにする。
init() { ... automaton.replies.observeValues { switch $0 { case .success: self.handled = true default: () } } } func handle(_ input: UserInput) -> Bool { handled = false observer.send(value: input) return handled }
入力コントローラで、キー入力に応じて、このメソッドを呼びだす。
public override func handle(_ event: NSEvent!, client sender: Any!) -> Bool { if event.keyCode == 36 { return automaton.handle(.enter) } else if event.keyCode == 51 { return automaton.handle(.backspace) } else if let text = event.characters { return automaton.handle(.input(text: text)) } else { return false } }
未確定文字列・確定文字列の反映
オートマトンが持つ入力文字列、未確定文字列を監視し、更新があった際にクライアントアプリケーションに反映するようにする。これはReaciveSwiftのイベント監視の仕組みを用いる。
public override init!(server: IMKServer!, delegate: Any!, client inputClient: Any!) { super.init(server: server, delegate: delegate, client: inputClient) guard let client = inputClient as? IMKTextInput else { return } // 未確定文字列の反映 automaton.markedText.signal.observeValues { text in let notFound = NSRange(location: NSNotFound, length: NSNotFound) client.setMarkedText(text, selectionRange: notFound, replacementRange: notFound) } // 確定文字列の挿入 automaton.text.observeValues { let notFound = NSRange(location: NSNotFound, length: NSNotFound) client.insertText($0, replacementRange: notFound) } }