Implement Actions, create a theme switcher action window

This commit is contained in:
Aleksandras Kostarevas 2023-08-22 15:36:40 +03:00
parent ad151d7f11
commit f91a626955
15 changed files with 605 additions and 163 deletions

View File

@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="26dp"
android:height="26dp"
android:viewportWidth="26"
android:viewportHeight="26">
<path
android:pathData="M20,13L6,13"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeLineCap="round"/>
<path
android:pathData="M13,20L6,13L13,6"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeLineCap="round"/>
</vector>

20
java/res/drawable/eye.xml Normal file
View File

@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="26dp"
android:height="26dp"
android:viewportWidth="26"
android:viewportHeight="26">
<path
android:pathData="M2,13C2,13 6,5 13,5C20,5 24,13 24,13C24,13 20,21 13,21C6,21 2,13 2,13ZZ"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeLineCap="round"/>
<path
android:pathData="M10,13a3,3 0,1 0,6 0a3,3 0,1 0,-6 0z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeLineCap="round"/>
</vector>

View File

@ -98,13 +98,19 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
}
}
private boolean themeSwitchPending = false;
public void queueThemeSwitch() {
themeSwitchPending = true;
}
private boolean updateKeyboardThemeAndContextThemeWrapper(final Context context,
final KeyboardTheme keyboardTheme) {
if (mThemeContext == null || !keyboardTheme.equals(mKeyboardTheme)
if (themeSwitchPending || mThemeContext == null || !keyboardTheme.equals(mKeyboardTheme)
|| !mThemeContext.getResources().equals(context.getResources())) {
mKeyboardTheme = keyboardTheme;
mThemeContext = new ContextThemeWrapper(context, keyboardTheme.mStyleId);
KeyboardLayoutSet.onKeyboardThemeChanged();
themeSwitchPending = false;
return true;
}
return false;

View File

@ -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.KeyboardDrawableProvider;
import org.futo.inputmethod.latin.KeyboardDrawableProviderOwner;
import org.futo.inputmethod.latin.DynamicThemeProvider;
import org.futo.inputmethod.latin.DynamicThemeProviderOwner;
import org.futo.inputmethod.latin.R;
import org.futo.inputmethod.latin.common.Constants;
import org.futo.inputmethod.latin.utils.TypefaceUtils;
@ -96,7 +96,7 @@ public class KeyboardView extends View {
private final Drawable mKeyBackground;
private final Drawable mFunctionalKeyBackground;
private final Drawable mSpacebarBackground;
protected final KeyboardDrawableProvider mDrawableProvider;
protected final DynamicThemeProvider mDrawableProvider;
private final float mSpacebarIconWidthRatio;
private final Rect mKeyBackgroundPadding = new Rect();
private static final float KET_TEXT_SHADOW_RADIUS_DISABLED = -1.0f;
@ -138,9 +138,9 @@ public class KeyboardView extends View {
R.styleable.KeyboardView, defStyle, R.style.KeyboardView);
assert(context instanceof ContextThemeWrapper);
assert(((ContextThemeWrapper) context).getBaseContext() instanceof KeyboardDrawableProviderOwner);
assert(((ContextThemeWrapper) context).getBaseContext() instanceof DynamicThemeProviderOwner);
mDrawableProvider = ((KeyboardDrawableProviderOwner) ((ContextThemeWrapper) context).getBaseContext()).getDrawableProvider();
mDrawableProvider = ((DynamicThemeProviderOwner) ((ContextThemeWrapper) context).getBaseContext()).getDrawableProvider();
boolean isMoreKeys = defStyle == R.attr.moreKeysKeyboardViewStyle || defStyle == R.attr.moreKeysKeyboardViewForActionStyle;

View File

@ -28,7 +28,6 @@ import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.Typeface;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
@ -51,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.KeyboardDrawableProvider;
import org.futo.inputmethod.latin.DynamicThemeProvider;
import org.futo.inputmethod.latin.R;
import org.futo.inputmethod.latin.RichInputMethodSubtype;
import org.futo.inputmethod.latin.SuggestedWords;
@ -61,7 +60,6 @@ import org.futo.inputmethod.latin.settings.DebugSettings;
import org.futo.inputmethod.latin.utils.LanguageOnSpacebarUtils;
import org.futo.inputmethod.latin.utils.TypefaceUtils;
import java.util.Locale;
import java.util.WeakHashMap;
import javax.annotation.Nonnull;
@ -215,7 +213,7 @@ public final class MainKeyboardView extends KeyboardView implements DrawingProxy
mBackgroundDimAlphaPaint.setAlpha(backgroundDimAlpha);
mLanguageOnSpacebarTextRatio = mainKeyboardViewAttr.getFraction(
R.styleable.MainKeyboardView_languageOnSpacebarTextRatio, 1, 1, 1.0f);
mLanguageOnSpacebarTextColor = KeyboardDrawableProvider.Companion.getColorOrDefault(
mLanguageOnSpacebarTextColor = DynamicThemeProvider.Companion.getColorOrDefault(
R.styleable.MainKeyboardView_languageOnSpacebarTextColor, 0,
mainKeyboardViewAttr, mDrawableProvider
);
@ -461,6 +459,10 @@ public final class MainKeyboardView extends KeyboardView implements DrawingProxy
Log.w(TAG, "Cannot find android.R.id.content view to add DrawingPreviewPlacerView");
return;
}
if(mDrawingPreviewPlacerView.getParent() != null) {
((ViewGroup)mDrawingPreviewPlacerView.getParent()).removeView(mDrawingPreviewPlacerView);
}
windowContentView.addView(mDrawingPreviewPlacerView);
}

View File

@ -26,7 +26,7 @@ import android.view.View;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import org.futo.inputmethod.latin.KeyboardDrawableProvider;
import org.futo.inputmethod.latin.DynamicThemeProvider;
import org.futo.inputmethod.latin.R;
public final class KeyPreviewDrawParams {
@ -71,7 +71,7 @@ public final class KeyPreviewDrawParams {
// preview background.
private int mVisibleOffset;
public KeyPreviewDrawParams(final TypedArray mainKeyboardViewAttr, final KeyboardDrawableProvider provider) {
public KeyPreviewDrawParams(final TypedArray mainKeyboardViewAttr, final DynamicThemeProvider provider) {
mPreviewOffset = mainKeyboardViewAttr.getDimensionPixelOffset(
R.styleable.MainKeyboardView_keyPreviewOffset, 0);
mPreviewHeight = mainKeyboardViewAttr.getDimensionPixelSize(

View File

@ -20,7 +20,7 @@ import android.content.res.TypedArray;
import android.graphics.Typeface;
import android.util.SparseIntArray;
import org.futo.inputmethod.latin.KeyboardDrawableProvider;
import org.futo.inputmethod.latin.DynamicThemeProvider;
import org.futo.inputmethod.latin.R;
import org.futo.inputmethod.latin.utils.ResourceUtils;
@ -87,7 +87,7 @@ public final class KeyVisualAttributes {
}
@Nullable
public static KeyVisualAttributes newInstance(@Nonnull final TypedArray keyAttr, @Nullable final KeyboardDrawableProvider provider) {
public static KeyVisualAttributes newInstance(@Nonnull final TypedArray keyAttr, @Nullable final DynamicThemeProvider provider) {
final int indexCount = keyAttr.getIndexCount();
for (int i = 0; i < indexCount; i++) {
final int attrId = keyAttr.getIndex(i);
@ -99,7 +99,7 @@ public final class KeyVisualAttributes {
return null;
}
private KeyVisualAttributes(@Nonnull final TypedArray keyAttr, @Nullable final KeyboardDrawableProvider provider) {
private KeyVisualAttributes(@Nonnull final TypedArray keyAttr, @Nullable final DynamicThemeProvider provider) {
if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyTypeface)) {
mTypeface = Typeface.defaultFromStyle(
keyAttr.getInt(R.styleable.Keyboard_Key_keyTypeface, Typeface.NORMAL));
@ -126,23 +126,23 @@ public final class KeyVisualAttributes {
mPreviewTextRatio = ResourceUtils.getFraction(keyAttr,
R.styleable.Keyboard_Key_keyPreviewTextRatio);
mTextColor = KeyboardDrawableProvider.Companion.getColorOrDefault(
mTextColor = DynamicThemeProvider.Companion.getColorOrDefault(
R.styleable.Keyboard_Key_keyTextColor, 0, keyAttr, provider);
mTextInactivatedColor = KeyboardDrawableProvider.Companion.getColorOrDefault(
mTextInactivatedColor = DynamicThemeProvider.Companion.getColorOrDefault(
R.styleable.Keyboard_Key_keyTextInactivatedColor, 0, keyAttr, provider);
mTextShadowColor = KeyboardDrawableProvider.Companion.getColorOrDefault(
mTextShadowColor = DynamicThemeProvider.Companion.getColorOrDefault(
R.styleable.Keyboard_Key_keyTextShadowColor, 0, keyAttr, provider);
mFunctionalTextColor = KeyboardDrawableProvider.Companion.getColorOrDefault(
mFunctionalTextColor = DynamicThemeProvider.Companion.getColorOrDefault(
R.styleable.Keyboard_Key_functionalTextColor, 0, keyAttr, provider);
mHintLetterColor = KeyboardDrawableProvider.Companion.getColorOrDefault(
mHintLetterColor = DynamicThemeProvider.Companion.getColorOrDefault(
R.styleable.Keyboard_Key_keyHintLetterColor, 0, keyAttr, provider);
mHintLabelColor = KeyboardDrawableProvider.Companion.getColorOrDefault(
mHintLabelColor = DynamicThemeProvider.Companion.getColorOrDefault(
R.styleable.Keyboard_Key_keyHintLabelColor, 0, keyAttr, provider);
mShiftedLetterHintInactivatedColor = KeyboardDrawableProvider.Companion.getColorOrDefault(
mShiftedLetterHintInactivatedColor = DynamicThemeProvider.Companion.getColorOrDefault(
R.styleable.Keyboard_Key_keyShiftedLetterHintInactivatedColor, 0, keyAttr, provider);
mShiftedLetterHintActivatedColor = KeyboardDrawableProvider.Companion.getColorOrDefault(
mShiftedLetterHintActivatedColor = DynamicThemeProvider.Companion.getColorOrDefault(
R.styleable.Keyboard_Key_keyShiftedLetterHintActivatedColor, 0, keyAttr, provider);
mPreviewTextColor = KeyboardDrawableProvider.Companion.getColorOrDefault(
mPreviewTextColor = DynamicThemeProvider.Companion.getColorOrDefault(
R.styleable.Keyboard_Key_keyPreviewTextColor, 0, keyAttr, provider);
mHintLabelVerticalAdjustment = ResourceUtils.getFraction(keyAttr,

View File

@ -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.KeyboardDrawableProvider;
import org.futo.inputmethod.latin.KeyboardDrawableProviderOwner;
import org.futo.inputmethod.latin.DynamicThemeProvider;
import org.futo.inputmethod.latin.DynamicThemeProviderOwner;
import org.futo.inputmethod.latin.R;
import org.futo.inputmethod.latin.common.Constants;
import org.futo.inputmethod.latin.common.StringUtils;
@ -153,16 +153,16 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
private boolean mTopEdge;
private Key mRightEdgeKey = null;
private KeyboardDrawableProvider mProvider = null;
private DynamicThemeProvider mProvider = null;
public KeyboardBuilder(final Context context, @Nonnull final KP params) {
mContext = context;
if(mContext instanceof KeyboardDrawableProviderOwner) {
mProvider = ((KeyboardDrawableProviderOwner) mContext).getDrawableProvider();
if(mContext instanceof DynamicThemeProviderOwner) {
mProvider = ((DynamicThemeProviderOwner) mContext).getDrawableProvider();
}else if(mContext instanceof ContextThemeWrapper) {
Context baseContext = ((ContextThemeWrapper) mContext).getBaseContext();
if(baseContext instanceof KeyboardDrawableProviderOwner) {
mProvider = ((KeyboardDrawableProviderOwner) baseContext).getDrawableProvider();
if(baseContext instanceof DynamicThemeProviderOwner) {
mProvider = ((DynamicThemeProviderOwner) baseContext).getDrawableProvider();
}
}

View File

@ -22,7 +22,7 @@ import android.graphics.drawable.Drawable;
import android.util.Log;
import android.util.SparseIntArray;
import org.futo.inputmethod.latin.KeyboardDrawableProvider;
import org.futo.inputmethod.latin.DynamicThemeProvider;
import org.futo.inputmethod.latin.R;
import java.util.HashMap;
@ -108,12 +108,12 @@ public final class KeyboardIconsSet {
}
}
public void loadIcons(final TypedArray keyboardAttrs, @Nullable KeyboardDrawableProvider provider) {
public void loadIcons(final TypedArray keyboardAttrs, @Nullable DynamicThemeProvider provider) {
final int size = ATTR_ID_TO_ICON_ID.size();
for (int index = 0; index < size; index++) {
final int attrId = ATTR_ID_TO_ICON_ID.keyAt(index);
try {
final Drawable icon = KeyboardDrawableProvider.Companion.getDrawableOrDefault(attrId, keyboardAttrs, provider);
final Drawable icon = DynamicThemeProvider.Companion.getDrawableOrDefault(attrId, keyboardAttrs, provider);
setDefaultBounds(icon);
final Integer iconId = ATTR_ID_TO_ICON_ID.get(attrId);
mIcons[iconId] = icon;

View File

@ -6,13 +6,11 @@ import android.content.res.TypedArray
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
import android.graphics.drawable.InsetDrawable
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.AttributeSet
import android.util.TypedValue
import android.view.KeyEvent
import android.view.View
@ -22,22 +20,34 @@ 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.dynamicDarkColorScheme
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.core.graphics.toColor
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
@ -53,12 +63,16 @@ import androidx.savedstate.SavedStateRegistryOwner
import androidx.savedstate.findViewTreeSavedStateRegistryOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import com.google.android.material.color.DynamicColors
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.KeyboardManagerForAction
import org.futo.inputmethod.latin.uix.theme.DarkColorScheme
import org.futo.inputmethod.latin.uix.theme.Typography
import org.futo.inputmethod.latin.uix.theme.UixThemeWrapper
import kotlin.math.roundToInt
interface KeyboardDrawableProvider {
interface DynamicThemeProvider {
val primaryKeyboardColor: Int
val keyboardBackground: Drawable
@ -77,11 +91,11 @@ interface KeyboardDrawableProvider {
companion object {
@ColorInt
fun getColorOrDefault(i: Int, @ColorInt default: Int, keyAttr: TypedArray, provider: KeyboardDrawableProvider?): Int {
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: KeyboardDrawableProvider?): Drawable? {
fun getDrawableOrDefault(i: Int, keyAttr: TypedArray, provider: DynamicThemeProvider?): Drawable? {
return (provider?.getDrawable(i)) ?: keyAttr.getDrawable(i)
}
}
@ -89,7 +103,7 @@ interface KeyboardDrawableProvider {
// 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) : KeyboardDrawableProvider {
class BasicThemeProvider(val context: Context, val overrideColorScheme: ColorScheme? = null) : DynamicThemeProvider {
override val primaryKeyboardColor: Int
override val keyboardBackground: Drawable
@ -152,7 +166,9 @@ class BasicThemeProvider(val context: Context) : KeyboardDrawableProvider {
}
init {
val colorScheme = if(!DynamicColors.isDynamicColorAvailable()) {
val colorScheme = if(overrideColorScheme != null) {
overrideColorScheme
}else if(!DynamicColors.isDynamicColorAvailable()) {
DarkColorScheme
} else {
val dCtx = DynamicColors.wrapContextIfAvailable(context)
@ -213,7 +229,7 @@ class BasicThemeProvider(val context: Context) : KeyboardDrawableProvider {
)
addStateWithHighlightLayerOnPressed(highlight, intArrayOf(android.R.attr.state_checkable, android.R.attr.state_checked),
coloredRoundedRectangle(secondary, dp(8.dp))
coloredRoundedRectangle(colorScheme.secondaryContainer.toArgb(), dp(8.dp))
)
addStateWithHighlightLayerOnPressed(highlight, intArrayOf(android.R.attr.state_checkable),
@ -264,20 +280,39 @@ class BasicThemeProvider(val context: Context) : KeyboardDrawableProvider {
}
interface KeyboardDrawableProviderOwner {
fun getDrawableProvider(): KeyboardDrawableProvider
interface DynamicThemeProviderOwner {
fun getDrawableProvider(): DynamicThemeProvider
}
class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner, LatinIMELegacy.SuggestionStripController, KeyboardDrawableProviderOwner {
private var drawableProvider: KeyboardDrawableProvider? = null
override fun getDrawableProvider(): KeyboardDrawableProvider {
class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner, LatinIMELegacy.SuggestionStripController, DynamicThemeProviderOwner,
KeyboardManagerForAction {
private var activeColorScheme = DarkColorScheme
private var drawableProvider: DynamicThemeProvider? = null
override fun getDrawableProvider(): DynamicThemeProvider {
if(drawableProvider == null) {
drawableProvider = BasicThemeProvider(this)
drawableProvider = BasicThemeProvider(this, activeColorScheme)
}
return drawableProvider!!
}
private fun updateDrawableProvider(colorScheme: ColorScheme) {
activeColorScheme = colorScheme
// ... update drawableProvider with params
drawableProvider = BasicThemeProvider(this, overrideColorScheme = colorScheme)
// ... force change keyboard view
legacyInputView = latinIMELegacy.onCreateInputView()
latinIMELegacy.loadKeyboard()
setContent()
window.window?.navigationBarColor = drawableProvider!!.primaryKeyboardColor
}
private val latinIMELegacy = LatinIMELegacy(
this as InputMethodService,
this as LatinIMELegacy.SuggestionStripController
@ -305,6 +340,15 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
override fun onCreate() {
super.onCreate()
activeColorScheme = if(!DynamicColors.isDynamicColorAvailable()) {
DarkColorScheme
} else {
val dCtx = DynamicColors.wrapContextIfAvailable(this)
dynamicLightColorScheme(dCtx)
}
mSavedStateRegistryController.performRestore(null)
handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
@ -359,26 +403,95 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
return composeView!!
}
private var currWindowAction: Action? = null
private fun onActionActivated(action: Action) {
if(action.windowImpl != null) {
currWindowAction = action
setContent()
} 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")
}
}
private var inputViewHeight: Int = -1
private var shouldShowSuggestionStrip: Boolean = true
private var suggestedWords: SuggestedWords? = null
@Composable
private fun LegacyKeyboardView() {
key(legacyInputView) {
AndroidView(factory = {
legacyInputView!!
}, update = { }, modifier = Modifier.onSizeChanged {
inputViewHeight = it.height
})
}
}
@Composable
private fun MainKeyboardViewWithActionBar() {
Column {
if (shouldShowSuggestionStrip) {
ActionBar(
suggestedWords,
latinIMELegacy,
onActionActivated = { onActionActivated(it) }
)
}
LegacyKeyboardView()
}
}
private fun returnBackToMainKeyboardViewFromAction() {
assert(currWindowAction != null)
currWindowAction = null
setContent()
}
@Composable
private fun ActionViewWithHeader(action: Action) {
val windowImpl = action.windowImpl!!
println("The height is $inputViewHeight, which in DP is ${ with(LocalDensity.current) { inputViewHeight.toDp() }}")
Column {
Surface(modifier = Modifier
.fillMaxWidth()
.height(40.dp), color = MaterialTheme.colorScheme.background)
{
Row {
IconButton(onClick = {
returnBackToMainKeyboardViewFromAction()
}) {
Icon(
painter = painterResource(id = R.drawable.arrow_left),
contentDescription = "Back"
)
}
Text(windowImpl.windowName(), style = Typography.titleMedium, modifier = Modifier.align(CenterVertically))
}
}
Box(modifier = Modifier
.fillMaxWidth()
.height(with(LocalDensity.current) { inputViewHeight.toDp() })) {
windowImpl.WindowContents(manager = this@LatinIME)
}
}
}
private fun setContent() {
composeView?.setContent {
Column {
Spacer(modifier = Modifier.weight(1.0f))
Surface(modifier = Modifier.onSizeChanged {
touchableHeight = it.height
}, color = MaterialTheme.colorScheme.surface) {
Column {
if(shouldShowSuggestionStrip) {
ActionBar(
suggestedWords,
latinIMELegacy
)
}
key(legacyInputView) {
AndroidView(factory = {
legacyInputView!!
}, update = { })
UixThemeWrapper(activeColorScheme) {
Column {
Spacer(modifier = Modifier.weight(1.0f))
Surface(modifier = Modifier.onSizeChanged {
touchableHeight = it.height
}) {
when {
currWindowAction != null -> ActionViewWithHeader(currWindowAction!!)
else -> MainKeyboardViewWithActionBar()
}
}
}
@ -500,7 +613,7 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
val touchLeft = 0
val touchTop = visibleTopY
val touchRight = legacyInputView!!.width
val touchRight = composeView!!.width
val touchBottom = inputHeight
latinIMELegacy.setInsets(outInsets!!.apply {
@ -549,4 +662,17 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
override fun maybeShowImportantNoticeTitle(): Boolean {
return false
}
override fun triggerSystemVoiceInput() {
latinIMELegacy.onCodeInput(
Constants.CODE_SHORTCUT,
Constants.SUGGESTION_STRIP_COORDINATE,
Constants.SUGGESTION_STRIP_COORDINATE,
false
);
}
override fun updateTheme(newTheme: ColorScheme) {
updateDrawableProvider(newTheme)
}
}

View File

@ -846,6 +846,7 @@ public class LatinIMELegacy implements KeyboardActionListener,
public View onCreateInputView() {
StatsUtils.onCreateInputView();
assert mDisplayContext != null;
mKeyboardSwitcher.queueThemeSwitch();
return mKeyboardSwitcher.onCreateInputView(mDisplayContext,
mIsHardwareAcceleratedDrawingEnabled);
}
@ -1948,7 +1949,7 @@ public class LatinIMELegacy implements KeyboardActionListener,
}
private void setNavigationBarVisibility(final boolean visible) {
int color = ((KeyboardDrawableProviderOwner)getInputMethodService()).getDrawableProvider().getPrimaryKeyboardColor();
int color = ((DynamicThemeProviderOwner)getInputMethodService()).getDrawableProvider().getPrimaryKeyboardColor();
if (BuildCompatUtils.EFFECTIVE_SDK_INT > Build.VERSION_CODES.M) {
// For N and later, IMEs can specify Color.TRANSPARENT to make the navigation bar
// transparent. For other colors the system uses the default color.

View File

@ -0,0 +1,27 @@
package org.futo.inputmethod.latin.uix
import androidx.annotation.DrawableRes
import androidx.compose.material3.ColorScheme
import androidx.compose.runtime.Composable
interface KeyboardManagerForAction {
fun triggerSystemVoiceInput()
fun updateTheme(newTheme: ColorScheme)
}
interface ActionWindow {
@Composable
fun windowName(): String
@Composable
fun WindowContents(manager: KeyboardManagerForAction)
}
data class Action(
@DrawableRes val icon: Int,
val name: String, // TODO: @StringRes Int
val windowImpl: ActionWindow?,
val simplePressImpl: ((KeyboardManagerForAction) -> Unit)?
)

View File

@ -1,9 +1,12 @@
package org.futo.inputmethod.latin.uix
import androidx.annotation.DrawableRes
import android.os.Build
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
@ -12,14 +15,21 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonColors
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@ -31,10 +41,11 @@ import androidx.compose.ui.draw.rotate
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.drawscope.scale
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.ExperimentalTextApi
@ -55,8 +66,8 @@ import org.futo.inputmethod.latin.SuggestedWords.SuggestedWordInfo
import org.futo.inputmethod.latin.SuggestedWords.SuggestedWordInfo.KIND_TYPED
import org.futo.inputmethod.latin.common.Constants
import org.futo.inputmethod.latin.suggestions.SuggestionStripView
import org.futo.inputmethod.latin.uix.theme.Typography
import org.futo.inputmethod.latin.uix.theme.WhisperVoiceInputTheme
import org.futo.inputmethod.latin.uix.theme.DarkColorScheme
import org.futo.inputmethod.latin.uix.theme.UixThemeWrapper
import java.lang.Integer.min
import kotlin.math.ceil
import kotlin.math.roundToInt
@ -249,48 +260,54 @@ fun RowScope.SuggestionItems(words: SuggestedWords, onClick: (i: Int) -> Unit) {
}
}
data class Action(
@DrawableRes val icon: Int
// TODO: How should the actual action abstraction look?
)
@Composable
fun ActionItem() {
fun ActionItem(action: Action, onSelect: (Action) -> Unit) {
val col = MaterialTheme.colorScheme.secondary
val contentCol = MaterialTheme.colorScheme.onSecondary
IconButton(onClick = { /*TODO*/ }, modifier = Modifier
IconButton(onClick = { onSelect(action) }, modifier = Modifier
.drawBehind {
val radius = size.height / 4.0f
drawRoundRect(
col,
topLeft = Offset(size.width * 0.1f, size.height * 0.1f),
size = Size(size.width * 0.8f, size.height * 0.8f),
topLeft = Offset(size.width * 0.1f, size.height * 0.05f),
size = Size(size.width * 0.8f, size.height * 0.9f),
cornerRadius = CornerRadius(radius, radius)
)
}
.width(50.dp)
.width(64.dp)
.fillMaxHeight(),
colors = IconButtonDefaults.iconButtonColors(contentColor = contentCol)
) {
Icon(
painter = painterResource(id = R.drawable.mic_fill),
contentDescription = "Voice Input"
painter = painterResource(id = action.icon),
contentDescription = action.name
)
}
}
@Composable
fun RowScope.ActionItems() {
// TODO
ActionItem()
ActionItem()
ActionItem()
fun ActionItemSmall(action: Action, onSelect: (Action) -> Unit) {
IconButton(onClick = {
onSelect(action)
}, modifier = Modifier
.width(42.dp)
.fillMaxHeight()) {
Icon(
painter = painterResource(id = action.icon),
contentDescription = action.name
)
}
}
@Composable
fun RowScope.ActionItems(onSelect: (Action) -> Unit) {
ActionItem(VoiceInputAction, onSelect)
ActionItem(ThemeAction, onSelect)
Box(modifier = Modifier
.fillMaxHeight()
.weight(1.0f)) {
AutoFitText("Note: Actions not yet implemented", style = Typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onBackground))
}
}
@ -341,49 +358,31 @@ fun ExpandActionsButton(isActionsOpen: Boolean, onClick: () -> Unit) {
fun ActionBar(
words: SuggestedWords?,
suggestionStripListener: SuggestionStripView.Listener,
forceOpenActionsInitially: Boolean = false
onActionActivated: (Action) -> Unit,
forceOpenActionsInitially: Boolean = false,
) {
val isActionsOpen = remember { mutableStateOf(forceOpenActionsInitially) }
WhisperVoiceInputTheme {
Surface(modifier = Modifier
.fillMaxWidth()
.height(40.dp), color = MaterialTheme.colorScheme.background)
{
Row {
ExpandActionsButton(isActionsOpen.value) { isActionsOpen.value = !isActionsOpen.value }
if(isActionsOpen.value) {
ActionItems()
} else if(words != null) {
SuggestionItems(words) {
suggestionStripListener.pickSuggestionManually(
words.getInfo(it)
)
}
} else {
Spacer(modifier = Modifier.weight(1.0f))
}
Surface(modifier = Modifier
.fillMaxWidth()
.height(40.dp), color = MaterialTheme.colorScheme.background)
{
Row {
ExpandActionsButton(isActionsOpen.value) { isActionsOpen.value = !isActionsOpen.value }
// TODO: For now, this calls CODE_SHORTCUT. In the future, we will want to
// ask the main UI to hide the keyboard and show our own voice input menu
IconButton(onClick = {
suggestionStripListener.onCodeInput(
Constants.CODE_SHORTCUT,
Constants.SUGGESTION_STRIP_COORDINATE,
Constants.SUGGESTION_STRIP_COORDINATE,
false
);
}, modifier = Modifier
.width(42.dp)
.fillMaxHeight()) {
Icon(
painter = painterResource(id = R.drawable.mic_fill),
contentDescription = "Voice Input"
if(isActionsOpen.value) {
ActionItems(onActionActivated)
} else if(words != null) {
SuggestionItems(words) {
suggestionStripListener.pickSuggestionManually(
words.getInfo(it)
)
}
} else {
Spacer(modifier = Modifier.weight(1.0f))
}
ActionItemSmall(VoiceInputAction, onActionActivated)
}
}
}
@ -437,18 +436,74 @@ val exampleSuggestedWordsEmpty = SuggestedWords(
@Composable
@Preview
fun PreviewActionBarWithSuggestions() {
ActionBar(words = exampleSuggestedWords, suggestionStripListener = ExampleListener())
fun PreviewActionBarWithSuggestions(colorScheme: ColorScheme = DarkColorScheme) {
UixThemeWrapper(colorScheme) {
ActionBar(
words = exampleSuggestedWords,
onActionActivated = { },
suggestionStripListener = ExampleListener()
)
}
}
@Composable
@Preview
fun PreviewActionBarWithEmptySuggestions() {
ActionBar(words = exampleSuggestedWordsEmpty, suggestionStripListener = ExampleListener())
fun PreviewActionBarWithEmptySuggestions(colorScheme: ColorScheme = DarkColorScheme) {
UixThemeWrapper(colorScheme) {
ActionBar(
words = exampleSuggestedWordsEmpty,
onActionActivated = { },
suggestionStripListener = ExampleListener()
)
}
}
@Composable
@Preview
fun PreviewExpandedActionBar() {
ActionBar(words = exampleSuggestedWordsEmpty, suggestionStripListener = ExampleListener(), forceOpenActionsInitially = true)
fun PreviewExpandedActionBar(colorScheme: ColorScheme = DarkColorScheme) {
UixThemeWrapper(colorScheme) {
ActionBar(
words = exampleSuggestedWordsEmpty,
onActionActivated = { },
suggestionStripListener = ExampleListener(),
forceOpenActionsInitially = true
)
}
}
@Composable
@Preview
fun PreviewActionBarWithSuggestionsDynamicLight() {
PreviewActionBarWithSuggestions(dynamicLightColorScheme(LocalContext.current))
}
@Composable
@Preview
fun PreviewActionBarWithEmptySuggestionsDynamicLight() {
PreviewActionBarWithEmptySuggestions(dynamicLightColorScheme(LocalContext.current))
}
@Composable
@Preview
fun PreviewExpandedActionBarDynamicLight() {
PreviewExpandedActionBar(dynamicLightColorScheme(LocalContext.current))
}
@Composable
@Preview
fun PreviewActionBarWithSuggestionsDynamicDark() {
PreviewActionBarWithSuggestions(dynamicDarkColorScheme(LocalContext.current))
}
@Composable
@Preview
fun PreviewActionBarWithEmptySuggestionsDynamicDark() {
PreviewActionBarWithEmptySuggestions(dynamicDarkColorScheme(LocalContext.current))
}
@Composable
@Preview
fun PreviewExpandedActionBarDynamicDark() {
PreviewExpandedActionBar(dynamicDarkColorScheme(LocalContext.current))
}

View File

@ -0,0 +1,211 @@
@file:Suppress("LocalVariableName")
package org.futo.inputmethod.latin.uix
import android.os.Build
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import org.futo.inputmethod.latin.R
import org.futo.inputmethod.latin.uix.theme.DarkColorScheme
// TODO: For now, this calls CODE_SHORTCUT. In the future, we will want to
// make this a window
val VoiceInputAction = Action(
icon = R.drawable.mic_fill,
name = "Voice Input",
simplePressImpl = {
it.triggerSystemVoiceInput()
},
windowImpl = null
)
val ThemeAction = Action(
icon = R.drawable.eye,
name = "Theme Switcher",
simplePressImpl = null,
windowImpl = object : ActionWindow {
@Composable
override fun windowName(): String {
return "Theme Switcher"
}
@Composable
override fun WindowContents(manager: KeyboardManagerForAction) {
val context = LocalContext.current
Column(modifier = Modifier.fillMaxSize().scrollable(rememberScrollState(), Orientation.Vertical)) {
Button(onClick = {
manager.updateTheme(DarkColorScheme)
}) {
Text("Default voice input theme")
}
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
Button(onClick = {
manager.updateTheme(dynamicLightColorScheme(context))
}) {
Text("Dynamic light color scheme")
}
Button(onClick = {
manager.updateTheme(dynamicDarkColorScheme(context))
}) {
Text("Dynamic dark color scheme")
}
}
Button(onClick = {
val md_theme_light_primary = Color(0xFF6750A4)
val md_theme_light_onPrimary = Color(0xFFFFFFFF)
val md_theme_light_primaryContainer = Color(0xFFEADDFF)
val md_theme_light_onPrimaryContainer = Color(0xFF21005D)
val md_theme_light_secondary = Color(0xFF625B71)
val md_theme_light_onSecondary = Color(0xFFFFFFFF)
val md_theme_light_secondaryContainer = Color(0xFFE8DEF8)
val md_theme_light_onSecondaryContainer = Color(0xFF1D192B)
val md_theme_light_tertiary = Color(0xFF7D5260)
val md_theme_light_onTertiary = Color(0xFFFFFFFF)
val md_theme_light_tertiaryContainer = Color(0xFFFFD8E4)
val md_theme_light_onTertiaryContainer = Color(0xFF31111D)
val md_theme_light_error = Color(0xFFB3261E)
val md_theme_light_onError = Color(0xFFFFFFFF)
val md_theme_light_errorContainer = Color(0xFFF9DEDC)
val md_theme_light_onErrorContainer = Color(0xFF410E0B)
val md_theme_light_outline = Color(0xFF79747E)
val md_theme_light_background = Color(0xFFFFFBFE)
val md_theme_light_onBackground = Color(0xFF1C1B1F)
val md_theme_light_surface = Color(0xFFFFFBFE)
val md_theme_light_onSurface = Color(0xFF1C1B1F)
val md_theme_light_surfaceVariant = Color(0xFFE7E0EC)
val md_theme_light_onSurfaceVariant = Color(0xFF49454F)
val md_theme_light_inverseSurface = Color(0xFF313033)
val md_theme_light_inverseOnSurface = Color(0xFFF4EFF4)
val md_theme_light_inversePrimary = Color(0xFFD0BCFF)
val md_theme_light_shadow = Color(0xFF000000)
val md_theme_light_surfaceTint = Color(0xFF6750A4)
val md_theme_light_outlineVariant = Color(0xFFCAC4D0)
val md_theme_light_scrim = Color(0xFF000000)
manager.updateTheme(
lightColorScheme(
primary = md_theme_light_primary,
onPrimary = md_theme_light_onPrimary,
primaryContainer = md_theme_light_primaryContainer,
onPrimaryContainer = md_theme_light_onPrimaryContainer,
secondary = md_theme_light_secondary,
onSecondary = md_theme_light_onSecondary,
secondaryContainer = md_theme_light_secondaryContainer,
onSecondaryContainer = md_theme_light_onSecondaryContainer,
tertiary = md_theme_light_tertiary,
onTertiary = md_theme_light_onTertiary,
tertiaryContainer = md_theme_light_tertiaryContainer,
onTertiaryContainer = md_theme_light_onTertiaryContainer,
error = md_theme_light_error,
onError = md_theme_light_onError,
errorContainer = md_theme_light_errorContainer,
onErrorContainer = md_theme_light_onErrorContainer,
outline = md_theme_light_outline,
background = md_theme_light_background,
onBackground = md_theme_light_onBackground,
surface = md_theme_light_surface,
onSurface = md_theme_light_onSurface,
surfaceVariant = md_theme_light_surfaceVariant,
onSurfaceVariant = md_theme_light_onSurfaceVariant,
inverseSurface = md_theme_light_inverseSurface,
inverseOnSurface = md_theme_light_inverseOnSurface,
inversePrimary = md_theme_light_inversePrimary,
surfaceTint = md_theme_light_surfaceTint,
outlineVariant = md_theme_light_outlineVariant,
scrim = md_theme_light_scrim,
)
)
}) {
Text("Some random light theme")
}
Button(onClick = {
val md_theme_dark_primary = Color(0xFFD0BCFF)
val md_theme_dark_onPrimary = Color(0xFF381E72)
val md_theme_dark_primaryContainer = Color(0xFF4F378B)
val md_theme_dark_onPrimaryContainer = Color(0xFFEADDFF)
val md_theme_dark_secondary = Color(0xFFCCC2DC)
val md_theme_dark_onSecondary = Color(0xFF332D41)
val md_theme_dark_secondaryContainer = Color(0xFF4A4458)
val md_theme_dark_onSecondaryContainer = Color(0xFFE8DEF8)
val md_theme_dark_tertiary = Color(0xFFEFB8C8)
val md_theme_dark_onTertiary = Color(0xFF492532)
val md_theme_dark_tertiaryContainer = Color(0xFF633B48)
val md_theme_dark_onTertiaryContainer = Color(0xFFFFD8E4)
val md_theme_dark_error = Color(0xFFF2B8B5)
val md_theme_dark_onError = Color(0xFF601410)
val md_theme_dark_errorContainer = Color(0xFF8C1D18)
val md_theme_dark_onErrorContainer = Color(0xFFF9DEDC)
val md_theme_dark_outline = Color(0xFF938F99)
val md_theme_dark_background = Color(0xFF1C1B1F)
val md_theme_dark_onBackground = Color(0xFFE6E1E5)
val md_theme_dark_surface = Color(0xFF1C1B1F)
val md_theme_dark_onSurface = Color(0xFFE6E1E5)
val md_theme_dark_surfaceVariant = Color(0xFF49454F)
val md_theme_dark_onSurfaceVariant = Color(0xFFCAC4D0)
val md_theme_dark_inverseSurface = Color(0xFFE6E1E5)
val md_theme_dark_inverseOnSurface = Color(0xFF313033)
val md_theme_dark_inversePrimary = Color(0xFF6750A4)
val md_theme_dark_shadow = Color(0xFF000000)
val md_theme_dark_surfaceTint = Color(0xFFD0BCFF)
val md_theme_dark_outlineVariant = Color(0xFF49454F)
val md_theme_dark_scrim = Color(0xFF000000)
manager.updateTheme(
darkColorScheme(
primary = md_theme_dark_primary,
onPrimary = md_theme_dark_onPrimary,
primaryContainer = md_theme_dark_primaryContainer,
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
secondary = md_theme_dark_secondary,
onSecondary = md_theme_dark_onSecondary,
secondaryContainer = md_theme_dark_secondaryContainer,
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
tertiary = md_theme_dark_tertiary,
onTertiary = md_theme_dark_onTertiary,
tertiaryContainer = md_theme_dark_tertiaryContainer,
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
error = md_theme_dark_error,
onError = md_theme_dark_onError,
errorContainer = md_theme_dark_errorContainer,
onErrorContainer = md_theme_dark_onErrorContainer,
outline = md_theme_dark_outline,
background = md_theme_dark_background,
onBackground = md_theme_dark_onBackground,
surface = md_theme_dark_surface,
onSurface = md_theme_dark_onSurface,
surfaceVariant = md_theme_dark_surfaceVariant,
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
inverseSurface = md_theme_dark_inverseSurface,
inverseOnSurface = md_theme_dark_inverseOnSurface,
inversePrimary = md_theme_dark_inversePrimary,
surfaceTint = md_theme_dark_surfaceTint,
outlineVariant = md_theme_dark_outlineVariant,
scrim = md_theme_dark_scrim,
)
)
}) {
Text("Some random dark theme")
}
}
}
}
)

View File

@ -2,6 +2,7 @@ package org.futo.inputmethod.latin.uix.theme
import android.app.Activity
import android.os.Build
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
@ -45,37 +46,10 @@ val DarkColorScheme = darkColorScheme(
)
@Composable
fun WhisperVoiceInputTheme(content: @Composable () -> Unit) {
// TODO: Switch light or dark mode
val colorScheme = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
dynamicLightColorScheme(context)
}
else -> DarkColorScheme
}
//val colorScheme = DarkColorScheme // TODO: Figure out light/dynamic if it's worth it
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
if(view.context is Activity) {
val window = (view.context as Activity).window
window.statusBarColor = colorScheme.primary.toArgb()
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars =
false
}
}
}
fun UixThemeWrapper(colorScheme: ColorScheme, content: @Composable () -> Unit) {
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content,
)
}