tawasuyu

sergio/humanoid

HTML 38Markdown 1TOML 10 Definitions

⎇ b021d6829a602218866114f86e2350d80f0500e2 keyboard/src/main/java/com/rizoma/trazo/ime/KeyboardController.kt 218 lines · 6.6 KB History raw

Trazo: pulido v0.4 — háptica, palabra siguiente, emojis y ajustes b021d6829 · 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    /** El editor terminó la composición por su cuenta (cursor movido fuera, etc.). */
179    fun abandonComposing() {
180        if (composing.isEmpty()) return
181        composing.clear()
182        suggestions = emptyList()
183        lastWord = null
184    }
185
186    fun hasComposing(): Boolean = composing.isNotEmpty()
187
188    /** Recalcula la mayúscula automática según el contexto del editor. */
189    fun syncAutoShift() {
190        if (shift == ShiftState.LOCK) return
191        shift = if (host.cursorCapsMode() != 0) ShiftState.ON else ShiftState.OFF
192    }
193
194    private fun haptic() {
195        if (settings.haptics) haptics.tap(settings.hapticLevel)
196    }
197
198    private fun finishWord() {
199        if (composing.isNotEmpty()) {
200            host.finishComposing()
201            val word = composing.toString()
202            engine.learn(word)
203            lastWord?.let { engine.learnBigram(it, word) }
204            lastWord = word.lowercase().takeIf { it.length >= 2 && it.all { c -> c.isLetter() } }
205            composing.clear()
206        }
207        suggestions = emptyList()
208    }
209
210    private fun showNextWords() {
211        val prev = lastWord
212        suggestions = if (prev != null) engine.nextWords(prev, 3) else emptyList()
213    }
214
215    private fun refreshSuggestions() {
216        suggestions = engine.suggest(composing.toString(), settings.spanish, settings.english, 3)
217    }
218}