tawasuyu

sergio/humanoid

HTML 13TOML 10 Sutichaykuna

⎇ b021d6829a602218866114f86e2350d80f0500e2 keyboard/src/main/java/com/rizoma/trazo/TrazoImeService.kt 165 siq'ikuna · 6.7 KB Ñawpa willay ch'uya

Trazo: pulido v0.4 — háptica, palabra siguiente, emojis y ajustes b021d6829 · sergio ·

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        // Si el editor ya no tiene región en composición pero nosotros creíamos tenerla,
148        // significa que el cursor se movió fuera: abandonamos la palabra local.
149        if (candidatesStart == -1 && controller.hasComposing()) {
150            controller.abandonComposing()
151        }
152    }
153
154    override fun onFinishInputView(finishingInput: Boolean) {
155        super.onFinishInputView(finishingInput)
156        lifecycleRegistry.currentState = Lifecycle.State.CREATED
157    }
158
159    override fun onDestroy() {
160        lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
161        store.clear()
162        scope.cancel()
163        super.onDestroy()
164    }
165}