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:
parent
eb5fdbb08c
commit
da5b316acd
@ -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()
|
||||
|
@ -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(),
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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() },
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user