package com.rizoma.trazo import android.inputmethodservice.InputMethodService import android.text.InputType import android.view.KeyEvent import android.view.View import android.view.inputmethod.EditorInfo import androidx.compose.ui.platform.ComposeView import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import androidx.lifecycle.ViewModelStore import androidx.lifecycle.ViewModelStoreOwner import androidx.lifecycle.setViewTreeLifecycleOwner import androidx.lifecycle.setViewTreeViewModelStoreOwner import androidx.savedstate.SavedStateRegistry import androidx.savedstate.SavedStateRegistryController import androidx.savedstate.SavedStateRegistryOwner import androidx.savedstate.setViewTreeSavedStateRegistryOwner import com.rizoma.trazo.data.SuggestionEngine import com.rizoma.trazo.ime.ImeHost import com.rizoma.trazo.ime.KeyboardController import com.rizoma.trazo.ui.KeyboardScreen import com.rizoma.trazo.ui.theme.TrazoTheme import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlin.math.abs /** * Trazo IME. Hospeda un teclado Compose dentro del InputMethodService, para lo cual * el servicio actúa como Lifecycle/ViewModelStore/SavedState owner del ComposeView. */ class TrazoImeService : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner { private val lifecycleRegistry = LifecycleRegistry(this) override val lifecycle: Lifecycle get() = lifecycleRegistry private val store = ViewModelStore() override val viewModelStore: ViewModelStore get() = store private val savedStateController = SavedStateRegistryController.create(this) override val savedStateRegistry: SavedStateRegistry get() = savedStateController.savedStateRegistry private val scope = CoroutineScope(Dispatchers.Main.immediate + SupervisorJob()) private var editorInputType: Int = InputType.TYPE_CLASS_TEXT private val host = object : ImeHost { override fun commit(text: String) { currentInputConnection?.commitText(text, 1) } override fun backspace() { val ic = currentInputConnection ?: return val selected = ic.getSelectedText(0) if (selected.isNullOrEmpty()) ic.deleteSurroundingText(1, 0) else ic.commitText("", 1) } override fun newline() { val info = currentInputEditorInfo val opt = (info?.imeOptions ?: 0) and EditorInfo.IME_MASK_ACTION val noEnterAction = ((info?.imeOptions ?: 0) and EditorInfo.IME_FLAG_NO_ENTER_ACTION) != 0 if (!noEnterAction && opt != EditorInfo.IME_ACTION_NONE && opt != EditorInfo.IME_ACTION_UNSPECIFIED) { currentInputConnection?.performEditorAction(opt) } else { currentInputConnection?.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER)) currentInputConnection?.sendKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER)) } } override fun setComposing(text: String) { currentInputConnection?.setComposingText(text, 1) } override fun finishComposing() { currentInputConnection?.finishComposingText() } override fun moveCursor(steps: Int) { val ic = currentInputConnection ?: return val code = if (steps > 0) KeyEvent.KEYCODE_DPAD_RIGHT else KeyEvent.KEYCODE_DPAD_LEFT repeat(abs(steps)) { ic.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, code)) ic.sendKeyEvent(KeyEvent(KeyEvent.ACTION_UP, code)) } } override fun textBeforeCursor(n: Int): CharSequence = currentInputConnection?.getTextBeforeCursor(n, 0) ?: "" override fun cursorCapsMode(): Int = currentInputConnection?.getCursorCapsMode(editorInputType) ?: 0 } private lateinit var engine: SuggestionEngine private lateinit var controller: KeyboardController override fun onCreate() { super.onCreate() savedStateController.performRestore(null) engine = SuggestionEngine(this, scope) controller = KeyboardController(host, engine) lifecycleRegistry.currentState = Lifecycle.State.CREATED } override fun onCreateInputView(): View { return ComposeView(this).apply { setViewTreeLifecycleOwner(this@TrazoImeService) setViewTreeViewModelStoreOwner(this@TrazoImeService) setViewTreeSavedStateRegistryOwner(this@TrazoImeService) setContent { TrazoTheme { KeyboardScreen(controller) } } } } override fun onStartInputView(info: EditorInfo?, restarting: Boolean) { super.onStartInputView(info, restarting) editorInputType = info?.inputType ?: InputType.TYPE_CLASS_TEXT controller.reset() controller.syncAutoShift() lifecycleRegistry.currentState = Lifecycle.State.RESUMED } override fun onUpdateSelection( oldSelStart: Int, oldSelEnd: Int, newSelStart: Int, newSelEnd: Int, candidatesStart: Int, candidatesEnd: Int, ) { super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, candidatesStart, candidatesEnd) // Si el editor ya no tiene región en composición pero nosotros creíamos tenerla, // significa que el cursor se movió fuera: abandonamos la palabra local. if (candidatesStart == -1 && controller.hasComposing()) { controller.abandonComposing() } } override fun onFinishInputView(finishingInput: Boolean) { super.onFinishInputView(finishingInput) lifecycleRegistry.currentState = Lifecycle.State.CREATED } override fun onDestroy() { lifecycleRegistry.currentState = Lifecycle.State.DESTROYED store.clear() scope.cancel() super.onDestroy() } }