azooKey icon indicating copy to clipboard operation
azooKey copied to clipboard

[Documentation] Get more detailed information of `markedText` via textDocumentProxy

Open ensan-hcl opened this issue 2 years ago • 4 comments
trafficstars

プライベートなAPIであること、特に現状必要がないことから使うことはなさそう。

// このcomputed propertyをKeyboardViewControllerに追加
@objc var markedText: NSString {
    fatalError("Do not directly call this getter")
}
// trueになる
debug("markedText", self.textDocumentProxy.responds(to: #selector(getter: markedText)))
let result = self.textDocumentProxy.perform(#selector(getter: markedText))
if let result {
    // marked textがある場合は値が返ってくる
    debug("markedText", result)
}

ensan-hcl avatar Aug 27 '23 01:08 ensan-hcl

これは使えるかもしれない!?!?!?

    private func getMarkedTextDetailedInformation() {
        // textDocumentProxyに対して`_controllerState`を得るようselectorでリクエストすると、UIInputViewControllerStateのインスタンスが得られる
        // このインスタンスのdescriptionをとると、`documentState = <TIDocumentState: 0x2812794c0; text = "なしあ|{{}ああ""|">;`のようなテキストが含まれる。
        // このテキストは周辺の行の状態を表しており、入力管理に利用できる
        // なお、同様にしてdocumentStateを取得することもできるが、クラッシュを招く。これは排他アクセスが失敗するからと見られるが、詳しい理由は不明。
        // 「text」は扱いづらい仕様であるが、うまく利用すれば適切にmarkedTextの状態をトラッキングするのに役立つ
        // 1. SelectedTextは[]で囲まれる
        // 2. MarkedTextの範囲は{}で囲まれる
        // 3. Cursorの位置は|で示される
        // 4. これらの特殊記号は特にエスケープされないので、重複しうる。
        let controllerStateSelector = sel_registerName("_controllerState")
        let isValidSelector = self.textDocumentProxy.responds(to: controllerStateSelector)
        guard isValidSelector else {
            debug("getDocumentState", controllerStateSelector, "is not a valid selector")
            return
        }
        let _uiInputViewControllerState = self.textDocumentProxy.perform(controllerStateSelector)
        debug("getDocumentState", _uiInputViewControllerState.debugDescription)
        let regex = #"<TIDocumentState:.*text = "(.*)">"#
        let captureRegex = try! NSRegularExpression(
            pattern: regex,
            options: []
        )
        let description = _uiInputViewControllerState.debugDescription
        let matches = captureRegex.matches(
            in: description,
            options: [],
            range: NSRange(description.startIndex ..< description.endIndex, in: description)
        )

        if let match = matches.first {
            debug("getDocumentState", match)
            // 0番目はmatch全体、captureは1番目に当たる。
            let matchRange = match.range(at: 1)
            debug("getDocumentState", matchRange)

            // Extract the substring matching the capture group
            if let substringRange = Range(matchRange, in: description) {
                let capture = String(description[substringRange])
                debug("getDocumentState", capture)
            }
        }
    }

ensan-hcl avatar Aug 27 '23 03:08 ensan-hcl

これが通るなら「入力中のテキストを保護」をデフォルト挙動にできちゃうぞ……

ensan-hcl avatar Aug 27 '23 03:08 ensan-hcl

情報のフォーマットがフォーマットなので綺麗にはできないけど、多分こんな感じでできそう。エッジケースの多い実装になると思うからフェイルセーフを徹底する。

  1. textの中に{...}というフォーマットの部分を探す
    • ない場合→Marked Textなし(return)
    • 1つある場合→そこがMarked Text
    • 2つ以上ある場合→絞り込みを実施
      • |をreplaceして消去し、DisplayedTextCotroller側が持っているdisplayedTextの|をreplaceして消去したものと一致するものを選ぶ。ただし|がそもそも含まれていない場合は除外する。
        • ない場合→諦め
        • 1つある場合→そこがMarked Text
        • 2つ以上ある場合→諦め
  2. Marked Textの中で|の位置を探す
    • ない場合→謎。諦め
    • 1つある場合→そこがカーソルの位置
    • 2つ以上ある場合→前にあるものから見ていき、displayedTextと比較して異なる位置にあるものを選ぶ。そこがカーソルの位置

ただし依然通知は来ないので、数十ミリ秒おきにこの値を確認するタイマーをかける必要がある。多分0.2秒に一回とかで十分。

ensan-hcl avatar Aug 27 '23 03:08 ensan-hcl

なので、どうしようもないケースとしてはたとえば以下のような例がある。

  • {a|aa} {aaa|} (後者はMarkedText、前者はただのテキスト)

これは十分レアだろうと推測できる。

ensan-hcl avatar Aug 27 '23 04:08 ensan-hcl