⎇ 5227fbf1abfb2a437968ebc63692560c3a929230
keyboard/src/main/java/com/rizoma/trazo/TrazoImeService.kt
History
raw
Trazo: arreglar cursor al reposicionar + mayúsculas en R↑ y columna reordenada
1package com.rizoma.trazo
2
3import android.inputmethodservice.InputMethodService
4import android.text.InputType
5import android.view.KeyEvent
6import android.view.View
7import android.view.inputmethod.EditorInfo
8import androidx.compose.ui.platform.ComposeView
9import androidx.lifecycle.Lifecycle
10import androidx.lifecycle.LifecycleOwner
11import androidx.lifecycle.LifecycleRegistry
12import androidx.lifecycle.ViewModelStore
13import androidx.lifecycle.ViewModelStoreOwner
14import androidx.lifecycle.setViewTreeLifecycleOwner
15import androidx.lifecycle.setViewTreeViewModelStoreOwner
16import androidx.savedstate.SavedStateRegistry
17import androidx.savedstate.SavedStateRegistryController
18import androidx.savedstate.SavedStateRegistryOwner
19import androidx.savedstate.setViewTreeSavedStateRegistryOwner
20import com.rizoma.trazo.data.SuggestionEngine
21import com.rizoma.trazo.data.TrazoSettings
22import com.rizoma.trazo.ime.Haptics
23import com.rizoma.trazo.ime.ImeHost
24import com.rizoma.trazo.ime.KeyboardController
25import com.rizoma.trazo.ui.KeyboardScreen
26import com.rizoma.trazo.ui.theme.TrazoTheme
27import kotlinx.coroutines.CoroutineScope
28import kotlinx.coroutines.Dispatchers
29import kotlinx.coroutines.SupervisorJob
30import kotlinx.coroutines.cancel
31import kotlin.math.abs
32
33/**
34 * Trazo IME. Hospeda un teclado Compose dentro del InputMethodService, para lo cual
35 * el servicio actúa como Lifecycle/ViewModelStore/SavedState owner del ComposeView.
36 */
37class TrazoImeService : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner {
38
39 private val lifecycleRegistry = LifecycleRegistry(this)
40 override val lifecycle: Lifecycle get() = lifecycleRegistry
41
42 private val store = ViewModelStore()
43 override val viewModelStore: ViewModelStore get() = store
44
45 private val savedStateController = SavedStateRegistryController.create(this)
46 override val savedStateRegistry: SavedStateRegistry get() = savedStateController.savedStateRegistry
47
48 private val scope = CoroutineScope(Dispatchers.Main.immediate + SupervisorJob())
49
50 private var editorInputType: Int = InputType.TYPE_CLASS_TEXT
51
52 private val host = object : ImeHost {
53 override fun commit(text: String) {
54 currentInputConnection?.commitText(text, 1)
55 }
56
57 override fun backspace() {
58 val ic = currentInputConnection ?: return
59 val selected = ic.getSelectedText(0)
60 if (selected.isNullOrEmpty()) ic.deleteSurroundingText(1, 0) else ic.commitText("", 1)
61 }
62
63 override fun newline() {
64 val info = currentInputEditorInfo
65 val opt = (info?.imeOptions ?: 0) and EditorInfo.IME_MASK_ACTION
66 val noEnterAction = ((info?.imeOptions ?: 0) and EditorInfo.IME_FLAG_NO_ENTER_ACTION) != 0
67 if (!noEnterAction && opt != EditorInfo.IME_ACTION_NONE && opt != EditorInfo.IME_ACTION_UNSPECIFIED) {
68 currentInputConnection?.performEditorAction(opt)
69 } else {
70 currentInputConnection?.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER))
71 currentInputConnection?.sendKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER))
72 }
73 }
74
75 override fun setComposing(text: String) {
76 currentInputConnection?.setComposingText(text, 1)
77 }
78
79 override fun finishComposing() {
80 currentInputConnection?.finishComposingText()
81 }
82
83 override fun moveCursor(steps: Int) {
84 val ic = currentInputConnection ?: return
85 val code = if (steps > 0) KeyEvent.KEYCODE_DPAD_RIGHT else KeyEvent.KEYCODE_DPAD_LEFT
86 repeat(abs(steps)) {
87 ic.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, code))
88 ic.sendKeyEvent(KeyEvent(KeyEvent.ACTION_UP, code))
89 }
90 }
91
92 override fun textBeforeCursor(n: Int): CharSequence =
93 currentInputConnection?.getTextBeforeCursor(n, 0) ?: ""
94
95 override fun cursorCapsMode(): Int =
96 currentInputConnection?.getCursorCapsMode(editorInputType) ?: 0
97 }
98
99 private lateinit var engine: SuggestionEngine
100 private lateinit var settings: TrazoSettings
101 private lateinit var controller: KeyboardController
102
103 override fun onCreate() {
104 super.onCreate()
105 savedStateController.performRestore(null)
106 engine = SuggestionEngine(this, scope)
107 settings = TrazoSettings(this, scope)
108 controller = KeyboardController(host, engine, settings, Haptics(this))
109 lifecycleRegistry.currentState = Lifecycle.State.CREATED
110 }
111
112 override fun onCreateInputView(): View {
113 // La ventana del IME es internamente un Dialog. Compose resuelve el
114 // ViewTreeLifecycleOwner desde el hijo de android.R.id.content y hacia arriba,
115 // así que los owners DEBEN estar en el decorView de la ventana (no basta el ComposeView).
116 window.window?.decorView?.let { decor ->
117 decor.setViewTreeLifecycleOwner(this)
118 decor.setViewTreeViewModelStoreOwner(this)
119 decor.setViewTreeSavedStateRegistryOwner(this)
120 }
121 return ComposeView(this).apply {
122 setViewTreeLifecycleOwner(this@TrazoImeService)
123 setViewTreeViewModelStoreOwner(this@TrazoImeService)
124 setViewTreeSavedStateRegistryOwner(this@TrazoImeService)
125 setContent {
126 TrazoTheme {
127 KeyboardScreen(controller, settings)
128 }
129 }
130 }
131 }
132
133 override fun onStartInputView(info: EditorInfo?, restarting: Boolean) {
134 super.onStartInputView(info, restarting)
135 editorInputType = info?.inputType ?: InputType.TYPE_CLASS_TEXT
136 controller.reset()
137 controller.syncAutoShift()
138 lifecycleRegistry.currentState = Lifecycle.State.RESUMED
139 }
140
141 override fun onUpdateSelection(
142 oldSelStart: Int, oldSelEnd: Int,
143 newSelStart: Int, newSelEnd: Int,
144 candidatesStart: Int, candidatesEnd: Int,
145 ) {
146 super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, candidatesStart, candidatesEnd)
147 if (!controller.hasComposing()) return
148 // Tras NUESTRA propia edición, el caret queda justo al final de la región
149 // en composición (sin selección). Cualquier otra cosa —un toque en el
150 // texto, una selección, o que el editor cerrara la composición
151 // (candidatesEnd == -1)— significa que el cursor se movió por el usuario:
152 // cerramos la palabra para que lo siguiente caiga donde tocó.
153 val nuestraEdicion = newSelStart == newSelEnd &&
154 candidatesEnd != -1 && newSelEnd == candidatesEnd
155 if (!nuestraEdicion) controller.finishComposingForCursorMove()
156 }
157
158 override fun onFinishInputView(finishingInput: Boolean) {
159 super.onFinishInputView(finishingInput)
160 lifecycleRegistry.currentState = Lifecycle.State.CREATED
161 }
162
163 override fun onDestroy() {
164 lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
165 store.clear()
166 scope.cancel()
167 super.onDestroy()
168 }
169}