tawasuyu

sergio/humanoid

HTML 13TOML 10 Definiciones

⎇ 5227fbf1abfb2a437968ebc63692560c3a929230 keyboard/src/main/java/com/rizoma/trazo/ime/KeyboardController.kt 227 líneas · 7.0 KB Historia crudo

Trazo: arreglar cursor al reposicionar + mayúsculas en R↑ y columna reordenada 5227fbf1a · Sergio ·

1package com.rizoma.trazo.ime
2
3import androidx.compose.runtime.getValue
4import androidx.compose.runtime.mutableStateOf
5import androidx.compose.runtime.setValue
6import com.rizoma.trazo.data.HapticLevel
7import com.rizoma.trazo.data.SuggestionEngine
8import com.rizoma.trazo.data.TrazoSettings
9import com.rizoma.trazo.model.Layouts
10
11/** Puente con el editor real. Lo implementa el InputMethodService. */
12interface ImeHost {
13    fun commit(text: String)
14    fun backspace()
15    fun newline()
16    fun setComposing(text: String)
17    fun finishComposing()
18    fun moveCursor(steps: Int)
19    fun textBeforeCursor(n: Int): CharSequence
20    /** TextUtils.CAP_MODE_* combinado: !=0 si el editor quiere mayúscula aquí. */
21    fun cursorCapsMode(): Int
22}
23
24enum class ShiftState { OFF, ON, LOCK }
25enum class Layer { LETTERS, SYMBOLS, EMOJIS }
26
27/**
28 * Toda la lógica de escritura. La palabra en curso vive como "texto en composición"
29 * del editor (subrayada) para poder reemplazarla por una sugerencia de un toque.
30 */
31class KeyboardController(
32    private val host: ImeHost,
33    private val engine: SuggestionEngine,
34    private val settings: TrazoSettings,
35    private val haptics: Haptics,
36) {
37
38    var shift by mutableStateOf(ShiftState.ON)
39        private set
40    var layer by mutableStateOf(Layer.LETTERS)
41        private set
42    var suggestions by mutableStateOf<List<String>>(emptyList())
43        private set
44
45    private val composing = StringBuilder()
46    // Última palabra cerrada: ancla de los bigramas para predecir la siguiente.
47    private var lastWord: String? = null
48
49    val keys get() = if (layer == Layer.SYMBOLS) Layouts.symbols else Layouts.letters
50    val shifted get() = shift != ShiftState.OFF
51
52    fun onChar(raw: Char) {
53        haptic()
54        val out = if (shifted) raw.uppercaseChar() else raw
55        if (layer == Layer.LETTERS && raw.isLetter()) {
56            composing.append(out)
57            if (shift == ShiftState.ON) shift = ShiftState.OFF
58            host.setComposing(composing.toString())
59            refreshSuggestions()
60        } else {
61            finishWord()
62            host.commit(out.toString())
63            if (shift == ShiftState.ON) shift = ShiftState.OFF
64            syncAutoShift()
65        }
66    }
67
68    fun onSpace() {
69        haptic()
70        finishWord()
71        val before = host.textBeforeCursor(2)
72        if (settings.doubleSpacePeriod &&
73            before.length == 2 && before[1] == ' ' && before[0].isLetterOrDigit()
74        ) {
75            host.backspace()
76            host.commit(". ")
77        } else {
78            host.commit(" ")
79        }
80        syncAutoShift()
81        showNextWords()
82    }
83
84    fun onBackspace() {
85        haptic()
86        if (composing.isNotEmpty()) {
87            composing.deleteCharAt(composing.length - 1)
88            if (composing.isEmpty()) {
89                host.finishComposing()
90                suggestions = emptyList()
91            } else {
92                host.setComposing(composing.toString())
93                refreshSuggestions()
94            }
95        } else {
96            host.backspace()
97            lastWord = null
98            suggestions = emptyList()
99            syncAutoShift()
100        }
101    }
102
103    fun onEnter() {
104        haptic()
105        finishWord()
106        host.newline()
107        lastWord = null
108        syncAutoShift()
109    }
110
111    fun onShift() {
112        haptic()
113        shift = when (shift) {
114            ShiftState.OFF -> ShiftState.ON
115            ShiftState.ON -> ShiftState.LOCK
116            ShiftState.LOCK -> ShiftState.OFF
117        }
118    }
119
120    /** Alterna letras ↔ símbolos (desde emojis, vuelve a letras). */
121    fun toggleLayer() {
122        haptic()
123        finishWord()
124        layer = if (layer == Layer.LETTERS) Layer.SYMBOLS else Layer.LETTERS
125    }
126
127    fun showEmojis() {
128        haptic()
129        finishWord()
130        layer = Layer.EMOJIS
131    }
132
133    /** Inserta un emoji y lo recuerda como reciente. */
134    fun onEmoji(emoji: String) {
135        haptic()
136        finishWord()
137        host.commit(emoji)
138        settings.pushRecentEmoji(emoji)
139        lastWord = null
140        suggestions = emptyList()
141    }
142
143    /**
144     * Elige una sugerencia. Si hay palabra en composición, la reemplaza; si no, es una
145     * predicción de palabra siguiente y se inserta en el cursor. En ambos casos refuerza
146     * el bigrama y encadena nuevas predicciones.
147     */
148    fun commitSuggestion(word: String) {
149        haptic()
150        val capitalize = composing.firstOrNull()?.isUpperCase() == true ||
151            (composing.isEmpty() && shifted)
152        val cased = if (capitalize) word.replaceFirstChar { it.uppercaseChar() } else word
153        host.commit("$cased ") // commitText reemplaza la región en composición si la hay
154        lastWord?.let { engine.learnBigram(it, word) }
155        engine.learn(word)
156        lastWord = word.lowercase()
157        composing.clear()
158        syncAutoShift()
159        showNextWords()
160    }
161
162    fun moveCursor(steps: Int) {
163        if (settings.haptics) haptics.tap(HapticLevel.SUAVE)
164        if (composing.isNotEmpty()) finishWord()
165        host.moveCursor(steps)
166        lastWord = null
167        suggestions = emptyList()
168        syncAutoShift()
169    }
170
171    /** Nuevo campo de entrada: descarta cualquier composición local. */
172    fun reset() {
173        composing.clear()
174        suggestions = emptyList()
175        lastWord = null
176    }
177
178    /**
179     * El cursor se movió por acción del usuario (un toque en el texto) con una
180     * palabra a medio componer. Finalizamos la composición EN SU SITIO con
181     * [ImeHost.finishComposing]: si no, el editor conserva la región vieja y el
182     * siguiente carácter la reemplazaría, devolviendo el cursor a la posición
183     * anterior. Tras cerrarla, lo que se escriba cae donde tocó el usuario.
184     */
185    fun finishComposingForCursorMove() {
186        if (composing.isNotEmpty()) {
187            host.finishComposing()
188            composing.clear()
189        }
190        suggestions = emptyList()
191        lastWord = null
192        syncAutoShift()
193    }
194
195    fun hasComposing(): Boolean = composing.isNotEmpty()
196
197    /** Recalcula la mayúscula automática según el contexto del editor. */
198    fun syncAutoShift() {
199        if (shift == ShiftState.LOCK) return
200        shift = if (host.cursorCapsMode() != 0) ShiftState.ON else ShiftState.OFF
201    }
202
203    private fun haptic() {
204        if (settings.haptics) haptics.tap(settings.hapticLevel)
205    }
206
207    private fun finishWord() {
208        if (composing.isNotEmpty()) {
209            host.finishComposing()
210            val word = composing.toString()
211            engine.learn(word)
212            lastWord?.let { engine.learnBigram(it, word) }
213            lastWord = word.lowercase().takeIf { it.length >= 2 && it.all { c -> c.isLetter() } }
214            composing.clear()
215        }
216        suggestions = emptyList()
217    }
218
219    private fun showNextWords() {
220        val prev = lastWord
221        suggestions = if (prev != null) engine.nextWords(prev, 3) else emptyList()
222    }
223
224    private fun refreshSuggestions() {
225        suggestions = engine.suggest(composing.toString(), settings.spanish, settings.english, 3)
226    }
227}