diff --git a/java/res/drawable/arrow_down.xml b/java/res/drawable/arrow_down.xml new file mode 100644 index 000000000..9c8ef5e6c --- /dev/null +++ b/java/res/drawable/arrow_down.xml @@ -0,0 +1,20 @@ + + + + diff --git a/java/res/drawable/arrow_left.xml b/java/res/drawable/arrow_left.xml index f53ed4d37..51868088b 100644 --- a/java/res/drawable/arrow_left.xml +++ b/java/res/drawable/arrow_left.xml @@ -1,20 +1,20 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/java/res/drawable/arrow_left_26.xml b/java/res/drawable/arrow_left_26.xml new file mode 100644 index 000000000..f53ed4d37 --- /dev/null +++ b/java/res/drawable/arrow_left_26.xml @@ -0,0 +1,20 @@ + + + + diff --git a/java/res/drawable/arrow_right.xml b/java/res/drawable/arrow_right.xml new file mode 100644 index 000000000..5109cd87b --- /dev/null +++ b/java/res/drawable/arrow_right.xml @@ -0,0 +1,20 @@ + + + + diff --git a/java/res/drawable/arrow_up.xml b/java/res/drawable/arrow_up.xml new file mode 100644 index 000000000..8d338651a --- /dev/null +++ b/java/res/drawable/arrow_up.xml @@ -0,0 +1,20 @@ + + + + diff --git a/java/res/drawable/chevron_down.xml b/java/res/drawable/chevron_down.xml new file mode 100644 index 000000000..746f6c06d --- /dev/null +++ b/java/res/drawable/chevron_down.xml @@ -0,0 +1,13 @@ + + + diff --git a/java/res/drawable/clipboard.xml b/java/res/drawable/clipboard.xml new file mode 100644 index 000000000..e4cf2e78d --- /dev/null +++ b/java/res/drawable/clipboard.xml @@ -0,0 +1,20 @@ + + + + diff --git a/java/res/drawable/close.xml b/java/res/drawable/close.xml new file mode 100644 index 000000000..991e1ca0d --- /dev/null +++ b/java/res/drawable/close.xml @@ -0,0 +1,20 @@ + + + + diff --git a/java/res/drawable/copy.xml b/java/res/drawable/copy.xml new file mode 100644 index 000000000..be18c41bb --- /dev/null +++ b/java/res/drawable/copy.xml @@ -0,0 +1,20 @@ + + + + diff --git a/java/res/drawable/ctrl.xml b/java/res/drawable/ctrl.xml new file mode 100644 index 000000000..9c0a06d18 --- /dev/null +++ b/java/res/drawable/ctrl.xml @@ -0,0 +1,27 @@ + + + + + + + + diff --git a/java/res/drawable/cut.xml b/java/res/drawable/cut.xml new file mode 100644 index 000000000..3a734cc16 --- /dev/null +++ b/java/res/drawable/cut.xml @@ -0,0 +1,41 @@ + + + + + + + diff --git a/java/res/drawable/edit_text.xml b/java/res/drawable/edit_text.xml new file mode 100644 index 000000000..f2d53814c --- /dev/null +++ b/java/res/drawable/edit_text.xml @@ -0,0 +1,20 @@ + + + + diff --git a/java/res/drawable/redo.xml b/java/res/drawable/redo.xml new file mode 100644 index 000000000..290274cdb --- /dev/null +++ b/java/res/drawable/redo.xml @@ -0,0 +1,20 @@ + + + + diff --git a/java/res/drawable/undo.xml b/java/res/drawable/undo.xml new file mode 100644 index 000000000..8ad21710b --- /dev/null +++ b/java/res/drawable/undo.xml @@ -0,0 +1,20 @@ + + + + diff --git a/java/res/values/attrs.xml b/java/res/values/attrs.xml index 38bb14f92..68c620e88 100644 --- a/java/res/values/attrs.xml +++ b/java/res/values/attrs.xml @@ -421,6 +421,7 @@ + diff --git a/java/res/values/strings-uix.xml b/java/res/values/strings-uix.xml index 579c94cef..ae8682877 100644 --- a/java/res/values/strings-uix.xml +++ b/java/res/values/strings-uix.xml @@ -2,6 +2,11 @@ Voice Input Theme Switcher + Emojis + Paste from Clipboard + Undo + Redo + Text Editor AMOLED Dark Purple AOSP Material Dark diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml index d3a7f826c..e7b51e048 100644 --- a/java/res/values/strings.xml +++ b/java/res/values/strings.xml @@ -573,5 +573,4 @@ Tip: You can download and remove dictionaries by going to <b>Languages & This resource is copied from packages/apps/Settings/res/values/strings.xml --> \u0020ABCDEFGHIJKLMNOPQRSTUVWXYZ - Emojis diff --git a/java/res/values/themes-lxx-dark.xml b/java/res/values/themes-lxx-dark.xml index 2b53e747f..126d24aea 100644 --- a/java/res/values/themes-lxx-dark.xml +++ b/java/res/values/themes-lxx-dark.xml @@ -48,6 +48,7 @@ @drawable/btn_keyboard_spacebar_lxx_dark @color/key_text_color_lxx_dark @color/key_functional_text_color_lxx_dark + @color/key_text_color_lxx_dark @color/key_text_color_lxx_dark @color/key_hint_letter_color_lxx_dark @color/key_text_inactive_color_lxx_dark diff --git a/java/res/values/themes-lxx-light.xml b/java/res/values/themes-lxx-light.xml index aac14fa41..f11e45ff7 100644 --- a/java/res/values/themes-lxx-light.xml +++ b/java/res/values/themes-lxx-light.xml @@ -48,6 +48,7 @@ @drawable/btn_keyboard_spacebar_lxx_light @color/key_text_color_lxx_light @color/key_text_inactive_color_lxx_light + @color/key_text_color_lxx_light @color/key_functional_text_color_lxx_light @color/key_hint_letter_color_lxx_light @color/key_text_inactive_color_lxx_light diff --git a/java/res/xml/rowkeys_qwerty2_left5.xml b/java/res/xml/rowkeys_qwerty2_left5.xml index 540b29b49..cfd1a500b 100644 --- a/java/res/xml/rowkeys_qwerty2_left5.xml +++ b/java/res/xml/rowkeys_qwerty2_left5.xml @@ -23,16 +23,26 @@ > + latin:keySpec="f" + latin:keyHintLabel="%" + latin:additionalMoreKeys="%" /> diff --git a/java/res/xml/rowkeys_qwerty2_right4.xml b/java/res/xml/rowkeys_qwerty2_right4.xml index 685c5b81d..555b9e344 100644 --- a/java/res/xml/rowkeys_qwerty2_right4.xml +++ b/java/res/xml/rowkeys_qwerty2_right4.xml @@ -23,14 +23,22 @@ > diff --git a/java/res/xml/rowkeys_qwerty3_left4.xml b/java/res/xml/rowkeys_qwerty3_left4.xml index 09de5a7e5..d263eeb2a 100644 --- a/java/res/xml/rowkeys_qwerty3_left4.xml +++ b/java/res/xml/rowkeys_qwerty3_left4.xml @@ -23,14 +23,22 @@ > diff --git a/java/res/xml/rowkeys_qwerty3_right3.xml b/java/res/xml/rowkeys_qwerty3_right3.xml index ccacb2cf2..dcf1f4ad4 100644 --- a/java/res/xml/rowkeys_qwerty3_right3.xml +++ b/java/res/xml/rowkeys_qwerty3_right3.xml @@ -22,10 +22,16 @@ xmlns:latin="http://schemas.android.com/apk/res/org.futo.inputmethod.latin" > + latin:keySpec="b" + latin:keyHintLabel=";" + latin:additionalMoreKeys=";" /> + latin:keySpec="m" + latin:keyHintLabel="?" + latin:additionalMoreKeys="?,/" /> diff --git a/java/src/org/futo/inputmethod/keyboard/Key.java b/java/src/org/futo/inputmethod/keyboard/Key.java index 683e60245..9d19098b3 100644 --- a/java/src/org/futo/inputmethod/keyboard/Key.java +++ b/java/src/org/futo/inputmethod/keyboard/Key.java @@ -665,6 +665,10 @@ public class Key implements Comparable { if ((mLabelFlags & LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR) != 0) { return params.mFunctionalTextColor; } + if (mPressed) { + return params.mPressedTextColor; + } + return isShiftedLetterActivated() ? params.mTextInactivatedColor : params.mTextColor; } diff --git a/java/src/org/futo/inputmethod/keyboard/KeyboardActionListener.java b/java/src/org/futo/inputmethod/keyboard/KeyboardActionListener.java index a8e75de45..1bb2ed695 100644 --- a/java/src/org/futo/inputmethod/keyboard/KeyboardActionListener.java +++ b/java/src/org/futo/inputmethod/keyboard/KeyboardActionListener.java @@ -101,6 +101,11 @@ public interface KeyboardActionListener { */ public boolean onCustomRequest(int requestCode); + public void onMovePointer(int steps); + public void onMoveDeletePointer(int steps); + public void onUpWithDeletePointerActive(); + public void onUpWithPointerActive(); + public static final KeyboardActionListener EMPTY_LISTENER = new Adapter(); public static class Adapter implements KeyboardActionListener { @@ -125,8 +130,14 @@ public interface KeyboardActionListener { @Override public void onFinishSlidingInput() {} @Override - public boolean onCustomRequest(int requestCode) { - return false; - } + public boolean onCustomRequest(int requestCode) { return false; } + @Override + public void onMovePointer(int steps) {} + @Override + public void onMoveDeletePointer(int steps) {} + @Override + public void onUpWithDeletePointerActive() {} + @Override + public void onUpWithPointerActive() {} } } diff --git a/java/src/org/futo/inputmethod/keyboard/KeyboardView.java b/java/src/org/futo/inputmethod/keyboard/KeyboardView.java index e95878ec5..8418830e6 100644 --- a/java/src/org/futo/inputmethod/keyboard/KeyboardView.java +++ b/java/src/org/futo/inputmethod/keyboard/KeyboardView.java @@ -176,6 +176,11 @@ public class KeyboardView extends View { R.styleable.Keyboard_Key, defStyle, R.style.KeyboardView); mDefaultKeyLabelFlags = keyAttr.getInt(R.styleable.Keyboard_Key_keyLabelFlags, 0); mKeyVisualAttributes = KeyVisualAttributes.newInstance(keyAttr, mDrawableProvider); + + if(isMoreKeys && mKeyVisualAttributes != null) { + mKeyVisualAttributes.mTextColor = mDrawableProvider.getMoreKeysTextColor(); + } + keyAttr.recycle(); mPaint.setAntiAlias(true); diff --git a/java/src/org/futo/inputmethod/keyboard/PointerTracker.java b/java/src/org/futo/inputmethod/keyboard/PointerTracker.java index d19d91ad0..4e59e44ba 100644 --- a/java/src/org/futo/inputmethod/keyboard/PointerTracker.java +++ b/java/src/org/futo/inputmethod/keyboard/PointerTracker.java @@ -85,6 +85,8 @@ public final class PointerTracker implements PointerTrackerQueue.Element, // Parameters for pointer handling. private static PointerTrackerParams sParams; + private static final int sPointerStep = (int)(16.0 * Resources.getSystem().getDisplayMetrics().density); + private static GestureStrokeRecognitionParams sGestureStrokeRecognitionParams; private static GestureStrokeDrawingParams sGestureStrokeDrawingParams; private static boolean sNeedsPhantomSuddenMoveEventHack; @@ -128,6 +130,11 @@ public final class PointerTracker implements PointerTrackerQueue.Element, private int mLastX; private int mLastY; + private int mStartX; + private int mStartY; + private long mStartTime; + private boolean mCursorMoved = false; + // true if keyboard layout has been changed. private boolean mKeyboardLayoutHasBeenChanged; @@ -691,6 +698,10 @@ public final class PointerTracker implements PointerTrackerQueue.Element, startRepeatKey(key); startLongPressTimer(key); setPressedKeyGraphics(key, eventTime); + + mStartX = x; + mStartY = y; + mStartTime = System.currentTimeMillis(); } } @@ -892,6 +903,29 @@ public final class PointerTracker implements PointerTrackerQueue.Element, final int lastX = mLastX; final int lastY = mLastY; final Key oldKey = mCurrentKey; + + if (oldKey != null && oldKey.getCode() == Constants.CODE_SPACE) { + int steps = (x - mStartX) / sPointerStep; + final int swipeIgnoreTime = Settings.getInstance().getCurrent().mKeyLongpressTimeout / MULTIPLIER_FOR_LONG_PRESS_TIMEOUT_IN_SLIDING_INPUT; + if (steps != 0 && mStartTime + swipeIgnoreTime < System.currentTimeMillis()) { + mCursorMoved = true; + mStartX += steps * sPointerStep; + sListener.onMovePointer(steps); + } + return; + } + + if (oldKey != null && oldKey.getCode() == Constants.CODE_DELETE) { + int steps = (x - mStartX) / sPointerStep; + if (steps != 0) { + sTimerProxy.cancelKeyTimersOf(this); + mCursorMoved = true; + mStartX += steps * sPointerStep; + sListener.onMoveDeletePointer(steps); + } + return; + } + final Key newKey = onMoveKey(x, y); if (sGestureEnabler.shouldHandleGesture()) { @@ -966,6 +1000,14 @@ public final class PointerTracker implements PointerTrackerQueue.Element, // Release the last pressed key. setReleasedKeyGraphics(currentKey, true /* withAnimation */); + if(mCursorMoved && currentKey.getCode() == Constants.CODE_DELETE) { + sListener.onUpWithDeletePointerActive(); + } + + if(mCursorMoved) { + sListener.onUpWithPointerActive(); + } + if (isShowingMoreKeysPanel()) { if (!mIsTrackingForActionDisabled) { final int translatedX = mMoreKeysPanel.translateX(x); @@ -988,6 +1030,10 @@ public final class PointerTracker implements PointerTrackerQueue.Element, return; } + if (mCursorMoved) { + mCursorMoved = false; + return; + } if (mIsTrackingForActionDisabled) { return; } @@ -1018,6 +1064,9 @@ public final class PointerTracker implements PointerTrackerQueue.Element, if (isShowingMoreKeysPanel()) { return; } + if (mCursorMoved) { + return; + } final Key key = getKey(); if (key == null) { return; diff --git a/java/src/org/futo/inputmethod/keyboard/internal/KeyDrawParams.java b/java/src/org/futo/inputmethod/keyboard/internal/KeyDrawParams.java index b3a913baa..8447aaae4 100644 --- a/java/src/org/futo/inputmethod/keyboard/internal/KeyDrawParams.java +++ b/java/src/org/futo/inputmethod/keyboard/internal/KeyDrawParams.java @@ -37,6 +37,7 @@ public final class KeyDrawParams { public int mTextColor; public int mTextInactivatedColor; + public int mPressedTextColor; public int mTextShadowColor; public int mFunctionalTextColor; public int mHintLetterColor; @@ -66,6 +67,7 @@ public final class KeyDrawParams { mTextColor = copyFrom.mTextColor; mTextInactivatedColor = copyFrom.mTextInactivatedColor; + mPressedTextColor = copyFrom.mPressedTextColor; mTextShadowColor = copyFrom.mTextShadowColor; mFunctionalTextColor = copyFrom.mFunctionalTextColor; mHintLetterColor = copyFrom.mHintLetterColor; @@ -103,6 +105,7 @@ public final class KeyDrawParams { mTextColor = selectColor(attr.mTextColor, mTextColor); mTextInactivatedColor = selectColor(attr.mTextInactivatedColor, mTextInactivatedColor); + mPressedTextColor = selectColor(attr.mPressedTextColor, mPressedTextColor); mTextShadowColor = selectColor(attr.mTextShadowColor, mTextShadowColor); mFunctionalTextColor = selectColor(attr.mFunctionalTextColor, mFunctionalTextColor); mHintLetterColor = selectColor(attr.mHintLetterColor, mHintLetterColor); diff --git a/java/src/org/futo/inputmethod/keyboard/internal/KeyVisualAttributes.java b/java/src/org/futo/inputmethod/keyboard/internal/KeyVisualAttributes.java index dd5e8704c..5aaf75aad 100644 --- a/java/src/org/futo/inputmethod/keyboard/internal/KeyVisualAttributes.java +++ b/java/src/org/futo/inputmethod/keyboard/internal/KeyVisualAttributes.java @@ -41,8 +41,9 @@ public final class KeyVisualAttributes { public final float mHintLabelRatio; public final float mPreviewTextRatio; - public final int mTextColor; + public int mTextColor; public final int mTextInactivatedColor; + public final int mPressedTextColor; public final int mTextShadowColor; public final int mFunctionalTextColor; public final int mHintLetterColor; @@ -130,6 +131,8 @@ public final class KeyVisualAttributes { R.styleable.Keyboard_Key_keyTextColor, 0, keyAttr, provider); mTextInactivatedColor = DynamicThemeProvider.Companion.getColorOrDefault( R.styleable.Keyboard_Key_keyTextInactivatedColor, 0, keyAttr, provider); + mPressedTextColor = DynamicThemeProvider.Companion.getColorOrDefault( + R.styleable.Keyboard_Key_keyPressedTextColor, 0, keyAttr, provider); mTextShadowColor = DynamicThemeProvider.Companion.getColorOrDefault( R.styleable.Keyboard_Key_keyTextShadowColor, 0, keyAttr, provider); mFunctionalTextColor = DynamicThemeProvider.Companion.getColorOrDefault( diff --git a/java/src/org/futo/inputmethod/keyboard/internal/KeyboardTextsTable.java b/java/src/org/futo/inputmethod/keyboard/internal/KeyboardTextsTable.java index 96f606605..3a8018850 100644 --- a/java/src/org/futo/inputmethod/keyboard/internal/KeyboardTextsTable.java +++ b/java/src/org/futo/inputmethod/keyboard/internal/KeyboardTextsTable.java @@ -302,7 +302,7 @@ public final class KeyboardTextsTable { /* ~ additional_morekeys_symbols_0 */ /* morekeys_tablet_period */ "!text/morekeys_tablet_punctuation", /* morekeys_nordic_row2_11 */ EMPTY, - /* morekeys_punctuation */ "!autoColumnOrder!8,\\,,?,!,#,!text/keyspec_right_parenthesis,!text/keyspec_left_parenthesis,/,;,',@,:,-,\",+,\\%,&", + /* morekeys_punctuation */ "_,\\\\,|,=", /* keyspec_tablet_comma */ ",", // Period key /* keyspec_period */ ".", diff --git a/java/src/org/futo/inputmethod/latin/LatinIME.kt b/java/src/org/futo/inputmethod/latin/LatinIME.kt index fa9d29185..d4ce31926 100644 --- a/java/src/org/futo/inputmethod/latin/LatinIME.kt +++ b/java/src/org/futo/inputmethod/latin/LatinIME.kt @@ -1,6 +1,5 @@ package org.futo.inputmethod.latin -import android.content.Context import android.content.res.Configuration import android.inputmethodservice.InputMethodService import android.os.Build @@ -13,34 +12,17 @@ import android.view.inputmethod.InlineSuggestionsRequest import android.view.inputmethod.InlineSuggestionsResponse import android.view.inputmethod.InputMethodSubtype import androidx.annotation.RequiresApi -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.foundation.layout.size 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.runtime.Composable -import androidx.compose.runtime.MutableState +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.key -import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clipToBounds 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.viewinterop.AndroidView import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import androidx.lifecycle.ViewModelStore @@ -56,34 +38,24 @@ import androidx.savedstate.SavedStateRegistryOwner import androidx.savedstate.findViewTreeSavedStateRegistryOwner import androidx.savedstate.setViewTreeSavedStateRegistryOwner import kotlinx.coroutines.Job -import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -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.ActionInputTransaction -import org.futo.inputmethod.latin.uix.ActionWindow 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.PersistentActionState import org.futo.inputmethod.latin.uix.THEME_KEY +import org.futo.inputmethod.latin.uix.UixManager import org.futo.inputmethod.latin.uix.createInlineSuggestionsRequest import org.futo.inputmethod.latin.uix.deferGetSetting import org.futo.inputmethod.latin.uix.deferSetSetting import org.futo.inputmethod.latin.uix.differsFrom -import org.futo.inputmethod.latin.uix.inflateInlineSuggestion import org.futo.inputmethod.latin.uix.theme.DarkColorScheme import org.futo.inputmethod.latin.uix.theme.ThemeOption import org.futo.inputmethod.latin.uix.theme.ThemeOptions -import org.futo.inputmethod.latin.uix.theme.Typography -import org.futo.inputmethod.latin.uix.theme.UixThemeWrapper import org.futo.inputmethod.latin.uix.theme.presets.VoiceInputTheme import org.futo.inputmethod.latin.xlm.LanguageModelFacilitator class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner, - LatinIMELegacy.SuggestionStripController, DynamicThemeProviderOwner, KeyboardManagerForAction { + LatinIMELegacy.SuggestionStripController, DynamicThemeProviderOwner { private val mSavedStateRegistryController = SavedStateRegistryController.create(this) @@ -101,7 +73,7 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save override val viewModelStore get() = store - private fun setOwners() { + fun setOwners() { val decorView = window.window?.decorView if (decorView?.findViewTreeLifecycleOwner() == null) { decorView?.setViewTreeLifecycleOwner(this) @@ -114,14 +86,14 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save } } - private var composeView: ComposeView? = null - - private val latinIMELegacy = LatinIMELegacy( + val latinIMELegacy = LatinIMELegacy( this as InputMethodService, this as LatinIMELegacy.SuggestionStripController ) - public val languageModelFacilitator = LanguageModelFacilitator( + val inputLogic get() = latinIMELegacy.mInputLogic + + val languageModelFacilitator = LanguageModelFacilitator( this, latinIMELegacy.mInputLogic, latinIMELegacy.mDictionaryFacilitator, @@ -130,21 +102,18 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save lifecycleScope ) + val uixManager = UixManager(this) + private var activeThemeOption: ThemeOption? = null private var activeColorScheme = DarkColorScheme private var colorSchemeLoaderJob: Job? = null + private var pendingRecreateKeyboard: Boolean = false + + val themeOption get() = activeThemeOption + val colorScheme get() = activeColorScheme private var drawableProvider: DynamicThemeProvider? = null - private var currWindowAction: Action? = null - private var currWindowActionWindow: ActionWindow? = null - private var persistentStates: HashMap = hashMapOf() - private fun isActionWindowOpen(): Boolean { - return currWindowActionWindow != null - } - - private var inlineSuggestions: List> = listOf() - private var lastEditorInfo: EditorInfo? = null // TODO: Calling this repeatedly as the theme changes tends to slow everything to a crawl @@ -158,7 +127,7 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save drawableProvider = BasicThemeProvider(this, overrideColorScheme = colorScheme) window.window?.navigationBarColor = drawableProvider!!.primaryKeyboardColor - setContent() + uixManager.onColorSchemeChanged() } override fun getDrawableProvider(): DynamicThemeProvider { @@ -193,6 +162,32 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save } } + fun updateTheme(newTheme: ThemeOption) { + assert(newTheme.available(this)) + + if (activeThemeOption != newTheme) { + activeThemeOption = newTheme + updateDrawableProvider(newTheme.obtainColors(this)) + deferSetSetting(THEME_KEY, newTheme.key) + + if(!uixManager.isMainKeyboardHidden) { + recreateKeyboard() + } else { + pendingRecreateKeyboard = true + } + } + } + + // Called by UixManager when the intention is to subsequently call LegacyKeyboardView with hidden=false + // Maybe this can be changed to LaunchedEffect + fun onKeyboardShown() { + //if(pendingRecreateKeyboard) { + // pendingRecreateKeyboard = false + // recreateKeyboard() + //} + } + + override fun onCreate() { super.onCreate() @@ -241,41 +236,38 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save private var touchableHeight: Int = 0 override fun onCreateInputView(): View { legacyInputView = latinIMELegacy.onCreateInputView() - composeView = ComposeView(this).apply { - setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) - setParentCompositionContext(null) - - this@LatinIME.setOwners() - } - - setContent() + val composeView = uixManager.createComposeView() latinIMELegacy.setComposeInputView(composeView) - return composeView!! - } - - private fun onActionActivated(action: Action) { - // Finish what we are typing so far - latinIMELegacy.onFinishInputViewInternal(false) - - if (action.windowImpl != null) { - enterActionWindowView(action) - } else if (action.simplePressImpl != null) { - action.simplePressImpl.invoke(this, persistentStates[action]) - } else { - throw IllegalStateException("An action must have either a window implementation or a simple press implementation") - } + return composeView } private var inputViewHeight: Int = -1 - private var shouldShowSuggestionStrip: Boolean = true - private var suggestedWords: SuggestedWords? = null + // Both called by UixManager + fun updateTouchableHeight(to: Int) { touchableHeight = to } + fun getInputViewHeight(): Int = inputViewHeight + + // The keyboard view really doesn't like being detached, so it's always + // shown, but resized to 0 if an action window is open @Composable - private fun LegacyKeyboardView(hidden: Boolean) { + internal fun LegacyKeyboardView(hidden: Boolean) { + LaunchedEffect(hidden) { + if(hidden) { + latinIMELegacy.mKeyboardSwitcher.saveKeyboardState() + } else { + if(pendingRecreateKeyboard) { + pendingRecreateKeyboard = false + recreateKeyboard() + } + } + } + val modifier = if(hidden) { - Modifier.clipToBounds().size(0.dp) + Modifier + .clipToBounds() + .size(0.dp) } else { Modifier.onSizeChanged { inputViewHeight = it.height @@ -288,127 +280,13 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save } } - @Composable - private fun MainKeyboardViewWithActionBar() { - Column { - // Don't show suggested words when it's not meant to be shown - val suggestedWordsOrNull = if(shouldShowSuggestionStrip) { - suggestedWords - } else { - null - } - - ActionBar( - suggestedWordsOrNull, - latinIMELegacy, - inlineSuggestions = inlineSuggestions, - onActionActivated = { onActionActivated(it) } - ) - } - } - - private fun enterActionWindowView(action: Action) { - assert(action.windowImpl != null) - - latinIMELegacy.mKeyboardSwitcher.saveKeyboardState() - - currWindowAction = action - - if (persistentStates[action] == null) { - persistentStates[action] = action.persistentState?.let { it(this) } - } - - currWindowActionWindow = action.windowImpl?.let { it(this, persistentStates[action]) } - - setContent() - } - - private fun returnBackToMainKeyboardViewFromAction() { - if(currWindowActionWindow == null) return - - currWindowActionWindow!!.close() - - currWindowAction = null - currWindowActionWindow = null - - if(hasThemeChanged) { - hasThemeChanged = false - recreateKeyboard() - } - - setContent() - } - - @Composable - private fun ActionViewWithHeader(windowImpl: ActionWindow) { - 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() - } - } - } - - private fun setContent() { - composeView?.setContent { - UixThemeWrapper(activeColorScheme) { - Column { - Spacer(modifier = Modifier.weight(1.0f)) - Surface(modifier = Modifier.onSizeChanged { - touchableHeight = it.height - }) { - Column { - when { - isActionWindowOpen() -> ActionViewWithHeader( - currWindowActionWindow!! - ) - - else -> MainKeyboardViewWithActionBar() - } - - // The keyboard view really doesn't like being detached, so it's always - // shown, but resized to 0 if an action window is open - LegacyKeyboardView(hidden = isActionWindowOpen()) - } - } - } - } - } - } - // necessary for when KeyboardSwitcher updates the theme fun updateLegacyView(newView: View) { legacyInputView = newView - setContent() - if (composeView != null) { - latinIMELegacy.setComposeInputView(composeView) + uixManager.setContent() + uixManager.getComposeView()?.let { + latinIMELegacy.setComposeInputView(it) } latinIMELegacy.setInputView(legacyInputView) @@ -417,8 +295,8 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save override fun setInputView(view: View?) { super.setInputView(view) - if (composeView != null) { - latinIMELegacy.setComposeInputView(composeView) + uixManager.getComposeView()?.let { + latinIMELegacy.setComposeInputView(it) } latinIMELegacy.setInputView(legacyInputView) @@ -443,15 +321,14 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save override fun onFinishInputView(finishingInput: Boolean) { super.onFinishInputView(finishingInput) latinIMELegacy.onFinishInputView(finishingInput) - - closeActionWindow() + uixManager.onInputFinishing() } override fun onFinishInput() { super.onFinishInput() latinIMELegacy.onFinishInput() - closeActionWindow() + uixManager.onInputFinishing() languageModelFacilitator.saveHistoryLog() } @@ -471,7 +348,7 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save super.onWindowHidden() latinIMELegacy.onWindowHidden() - closeActionWindow() + uixManager.onInputFinishing() } override fun onUpdateSelection( @@ -521,12 +398,14 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save } override fun onComputeInsets(outInsets: Insets?) { + val composeView = uixManager.getComposeView() + // This method may be called before {@link #setInputView(View)}. if (legacyInputView == null || composeView == null) { return } - val inputHeight: Int = composeView!!.height + val inputHeight: Int = composeView.height if (latinIMELegacy.isImeSuppressedByHardwareKeyboard && !legacyInputView!!.isShown) { // If there is a hardware keyboard and a visible software keyboard view has been hidden, // no visual element will be shown on the screen. @@ -541,7 +420,7 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save val touchLeft = 0 val touchTop = visibleTopY - val touchRight = composeView!!.width + val touchRight = composeView.width val touchBottom = inputHeight latinIMELegacy.setInsets(outInsets!!.apply { @@ -581,124 +460,25 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save } override fun updateVisibility(shouldShowSuggestionsStrip: Boolean, fullscreenMode: Boolean) { - this.shouldShowSuggestionStrip = shouldShowSuggestionsStrip - setContent() + uixManager.updateVisibility(shouldShowSuggestionsStrip, fullscreenMode) } override fun setSuggestions(suggestedWords: SuggestedWords?, rtlSubtype: Boolean) { - this.suggestedWords = suggestedWords - setContent() + uixManager.setSuggestions(suggestedWords, rtlSubtype) } override fun maybeShowImportantNoticeTitle(): Boolean { return false } - private fun cleanUpPersistentStates() { - println("Cleaning up persistent states") - for((key, value) in persistentStates.entries) { - if(currWindowAction != key) { - lifecycleScope.launch { value?.cleanUp() } - } - } - } - override fun onLowMemory() { super.onLowMemory() - cleanUpPersistentStates() + uixManager.cleanUpPersistentStates() } override fun onTrimMemory(level: Int) { super.onTrimMemory(level) - cleanUpPersistentStates() - } - - override fun getContext(): Context { - return this - } - - override fun getLifecycleScope(): LifecycleCoroutineScope { - return lifecycleScope - } - - override fun triggerContentUpdate() { - setContent() - } - - private class LatinIMEActionInputTransaction( - private val latinIME: LatinIME, - shouldApplySpace: Boolean - ): ActionInputTransaction { - private val isSpaceNecessary: Boolean - init { - val priorText = latinIME.latinIMELegacy.mInputLogic.mConnection.getTextBeforeCursor(1, 0) - isSpaceNecessary = shouldApplySpace && !priorText.isNullOrEmpty() && !priorText.last().isWhitespace() - } - - private fun transformText(text: String): String { - return if(isSpaceNecessary) { " $text" } else { text } - } - - override fun updatePartial(text: String) { - latinIME.latinIMELegacy.mInputLogic.mConnection.setComposingText( - transformText(text), - 1 - ) - } - - override fun commit(text: String) { - latinIME.latinIMELegacy.mInputLogic.mConnection.commitText( - transformText(text), - 1 - ) - } - - override fun cancel() { - // TODO: Do we want to leave the composing text as-is, or delete it? - latinIME.latinIMELegacy.mInputLogic.mConnection.finishComposingText() - } - } - - override fun createInputTransaction(applySpaceIfNeeded: Boolean): ActionInputTransaction { - return LatinIMEActionInputTransaction(this, applySpaceIfNeeded) - } - - override fun typeText(v: String) { - latinIMELegacy.mInputLogic.mConnection.commitText(v, 1) - } - - override fun backspace(amount: Int) { - latinIMELegacy.mInputLogic.mConnection.deleteTextBeforeCursor(amount) - } - - override fun closeActionWindow() { - if(currWindowActionWindow == null) return - returnBackToMainKeyboardViewFromAction() - } - - override fun triggerSystemVoiceInput() { - latinIMELegacy.onCodeInput( - Constants.CODE_SHORTCUT, - Constants.SUGGESTION_STRIP_COORDINATE, - Constants.SUGGESTION_STRIP_COORDINATE, - false - ); - } - - private var hasThemeChanged: Boolean = false - override fun updateTheme(newTheme: ThemeOption) { - assert(newTheme.available(this)) - - if (activeThemeOption != newTheme) { - activeThemeOption = newTheme - updateDrawableProvider(newTheme.obtainColors(this)) - deferSetSetting(THEME_KEY, newTheme.key) - - hasThemeChanged = true - if(!isActionWindowOpen()) { - recreateKeyboard() - } - } + uixManager.cleanUpPersistentStates() } @RequiresApi(Build.VERSION_CODES.R) @@ -708,12 +488,7 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save @RequiresApi(Build.VERSION_CODES.R) override fun onInlineSuggestionsResponse(response: InlineSuggestionsResponse): Boolean { - inlineSuggestions = response.inlineSuggestions.map { - inflateInlineSuggestion(it) - } - setContent() - - return true + return uixManager.onInlineSuggestionsResponse(response) } fun postUpdateSuggestionStrip(inputStyle: Int) { diff --git a/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java b/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java index 74f123d34..a6dfe0782 100644 --- a/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java +++ b/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java @@ -41,6 +41,7 @@ import android.os.IBinder; import android.os.Message; import android.preference.PreferenceManager; import android.text.InputType; +import android.text.TextUtils; import android.util.Log; import android.util.PrintWriterPrinter; import android.util.Printer; @@ -158,6 +159,7 @@ public class LatinIMELegacy implements KeyboardActionListener, private static final String SCHEME_PACKAGE = "package"; final Settings mSettings; + private Locale mLocale; final DictionaryFacilitator mDictionaryFacilitator = DictionaryFacilitatorProvider.getDictionaryFacilitator( false /* isNeededForSpellChecking */); @@ -671,18 +673,18 @@ public class LatinIMELegacy implements KeyboardActionListener, // Has to be package-visible for unit tests @UsedForTesting void loadSettings() { - final Locale locale = mRichImm.getCurrentSubtypeLocale(); + mLocale = mRichImm.getCurrentSubtypeLocale(); final EditorInfo editorInfo = mInputMethodService.getCurrentInputEditorInfo(); final InputAttributes inputAttributes = new InputAttributes( editorInfo, mInputMethodService.isFullscreenMode(), mInputMethodService.getPackageName()); - mSettings.loadSettings(mInputMethodService, locale, inputAttributes); + mSettings.loadSettings(mInputMethodService, mLocale, inputAttributes); final SettingsValues currentSettingsValues = mSettings.getCurrent(); AudioAndHapticFeedbackManager.getInstance().onSettingsChanged(currentSettingsValues); // This method is called on startup and language switch, before the new layout has // been displayed. Opening dictionaries never affects responsivity as dictionaries are // asynchronously loaded. if (!mHandler.hasPendingReopenDictionaries()) { - resetDictionaryFacilitator(locale); + resetDictionaryFacilitator(mLocale); } refreshPersonalizationDictionarySession(currentSettingsValues); resetDictionaryFacilitatorIfNecessary(); @@ -1365,6 +1367,54 @@ public class LatinIMELegacy implements KeyboardActionListener, return false; } + @Override + public void onMovePointer(int steps) { + if (mInputLogic.mConnection.hasCursorPosition()) { + if (TextUtils.getLayoutDirectionFromLocale(mLocale) == View.LAYOUT_DIRECTION_RTL) + steps = -steps; + + steps = mInputLogic.mConnection.getUnicodeSteps(steps, true); + final int end = mInputLogic.mConnection.getExpectedSelectionEnd() + steps; + final int start = mInputLogic.mConnection.hasSelection() ? mInputLogic.mConnection.getExpectedSelectionStart() : end; + + mInputLogic.finishInput(); + mInputLogic.mConnection.setSelection(start, end); + } else { + for (; steps < 0; steps++) + mInputLogic.sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_LEFT, 0); + for (; steps > 0; steps--) + mInputLogic.sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_RIGHT, 0); + } + } + + @Override + public void onMoveDeletePointer(int steps) { + if (mInputLogic.mConnection.hasCursorPosition()) { + steps = mInputLogic.mConnection.getUnicodeSteps(steps, false); + final int end = mInputLogic.mConnection.getExpectedSelectionEnd(); + final int start = mInputLogic.mConnection.getExpectedSelectionStart() + steps; + if (start > end) + return; + + mInputLogic.finishInput(); + mInputLogic.mConnection.setSelection(start, end); + } else { + for (; steps < 0; steps++) + mInputLogic.sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL, 0); + } + } + + @Override + public void onUpWithDeletePointerActive() { + if (mInputLogic.mConnection.hasSelection()) + mInputLogic.sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL, 0); + } + + @Override + public void onUpWithPointerActive() { + mInputLogic.restartSuggestionsOnWordTouchedByCursor(mSettings.getCurrent(), false, mKeyboardSwitcher.getCurrentKeyboardScriptId()); + } + private boolean isShowingOptionDialog() { return mOptionsDialog != null && mOptionsDialog.isShowing(); } diff --git a/java/src/org/futo/inputmethod/latin/RichInputConnection.java b/java/src/org/futo/inputmethod/latin/RichInputConnection.java index 5c07d7e4d..6e719a1dc 100644 --- a/java/src/org/futo/inputmethod/latin/RichInputConnection.java +++ b/java/src/org/futo/inputmethod/latin/RichInputConnection.java @@ -1044,4 +1044,41 @@ public final class RichInputConnection implements PrivateCommandPerformer { return InputConnectionCompatUtils.requestCursorUpdates( mIC, enableMonitor, requestImmediateCallback); } + + public boolean hasCursorPosition() { + return mExpectedSelStart != INVALID_CURSOR_POSITION && mExpectedSelEnd != INVALID_CURSOR_POSITION; + } + + /** + * Some chars, such as emoji consist of 2 chars (surrogate pairs). We should treat them as one character. + */ + public int getUnicodeSteps(int chars, boolean rightSidePointer) { + int steps = 0; + if (chars < 0) { + CharSequence charsBeforeCursor = rightSidePointer && hasSelection() ? + getSelectedText(0) : + getTextBeforeCursor(-chars * 2, 0); + if (charsBeforeCursor != null) { + for (int i = charsBeforeCursor.length() - 1; i >= 0 && chars < 0; i--, chars++, steps--) { + if (Character.isSurrogate(charsBeforeCursor.charAt(i))) { + steps--; + i--; + } + } + } + } else if (chars > 0) { + CharSequence charsAfterCursor = !rightSidePointer && hasSelection() ? + getSelectedText(0) : + getTextAfterCursor(chars * 2, 0); + if (charsAfterCursor != null) { + for (int i = 0; i < charsAfterCursor.length() && chars > 0; i++, chars--, steps++) { + if (Character.isSurrogate(charsAfterCursor.charAt(i))) { + steps++; + i++; + } + } + } + } + return steps; + } } diff --git a/java/src/org/futo/inputmethod/latin/inputlogic/InputLogic.java b/java/src/org/futo/inputmethod/latin/inputlogic/InputLogic.java index 41ff7f60b..96a3fcf11 100644 --- a/java/src/org/futo/inputmethod/latin/inputlogic/InputLogic.java +++ b/java/src/org/futo/inputmethod/latin/inputlogic/InputLogic.java @@ -1132,7 +1132,7 @@ public final class InputLogic { // As for the case where we don't know the cursor position, it can happen // because of bugs in the framework. But the framework should know, so the next // best thing is to leave it to whatever it thinks is best. - sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL); + sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL, 0); int totalDeletedLength = 1; if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) { // If this is an accelerated (i.e., double) deletion, then we need to @@ -1140,7 +1140,7 @@ public final class InputLogic { // the previous word, and will lose it after next deletion. hasUnlearnedWordBeingDeleted |= unlearnWordBeingDeleted( inputTransaction.mSettingsValues, currentKeyboardScriptId); - sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL); + sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL, 0); totalDeletedLength++; } StatsUtils.onBackspacePressed(totalDeletedLength); @@ -2021,13 +2021,13 @@ public final class InputLogic { * * @param keyCode the key code to send inside the key event. */ - private void sendDownUpKeyEvent(final int keyCode) { + public void sendDownUpKeyEvent(final int keyCode, final int metaState) { final long eventTime = SystemClock.uptimeMillis(); mConnection.sendKeyEvent(new KeyEvent(eventTime, eventTime, - KeyEvent.ACTION_DOWN, keyCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, + KeyEvent.ACTION_DOWN, keyCode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE)); mConnection.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime, - KeyEvent.ACTION_UP, keyCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, + KeyEvent.ACTION_UP, keyCode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE)); } @@ -2045,7 +2045,7 @@ public final class InputLogic { // TODO: Remove this special handling of digit letters. // For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}. if (codePoint >= '0' && codePoint <= '9') { - sendDownUpKeyEvent(codePoint - '0' + KeyEvent.KEYCODE_0); + sendDownUpKeyEvent(codePoint - '0' + KeyEvent.KEYCODE_0, 0); return; } @@ -2055,7 +2055,7 @@ public final class InputLogic { // a hardware keyboard event on pressing enter or delete. This is bad for many // reasons (there are race conditions with commits) but some applications are // relying on this behavior so we continue to support it for older apps. - sendDownUpKeyEvent(KeyEvent.KEYCODE_ENTER); + sendDownUpKeyEvent(KeyEvent.KEYCODE_ENTER, 0); } else { mConnection.commitText(StringUtils.newSingleCodePointString(codePoint), 1); } diff --git a/java/src/org/futo/inputmethod/latin/uix/Action.kt b/java/src/org/futo/inputmethod/latin/uix/Action.kt index aa21b1bfd..c23244ed5 100644 --- a/java/src/org/futo/inputmethod/latin/uix/Action.kt +++ b/java/src/org/futo/inputmethod/latin/uix/Action.kt @@ -29,6 +29,9 @@ interface KeyboardManagerForAction { fun triggerSystemVoiceInput() fun updateTheme(newTheme: ThemeOption) + + fun sendCodePointEvent(codePoint: Int) + fun sendKeyEvent(keyCode: Int, metaState: Int) } interface ActionWindow { @@ -36,7 +39,7 @@ interface ActionWindow { fun windowName(): String @Composable - fun WindowContents() + fun WindowContents(keyboardShown: Boolean) fun close() } @@ -54,6 +57,8 @@ interface PersistentActionState { data class Action( @DrawableRes val icon: Int, @StringRes val name: Int, + val canShowKeyboard: Boolean = false, + val windowImpl: ((KeyboardManagerForAction, PersistentActionState?) -> ActionWindow)?, val simplePressImpl: ((KeyboardManagerForAction, PersistentActionState?) -> Unit)?, val persistentState: ((KeyboardManagerForAction) -> PersistentActionState)? = null, diff --git a/java/src/org/futo/inputmethod/latin/uix/ActionBar.kt b/java/src/org/futo/inputmethod/latin/uix/ActionBar.kt index df22b8249..0b6abfc03 100644 --- a/java/src/org/futo/inputmethod/latin/uix/ActionBar.kt +++ b/java/src/org/futo/inputmethod/latin/uix/ActionBar.kt @@ -13,6 +13,7 @@ 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.lazy.LazyRow import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ColorScheme import androidx.compose.material3.Icon @@ -20,6 +21,7 @@ import androidx.compose.material3.IconButton 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.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme @@ -60,10 +62,15 @@ import org.futo.inputmethod.latin.SuggestedWords import org.futo.inputmethod.latin.SuggestedWords.SuggestedWordInfo import org.futo.inputmethod.latin.SuggestedWords.SuggestedWordInfo.KIND_TYPED import org.futo.inputmethod.latin.suggestions.SuggestionStripView +import org.futo.inputmethod.latin.uix.actions.ClipboardAction import org.futo.inputmethod.latin.uix.actions.EmojiAction +import org.futo.inputmethod.latin.uix.actions.RedoAction +import org.futo.inputmethod.latin.uix.actions.TextEditAction import org.futo.inputmethod.latin.uix.actions.ThemeAction +import org.futo.inputmethod.latin.uix.actions.UndoAction import org.futo.inputmethod.latin.uix.actions.VoiceInputAction 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 java.lang.Integer.min import kotlin.math.ceil @@ -287,7 +294,7 @@ fun ActionItem(action: Action, onSelect: (Action) -> Unit) { cornerRadius = CornerRadius(radius, radius) ) } - .width(64.dp) + .width(50.dp) .fillMaxHeight(), colors = IconButtonDefaults.iconButtonColors(contentColor = contentCol) ) { @@ -317,12 +324,10 @@ fun RowScope.ActionItems(onSelect: (Action) -> Unit) { ActionItem(EmojiAction, onSelect) ActionItem(VoiceInputAction, onSelect) ActionItem(ThemeAction, onSelect) - - Box(modifier = Modifier - .fillMaxHeight() - .weight(1.0f)) { - - } + ActionItem(UndoAction, onSelect) + ActionItem(RedoAction, onSelect) + ActionItem(ClipboardAction, onSelect) + ActionItem(TextEditAction, onSelect) } @@ -386,7 +391,11 @@ fun ActionBar( ExpandActionsButton(isActionsOpen.value) { isActionsOpen.value = !isActionsOpen.value } if(isActionsOpen.value) { - ActionItems(onActionActivated) + LazyRow { + item { + ActionItems(onActionActivated) + } + } } else if(inlineSuggestions.isNotEmpty() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { InlineSuggestions(inlineSuggestions) } else if(words != null) { @@ -399,7 +408,110 @@ fun ActionBar( Spacer(modifier = Modifier.weight(1.0f)) } - ActionItemSmall(VoiceInputAction, onActionActivated) + if(!isActionsOpen.value) { + ActionItemSmall(VoiceInputAction, onActionActivated) + } + } + } +} + +@Composable +fun ActionWindowBar( + windowName: String, + canExpand: Boolean, + onBack: () -> Unit, + onExpand: () -> Unit +) { + Surface( + modifier = Modifier + .fillMaxWidth() + .height(40.dp), color = MaterialTheme.colorScheme.background + ) + { + Row { + IconButton(onClick = onBack) { + Icon( + painter = painterResource(id = R.drawable.arrow_left_26), + contentDescription = "Back" + ) + } + + Text( + windowName, + style = Typography.titleMedium, + modifier = Modifier.align(CenterVertically) + ) + + Spacer(modifier = Modifier.weight(1.0f)) + + if(canExpand) { + IconButton(onClick = onExpand) { + Icon( + painter = painterResource(id = R.drawable.arrow_up), + contentDescription = "Show Keyboard" + ) + } + } + } + } +} + +@Composable +fun CollapsibleSuggestionsBar( + onClose: () -> Unit, + onCollapse: () -> Unit, + words: SuggestedWords?, + suggestionStripListener: SuggestionStripView.Listener, + inlineSuggestions: List>, +) { + Surface(modifier = Modifier + .fillMaxWidth() + .height(40.dp), color = MaterialTheme.colorScheme.background) + { + Row { + val color = MaterialTheme.colorScheme.primary + + IconButton( + onClick = onClose, + modifier = Modifier + .width(42.dp) + .fillMaxHeight() + .drawBehind { + drawCircle(color = color, radius = size.width / 3.0f + 1.0f) + }, + + colors = IconButtonDefaults.iconButtonColors(contentColor = MaterialTheme.colorScheme.onPrimary) + ) { + Icon( + painter = painterResource(id = R.drawable.close), + contentDescription = "Close" + ) + } + + if(inlineSuggestions.isNotEmpty() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + InlineSuggestions(inlineSuggestions) + } else if(words != null) { + SuggestionItems(words) { + suggestionStripListener.pickSuggestionManually( + words.getInfo(it) + ) + } + } else { + Spacer(modifier = Modifier.weight(1.0f)) + } + + IconButton( + onClick = onCollapse, + modifier = Modifier + .width(42.dp) + .fillMaxHeight(), + colors = IconButtonDefaults.iconButtonColors(contentColor = MaterialTheme.colorScheme.onBackground) + ) { + Icon( + painter = painterResource(id = R.drawable.arrow_down), + contentDescription = "Collapse" + ) + } } } } @@ -491,6 +603,18 @@ fun PreviewExpandedActionBar(colorScheme: ColorScheme = DarkColorScheme) { } } +@Composable +@Preview +fun PreviewCollapsibleBar(colorScheme: ColorScheme = DarkColorScheme) { + CollapsibleSuggestionsBar( + onCollapse = { }, + onClose = { }, + words = exampleSuggestedWords, + suggestionStripListener = ExampleListener(), + inlineSuggestions = listOf() + ) +} + @Composable @Preview diff --git a/java/src/org/futo/inputmethod/latin/uix/BasicThemeProvider.kt b/java/src/org/futo/inputmethod/latin/uix/BasicThemeProvider.kt index cc18d61d3..2d4359cde 100644 --- a/java/src/org/futo/inputmethod/latin/uix/BasicThemeProvider.kt +++ b/java/src/org/futo/inputmethod/latin/uix/BasicThemeProvider.kt @@ -31,6 +31,7 @@ class BasicThemeProvider(val context: Context, val overrideColorScheme: ColorSch override val keyFeedback: Drawable + override val moreKeysTextColor: Int override val moreKeysKeyboardBackground: Drawable override val popupKey: Drawable @@ -107,6 +108,10 @@ class BasicThemeProvider(val context: Context, val overrideColorScheme: ColorSch val surface = colorScheme.background.toArgb() val outline = colorScheme.outline.toArgb() + val primaryContainer = colorScheme.primaryContainer.toArgb() + val onPrimaryContainer = colorScheme.onPrimaryContainer.toArgb() + + val onPrimary = colorScheme.onPrimary.toArgb() val onSecondary = colorScheme.onSecondary.toArgb() val onBackground = colorScheme.onBackground.toArgb() val onBackgroundHalf = colorScheme.onBackground.copy(alpha = 0.5f).toArgb() @@ -115,6 +120,7 @@ class BasicThemeProvider(val context: Context, val overrideColorScheme: ColorSch colors[R.styleable.Keyboard_Key_keyTextColor] = onBackground colors[R.styleable.Keyboard_Key_keyTextInactivatedColor] = onBackgroundHalf + colors[R.styleable.Keyboard_Key_keyPressedTextColor] = onPrimary colors[R.styleable.Keyboard_Key_keyTextShadowColor] = 0 colors[R.styleable.Keyboard_Key_functionalTextColor] = onBackground colors[R.styleable.Keyboard_Key_keyHintLetterColor] = onBackgroundHalf @@ -208,10 +214,11 @@ class BasicThemeProvider(val context: Context, val overrideColorScheme: ColorSch setPadding(0, 0, 0, dp(50.dp).roundToInt()) } - moreKeysKeyboardBackground = coloredRoundedRectangle(surface, dp(8.dp)) + moreKeysTextColor = onPrimaryContainer + moreKeysKeyboardBackground = coloredRoundedRectangle(primaryContainer, dp(8.dp)) popupKey = StateListDrawable().apply { - addStateWithHighlightLayerOnPressed(highlight, intArrayOf(), - coloredRoundedRectangle(surface, dp(8.dp)) + addStateWithHighlightLayerOnPressed(primary, intArrayOf(), + coloredRoundedRectangle(primaryContainer, dp(8.dp)) ) } } diff --git a/java/src/org/futo/inputmethod/latin/uix/DynamicThemeProvider.kt b/java/src/org/futo/inputmethod/latin/uix/DynamicThemeProvider.kt index b090921c2..8ab4a5b2c 100644 --- a/java/src/org/futo/inputmethod/latin/uix/DynamicThemeProvider.kt +++ b/java/src/org/futo/inputmethod/latin/uix/DynamicThemeProvider.kt @@ -13,6 +13,7 @@ interface DynamicThemeProvider { val keyFeedback: Drawable + val moreKeysTextColor: Int val moreKeysKeyboardBackground: Drawable val popupKey: Drawable diff --git a/java/src/org/futo/inputmethod/latin/uix/UixManager.kt b/java/src/org/futo/inputmethod/latin/uix/UixManager.kt new file mode 100644 index 000000000..1e8463c92 --- /dev/null +++ b/java/src/org/futo/inputmethod/latin/uix/UixManager.kt @@ -0,0 +1,344 @@ +package org.futo.inputmethod.latin.uix + +import android.content.Context +import android.os.Build +import android.view.View +import android.view.inputmethod.InlineSuggestionsResponse +import androidx.annotation.RequiresApi +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.ui.Modifier +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.lifecycle.LifecycleCoroutineScope +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.launch +import org.futo.inputmethod.latin.LatinIME +import org.futo.inputmethod.latin.SuggestedWords +import org.futo.inputmethod.latin.common.Constants +import org.futo.inputmethod.latin.inputlogic.InputLogic +import org.futo.inputmethod.latin.suggestions.SuggestionStripView +import org.futo.inputmethod.latin.uix.theme.ThemeOption +import org.futo.inputmethod.latin.uix.theme.UixThemeWrapper + +private class LatinIMEActionInputTransaction( + private val inputLogic: InputLogic, + shouldApplySpace: Boolean +): ActionInputTransaction { + private val isSpaceNecessary: Boolean + init { + val priorText = inputLogic.mConnection.getTextBeforeCursor(1, 0) + isSpaceNecessary = shouldApplySpace && !priorText.isNullOrEmpty() && !priorText.last().isWhitespace() + } + + private fun transformText(text: String): String { + return if(isSpaceNecessary) { " $text" } else { text } + } + + override fun updatePartial(text: String) { + inputLogic.mConnection.setComposingText( + transformText(text), + 1 + ) + } + + override fun commit(text: String) { + inputLogic.mConnection.commitText( + transformText(text), + 1 + ) + } + + override fun cancel() { + inputLogic.mConnection.finishComposingText() + } +} + +class UixActionKeyboardManager(val uixManager: UixManager, val latinIME: LatinIME) : KeyboardManagerForAction { + override fun getContext(): Context { + return latinIME + } + + override fun getLifecycleScope(): LifecycleCoroutineScope { + return latinIME.lifecycleScope + } + + override fun triggerContentUpdate() { + uixManager.setContent() + } + + override fun createInputTransaction(applySpaceIfNeeded: Boolean): ActionInputTransaction { + return LatinIMEActionInputTransaction(latinIME.inputLogic, applySpaceIfNeeded) + } + + override fun typeText(v: String) { + latinIME.latinIMELegacy.onTextInput(v) + } + + override fun backspace(amount: Int) { + latinIME.latinIMELegacy.onCodeInput( + Constants.CODE_DELETE, + Constants.NOT_A_COORDINATE, + Constants.NOT_A_COORDINATE, false) + } + + override fun closeActionWindow() { + if(uixManager.currWindowActionWindow == null) return + uixManager.returnBackToMainKeyboardViewFromAction() + } + + override fun triggerSystemVoiceInput() { + latinIME.latinIMELegacy.onCodeInput( + Constants.CODE_SHORTCUT, + Constants.SUGGESTION_STRIP_COORDINATE, + Constants.SUGGESTION_STRIP_COORDINATE, + false + ); + } + + override fun updateTheme(newTheme: ThemeOption) { + latinIME.updateTheme(newTheme) + } + + override fun sendCodePointEvent(codePoint: Int) { + latinIME.latinIMELegacy.onCodeInput(codePoint, + Constants.NOT_A_COORDINATE, + Constants.NOT_A_COORDINATE, false) + } + + override fun sendKeyEvent(keyCode: Int, metaState: Int) { + latinIME.inputLogic.sendDownUpKeyEvent(keyCode, metaState) + } +} + +class UixManager(private val latinIME: LatinIME) { + private var shouldShowSuggestionStrip: Boolean = true + private var suggestedWords: SuggestedWords? = null + + private var composeView: ComposeView? = null + + private var currWindowAction: Action? = null + private var persistentStates: HashMap = hashMapOf() + + private var inlineSuggestions: List> = listOf() + private val keyboardManagerForAction = UixActionKeyboardManager(this, latinIME) + + private var mainKeyboardHidden = false + + var currWindowActionWindow: ActionWindow? = null + + val isMainKeyboardHidden get() = mainKeyboardHidden + + private fun onActionActivated(action: Action) { + latinIME.inputLogic.finishInput() + + if (action.windowImpl != null) { + enterActionWindowView(action) + } else if (action.simplePressImpl != null) { + action.simplePressImpl.invoke(keyboardManagerForAction, persistentStates[action]) + } else { + throw IllegalStateException("An action must have either a window implementation or a simple press implementation") + } + } + + @Composable + private fun MainKeyboardViewWithActionBar() { + Column { + // Don't show suggested words when it's not meant to be shown + val suggestedWordsOrNull = if(shouldShowSuggestionStrip) { + suggestedWords + } else { + null + } + + ActionBar( + suggestedWordsOrNull, + latinIME.latinIMELegacy as SuggestionStripView.Listener, + inlineSuggestions = inlineSuggestions, + onActionActivated = { onActionActivated(it) } + ) + } + } + + private fun enterActionWindowView(action: Action) { + assert(action.windowImpl != null) + + mainKeyboardHidden = true + + currWindowAction = action + + if (persistentStates[action] == null) { + persistentStates[action] = action.persistentState?.let { it(keyboardManagerForAction) } + } + + currWindowActionWindow = action.windowImpl?.let { it(keyboardManagerForAction, persistentStates[action]) } + + setContent() + } + + fun returnBackToMainKeyboardViewFromAction() { + if(currWindowActionWindow == null) return + + currWindowActionWindow!!.close() + + currWindowAction = null + currWindowActionWindow = null + + mainKeyboardHidden = false + + latinIME.onKeyboardShown() + + setContent() + } + + private fun toggleExpandAction() { + mainKeyboardHidden = !mainKeyboardHidden + if(!mainKeyboardHidden) { + latinIME.onKeyboardShown() + } + + setContent() + } + + @Composable + private fun ActionViewWithHeader(windowImpl: ActionWindow) { + val heightDiv = if(mainKeyboardHidden) { + 1 + } else { + 1.5 + } + Column { + if(mainKeyboardHidden) { + ActionWindowBar( + onBack = { returnBackToMainKeyboardViewFromAction() }, + canExpand = currWindowAction!!.canShowKeyboard, + onExpand = { toggleExpandAction() }, + windowName = windowImpl.windowName() + ) + } + + Box(modifier = Modifier + .fillMaxWidth() + .height(with(LocalDensity.current) { + (latinIME.getInputViewHeight().toFloat() / heightDiv.toFloat()).toDp() + }) + ) { + windowImpl.WindowContents(keyboardShown = !isMainKeyboardHidden) + } + + if(!mainKeyboardHidden) { + val suggestedWordsOrNull = if (shouldShowSuggestionStrip) { + suggestedWords + } else { + null + } + + CollapsibleSuggestionsBar( + onCollapse = { toggleExpandAction() }, + onClose = { returnBackToMainKeyboardViewFromAction() }, + words = suggestedWordsOrNull, + suggestionStripListener = latinIME.latinIMELegacy as SuggestionStripView.Listener, + inlineSuggestions = inlineSuggestions + ) + } + } + } + + fun setContent() { + composeView?.setContent { + UixThemeWrapper(latinIME.colorScheme) { + Column { + Spacer(modifier = Modifier.weight(1.0f)) + Surface(modifier = Modifier.onSizeChanged { + latinIME.updateTouchableHeight(it.height) + }) { + Column { + when { + currWindowActionWindow != null -> ActionViewWithHeader( + currWindowActionWindow!! + ) + + else -> MainKeyboardViewWithActionBar() + } + + latinIME.LegacyKeyboardView(hidden = isMainKeyboardHidden) + } + } + } + } + } + } + + fun createComposeView(): View { + if(composeView != null) { + composeView = null + //throw IllegalStateException("Attempted to create compose view, when one is already created!") + } + + composeView = ComposeView(latinIME).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setParentCompositionContext(null) + + latinIME.setOwners() + } + + setContent() + + return composeView!! + } + + fun getComposeView(): View? { + return composeView + } + + fun onColorSchemeChanged() { + setContent() + } + + fun onInputFinishing() { + closeActionWindow() + } + + fun cleanUpPersistentStates() { + println("Cleaning up persistent states") + for((key, value) in persistentStates.entries) { + if(currWindowAction != key) { + latinIME.lifecycleScope.launch { value?.cleanUp() } + } + } + } + + fun closeActionWindow() { + if(currWindowActionWindow == null) return + returnBackToMainKeyboardViewFromAction() + } + + + fun updateVisibility(shouldShowSuggestionsStrip: Boolean, fullscreenMode: Boolean) { + this.shouldShowSuggestionStrip = shouldShowSuggestionsStrip + setContent() + } + + fun setSuggestions(suggestedWords: SuggestedWords?, rtlSubtype: Boolean) { + this.suggestedWords = suggestedWords + setContent() + } + + @RequiresApi(Build.VERSION_CODES.R) + fun onInlineSuggestionsResponse(response: InlineSuggestionsResponse): Boolean { + inlineSuggestions = response.inlineSuggestions.map { + latinIME.inflateInlineSuggestion(it) + } + setContent() + + return true + } +} \ No newline at end of file diff --git a/java/src/org/futo/inputmethod/latin/uix/actions/ClipboardAction.kt b/java/src/org/futo/inputmethod/latin/uix/actions/ClipboardAction.kt new file mode 100644 index 000000000..07ee7f3f1 --- /dev/null +++ b/java/src/org/futo/inputmethod/latin/uix/actions/ClipboardAction.kt @@ -0,0 +1,14 @@ +package org.futo.inputmethod.latin.uix.actions + +import android.view.KeyEvent +import org.futo.inputmethod.latin.R +import org.futo.inputmethod.latin.uix.Action + +val ClipboardAction = Action( + icon = R.drawable.clipboard, + name = R.string.clipboard_action_title, + simplePressImpl = { manager, _ -> + manager.sendKeyEvent(KeyEvent.KEYCODE_V, KeyEvent.META_CTRL_ON) + }, + windowImpl = null, +) \ No newline at end of file diff --git a/java/src/org/futo/inputmethod/latin/uix/actions/EmojiAction.kt b/java/src/org/futo/inputmethod/latin/uix/actions/EmojiAction.kt index d23637fda..ec1691ef1 100644 --- a/java/src/org/futo/inputmethod/latin/uix/actions/EmojiAction.kt +++ b/java/src/org/futo/inputmethod/latin/uix/actions/EmojiAction.kt @@ -55,6 +55,7 @@ import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import org.futo.inputmethod.latin.R +import org.futo.inputmethod.latin.common.Constants import org.futo.inputmethod.latin.uix.Action import org.futo.inputmethod.latin.uix.ActionWindow import org.futo.inputmethod.latin.uix.PersistentActionState @@ -161,7 +162,7 @@ data class BitmapRecycler( } @Composable -fun EmojiGrid(onClick: (EmojiItem) -> Unit, onExit: () -> Unit, onBackspace: () -> Unit, onSpace: () -> Unit, bitmaps: BitmapRecycler, emojis: List) { +fun EmojiGrid(onClick: (EmojiItem) -> Unit, onExit: () -> Unit, onBackspace: () -> Unit, onSpace: () -> Unit, bitmaps: BitmapRecycler, emojis: List, keyboardShown: Boolean) { val context = LocalContext.current val spToDp = context.resources.displayMetrics.scaledDensity / context.resources.displayMetrics.density @@ -181,41 +182,48 @@ fun EmojiGrid(onClick: (EmojiItem) -> Unit, onExit: () -> Unit, onBackspace: () } } } - Surface(color = MaterialTheme.colorScheme.background, modifier = Modifier - .fillMaxWidth() - .height(48.dp)) { - Row(modifier = Modifier.padding(2.dp, 8.dp, 2.dp, 0.dp)) { - IconButton(onClick = { onExit() }) { - Text("ABC", fontSize = 14.sp) - } - Button(onClick = { onSpace() }, modifier = Modifier - .weight(1.0f) - .padding(8.dp, 2.dp), colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.33f), - contentColor = MaterialTheme.colorScheme.onBackground, - disabledContainerColor = MaterialTheme.colorScheme.outline, - disabledContentColor = MaterialTheme.colorScheme.onBackground, - ), shape = RoundedCornerShape(32.dp)) { - Text("") - } + if(!keyboardShown) { + Surface( + color = MaterialTheme.colorScheme.background, modifier = Modifier + .fillMaxWidth() + .height(48.dp) + ) { + Row(modifier = Modifier.padding(2.dp, 8.dp, 2.dp, 0.dp)) { + IconButton(onClick = { onExit() }) { + Text("ABC", fontSize = 14.sp) + } - IconButton(onClick = { onBackspace() }) { - val icon = painterResource(id = R.drawable.delete) - val iconColor = MaterialTheme.colorScheme.onBackground + Button( + onClick = { onSpace() }, modifier = Modifier + .weight(1.0f) + .padding(8.dp, 2.dp), colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.33f), + contentColor = MaterialTheme.colorScheme.onBackground, + disabledContainerColor = MaterialTheme.colorScheme.outline, + disabledContentColor = MaterialTheme.colorScheme.onBackground, + ), shape = RoundedCornerShape(32.dp) + ) { + Text("") + } - Canvas(modifier = Modifier.fillMaxSize()) { - translate( - left = this.size.width / 2.0f - icon.intrinsicSize.width / 2.0f, - top = this.size.height / 2.0f - icon.intrinsicSize.height / 2.0f - ) { - with(icon) { - draw( - icon.intrinsicSize, - colorFilter = androidx.compose.ui.graphics.ColorFilter.tint( - iconColor + IconButton(onClick = { onBackspace() }) { + val icon = painterResource(id = R.drawable.delete) + val iconColor = MaterialTheme.colorScheme.onBackground + + Canvas(modifier = Modifier.fillMaxSize()) { + translate( + left = this.size.width / 2.0f - icon.intrinsicSize.width / 2.0f, + top = this.size.height / 2.0f - icon.intrinsicSize.height / 2.0f + ) { + with(icon) { + draw( + icon.intrinsicSize, + colorFilter = androidx.compose.ui.graphics.ColorFilter.tint( + iconColor + ) ) - ) + } } } } @@ -266,7 +274,8 @@ class PersistentEmojiState: PersistentActionState { val EmojiAction = Action( icon = R.drawable.smile, - name = R.string.title_emojis, + name = R.string.emoji_action_title, + canShowKeyboard = true, simplePressImpl = null, persistentState = { manager -> val state = PersistentEmojiState() @@ -282,21 +291,21 @@ val EmojiAction = Action( object : ActionWindow { @Composable override fun windowName(): String { - return stringResource(R.string.title_emojis) + return stringResource(R.string.emoji_action_title) } @Composable - override fun WindowContents() { + override fun WindowContents(keyboardShown: Boolean) { state.emojis.value?.let { emojis -> EmojiGrid(onClick = { manager.typeText(it.emoji) }, onExit = { manager.closeActionWindow() }, onSpace = { - manager.typeText(" ") + manager.sendCodePointEvent(Constants.CODE_SPACE) }, onBackspace = { - manager.backspace(1) - }, bitmaps = state.bitmaps, emojis = emojis) + manager.sendCodePointEvent(Constants.CODE_DELETE) + }, bitmaps = state.bitmaps, emojis = emojis, keyboardShown = keyboardShown) } } diff --git a/java/src/org/futo/inputmethod/latin/uix/actions/TextEditAction.kt b/java/src/org/futo/inputmethod/latin/uix/actions/TextEditAction.kt new file mode 100644 index 000000000..2990bf2e6 --- /dev/null +++ b/java/src/org/futo/inputmethod/latin/uix/actions/TextEditAction.kt @@ -0,0 +1,379 @@ +package org.futo.inputmethod.latin.uix.actions + +import android.view.KeyEvent +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.LocalIndication +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.drawscope.translate +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.delay +import org.futo.inputmethod.latin.R +import org.futo.inputmethod.latin.common.Constants +import org.futo.inputmethod.latin.uix.Action +import org.futo.inputmethod.latin.uix.ActionWindow + +@Composable +fun IconWithColor(@DrawableRes iconId: Int, iconColor: Color, modifier: Modifier = Modifier) { + val icon = painterResource(id = iconId) + + Canvas(modifier = modifier) { + translate( + left = this.size.width / 2.0f - icon.intrinsicSize.width / 2.0f, + top = this.size.height / 2.0f - icon.intrinsicSize.height / 2.0f + ) { + with(icon) { + draw( + icon.intrinsicSize, + colorFilter = ColorFilter.tint( + iconColor + ) + ) + } + } + } +} + +@Composable +fun TogglableKey( + onToggle: (Boolean) -> Unit, + toggled: Boolean, + modifier: Modifier = Modifier, + contents: @Composable (color: Color) -> Unit +) { + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + + LaunchedEffect(isPressed) { + if(isPressed) { + onToggle(!toggled) + } + } + + Surface( + modifier = modifier + .padding(4.dp) + .clickable( + interactionSource = interactionSource, + indication = LocalIndication.current, + onClick = { } + ), + shape = RoundedCornerShape(8.dp), + color = if(toggled) { MaterialTheme.colorScheme.secondary } else { MaterialTheme.colorScheme.secondaryContainer } + ) { + contents(if(toggled) { MaterialTheme.colorScheme.onSecondary } else { MaterialTheme.colorScheme.onSecondaryContainer }) + } + +} + +@Composable +fun ActionKey( + onTrigger: () -> Unit, + modifier: Modifier = Modifier, + repeatable: Boolean = true, + color: Color = MaterialTheme.colorScheme.primary, + contents: @Composable () -> Unit +) { + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + + LaunchedEffect(isPressed) { + if(isPressed) { + onTrigger() + if(repeatable) { + delay(670L) + while (isPressed) { + onTrigger() + delay(50L) + } + } + } + } + + Surface( + modifier = modifier + .padding(4.dp) + .clickable( + interactionSource = interactionSource, + indication = LocalIndication.current, + onClick = { } + ), + shape = RoundedCornerShape(8.dp), + color = color + ) { + contents() + } +} + +@Composable +fun ArrowKeys(modifier: Modifier, sendEvent: (Int) -> Unit) { + Row(modifier = modifier) { + ActionKey( + modifier = Modifier + .weight(1.0f) + .fillMaxHeight(), + onTrigger = { sendEvent(KeyEvent.KEYCODE_DPAD_LEFT) } + ) { + IconWithColor( + iconId = R.drawable.arrow_left, + iconColor = MaterialTheme.colorScheme.onPrimary + ) + } + + Column(modifier = Modifier + .weight(1.0f) + .fillMaxHeight()) { + ActionKey( + modifier = Modifier + .weight(1.0f) + .fillMaxWidth(), + onTrigger = { sendEvent(KeyEvent.KEYCODE_DPAD_UP) } + ) { + IconWithColor( + iconId = R.drawable.arrow_up, + iconColor = MaterialTheme.colorScheme.onPrimary + ) + } + + + ActionKey( + modifier = Modifier + .weight(1.0f) + .fillMaxWidth(), + onTrigger = { sendEvent(KeyEvent.KEYCODE_DPAD_DOWN) } + ) { + IconWithColor( + iconId = R.drawable.arrow_down, + iconColor = MaterialTheme.colorScheme.onPrimary + ) + } + } + + ActionKey( + modifier = Modifier + .weight(1.0f) + .fillMaxHeight(), + onTrigger = { sendEvent(KeyEvent.KEYCODE_DPAD_RIGHT) } + ) { + IconWithColor( + iconId = R.drawable.arrow_right, + iconColor = MaterialTheme.colorScheme.onPrimary + ) + } + } +} + +@Composable +fun CtrlShiftMetaKeys(modifier: Modifier, ctrlState: MutableState, shiftState: MutableState) { + Row(modifier = modifier) { + TogglableKey( + onToggle = { ctrlState.value = it }, + toggled = ctrlState.value, + modifier = Modifier + .weight(1.0f) + .fillMaxHeight() + ) { + IconWithColor( + iconId = R.drawable.ctrl, + iconColor = it + ) + } + TogglableKey( + onToggle = { shiftState.value = it }, + toggled = shiftState.value, + modifier = Modifier + .weight(1.0f) + .fillMaxHeight() + ) { + IconWithColor( + iconId = R.drawable.shift, + iconColor = it + ) + } + } +} + +@Composable +fun SideKeys(modifier: Modifier, onEvent: (Int, Int) -> Unit, onCodePoint: (Int) -> Unit, keyboardShown: Boolean) { + Column(modifier = modifier) { + ActionKey( + modifier = Modifier + .weight(1.0f) + .fillMaxWidth(), + repeatable = false, + color = MaterialTheme.colorScheme.primaryContainer, + onTrigger = { onEvent(KeyEvent.KEYCODE_C, KeyEvent.META_CTRL_ON) } + ) { + IconWithColor( + iconId = R.drawable.copy, + iconColor = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + + ActionKey( + modifier = Modifier + .weight(1.0f) + .fillMaxWidth(), + repeatable = false, + color = MaterialTheme.colorScheme.primaryContainer, + onTrigger = { onEvent(KeyEvent.KEYCODE_V, KeyEvent.META_CTRL_ON) } + ) { + IconWithColor( + iconId = R.drawable.clipboard, + iconColor = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + + if(!keyboardShown) { + ActionKey( + modifier = Modifier + .weight(1.0f) + .fillMaxWidth(), + repeatable = true, + color = MaterialTheme.colorScheme.primaryContainer, + onTrigger = { onCodePoint(Constants.CODE_DELETE) } + ) { + IconWithColor( + iconId = R.drawable.delete, + iconColor = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + } + + + Row(modifier = Modifier + .weight(1.0f) + .fillMaxWidth()) { + ActionKey( + modifier = Modifier + .weight(1.0f) + .fillMaxHeight(), + repeatable = false, + color = MaterialTheme.colorScheme.primaryContainer, + onTrigger = { onEvent(KeyEvent.KEYCODE_Z, KeyEvent.META_CTRL_ON) } + ) { + IconWithColor( + iconId = R.drawable.undo, + iconColor = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + + ActionKey( + modifier = Modifier + .weight(1.0f) + .fillMaxHeight(), + repeatable = false, + color = MaterialTheme.colorScheme.primaryContainer, + onTrigger = { onEvent(KeyEvent.KEYCODE_Y, KeyEvent.META_CTRL_ON) } + ) { + IconWithColor( + iconId = R.drawable.redo, + iconColor = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + } + } +} + +@Composable +fun TextEditScreen(onCodePoint: (Int) -> Unit, onEvent: (Int, Int) -> Unit, keyboardShown: Boolean) { + val shiftState = remember { mutableStateOf(false) } + val ctrlState = remember { mutableStateOf(false) } + + val metaState = 0 or + (if(shiftState.value) { KeyEvent.META_SHIFT_ON } else { 0 }) or + (if(ctrlState.value) { KeyEvent.META_CTRL_ON } else { 0 }) + + val sendEvent = { keycode: Int -> onEvent(keycode, metaState) } + + Row(modifier = Modifier.fillMaxSize()) { + Column(modifier = Modifier + .fillMaxHeight() + .weight(3.0f)) { + ArrowKeys( + modifier = Modifier + .weight(3.0f) + .fillMaxWidth(), + sendEvent = sendEvent + ) + CtrlShiftMetaKeys( + modifier = Modifier + .weight(1.0f) + .fillMaxWidth(), + ctrlState = ctrlState, + shiftState = shiftState + ) + } + SideKeys( + modifier = Modifier + .fillMaxHeight() + .weight(1.0f), + onEvent = onEvent, + onCodePoint = onCodePoint, + keyboardShown = keyboardShown + ) + } +} + +val TextEditAction = Action( + icon = R.drawable.edit_text, + name = R.string.text_edit_action_title, + simplePressImpl = null, + persistentState = null, + canShowKeyboard = true, + windowImpl = { manager, persistentState -> + object : ActionWindow { + @Composable + override fun windowName(): String { + return stringResource(R.string.text_edit_action_title) + } + + @Composable + override fun WindowContents(keyboardShown: Boolean) { + TextEditScreen(onCodePoint = { a -> manager.sendCodePointEvent(a)}, onEvent = { a, b -> manager.sendKeyEvent(a, b) }, keyboardShown = keyboardShown) + } + + override fun close() { + } + } + } +) + +@Composable +@Preview(showBackground = true) +fun TextEditScreenPreview() { + Surface(modifier = Modifier.height(256.dp)) { + TextEditScreen(onCodePoint = { }, onEvent = { _, _ -> }, keyboardShown = false) + } +} +@Composable +@Preview(showBackground = true) +fun TextEditScreenPreviewWithKb() { + Surface(modifier = Modifier.height(256.dp)) { + TextEditScreen(onCodePoint = { }, onEvent = { _, _ -> }, keyboardShown = true) + } +} \ No newline at end of file diff --git a/java/src/org/futo/inputmethod/latin/uix/actions/ThemeAction.kt b/java/src/org/futo/inputmethod/latin/uix/actions/ThemeAction.kt index 63577e24a..b25bf7303 100644 --- a/java/src/org/futo/inputmethod/latin/uix/actions/ThemeAction.kt +++ b/java/src/org/futo/inputmethod/latin/uix/actions/ThemeAction.kt @@ -1,27 +1,17 @@ package org.futo.inputmethod.latin.uix.actions -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material3.Button -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp import org.futo.inputmethod.latin.R import org.futo.inputmethod.latin.uix.Action import org.futo.inputmethod.latin.uix.ActionWindow -import org.futo.inputmethod.latin.uix.KeyboardManagerForAction -import org.futo.inputmethod.latin.uix.theme.ThemeOptionKeys -import org.futo.inputmethod.latin.uix.theme.ThemeOptions import org.futo.inputmethod.latin.uix.theme.selector.ThemePicker val ThemeAction = Action( icon = R.drawable.eye, name = R.string.theme_switcher_action_title, simplePressImpl = null, + canShowKeyboard = true, windowImpl = { manager, _ -> object : ActionWindow { @Composable @@ -30,33 +20,8 @@ val ThemeAction = Action( } @Composable - override fun WindowContents() { - val context = LocalContext.current - + override fun WindowContents(keyboardShown: Boolean) { ThemePicker { manager.updateTheme(it) } - /* - LazyColumn( - modifier = Modifier - .padding(8.dp, 0.dp) - .fillMaxWidth() - ) - { - items(ThemeOptionKeys.count()) { - val key = ThemeOptionKeys[it] - val themeOption = ThemeOptions[key] - if (themeOption != null && themeOption.available(context)) { - Button(onClick = { - manager.updateTheme( - themeOption - ) - }) { - Text(stringResource(themeOption.name)) - } - } - } - } - - */ } override fun close() { diff --git a/java/src/org/futo/inputmethod/latin/uix/actions/UndoRedoActions.kt b/java/src/org/futo/inputmethod/latin/uix/actions/UndoRedoActions.kt new file mode 100644 index 000000000..09742728a --- /dev/null +++ b/java/src/org/futo/inputmethod/latin/uix/actions/UndoRedoActions.kt @@ -0,0 +1,22 @@ +package org.futo.inputmethod.latin.uix.actions + +import android.view.KeyEvent +import org.futo.inputmethod.latin.R +import org.futo.inputmethod.latin.uix.Action + +val UndoAction = Action( + icon = R.drawable.undo, + name = R.string.undo_action_title, + simplePressImpl = { manager, _ -> + manager.sendKeyEvent(KeyEvent.KEYCODE_Z, KeyEvent.META_CTRL_ON) + }, + windowImpl = null, +) +val RedoAction = Action( + icon = R.drawable.redo, + name = R.string.redo_action_title, + simplePressImpl = { manager, _ -> + manager.sendKeyEvent(KeyEvent.KEYCODE_Y, KeyEvent.META_CTRL_ON) + }, + windowImpl = null, +) \ No newline at end of file diff --git a/java/src/org/futo/inputmethod/latin/uix/actions/VoiceInputAction.kt b/java/src/org/futo/inputmethod/latin/uix/actions/VoiceInputAction.kt index d4ee4504f..4cc1e574c 100644 --- a/java/src/org/futo/inputmethod/latin/uix/actions/VoiceInputAction.kt +++ b/java/src/org/futo/inputmethod/latin/uix/actions/VoiceInputAction.kt @@ -180,7 +180,7 @@ private class VoiceInputActionWindow( } @Composable - override fun WindowContents() { + override fun WindowContents(keyboardShown: Boolean) { Box(modifier = Modifier .fillMaxSize() .clickable(enabled = true, diff --git a/native/jni/org_futo_inputmethod_latin_xlm_LanguageModel.cpp b/native/jni/org_futo_inputmethod_latin_xlm_LanguageModel.cpp index f2a2f4e94..a844ae0ee 100644 --- a/native/jni/org_futo_inputmethod_latin_xlm_LanguageModel.cpp +++ b/native/jni/org_futo_inputmethod_latin_xlm_LanguageModel.cpp @@ -651,6 +651,9 @@ namespace latinime { std::vector mixes; for(int i=0; i= 'a' && wc <= 'z') && !(wc >= 'A' && wc <= 'Z')) continue; + std::vector proportions = pInfo->decomposeTapPosition(xCoordinates[i], yCoordinates[i]); for(float &f : proportions) { if(f < 0.05f) f = 0.0f; @@ -701,12 +704,12 @@ namespace latinime { results.x = ((float)xCoordinates[i]) / ((float)pInfo->getKeyboardWidth()); results.y = ((float)yCoordinates[i]) / ((float)pInfo->getKeyboardHeight()); - AKLOGI("%d | Char %c, pos %.6f %.6f, nearest is %c at %.2f, then %c at %.2f, finally %c at %.2f", i, partialWordString[i], - results.x, results.y, - (char)(pInfo->getKeyCodePoint(index_value[0].second)), (float)(index_value[0].first), - (char)(pInfo->getKeyCodePoint(index_value[1].second)), (float)(index_value[1].first), - (char)(pInfo->getKeyCodePoint(index_value[2].second)), (float)(index_value[2].first) - ); + //AKLOGI("%d | Char %c, pos %.6f %.6f, nearest is %c at %.2f, then %c at %.2f, finally %c at %.2f", i, partialWordString[i], + // results.x, results.y, + // (char)(pInfo->getKeyCodePoint(index_value[0].second)), (float)(index_value[0].first), + // (char)(pInfo->getKeyCodePoint(index_value[1].second)), (float)(index_value[1].first), + // (char)(pInfo->getKeyCodePoint(index_value[2].second)), (float)(index_value[2].first) + // ); for(int j=0; j