tawasuyu

sergio/humanoid

HTML 37Markdown 1TOML 10 Sutichaykuna

⎇ 8380e44a0947bafb169773353ce2b2036aff9e70 keyboard/src/main/java/com/rizoma/trazo/TrazoImeService.kt 153 siq'ikuna · 6.0 KB Ñawpa willay ch'uya

Trazo: predicción, aprendizaje y gestos de espacio/borrado (v0.2) 8380e44a0 · 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.ime.ImeHost
22import com.rizoma.trazo.ime.KeyboardController
23import com.rizoma.trazo.ui.KeyboardScreen
24import com.rizoma.trazo.ui.theme.TrazoTheme
25import kotlinx.coroutines.CoroutineScope
26import kotlinx.coroutines.Dispatchers
27import kotlinx.coroutines.SupervisorJob
28import kotlinx.coroutines.cancel
29import kotlin.math.abs
30
31/**
32 * Trazo IME. Hospeda un teclado Compose dentro del InputMethodService, para lo cual
33 * el servicio actúa como Lifecycle/ViewModelStore/SavedState owner del ComposeView.
34 */
35class TrazoImeService : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner {
36
37    private val lifecycleRegistry = LifecycleRegistry(this)
38    override val lifecycle: Lifecycle get() = lifecycleRegistry
39
40    private val store = ViewModelStore()
41    override val viewModelStore: ViewModelStore get() = store
42
43    private val savedStateController = SavedStateRegistryController.create(this)
44    override val savedStateRegistry: SavedStateRegistry get() = savedStateController.savedStateRegistry
45
46    private val scope = CoroutineScope(Dispatchers.Main.immediate + SupervisorJob())
47
48    private var editorInputType: Int = InputType.TYPE_CLASS_TEXT
49
50    private val host = object : ImeHost {
51        override fun commit(text: String) {
52            currentInputConnection?.commitText(text, 1)
53        }
54
55        override fun backspace() {
56            val ic = currentInputConnection ?: return
57            val selected = ic.getSelectedText(0)
58            if (selected.isNullOrEmpty()) ic.deleteSurroundingText(1, 0) else ic.commitText("", 1)
59        }
60
61        override fun newline() {
62            val info = currentInputEditorInfo
63            val opt = (info?.imeOptions ?: 0) and EditorInfo.IME_MASK_ACTION
64            val noEnterAction = ((info?.imeOptions ?: 0) and EditorInfo.IME_FLAG_NO_ENTER_ACTION) != 0
65            if (!noEnterAction && opt != EditorInfo.IME_ACTION_NONE && opt != EditorInfo.IME_ACTION_UNSPECIFIED) {
66                currentInputConnection?.performEditorAction(opt)
67            } else {
68                currentInputConnection?.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER))
69                currentInputConnection?.sendKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER))
70            }
71        }
72
73        override fun setComposing(text: String) {
74            currentInputConnection?.setComposingText(text, 1)
75        }
76
77        override fun finishComposing() {
78            currentInputConnection?.finishComposingText()
79        }
80
81        override fun moveCursor(steps: Int) {
82            val ic = currentInputConnection ?: return
83            val code = if (steps > 0) KeyEvent.KEYCODE_DPAD_RIGHT else KeyEvent.KEYCODE_DPAD_LEFT
84            repeat(abs(steps)) {
85                ic.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, code))
86                ic.sendKeyEvent(KeyEvent(KeyEvent.ACTION_UP, code))
87            }
88        }
89
90        override fun textBeforeCursor(n: Int): CharSequence =
91            currentInputConnection?.getTextBeforeCursor(n, 0) ?: ""
92
93        override fun cursorCapsMode(): Int =
94            currentInputConnection?.getCursorCapsMode(editorInputType) ?: 0
95    }
96
97    private lateinit var engine: SuggestionEngine
98    private lateinit var controller: KeyboardController
99
100    override fun onCreate() {
101        super.onCreate()
102        savedStateController.performRestore(null)
103        engine = SuggestionEngine(this, scope)
104        controller = KeyboardController(host, engine)
105        lifecycleRegistry.currentState = Lifecycle.State.CREATED
106    }
107
108    override fun onCreateInputView(): View {
109        return ComposeView(this).apply {
110            setViewTreeLifecycleOwner(this@TrazoImeService)
111            setViewTreeViewModelStoreOwner(this@TrazoImeService)
112            setViewTreeSavedStateRegistryOwner(this@TrazoImeService)
113            setContent {
114                TrazoTheme {
115                    KeyboardScreen(controller)
116                }
117            }
118        }
119    }
120
121    override fun onStartInputView(info: EditorInfo?, restarting: Boolean) {
122        super.onStartInputView(info, restarting)
123        editorInputType = info?.inputType ?: InputType.TYPE_CLASS_TEXT
124        controller.reset()
125        controller.syncAutoShift()
126        lifecycleRegistry.currentState = Lifecycle.State.RESUMED
127    }
128
129    override fun onUpdateSelection(
130        oldSelStart: Int, oldSelEnd: Int,
131        newSelStart: Int, newSelEnd: Int,
132        candidatesStart: Int, candidatesEnd: Int,
133    ) {
134        super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, candidatesStart, candidatesEnd)
135        // Si el editor ya no tiene región en composición pero nosotros creíamos tenerla,
136        // significa que el cursor se movió fuera: abandonamos la palabra local.
137        if (candidatesStart == -1 && controller.hasComposing()) {
138            controller.abandonComposing()
139        }
140    }
141
142    override fun onFinishInputView(finishingInput: Boolean) {
143        super.onFinishInputView(finishingInput)
144        lifecycleRegistry.currentState = Lifecycle.State.CREATED
145    }
146
147    override fun onDestroy() {
148        lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
149        store.clear()
150        scope.cancel()
151        super.onDestroy()
152    }
153}