🍣入力メソッドを拡張し、テキストを入力し、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>] = [
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>] = [
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)
}
}