0
0
mirror of https://github.com/florisboard/florisboard.git synced 2024-09-19 19:42:20 +02:00

Rework KeyboardState and its observing logic (#2025)

This commit is contained in:
Patrick Goldinger 2022-08-30 00:58:07 +02:00
parent eb5fdbb08c
commit da5b316acd
No known key found for this signature in database
GPG Key ID: 533467C3DC7B9262
6 changed files with 87 additions and 133 deletions

View File

@ -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()

View File

@ -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(),

View File

@ -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<Toast>(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<KeyboardState> {
return activeState.observeAsNonNullState(neverEqualPolicy())
}
fun reevaluateInputShiftState() {
if (activeState.inputShiftState != InputShiftState.CAPS_LOCK && !inputEventDispatcher.isPressed(KeyCode.SHIFT)) {
val shift = prefs.correction.autoCapitalization.get()

View File

@ -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<KeyboardState>() {
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<KeyboardSta
const val STATE_ALL_ZERO: ULong = 0uL
const val INTEREST_ALL: ULong = ULong.MAX_VALUE
const val INTEREST_NONE: ULong = 0uL
const val BATCH_ZERO: Int = 0
fun new(value: ULong = STATE_ALL_ZERO) = KeyboardState(value)
}
private var rawValue by Delegates.observable(initValue) { _, old, new -> 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<KeyboardSta
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as KeyboardState
if (rawValue != other.rawValue) return false
return true
return other is KeyboardState && other.rawValue == rawValue
}
override fun toString(): String {
@ -309,3 +208,67 @@ class KeyboardState private constructor(initValue: ULong) : LiveData<KeyboardSta
get() = getFlag(F_DEBUG_SHOW_DRAG_AND_DROP_HELPERS)
set(v) { setFlag(F_DEBUG_SHOW_DRAG_AND_DROP_HELPERS, v) }
}
class ObservableKeyboardState private constructor(
initValue: ULong,
private val dispatchFlow: MutableStateFlow<KeyboardState> = MutableStateFlow(KeyboardState.new(initValue)),
) : KeyboardState(initValue), StateFlow<KeyboardState> 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()
}
}
}

View File

@ -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() },

View File

@ -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 {