From da5b316acd898b02dbfc1c2186a962925b101f11 Mon Sep 17 00:00:00 2001 From: Patrick Goldinger Date: Tue, 30 Aug 2022 00:58:07 +0200 Subject: [PATCH] Rework KeyboardState and its observing logic (#2025) --- .../florisboard/FlorisImeService.kt | 6 +- .../ime/clipboard/ClipboardInputLayout.kt | 3 +- .../ime/keyboard/KeyboardManager.kt | 17 +- .../florisboard/ime/keyboard/KeyboardState.kt | 181 +++++++----------- .../ime/sheet/BottomSheetHostUi.kt | 9 +- .../florisboard/ime/text/TextInputLayout.kt | 4 +- 6 files changed, 87 insertions(+), 133 deletions(-) diff --git a/app/src/main/kotlin/dev/patrickgold/florisboard/FlorisImeService.kt b/app/src/main/kotlin/dev/patrickgold/florisboard/FlorisImeService.kt index 620a598a..8e499f12 100644 --- a/app/src/main/kotlin/dev/patrickgold/florisboard/FlorisImeService.kt +++ b/app/src/main/kotlin/dev/patrickgold/florisboard/FlorisImeService.kt @@ -553,10 +553,10 @@ class FlorisImeService : LifecycleInputMethodService() { @OptIn(ExperimentalComposeUiApi::class) @Composable private fun ImeUi() { - val activeState by keyboardManager.observeActiveState() + val state by keyboardManager.activeState.collectAsState() val keyboardStyle = FlorisImeTheme.style.get( element = FlorisImeUi.Keyboard, - mode = activeState.inputShiftState.value, + mode = state.inputShiftState.value, ) val layoutDirection = LocalLayoutDirection.current SideEffect { @@ -606,7 +606,7 @@ class FlorisImeService : LifecycleInputMethodService() { .weight(keyboardWeight) .wrapContentHeight(), ) { - when (activeState.imeUiMode) { + when (state.imeUiMode) { ImeUiMode.TEXT -> TextInputLayout() ImeUiMode.MEDIA -> MediaInputLayout() ImeUiMode.CLIPBOARD -> ClipboardInputLayout() diff --git a/app/src/main/kotlin/dev/patrickgold/florisboard/ime/clipboard/ClipboardInputLayout.kt b/app/src/main/kotlin/dev/patrickgold/florisboard/ime/clipboard/ClipboardInputLayout.kt index 3ec16e50..c39cc5d2 100644 --- a/app/src/main/kotlin/dev/patrickgold/florisboard/ime/clipboard/ClipboardInputLayout.kt +++ b/app/src/main/kotlin/dev/patrickgold/florisboard/ime/clipboard/ClipboardInputLayout.kt @@ -126,7 +126,6 @@ fun ClipboardInputLayout( val keyboardManager by context.keyboardManager() val androidKeyguardManager = remember { context.systemService(AndroidKeyguardManager::class) } - val activeState by keyboardManager.observeActiveState() val deviceLocked = androidKeyguardManager.let { it.isDeviceLocked || it.isKeyguardLocked } val historyEnabled by prefs.clipboard.historyEnabled.observeAsState() val history by clipboardManager.history.observeAsNonNullState() @@ -151,7 +150,7 @@ fun ClipboardInputLayout( verticalAlignment = Alignment.CenterVertically, ) { FlorisIconButtonWithInnerPadding( - onClick = { activeState.imeUiMode = ImeUiMode.TEXT }, + onClick = { keyboardManager.activeState.imeUiMode = ImeUiMode.TEXT }, modifier = Modifier.autoMirrorForRtl(), icon = painterResource(R.drawable.ic_arrow_back), iconColor = headerStyle.foreground.solidColor(), diff --git a/app/src/main/kotlin/dev/patrickgold/florisboard/ime/keyboard/KeyboardManager.kt b/app/src/main/kotlin/dev/patrickgold/florisboard/ime/keyboard/KeyboardManager.kt index 4cb348c3..92047cc0 100644 --- a/app/src/main/kotlin/dev/patrickgold/florisboard/ime/keyboard/KeyboardManager.kt +++ b/app/src/main/kotlin/dev/patrickgold/florisboard/ime/keyboard/KeyboardManager.kt @@ -20,11 +20,8 @@ import android.content.Context import android.icu.lang.UCharacter import android.view.KeyEvent import android.widget.Toast -import androidx.compose.runtime.Composable -import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.neverEqualPolicy import androidx.compose.runtime.setValue import androidx.lifecycle.MutableLiveData import dev.patrickgold.florisboard.FlorisImeService @@ -65,7 +62,6 @@ import dev.patrickgold.florisboard.lib.kotlin.collectIn import dev.patrickgold.florisboard.lib.kotlin.collectLatestIn import dev.patrickgold.florisboard.lib.kotlin.titlecase import dev.patrickgold.florisboard.lib.kotlin.uppercase -import dev.patrickgold.florisboard.lib.observeAsNonNullState import dev.patrickgold.florisboard.lib.util.InputMethodUtils import dev.patrickgold.florisboard.nlpManager import dev.patrickgold.florisboard.subtypeManager @@ -95,7 +91,7 @@ class KeyboardManager(context: Context) : InputKeyEventReceiver { private val keyboardCache = TextKeyboardCache() val resources = KeyboardManagerResources() - val activeState = KeyboardState.new() + val activeState = ObservableKeyboardState.new() var smartbarVisibleDynamicActionsCount by mutableStateOf(0) private var lastToastReference = WeakReference(null) @@ -142,7 +138,7 @@ class KeyboardManager(context: Context) : InputKeyEventReceiver { prefs.keyboard.utilityKeyEnabled.observeForever { updateActiveEvaluators() } - activeState.observeForever { + activeState.collectLatestIn(scope) { updateActiveEvaluators() } subtypeManager.activeSubtypeFlow.collectLatestIn(scope) { @@ -174,7 +170,7 @@ class KeyboardManager(context: Context) : InputKeyEventReceiver { val editorInfo = editorInstance.activeInfo val state = activeState.snapshot() val subtype = subtypeManager.activeSubtype - val mode = activeState.keyboardMode + val mode = state.keyboardMode // We need to reset the snapshot input shift state for non-character layouts, because the shift mechanic // only makes sense for the character layouts. if (mode != KeyboardMode.CHARACTERS) { @@ -199,17 +195,12 @@ class KeyboardManager(context: Context) : InputKeyEventReceiver { } _activeEvaluator.value = computingEvaluator _activeSmartbarEvaluator.value = computingEvaluator.asSmartbarQuickActionsEvaluator() - if (computingEvaluator.keyboard.mode == KeyboardMode.CHARACTERS) { + if (computedKeyboard.mode == KeyboardMode.CHARACTERS) { _lastCharactersEvaluator.value = computingEvaluator } } } - @Composable - fun observeActiveState(): State { - return activeState.observeAsNonNullState(neverEqualPolicy()) - } - fun reevaluateInputShiftState() { if (activeState.inputShiftState != InputShiftState.CAPS_LOCK && !inputEventDispatcher.isPressed(KeyCode.SHIFT)) { val shift = prefs.correction.autoCapitalization.get() diff --git a/app/src/main/kotlin/dev/patrickgold/florisboard/ime/keyboard/KeyboardState.kt b/app/src/main/kotlin/dev/patrickgold/florisboard/ime/keyboard/KeyboardState.kt index 0d11d3e0..aec2d7c1 100644 --- a/app/src/main/kotlin/dev/patrickgold/florisboard/ime/keyboard/KeyboardState.kt +++ b/app/src/main/kotlin/dev/patrickgold/florisboard/ime/keyboard/KeyboardState.kt @@ -14,18 +14,14 @@ * limitations under the License. */ -@file:Suppress("MemberVisibilityCanBePrivate") - package dev.patrickgold.florisboard.ime.keyboard -import android.annotation.SuppressLint -import androidx.arch.core.executor.ArchTaskExecutor import androidx.compose.ui.unit.LayoutDirection -import androidx.lifecycle.LiveData import dev.patrickgold.florisboard.ime.ImeUiMode import dev.patrickgold.florisboard.ime.input.InputShiftState import dev.patrickgold.florisboard.ime.text.key.KeyVariation -import dev.patrickgold.florisboard.lib.devtools.flogError +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import java.util.concurrent.atomic.AtomicInteger import kotlin.contracts.InvocationKind import kotlin.contracts.contract @@ -69,7 +65,7 @@ import kotlin.properties.Delegates * @property rawValue The internal register used to store the flags and region ints that * this keyboard state represents. */ -class KeyboardState private constructor(initValue: ULong) : LiveData() { +open class KeyboardState protected constructor(open var rawValue: ULong) { companion object { const val M_KEYBOARD_MODE: ULong = 0x0Fu const val O_KEYBOARD_MODE: Int = 0 @@ -99,116 +95,26 @@ class KeyboardState private constructor(initValue: ULong) : LiveData if (old != new) dispatchState() } - private val batchEditCount = AtomicInteger(BATCH_ZERO) - - init { - dispatchState() - } - - override fun setValue(value: KeyboardState?) { - flogError { "Do not use setValue() directly" } - } - - override fun postValue(value: KeyboardState?) { - flogError { "Do not use postValue() directly" } - } - - /** - * Dispatches the new state to all observers if [batchEditCount] is [BATCH_ZERO] (= no active batch edits). - */ - @SuppressLint("RestrictedApi") - private fun dispatchState() { - if (batchEditCount.get() == BATCH_ZERO) { - if (ArchTaskExecutor.getInstance().isMainThread) { - super.setValue(this) - } else { - super.postValue(this) - } - } - } - - /** - * Begins a batch edit. Any modifications done during an active batch edit will not be dispatched to observers - * until [endBatchEdit] is called. At any time given there can be multiple active batch edits at once. This - * method is thread-safe and can be called from any thread. - */ - fun beginBatchEdit() { - batchEditCount.incrementAndGet() - } - - /** - * Ends a batch edit. Will dispatch the current state if there are no more other batch edits active. This method is - * thread-safe and can be called from any thread. - */ - fun endBatchEdit() { - batchEditCount.decrementAndGet() - dispatchState() - } - - /** - * Performs a batch edit by executing the modifier [block]. Any exception that [block] throws will be caught and - * re-thrown after correctly ending the batch edit. - */ - inline fun batchEdit(block: (KeyboardState) -> Unit) { - contract { - callsInPlace(block, InvocationKind.EXACTLY_ONCE) - } - beginBatchEdit() - try { - block(this) - } catch (e: Throwable) { - throw e - } finally { - endBatchEdit() - } - } - - /** - * Resets this state register. - * - * @param newValue Optional, used to initialize the register value after the reset. - * Defaults to [STATE_ALL_ZERO]. - */ - fun reset(newValue: ULong = STATE_ALL_ZERO) { - rawValue = newValue - } - - /** - * Resets this state register. - * - * @param newState A reference to a state which register value should be copied after - * the reset. - */ - fun reset(newState: KeyboardState) { - rawValue = newState.rawValue - } - fun snapshot(): KeyboardState { return new(rawValue) } - internal fun getFlag(f: ULong): Boolean { + private fun getFlag(f: ULong): Boolean { return (rawValue and f) != STATE_ALL_ZERO } - internal fun setFlag(f: ULong, v: Boolean) { + private fun setFlag(f: ULong, v: Boolean) { rawValue = if (v) { rawValue or f } else { rawValue and f.inv() } } - internal fun getRegion(m: ULong, o: Int): Int { + private fun getRegion(m: ULong, o: Int): Int { return ((rawValue shr o) and m).toInt() } - internal fun setRegion(m: ULong, o: Int, v: Int) { + private fun setRegion(m: ULong, o: Int, v: Int) { rawValue = (rawValue and (m shl o).inv()) or ((v.toULong() and m) shl o) } @@ -217,14 +123,7 @@ class KeyboardState private constructor(initValue: ULong) : LiveData = MutableStateFlow(KeyboardState.new(initValue)), +) : KeyboardState(initValue), StateFlow by dispatchFlow { + + companion object { + const val BATCH_ZERO: Int = 0 + + fun new(value: ULong = STATE_ALL_ZERO) = ObservableKeyboardState(value) + } + + override var rawValue by Delegates.observable(initValue) { _, old, new -> if (old != new) dispatchState() } + private val batchEditCount = AtomicInteger(BATCH_ZERO) + + init { + dispatchState() + } + + /** + * Dispatches the new state to all observers if [batchEditCount] is [BATCH_ZERO] (= no active batch edits). + */ + private fun dispatchState() { + if (batchEditCount.get() == BATCH_ZERO) { + dispatchFlow.value = this.snapshot() + } + } + + /** + * Begins a batch edit. Any modifications done during an active batch edit will not be dispatched to observers + * until [endBatchEdit] is called. At any time given there can be multiple active batch edits at once. This + * method is thread-safe and can be called from any thread. + */ + fun beginBatchEdit() { + batchEditCount.incrementAndGet() + } + + /** + * Ends a batch edit. Will dispatch the current state if there are no more other batch edits active. This method is + * thread-safe and can be called from any thread. + */ + fun endBatchEdit() { + batchEditCount.decrementAndGet() + dispatchState() + } + + /** + * Performs a batch edit by executing the modifier [block]. Any exception that [block] throws will be caught and + * re-thrown after correctly ending the batch edit. + */ + inline fun batchEdit(block: (ObservableKeyboardState) -> Unit) { + contract { + callsInPlace(block, InvocationKind.EXACTLY_ONCE) + } + beginBatchEdit() + try { + block(this) + } catch (e: Throwable) { + throw e + } finally { + endBatchEdit() + } + } +} diff --git a/app/src/main/kotlin/dev/patrickgold/florisboard/ime/sheet/BottomSheetHostUi.kt b/app/src/main/kotlin/dev/patrickgold/florisboard/ime/sheet/BottomSheetHostUi.kt index 24fcd5b2..15dbe2d6 100644 --- a/app/src/main/kotlin/dev/patrickgold/florisboard/ime/sheet/BottomSheetHostUi.kt +++ b/app/src/main/kotlin/dev/patrickgold/florisboard/ime/sheet/BottomSheetHostUi.kt @@ -27,6 +27,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -48,9 +49,9 @@ private val DialogContentExitTransition = slideOutVertically { it } fun BottomSheetHostUi() { val context = LocalContext.current val keyboardManager by context.keyboardManager() - val activeState by keyboardManager.observeActiveState() + val state by keyboardManager.activeState.collectAsState() - val isBottomSheetShowing = activeState.isBottomSheetShowing() + val isBottomSheetShowing = state.isBottomSheetShowing() val bgColorOutOfBounds by animateColorAsState( if (isBottomSheetShowing) SheetOutOfBoundsBgColorActive else SheetOutOfBoundsBgColorInactive ) @@ -64,7 +65,7 @@ fun BottomSheetHostUi() { .then(if (isBottomSheetShowing) { Modifier.pointerInput(Unit) { detectTapGestures { - activeState.isActionsEditorVisible = false + keyboardManager.activeState.isActionsEditorVisible = false } } } else { @@ -72,7 +73,7 @@ fun BottomSheetHostUi() { }), ) AnimatedVisibility( - visible = activeState.isActionsEditorVisible, + visible = state.isActionsEditorVisible, enter = DialogContentEnterTransition, exit = DialogContentExitTransition, content = { QuickActionsEditorPanel() }, diff --git a/app/src/main/kotlin/dev/patrickgold/florisboard/ime/text/TextInputLayout.kt b/app/src/main/kotlin/dev/patrickgold/florisboard/ime/text/TextInputLayout.kt index 3fae3a83..ca23815c 100644 --- a/app/src/main/kotlin/dev/patrickgold/florisboard/ime/text/TextInputLayout.kt +++ b/app/src/main/kotlin/dev/patrickgold/florisboard/ime/text/TextInputLayout.kt @@ -49,7 +49,7 @@ fun TextInputLayout( val context = LocalContext.current val keyboardManager by context.keyboardManager() - val activeState by keyboardManager.observeActiveState() + val state by keyboardManager.activeState.collectAsState() val evaluator by keyboardManager.activeEvaluator.collectAsState() CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { @@ -59,7 +59,7 @@ fun TextInputLayout( .wrapContentHeight(), ) { Smartbar() - if (activeState.isActionsOverflowVisible) { + if (state.isActionsOverflowVisible) { QuickActionsOverflowPanel() } else { Box {