From 9d4ea1f7c1d19f959ece881ce7e325fb70f109f9 Mon Sep 17 00:00:00 2001 From: Aleksandras Kostarevas Date: Tue, 22 Aug 2023 23:19:57 +0300 Subject: [PATCH] Move certain things into separate files --- .../inputmethod/keyboard/KeyboardView.java | 4 +- .../keyboard/MainKeyboardView.java | 2 +- .../internal/KeyPreviewDrawParams.java | 2 +- .../internal/KeyVisualAttributes.java | 2 +- .../keyboard/internal/KeyboardBuilder.java | 4 +- .../keyboard/internal/KeyboardIconsSet.java | 2 +- .../org/futo/inputmethod/latin/LatinIME.kt | 463 +++++------------- .../inputmethod/latin/LatinIMELegacy.java | 1 + .../latin/uix/BasicThemeProvider.kt | 221 +++++++++ .../latin/uix/DynamicThemeProvider.kt | 34 ++ .../latin/uix/DynamicThemeProviderOwner.kt | 5 + .../futo/inputmethod/latin/uix/Settings.kt | 73 +++ 12 files changed, 456 insertions(+), 357 deletions(-) create mode 100644 java/src/org/futo/inputmethod/latin/uix/BasicThemeProvider.kt create mode 100644 java/src/org/futo/inputmethod/latin/uix/DynamicThemeProvider.kt create mode 100644 java/src/org/futo/inputmethod/latin/uix/DynamicThemeProviderOwner.kt create mode 100644 java/src/org/futo/inputmethod/latin/uix/Settings.kt diff --git a/java/src/org/futo/inputmethod/keyboard/KeyboardView.java b/java/src/org/futo/inputmethod/keyboard/KeyboardView.java index 116ad922e..e95878ec5 100644 --- a/java/src/org/futo/inputmethod/keyboard/KeyboardView.java +++ b/java/src/org/futo/inputmethod/keyboard/KeyboardView.java @@ -35,8 +35,8 @@ import android.view.View; import org.futo.inputmethod.keyboard.internal.KeyDrawParams; import org.futo.inputmethod.keyboard.internal.KeyVisualAttributes; -import org.futo.inputmethod.latin.DynamicThemeProvider; -import org.futo.inputmethod.latin.DynamicThemeProviderOwner; +import org.futo.inputmethod.latin.uix.DynamicThemeProvider; +import org.futo.inputmethod.latin.uix.DynamicThemeProviderOwner; import org.futo.inputmethod.latin.R; import org.futo.inputmethod.latin.common.Constants; import org.futo.inputmethod.latin.utils.TypefaceUtils; diff --git a/java/src/org/futo/inputmethod/keyboard/MainKeyboardView.java b/java/src/org/futo/inputmethod/keyboard/MainKeyboardView.java index b53b4c1eb..a7808cc04 100644 --- a/java/src/org/futo/inputmethod/keyboard/MainKeyboardView.java +++ b/java/src/org/futo/inputmethod/keyboard/MainKeyboardView.java @@ -50,7 +50,7 @@ import org.futo.inputmethod.keyboard.internal.MoreKeySpec; import org.futo.inputmethod.keyboard.internal.NonDistinctMultitouchHelper; import org.futo.inputmethod.keyboard.internal.SlidingKeyInputDrawingPreview; import org.futo.inputmethod.keyboard.internal.TimerHandler; -import org.futo.inputmethod.latin.DynamicThemeProvider; +import org.futo.inputmethod.latin.uix.DynamicThemeProvider; import org.futo.inputmethod.latin.R; import org.futo.inputmethod.latin.RichInputMethodSubtype; import org.futo.inputmethod.latin.SuggestedWords; diff --git a/java/src/org/futo/inputmethod/keyboard/internal/KeyPreviewDrawParams.java b/java/src/org/futo/inputmethod/keyboard/internal/KeyPreviewDrawParams.java index 93b00b4fd..004930374 100644 --- a/java/src/org/futo/inputmethod/keyboard/internal/KeyPreviewDrawParams.java +++ b/java/src/org/futo/inputmethod/keyboard/internal/KeyPreviewDrawParams.java @@ -26,7 +26,7 @@ import android.view.View; import android.view.animation.AccelerateInterpolator; import android.view.animation.DecelerateInterpolator; -import org.futo.inputmethod.latin.DynamicThemeProvider; +import org.futo.inputmethod.latin.uix.DynamicThemeProvider; import org.futo.inputmethod.latin.R; public final class KeyPreviewDrawParams { diff --git a/java/src/org/futo/inputmethod/keyboard/internal/KeyVisualAttributes.java b/java/src/org/futo/inputmethod/keyboard/internal/KeyVisualAttributes.java index ad0388369..dd5e8704c 100644 --- a/java/src/org/futo/inputmethod/keyboard/internal/KeyVisualAttributes.java +++ b/java/src/org/futo/inputmethod/keyboard/internal/KeyVisualAttributes.java @@ -20,7 +20,7 @@ import android.content.res.TypedArray; import android.graphics.Typeface; import android.util.SparseIntArray; -import org.futo.inputmethod.latin.DynamicThemeProvider; +import org.futo.inputmethod.latin.uix.DynamicThemeProvider; import org.futo.inputmethod.latin.R; import org.futo.inputmethod.latin.utils.ResourceUtils; diff --git a/java/src/org/futo/inputmethod/keyboard/internal/KeyboardBuilder.java b/java/src/org/futo/inputmethod/keyboard/internal/KeyboardBuilder.java index 9e07dcb5b..0c383a8fd 100644 --- a/java/src/org/futo/inputmethod/keyboard/internal/KeyboardBuilder.java +++ b/java/src/org/futo/inputmethod/keyboard/internal/KeyboardBuilder.java @@ -33,8 +33,8 @@ import org.futo.inputmethod.keyboard.Key; import org.futo.inputmethod.keyboard.Keyboard; import org.futo.inputmethod.keyboard.KeyboardId; import org.futo.inputmethod.keyboard.KeyboardTheme; -import org.futo.inputmethod.latin.DynamicThemeProvider; -import org.futo.inputmethod.latin.DynamicThemeProviderOwner; +import org.futo.inputmethod.latin.uix.DynamicThemeProvider; +import org.futo.inputmethod.latin.uix.DynamicThemeProviderOwner; import org.futo.inputmethod.latin.R; import org.futo.inputmethod.latin.common.Constants; import org.futo.inputmethod.latin.common.StringUtils; diff --git a/java/src/org/futo/inputmethod/keyboard/internal/KeyboardIconsSet.java b/java/src/org/futo/inputmethod/keyboard/internal/KeyboardIconsSet.java index 3e4a1ff9c..100b2d4eb 100644 --- a/java/src/org/futo/inputmethod/keyboard/internal/KeyboardIconsSet.java +++ b/java/src/org/futo/inputmethod/keyboard/internal/KeyboardIconsSet.java @@ -22,7 +22,7 @@ import android.graphics.drawable.Drawable; import android.util.Log; import android.util.SparseIntArray; -import org.futo.inputmethod.latin.DynamicThemeProvider; +import org.futo.inputmethod.latin.uix.DynamicThemeProvider; import org.futo.inputmethod.latin.R; import java.util.HashMap; diff --git a/java/src/org/futo/inputmethod/latin/LatinIME.kt b/java/src/org/futo/inputmethod/latin/LatinIME.kt index abb14d0eb..8f0d39310 100644 --- a/java/src/org/futo/inputmethod/latin/LatinIME.kt +++ b/java/src/org/futo/inputmethod/latin/LatinIME.kt @@ -1,58 +1,35 @@ package org.futo.inputmethod.latin -import android.content.Context import android.content.res.Configuration -import android.content.res.TypedArray -import android.graphics.Color -import android.graphics.drawable.Drawable -import android.graphics.drawable.GradientDrawable -import android.graphics.drawable.LayerDrawable -import android.graphics.drawable.ShapeDrawable -import android.graphics.drawable.StateListDrawable -import android.graphics.drawable.shapes.RoundRectShape import android.inputmethodservice.InputMethodService -import android.util.TypedValue import android.view.KeyEvent import android.view.View import android.view.inputmethod.CompletionInfo import android.view.inputmethod.EditorInfo -import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodSubtype -import androidx.annotation.ColorInt -import androidx.appcompat.content.res.AppCompatResources import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.material3.Button import androidx.compose.material3.ColorScheme import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.key -import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.core.stringPreferencesKey -import androidx.datastore.preferences.preferencesDataStore import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry @@ -60,7 +37,6 @@ import androidx.lifecycle.ViewModelStore import androidx.lifecycle.ViewModelStoreOwner import androidx.lifecycle.findViewTreeLifecycleOwner import androidx.lifecycle.findViewTreeViewModelStoreOwner -import androidx.lifecycle.lifecycleScope import androidx.lifecycle.setViewTreeLifecycleOwner import androidx.lifecycle.setViewTreeViewModelStoreOwner import androidx.savedstate.SavedStateRegistry @@ -68,20 +44,18 @@ import androidx.savedstate.SavedStateRegistryController import androidx.savedstate.SavedStateRegistryOwner import androidx.savedstate.findViewTreeSavedStateRegistryOwner import androidx.savedstate.setViewTreeSavedStateRegistryOwner -import com.google.android.material.color.DynamicColors -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.take -import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext import org.futo.inputmethod.latin.common.Constants import org.futo.inputmethod.latin.uix.Action import org.futo.inputmethod.latin.uix.ActionBar +import org.futo.inputmethod.latin.uix.BasicThemeProvider +import org.futo.inputmethod.latin.uix.DynamicThemeProvider +import org.futo.inputmethod.latin.uix.DynamicThemeProviderOwner import org.futo.inputmethod.latin.uix.KeyboardManagerForAction +import org.futo.inputmethod.latin.uix.THEME_KEY +import org.futo.inputmethod.latin.uix.deferGetSetting +import org.futo.inputmethod.latin.uix.deferSetSetting import org.futo.inputmethod.latin.uix.theme.DarkColorScheme import org.futo.inputmethod.latin.uix.theme.ThemeOption import org.futo.inputmethod.latin.uix.theme.ThemeOptions @@ -89,278 +63,9 @@ import org.futo.inputmethod.latin.uix.theme.Typography import org.futo.inputmethod.latin.uix.theme.UixThemeWrapper import org.futo.inputmethod.latin.uix.theme.presets.DynamicSystemTheme import org.futo.inputmethod.latin.uix.theme.presets.VoiceInputTheme -import kotlin.math.roundToInt -interface DynamicThemeProvider { - val primaryKeyboardColor: Int - - val keyboardBackground: Drawable - val keyBackground: Drawable - val spaceBarBackground: Drawable - - val keyFeedback: Drawable - - val moreKeysKeyboardBackground: Drawable - val popupKey: Drawable - - @ColorInt - fun getColor(i: Int): Int? - - fun getDrawable(i: Int): Drawable? - - companion object { - @ColorInt - fun getColorOrDefault(i: Int, @ColorInt default: Int, keyAttr: TypedArray, provider: DynamicThemeProvider?): Int { - return (provider?.getColor(i)) ?: keyAttr.getColor(i, default) - } - - fun getDrawableOrDefault(i: Int, keyAttr: TypedArray, provider: DynamicThemeProvider?): Drawable? { - return (provider?.getDrawable(i)) ?: keyAttr.getDrawable(i) - } - } -} - -// TODO: Expand the number of drawables this provides so it covers the full theme, and -// build some system to dynamically change these colors -class BasicThemeProvider(val context: Context, val overrideColorScheme: ColorScheme? = null) : DynamicThemeProvider { - override val primaryKeyboardColor: Int - - override val keyboardBackground: Drawable - override val keyBackground: Drawable - override val spaceBarBackground: Drawable - - override val keyFeedback: Drawable - - override val moreKeysKeyboardBackground: Drawable - override val popupKey: Drawable - - private val colors: HashMap = HashMap() - override fun getColor(i: Int): Int? { - return colors[i] - } - - - private val drawables: HashMap = HashMap() - override fun getDrawable(i: Int): Drawable? { - return drawables[i] - } - - private fun dp(dp: Dp): Float { - return TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, - dp.value, - context.resources.displayMetrics - ); - } - - private fun coloredRectangle(@ColorInt color: Int): GradientDrawable { - return GradientDrawable().apply { - shape = GradientDrawable.RECTANGLE - setColor(color) - } - } - - private fun coloredRoundedRectangle(@ColorInt color: Int, radius: Float): GradientDrawable { - return GradientDrawable().apply { - shape = GradientDrawable.RECTANGLE - cornerRadius = radius - setColor(color) - } - } - - private fun coloredOval(@ColorInt color: Int): GradientDrawable { - return GradientDrawable().apply { - shape = GradientDrawable.OVAL - cornerRadius = Float.MAX_VALUE - setColor(color) - } - } - - private fun StateListDrawable.addStateWithHighlightLayerOnPressed(@ColorInt highlight: Int, stateSet: IntArray, drawable: Drawable) { - addState(intArrayOf(android.R.attr.state_pressed) + stateSet, LayerDrawable(arrayOf( - drawable, - coloredRoundedRectangle(highlight, dp(8.dp)) - ))) - addState(stateSet, drawable) - } - - init { - val colorScheme = if(overrideColorScheme != null) { - overrideColorScheme - }else if(!DynamicColors.isDynamicColorAvailable()) { - DarkColorScheme - } else { - val dCtx = DynamicColors.wrapContextIfAvailable(context) - - dynamicLightColorScheme(dCtx) - } - - - val primary = colorScheme.primary.toArgb() - val secondary = colorScheme.secondary.toArgb() - val highlight = colorScheme.outline.copy(alpha = 0.33f).toArgb() - - val background = colorScheme.surface.toArgb() - val surface = colorScheme.background.toArgb() - val outline = colorScheme.outline.toArgb() - - val onSecondary = colorScheme.onSecondary.toArgb() - val onBackground = colorScheme.onBackground.toArgb() - val onBackgroundHalf = colorScheme.onBackground.copy(alpha = 0.5f).toArgb() - - val transparent = Color.TRANSPARENT - - colors[R.styleable.Keyboard_Key_keyTextColor] = onBackground - colors[R.styleable.Keyboard_Key_keyTextInactivatedColor] = onBackgroundHalf - colors[R.styleable.Keyboard_Key_keyTextShadowColor] = 0 - colors[R.styleable.Keyboard_Key_functionalTextColor] = onBackground - colors[R.styleable.Keyboard_Key_keyHintLetterColor] = onBackgroundHalf - colors[R.styleable.Keyboard_Key_keyHintLabelColor] = onBackgroundHalf - colors[R.styleable.Keyboard_Key_keyShiftedLetterHintInactivatedColor] = onBackgroundHalf - colors[R.styleable.Keyboard_Key_keyShiftedLetterHintActivatedColor] = onBackgroundHalf - colors[R.styleable.Keyboard_Key_keyPreviewTextColor] = onSecondary - colors[R.styleable.MainKeyboardView_languageOnSpacebarTextColor] = onBackgroundHalf - - drawables[R.styleable.Keyboard_iconDeleteKey] = AppCompatResources.getDrawable(context, R.drawable.delete)!!.apply { - setTint(onBackground) - } - drawables[R.styleable.Keyboard_iconLanguageSwitchKey] = AppCompatResources.getDrawable(context, R.drawable.globe)!!.apply { - setTint(onBackground) - } - - drawables[R.styleable.Keyboard_iconShiftKey] = AppCompatResources.getDrawable(context, R.drawable.shift)!!.apply { - setTint(onBackground) - } - - drawables[R.styleable.Keyboard_iconShiftKeyShifted] = AppCompatResources.getDrawable(context, R.drawable.shiftshifted)!!.apply { - setTint(onBackground) - } - - primaryKeyboardColor = background - - keyboardBackground = coloredRectangle(background) - - keyBackground = StateListDrawable().apply { - addStateWithHighlightLayerOnPressed(highlight, intArrayOf(android.R.attr.state_active), - coloredRoundedRectangle(primary, dp(8.dp)).apply { - setSize(dp(64.dp).toInt(), dp(48.dp).toInt()) - } - ) - - addStateWithHighlightLayerOnPressed(highlight, intArrayOf(android.R.attr.state_checkable, android.R.attr.state_checked), - coloredRoundedRectangle(colorScheme.secondaryContainer.toArgb(), dp(8.dp)) - ) - - addStateWithHighlightLayerOnPressed(highlight, intArrayOf(android.R.attr.state_checkable), - coloredRectangle(transparent) - ) - - addStateWithHighlightLayerOnPressed(highlight, intArrayOf(android.R.attr.state_empty), - coloredRectangle(transparent) - ) - - addStateWithHighlightLayerOnPressed(highlight, intArrayOf(), - coloredRectangle(transparent) - ) - } - - spaceBarBackground = StateListDrawable().apply { - addState(intArrayOf(android.R.attr.state_pressed), - LayerDrawable(arrayOf( - coloredRoundedRectangle(highlight, dp(32.dp)), - coloredRoundedRectangle(highlight, dp(32.dp)) - )) - ) - addState(intArrayOf(), - coloredRoundedRectangle(highlight, dp(32.dp)) - ) - } - - keyFeedback = ShapeDrawable().apply { - paint.color = secondary - shape = RoundRectShape(floatArrayOf( - dp(8.dp),dp(8.dp),dp(8.dp),dp(8.dp), - dp(8.dp),dp(8.dp),dp(8.dp),dp(8.dp), - ), null, null) - - intrinsicWidth = dp(48.dp).roundToInt() - intrinsicHeight = dp(24.dp).roundToInt() - - setPadding(0, 0, 0, dp(50.dp).roundToInt()) - } - - moreKeysKeyboardBackground = coloredRoundedRectangle(surface, dp(8.dp)) - popupKey = StateListDrawable().apply { - addStateWithHighlightLayerOnPressed(highlight, intArrayOf(), - coloredRoundedRectangle(surface, dp(8.dp)) - ) - } - } - -} - -interface DynamicThemeProviderOwner { - fun getDrawableProvider(): DynamicThemeProvider -} - - -val Context.dataStore: DataStore by preferencesDataStore(name = "settings") -val THEME_KEY = stringPreferencesKey("activeThemeOption") - -suspend fun Context.getSetting(key: Preferences.Key, default: T): T { - val valueFlow: Flow = - this.dataStore.data.map { preferences -> preferences[key] ?: default }.take(1) - - return valueFlow.first() -} - -suspend fun Context.setSetting(key: Preferences.Key, value: T) { - this.dataStore.edit { preferences -> - preferences[key] = value - } -} - - -class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner, LatinIMELegacy.SuggestionStripController, DynamicThemeProviderOwner, - KeyboardManagerForAction { - private var activeColorScheme = DarkColorScheme - private var colorSchemeLoaderJob: Job? = null - - private var drawableProvider: DynamicThemeProvider? = null - override fun getDrawableProvider(): DynamicThemeProvider { - if(drawableProvider == null) { - if(colorSchemeLoaderJob != null && !colorSchemeLoaderJob!!.isCompleted) { - runBlocking { - colorSchemeLoaderJob!!.join() - } - } - drawableProvider = BasicThemeProvider(this, activeColorScheme) - } - - return drawableProvider!! - } - - private fun recreateKeyboard() { - legacyInputView = latinIMELegacy.onCreateInputView() - latinIMELegacy.loadKeyboard() - } - - private fun updateDrawableProvider(colorScheme: ColorScheme) { - activeColorScheme = colorScheme - drawableProvider = BasicThemeProvider(this, overrideColorScheme = colorScheme) - - // recreate the keyboard if not in action window, if we are in action window then - // it'll be recreated when we exit - if(currWindowAction == null) recreateKeyboard() - - window.window?.navigationBarColor = drawableProvider!!.primaryKeyboardColor - setContent() - } - - private val latinIMELegacy = LatinIMELegacy( - this as InputMethodService, - this as LatinIMELegacy.SuggestionStripController - ) +class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner, + LatinIMELegacy.SuggestionStripController, DynamicThemeProviderOwner, KeyboardManagerForAction { private val mSavedStateRegistryController = SavedStateRegistryController.create(this) @@ -368,6 +73,8 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save get() = mSavedStateRegistryController.savedStateRegistry private val mLifecycleRegistry = LifecycleRegistry(this) + private fun handleLifecycleEvent(event: Lifecycle.Event) = + mLifecycleRegistry.handleLifecycleEvent(event) override val lifecycle get() = mLifecycleRegistry @@ -376,32 +83,6 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save override val viewModelStore get() = store - private fun handleLifecycleEvent(event: Lifecycle.Event) = - mLifecycleRegistry.handleLifecycleEvent(event) - - private val inputMethodManager: InputMethodManager - get() = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - - override fun onCreate() { - super.onCreate() - - colorSchemeLoaderJob = lifecycleScope.launch { - var themeKey = this@LatinIME.getSetting(THEME_KEY, DynamicSystemTheme.key) - var themeOption = ThemeOptions[themeKey] - if(themeOption == null || !themeOption.available(this@LatinIME)) { - themeKey = VoiceInputTheme.key - themeOption = ThemeOptions[themeKey]!! - } - - activeColorScheme = themeOption.obtainColors(this@LatinIME) - } - - mSavedStateRegistryController.performRestore(null) - handleLifecycleEvent(Lifecycle.Event.ON_RESUME) - - latinIMELegacy.onCreate() - } - private fun setOwners() { val decorView = window.window?.decorView if (decorView?.findViewTreeLifecycleOwner() == null) { @@ -415,9 +96,74 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save } } - private var composeView: ComposeView? = null + private val latinIMELegacy = LatinIMELegacy( + this as InputMethodService, + this as LatinIMELegacy.SuggestionStripController + ) + + private var activeColorScheme = DarkColorScheme + private var colorSchemeLoaderJob: Job? = null + + private var drawableProvider: DynamicThemeProvider? = null + + private var currWindowAction: Action? = null + private fun isActionWindowOpen(): Boolean { + return currWindowAction != null + } + + private fun recreateKeyboard() { + legacyInputView = latinIMELegacy.onCreateInputView() + latinIMELegacy.loadKeyboard() + } + + private fun updateDrawableProvider(colorScheme: ColorScheme) { + activeColorScheme = colorScheme + drawableProvider = BasicThemeProvider(this, overrideColorScheme = colorScheme) + + // recreate the keyboard if not in action window, if we are in action window then + // it'll be recreated when we exit + if (!isActionWindowOpen()) recreateKeyboard() + + window.window?.navigationBarColor = drawableProvider!!.primaryKeyboardColor + setContent() + } + + override fun getDrawableProvider(): DynamicThemeProvider { + if (drawableProvider == null) { + if (colorSchemeLoaderJob != null && !colorSchemeLoaderJob!!.isCompleted) { + // Must have completed by now! + runBlocking { + colorSchemeLoaderJob!!.join() + } + } + drawableProvider = BasicThemeProvider(this, activeColorScheme) + } + + return drawableProvider!! + } + + override fun onCreate() { + super.onCreate() + + colorSchemeLoaderJob = deferGetSetting(THEME_KEY, DynamicSystemTheme.key) { + var themeKey = it + var themeOption = ThemeOptions[themeKey] + if (themeOption == null || !themeOption.available(this@LatinIME)) { + themeKey = VoiceInputTheme.key + themeOption = ThemeOptions[themeKey]!! + } + + activeColorScheme = themeOption.obtainColors(this@LatinIME) + } + + mSavedStateRegistryController.performRestore(null) + handleLifecycleEvent(Lifecycle.Event.ON_RESUME) + + latinIMELegacy.onCreate() + } + override fun onDestroy() { latinIMELegacy.onDestroy() super.onDestroy() @@ -450,11 +196,10 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save return composeView!! } - private var currWindowAction: Action? = null private fun onActionActivated(action: Action) { - if(action.windowImpl != null) { + if (action.windowImpl != null) { enterActionWindowView(action) - } else if(action.simplePressImpl != null) { + } else if (action.simplePressImpl != null) { action.simplePressImpl.invoke(this) } else { throw IllegalStateException("An action must have either a window implementation or a simple press implementation") @@ -512,9 +257,11 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save private fun ActionViewWithHeader(action: Action) { val windowImpl = action.windowImpl!! Column { - Surface(modifier = Modifier - .fillMaxWidth() - .height(40.dp), color = MaterialTheme.colorScheme.background) + Surface( + modifier = Modifier + .fillMaxWidth() + .height(40.dp), color = MaterialTheme.colorScheme.background + ) { Row { IconButton(onClick = { @@ -526,13 +273,18 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save ) } - Text(windowImpl.windowName(), style = Typography.titleMedium, modifier = Modifier.align(CenterVertically)) + Text( + windowImpl.windowName(), + style = Typography.titleMedium, + modifier = Modifier.align(CenterVertically) + ) } } Box(modifier = Modifier .fillMaxWidth() - .height(with(LocalDensity.current) { inputViewHeight.toDp() })) { + .height(with(LocalDensity.current) { inputViewHeight.toDp() }) + ) { windowImpl.WindowContents(manager = this@LatinIME) } } @@ -561,7 +313,7 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save legacyInputView = newView setContent() - if(composeView != null) { + if (composeView != null) { latinIMELegacy.setComposeInputView(composeView) } @@ -571,7 +323,7 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save override fun setInputView(view: View?) { super.setInputView(view) - if(composeView != null) { + if (composeView != null) { latinIMELegacy.setComposeInputView(composeView) } @@ -627,9 +379,23 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save candidatesStart: Int, candidatesEnd: Int ) { - super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, candidatesStart, candidatesEnd) + super.onUpdateSelection( + oldSelStart, + oldSelEnd, + newSelStart, + newSelEnd, + candidatesStart, + candidatesEnd + ) - latinIMELegacy.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, candidatesStart, candidatesEnd) + latinIMELegacy.onUpdateSelection( + oldSelStart, + oldSelEnd, + newSelStart, + newSelEnd, + candidatesStart, + candidatesEnd + ) } override fun onExtractedTextClicked() { @@ -653,7 +419,7 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save override fun onComputeInsets(outInsets: Insets?) { // This method may be called before {@link #setInputView(View)}. - if (legacyInputView == null) { + if (legacyInputView == null || composeView == null) { return } @@ -684,7 +450,10 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save } override fun onShowInputRequested(flags: Int, configChange: Boolean): Boolean { - return latinIMELegacy.onShowInputRequested(flags, configChange) || super.onShowInputRequested(flags, configChange) + return latinIMELegacy.onShowInputRequested( + flags, + configChange + ) || super.onShowInputRequested(flags, configChange) } override fun onEvaluateInputViewShown(): Boolean { @@ -735,10 +504,6 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save assert(newTheme.available(this)) updateDrawableProvider(newTheme.obtainColors(this)) - lifecycleScope.launch { - withContext(Dispatchers.Default) { - setSetting(THEME_KEY, newTheme.key) - } - } + deferSetSetting(THEME_KEY, newTheme.key) } } \ No newline at end of file diff --git a/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java b/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java index 995bc1dbe..a749ce52d 100644 --- a/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java +++ b/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java @@ -90,6 +90,7 @@ import org.futo.inputmethod.latin.settings.SettingsValues; import org.futo.inputmethod.latin.suggestions.SuggestionStripView; import org.futo.inputmethod.latin.suggestions.SuggestionStripViewAccessor; import org.futo.inputmethod.latin.touchinputconsumer.GestureConsumer; +import org.futo.inputmethod.latin.uix.DynamicThemeProviderOwner; import org.futo.inputmethod.latin.utils.ApplicationUtils; import org.futo.inputmethod.latin.utils.DialogUtils; import org.futo.inputmethod.latin.utils.ImportantNoticeUtils; diff --git a/java/src/org/futo/inputmethod/latin/uix/BasicThemeProvider.kt b/java/src/org/futo/inputmethod/latin/uix/BasicThemeProvider.kt new file mode 100644 index 000000000..b1bc31de4 --- /dev/null +++ b/java/src/org/futo/inputmethod/latin/uix/BasicThemeProvider.kt @@ -0,0 +1,221 @@ +package org.futo.inputmethod.latin.uix + +import android.content.Context +import android.graphics.Color +import android.graphics.drawable.Drawable +import android.graphics.drawable.GradientDrawable +import android.graphics.drawable.LayerDrawable +import android.graphics.drawable.ShapeDrawable +import android.graphics.drawable.StateListDrawable +import android.graphics.drawable.shapes.RoundRectShape +import android.util.TypedValue +import androidx.annotation.ColorInt +import androidx.appcompat.content.res.AppCompatResources +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.google.android.material.color.DynamicColors +import org.futo.inputmethod.latin.R +import org.futo.inputmethod.latin.uix.theme.DarkColorScheme +import kotlin.math.roundToInt + +// TODO: Expand the number of drawables this provides so it covers the full theme, and +// build some system to dynamically change these colors +class BasicThemeProvider(val context: Context, val overrideColorScheme: ColorScheme? = null) : + DynamicThemeProvider { + override val primaryKeyboardColor: Int + + override val keyboardBackground: Drawable + override val keyBackground: Drawable + override val spaceBarBackground: Drawable + + override val keyFeedback: Drawable + + override val moreKeysKeyboardBackground: Drawable + override val popupKey: Drawable + + private val colors: HashMap = HashMap() + override fun getColor(i: Int): Int? { + return colors[i] + } + + + private val drawables: HashMap = HashMap() + override fun getDrawable(i: Int): Drawable? { + return drawables[i] + } + + private fun dp(dp: Dp): Float { + return TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + dp.value, + context.resources.displayMetrics + ); + } + + private fun coloredRectangle(@ColorInt color: Int): GradientDrawable { + return GradientDrawable().apply { + shape = GradientDrawable.RECTANGLE + setColor(color) + } + } + + private fun coloredRoundedRectangle(@ColorInt color: Int, radius: Float): GradientDrawable { + return GradientDrawable().apply { + shape = GradientDrawable.RECTANGLE + cornerRadius = radius + setColor(color) + } + } + + private fun coloredOval(@ColorInt color: Int): GradientDrawable { + return GradientDrawable().apply { + shape = GradientDrawable.OVAL + cornerRadius = Float.MAX_VALUE + setColor(color) + } + } + + private fun StateListDrawable.addStateWithHighlightLayerOnPressed(@ColorInt highlight: Int, stateSet: IntArray, drawable: Drawable) { + addState(intArrayOf(android.R.attr.state_pressed) + stateSet, LayerDrawable( + arrayOf( + drawable, + coloredRoundedRectangle(highlight, dp(8.dp)) + ) + ) + ) + addState(stateSet, drawable) + } + + init { + val colorScheme = if(overrideColorScheme != null) { + overrideColorScheme + }else if(!DynamicColors.isDynamicColorAvailable()) { + DarkColorScheme + } else { + val dCtx = DynamicColors.wrapContextIfAvailable(context) + + dynamicLightColorScheme(dCtx) + } + + + val primary = colorScheme.primary.toArgb() + val secondary = colorScheme.secondary.toArgb() + val highlight = colorScheme.outline.copy(alpha = 0.33f).toArgb() + + val background = colorScheme.surface.toArgb() + val surface = colorScheme.background.toArgb() + val outline = colorScheme.outline.toArgb() + + val onSecondary = colorScheme.onSecondary.toArgb() + val onBackground = colorScheme.onBackground.toArgb() + val onBackgroundHalf = colorScheme.onBackground.copy(alpha = 0.5f).toArgb() + + val transparent = Color.TRANSPARENT + + colors[R.styleable.Keyboard_Key_keyTextColor] = onBackground + colors[R.styleable.Keyboard_Key_keyTextInactivatedColor] = onBackgroundHalf + colors[R.styleable.Keyboard_Key_keyTextShadowColor] = 0 + colors[R.styleable.Keyboard_Key_functionalTextColor] = onBackground + colors[R.styleable.Keyboard_Key_keyHintLetterColor] = onBackgroundHalf + colors[R.styleable.Keyboard_Key_keyHintLabelColor] = onBackgroundHalf + colors[R.styleable.Keyboard_Key_keyShiftedLetterHintInactivatedColor] = onBackgroundHalf + colors[R.styleable.Keyboard_Key_keyShiftedLetterHintActivatedColor] = onBackgroundHalf + colors[R.styleable.Keyboard_Key_keyPreviewTextColor] = onSecondary + colors[R.styleable.MainKeyboardView_languageOnSpacebarTextColor] = onBackgroundHalf + + drawables[R.styleable.Keyboard_iconDeleteKey] = AppCompatResources.getDrawable( + context, + R.drawable.delete + )!!.apply { + setTint(onBackground) + } + drawables[R.styleable.Keyboard_iconLanguageSwitchKey] = AppCompatResources.getDrawable( + context, + R.drawable.globe + )!!.apply { + setTint(onBackground) + } + + drawables[R.styleable.Keyboard_iconShiftKey] = AppCompatResources.getDrawable( + context, + R.drawable.shift + )!!.apply { + setTint(onBackground) + } + + drawables[R.styleable.Keyboard_iconShiftKeyShifted] = AppCompatResources.getDrawable( + context, + R.drawable.shiftshifted + )!!.apply { + setTint(onBackground) + } + + primaryKeyboardColor = background + + keyboardBackground = coloredRectangle(background) + + keyBackground = StateListDrawable().apply { + addStateWithHighlightLayerOnPressed(highlight, intArrayOf(android.R.attr.state_active), + coloredRoundedRectangle(primary, dp(8.dp)).apply { + setSize(dp(64.dp).toInt(), dp(48.dp).toInt()) + } + ) + + addStateWithHighlightLayerOnPressed(highlight, intArrayOf(android.R.attr.state_checkable, android.R.attr.state_checked), + coloredRoundedRectangle(colorScheme.secondaryContainer.toArgb(), dp(8.dp)) + ) + + addStateWithHighlightLayerOnPressed(highlight, intArrayOf(android.R.attr.state_checkable), + coloredRectangle(transparent) + ) + + addStateWithHighlightLayerOnPressed(highlight, intArrayOf(android.R.attr.state_empty), + coloredRectangle(transparent) + ) + + addStateWithHighlightLayerOnPressed(highlight, intArrayOf(), + coloredRectangle(transparent) + ) + } + + spaceBarBackground = StateListDrawable().apply { + addState(intArrayOf(android.R.attr.state_pressed), + LayerDrawable( + arrayOf( + coloredRoundedRectangle(highlight, dp(32.dp)), + coloredRoundedRectangle(highlight, dp(32.dp)) + ) + ) + ) + addState(intArrayOf(), + coloredRoundedRectangle(highlight, dp(32.dp)) + ) + } + + keyFeedback = ShapeDrawable().apply { + paint.color = secondary + shape = RoundRectShape( + floatArrayOf( + dp(8.dp), dp(8.dp), dp(8.dp), dp(8.dp), + dp(8.dp), dp(8.dp), dp(8.dp), dp(8.dp), + ), null, null + ) + + intrinsicWidth = dp(48.dp).roundToInt() + intrinsicHeight = dp(24.dp).roundToInt() + + setPadding(0, 0, 0, dp(50.dp).roundToInt()) + } + + moreKeysKeyboardBackground = coloredRoundedRectangle(surface, dp(8.dp)) + popupKey = StateListDrawable().apply { + addStateWithHighlightLayerOnPressed(highlight, intArrayOf(), + coloredRoundedRectangle(surface, dp(8.dp)) + ) + } + } + +} \ No newline at end of file diff --git a/java/src/org/futo/inputmethod/latin/uix/DynamicThemeProvider.kt b/java/src/org/futo/inputmethod/latin/uix/DynamicThemeProvider.kt new file mode 100644 index 000000000..b090921c2 --- /dev/null +++ b/java/src/org/futo/inputmethod/latin/uix/DynamicThemeProvider.kt @@ -0,0 +1,34 @@ +package org.futo.inputmethod.latin.uix + +import android.content.res.TypedArray +import android.graphics.drawable.Drawable +import androidx.annotation.ColorInt + +interface DynamicThemeProvider { + val primaryKeyboardColor: Int + + val keyboardBackground: Drawable + val keyBackground: Drawable + val spaceBarBackground: Drawable + + val keyFeedback: Drawable + + val moreKeysKeyboardBackground: Drawable + val popupKey: Drawable + + @ColorInt + fun getColor(i: Int): Int? + + fun getDrawable(i: Int): Drawable? + + companion object { + @ColorInt + fun getColorOrDefault(i: Int, @ColorInt default: Int, keyAttr: TypedArray, provider: DynamicThemeProvider?): Int { + return (provider?.getColor(i)) ?: keyAttr.getColor(i, default) + } + + fun getDrawableOrDefault(i: Int, keyAttr: TypedArray, provider: DynamicThemeProvider?): Drawable? { + return (provider?.getDrawable(i)) ?: keyAttr.getDrawable(i) + } + } +} \ No newline at end of file diff --git a/java/src/org/futo/inputmethod/latin/uix/DynamicThemeProviderOwner.kt b/java/src/org/futo/inputmethod/latin/uix/DynamicThemeProviderOwner.kt new file mode 100644 index 000000000..af00bd43d --- /dev/null +++ b/java/src/org/futo/inputmethod/latin/uix/DynamicThemeProviderOwner.kt @@ -0,0 +1,5 @@ +package org.futo.inputmethod.latin.uix + +interface DynamicThemeProviderOwner { + fun getDrawableProvider(): DynamicThemeProvider +} \ No newline at end of file diff --git a/java/src/org/futo/inputmethod/latin/uix/Settings.kt b/java/src/org/futo/inputmethod/latin/uix/Settings.kt new file mode 100644 index 000000000..90efce28e --- /dev/null +++ b/java/src/org/futo/inputmethod/latin/uix/Settings.kt @@ -0,0 +1,73 @@ +package org.futo.inputmethod.latin.uix + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import androidx.lifecycle.LifecycleCoroutineScope +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext + +val Context.dataStore: DataStore by preferencesDataStore(name = "settings") + +suspend fun Context.getSetting(key: Preferences.Key, default: T): T { + val valueFlow: Flow = + this.dataStore.data.map { preferences -> preferences[key] ?: default }.take(1) + + return valueFlow.first() +} + +suspend fun Context.setSetting(key: Preferences.Key, value: T) { + this.dataStore.edit { preferences -> + preferences[key] = value + } +} + + +fun Context.getSettingBlocking(key: Preferences.Key, default: T): T { + val context = this + + return runBlocking { + context.getSetting(key, default) + } +} + +fun Context.setSettingBlocking(key: Preferences.Key, value: T) { + val context = this + runBlocking { + context.setSetting(key, value) + } +} + +fun LifecycleOwner.deferGetSetting(key: Preferences.Key, default: T, onObtained: (T) -> Unit): Job { + val context = (this as Context) + return lifecycleScope.launch { + withContext(Dispatchers.Default) { + val value = context.getSetting(key, default) + onObtained(value) + } + } +} + +fun LifecycleOwner.deferSetSetting(key: Preferences.Key, value: T): Job { + val context = (this as Context) + return lifecycleScope.launch { + withContext(Dispatchers.Default) { + context.setSetting(key, value) + } + } +} + + +val THEME_KEY = stringPreferencesKey("activeThemeOption") \ No newline at end of file