diff --git a/java/res/values/strings-uix.xml b/java/res/values/strings-uix.xml index 6627fea9f4..124e4ce2cb 100644 --- a/java/res/values/strings-uix.xml +++ b/java/res/values/strings-uix.xml @@ -435,11 +435,18 @@ Misc. letters from common languages e.g. [ß] on [s] in all Latin script languages + + + Swipe input modes + Swipe Typing (alpha) + Disabled + Allow swiping from key to key to write words. + Swipe Actions (alpha) + Fleksy-style directional action swipes. + Turn off swipe typing and swipe actions. + Typing preferences - - Swipe Typing (alpha) - Allow swiping from key to key to write words. Emoji Suggestions Suggest emojis while you\'re typing Vibration @@ -638,4 +645,4 @@ Default is %1$s Delete extra dictionary file? %1$s will be deleted - \ No newline at end of file + diff --git a/java/src/org/futo/inputmethod/engine/IMEInterface.kt b/java/src/org/futo/inputmethod/engine/IMEInterface.kt index 4d8258d947..2922df7ae2 100644 --- a/java/src/org/futo/inputmethod/engine/IMEInterface.kt +++ b/java/src/org/futo/inputmethod/engine/IMEInterface.kt @@ -70,6 +70,7 @@ interface IMEInterface { fun onUpWithDeletePointerActive() fun onUpWithPointerActive() fun onSwipeLanguage(direction: Int) + fun onSwipeAction(direction: Int) fun onMovingCursorLockEvent(canMoveCursor: Boolean) fun clearUserHistoryDictionaries() diff --git a/java/src/org/futo/inputmethod/engine/general/ActionInputTransactionIME.kt b/java/src/org/futo/inputmethod/engine/general/ActionInputTransactionIME.kt index 8ebe7c81bc..43680e466b 100644 --- a/java/src/org/futo/inputmethod/engine/general/ActionInputTransactionIME.kt +++ b/java/src/org/futo/inputmethod/engine/general/ActionInputTransactionIME.kt @@ -59,6 +59,7 @@ class ActionInputTransactionIME(val helper: IMEHelper) : IMEInterface, ActionInp override fun onUpWithDeletePointerActive() {} override fun onUpWithPointerActive() {} override fun onSwipeLanguage(direction: Int) {} + override fun onSwipeAction(direction: Int) {} override fun onMovingCursorLockEvent(canMoveCursor: Boolean) {} override fun clearUserHistoryDictionaries() {} override fun requestSuggestionRefresh() {} @@ -104,4 +105,4 @@ class ActionInputTransactionIME(val helper: IMEHelper) : IMEInterface, ActionInp fun ensureFinished() { isFinished = true } -} \ No newline at end of file +} diff --git a/java/src/org/futo/inputmethod/engine/general/GeneralIME.kt b/java/src/org/futo/inputmethod/engine/general/GeneralIME.kt index 4509d688ad..6f6e4c5608 100644 --- a/java/src/org/futo/inputmethod/engine/general/GeneralIME.kt +++ b/java/src/org/futo/inputmethod/engine/general/GeneralIME.kt @@ -21,8 +21,10 @@ import org.futo.inputmethod.engine.IMEInterface import org.futo.inputmethod.engine.IMEMessage import org.futo.inputmethod.event.Event import org.futo.inputmethod.event.InputTransaction +import org.futo.inputmethod.keyboard.KeyboardActionListener import org.futo.inputmethod.keyboard.KeyboardSwitcher import org.futo.inputmethod.latin.BuildConfig +import org.futo.inputmethod.latin.Dictionary import org.futo.inputmethod.latin.DictionaryFacilitator import org.futo.inputmethod.latin.DictionaryFacilitatorProvider import org.futo.inputmethod.latin.NgramContext @@ -44,6 +46,7 @@ import org.futo.inputmethod.latin.uix.isDirectBootUnlocked import org.futo.inputmethod.latin.utils.AsyncResultHolder import org.futo.inputmethod.latin.xlm.LanguageModelFacilitator import org.futo.inputmethod.v2keyboard.KeyboardLayoutSetV2 +import java.util.LinkedHashSet import java.util.concurrent.atomic.AtomicInteger interface WordLearner { @@ -231,6 +234,7 @@ class GeneralIME(val helper: IMEHelper) : IMEInterface, WordLearner, SuggestionS override fun onStartInput() { useExpandableUi = helper.context.getSetting(UseExpandableSuggestionsForGeneralIME) + resetSwipeSuggestionSession() resetDictionaryFacilitator() setNeutralSuggestionStrip() @@ -269,6 +273,7 @@ class GeneralIME(val helper: IMEHelper) : IMEInterface, WordLearner, SuggestionS } override fun onFinishInput() { + resetSwipeSuggestionSession() inputLogic.finishInput() dictionaryFacilitator.onFinishInput(context) updateSuggestionJob?.cancel() @@ -297,6 +302,10 @@ class GeneralIME(val helper: IMEHelper) : IMEInterface, WordLearner, SuggestionS private fun onEventInternal(event: Event, ignoreSuggestionUpdate: Boolean = false) { helper.requestCursorUpdate() + if (isSwipeActionsModeEnabled() && event.eventType != Event.EVENT_TYPE_SUGGESTION_PICKED) { + resetSwipeSuggestionSession() + } + val inputTransaction = when (event.eventType) { Event.EVENT_TYPE_INPUT_KEYPRESS, Event.EVENT_TYPE_INPUT_KEYPRESS_RESUMED -> { @@ -477,6 +486,134 @@ class GeneralIME(val helper: IMEHelper) : IMEInterface, WordLearner, SuggestionS private val sequenceIdCompleted = AtomicInteger(0) private val computationMutex = Mutex() private var timeTakenToUpdate = 40L + private var swipeSuggestionIndex = -1 + private var swipeSuggestionWord: String? = null + private var swipeSuggestionCandidates: List? = null + + private fun resetSwipeSuggestionSession() { + swipeSuggestionIndex = -1 + swipeSuggestionWord = null + swipeSuggestionCandidates = null + } + + private fun isSwipeActionsModeEnabled(): Boolean { + return settings.current.mGestureActionsEnabled + } + + private fun sendDeleteKeypress() { + onEvent( + Event.createSoftwareKeypressEvent( + Event.NOT_A_CODE_POINT, + Constants.CODE_DELETE, + Constants.NOT_A_COORDINATE, + Constants.NOT_A_COORDINATE, + false + ) + ) + } + + private fun moveCursorToLastWordIfTrailingSpace(): Boolean { + val beforeCursor = inputLogic.mConnection.getTextBeforeCursor(1, 0)?.toString() + if (!beforeCursor.isNullOrEmpty() + && beforeCursor.last() == ' ' + && inputLogic.mConnection.hasCursorPosition() + && !inputLogic.mConnection.hasSelection()) { + inputLogic.cursorLeft(1, false, false) + return true + } + + return false + } + + private fun restoreCursorIfMoved(movedCursorToLastWord: Boolean) { + if (movedCursorToLastWord) { + inputLogic.cursorRight(1, false, false) + } + } + + private fun getSwipePunctuationCycle(): List { + val cycle = LinkedHashSet() + + val suggestedPunctuation = settings.current.mSpacingAndPunctuations.mSuggestPuncList + for (index in 0 until suggestedPunctuation.size()) { + val punctuation = suggestedPunctuation.getWord(index) + if (!punctuation.isNullOrEmpty() && punctuation.codePointCount(0, punctuation.length) == 1) { + cycle.add(punctuation) + } + } + + if (cycle.isEmpty()) { + return emptyList() + } + + val ordered = cycle.toMutableList() + val commaIndex = ordered.indexOf(",") + if (commaIndex > 0) { + ordered.removeAt(commaIndex) + ordered.add(0, ",") + } + + return ordered + } + + private fun replacePunctuationWith(replacement: String): Boolean { + if (replacement.codePointCount(0, replacement.length) != 1) { + return false + } + + resetSwipeSuggestionSession() + sendDeleteKeypress() + + val replacementCodePoint = replacement.codePointAt(0) + onEvent( + Event.createSoftwareKeypressEvent( + replacementCodePoint, + replacementCodePoint, + Constants.NOT_A_COORDINATE, + Constants.NOT_A_COORDINATE, + false + ) + ) + + return true + } + + private fun trySwipeCyclePunctuation(direction: Int): Boolean { + val beforeCursor = inputLogic.mConnection.getTextBeforeCursor(1, 0)?.toString() + if (beforeCursor.isNullOrEmpty()) { + return false + } + + val currentPunctuation = beforeCursor.last().toString() + val cycle = getSwipePunctuationCycle() + if (cycle.size < 2) { + return false + } + + val currentIndex = cycle.indexOf(currentPunctuation) + if (currentIndex < 0) { + if (currentPunctuation == ".") { + val replacement = if (direction == KeyboardActionListener.SWIPE_ACTION_UP) { + cycle.last() + } else { + cycle.first() + } + return replacePunctuationWith(replacement) + } + + return false + } + + val step = if (direction == KeyboardActionListener.SWIPE_ACTION_UP) -1 else 1 + val nextIndex = (currentIndex + step + cycle.size) % cycle.size + val replacement = cycle[nextIndex] + if (replacement == currentPunctuation) { + return false + } + + return replacePunctuationWith(replacement) + } + fun updateSuggestions(inputStyle: Int) { updateSuggestionJob?.cancel() @@ -687,6 +824,181 @@ class GeneralIME(val helper: IMEHelper) : IMEInterface, WordLearner, SuggestionS switchToNextLanguage(context, direction) } + override fun onSwipeAction(direction: Int) { + if (!isSwipeActionsModeEnabled()) return + + when (direction) { + KeyboardActionListener.SWIPE_ACTION_RIGHT -> { + resetSwipeSuggestionSession() + onEvent( + Event.createSoftwareKeypressEvent( + Constants.CODE_SPACE, + Constants.CODE_SPACE, + Constants.NOT_A_COORDINATE, + Constants.NOT_A_COORDINATE, + false + ) + ) + } + + KeyboardActionListener.SWIPE_ACTION_LEFT -> { + resetSwipeSuggestionSession() + setNeutralSuggestionStrip() + + val beforeCursor = inputLogic.mConnection.getTextBeforeCursor(1, 0)?.toString() + if (!beforeCursor.isNullOrEmpty() && beforeCursor.last() == ' ') { + sendDeleteKeypress() + return + } + + if (inputLogic.mConnection.hasCursorPosition()) { + val wordRangeAtCursor = inputLogic.mConnection.getWordRangeAtCursor( + settings.current.mSpacingAndPunctuations, + helper.currentKeyboardScriptId, + true + ) + + if (wordRangeAtCursor != null && wordRangeAtCursor.length() > 0) { + val selectionStart = inputLogic.mConnection.getExpectedSelectionStart() + val selectionEnd = inputLogic.mConnection.getExpectedSelectionEnd() + val startOfWord = (selectionStart - wordRangeAtCursor.numberOfCharsInWordBeforeCursor) + .coerceAtLeast(0) + val endOfWord = (selectionEnd + wordRangeAtCursor.numberOfCharsInWordAfterCursor) + .coerceAtLeast(startOfWord) + + inputLogic.mConnection.setSelection(startOfWord, endOfWord) + onUpWithDeletePointerActive() + } else { + sendDeleteKeypress() + } + } else { + sendDeleteKeypress() + } + } + + KeyboardActionListener.SWIPE_ACTION_UP, + KeyboardActionListener.SWIPE_ACTION_DOWN -> { + val lastComposedWordAtSwipeStart = inputLogic.mLastComposedWord + val movedCursorToLastWord = moveCursorToLastWordIfTrailingSpace() + + if (trySwipeCyclePunctuation(direction)) { + restoreCursorIfMoved(movedCursorToLastWord) + return + } + + inputLogic.restartSuggestionsOnWordTouchedByCursor( + settings.current, + null, + false, + helper.currentKeyboardScriptId + ) + + if (!ensureSuggestionsCompleted()) { + restoreCursorIfMoved(movedCursorToLastWord) + return + } + + val candidates = swipeSuggestionCandidates ?: run { + val suggestions = inputLogic.mSuggestedWords + val rebuiltCandidates = ArrayList() + val seen = HashSet() + for (index in 0 until suggestions.size()) { + val info = suggestions.getInfo(index) + if (!info.isKindOf(SuggestedWordInfo.KIND_UNDO) && seen.add(info.mWord)) { + rebuiltCandidates.add(info) + } + } + swipeSuggestionCandidates = rebuiltCandidates + rebuiltCandidates + } + + if (direction == KeyboardActionListener.SWIPE_ACTION_UP + && swipeSuggestionWord == null) { + val touchedWord = inputLogic.mWordComposer.typedWord + val committedWord = lastComposedWordAtSwipeStart.mCommittedWord?.toString() + val typedWordFromLastCommit = lastComposedWordAtSwipeStart.mTypedWord + + if (lastComposedWordAtSwipeStart.canRevertCommit() + && !typedWordFromLastCommit.isNullOrEmpty() + && !committedWord.isNullOrEmpty() + && touchedWord == committedWord + && typedWordFromLastCommit != committedWord) { + val typedWordIndexFromLastCommit = + candidates.indexOfFirst { it.mWord == typedWordFromLastCommit } + val selected = if (typedWordIndexFromLastCommit >= 0) { + candidates[typedWordIndexFromLastCommit] + } else { + SuggestedWordInfo( + typedWordFromLastCommit, + "", + SuggestedWordInfo.MAX_SCORE, + SuggestedWordInfo.KIND_TYPED, + Dictionary.DICTIONARY_USER_TYPED, + SuggestedWordInfo.NOT_AN_INDEX, + SuggestedWordInfo.NOT_A_CONFIDENCE + ) + } + + onEvent(Event.createSuggestionPickedEvent(selected)) + swipeSuggestionIndex = typedWordIndexFromLastCommit + swipeSuggestionWord = selected.mWord + restoreCursorIfMoved(movedCursorToLastWord) + return + } + } + + if (candidates.size < 2) { + resetSwipeSuggestionSession() + restoreCursorIfMoved(movedCursorToLastWord) + return + } + + val typedWord = inputLogic.mWordComposer.typedWord + val currentWord = swipeSuggestionWord ?: typedWord + val typedWordIndex = if (typedWord != null) { + candidates.indexOfFirst { it.mWord == typedWord } + } else { + -1 + } + val currentWordIndex = if (currentWord != null) { + candidates.indexOfFirst { it.mWord == currentWord } + } else { + -1 + } + + val baseIndex = if (swipeSuggestionWord != null + && swipeSuggestionIndex in candidates.indices + && candidates[swipeSuggestionIndex].mWord == swipeSuggestionWord) { + swipeSuggestionIndex + } else if (currentWordIndex >= 0) { + currentWordIndex + } else if (typedWordIndex >= 0) { + typedWordIndex + } else { + 0 + } + + val step = if (direction == KeyboardActionListener.SWIPE_ACTION_UP) -1 else 1 + var nextIndex = (baseIndex + step + candidates.size) % candidates.size + if (currentWord != null && candidates[nextIndex].mWord == currentWord) { + nextIndex = (nextIndex + step + candidates.size) % candidates.size + } + + val selected = candidates[nextIndex] + if (currentWord != null && selected.mWord == currentWord) { + restoreCursorIfMoved(movedCursorToLastWord) + return + } + + onEvent(Event.createSuggestionPickedEvent(selected)) + swipeSuggestionIndex = nextIndex + swipeSuggestionWord = selected.mWord + + restoreCursorIfMoved(movedCursorToLastWord) + } + } + } + override fun onMovingCursorLockEvent(canMoveCursor: Boolean) { // GeneralIME does nothing } @@ -753,4 +1065,4 @@ class GeneralIME(val helper: IMEHelper) : IMEInterface, WordLearner, SuggestionS @OptIn(ExperimentalCoroutinesApi::class) val dictionaryScope = Dispatchers.Default.limitedParallelism(1) } -} \ No newline at end of file +} diff --git a/java/src/org/futo/inputmethod/engine/general/JapaneseIME.kt b/java/src/org/futo/inputmethod/engine/general/JapaneseIME.kt index bb9ad475d6..8966ac94ef 100644 --- a/java/src/org/futo/inputmethod/engine/general/JapaneseIME.kt +++ b/java/src/org/futo/inputmethod/engine/general/JapaneseIME.kt @@ -1200,6 +1200,10 @@ class JapaneseIME(val helper: IMEHelper) : IMEInterface { } + override fun onSwipeAction(direction: Int) { + + } + override fun onMovingCursorLockEvent(canMoveCursor: Boolean) { } @@ -1237,4 +1241,4 @@ class JapaneseIME(val helper: IMEHelper) : IMEInterface { prevSuggestions = words helper.showSuggestionStrip(words, useExpandableUi) } -} \ No newline at end of file +} diff --git a/java/src/org/futo/inputmethod/keyboard/KeyboardActionListener.java b/java/src/org/futo/inputmethod/keyboard/KeyboardActionListener.java index 948fba78d3..70b82882e8 100644 --- a/java/src/org/futo/inputmethod/keyboard/KeyboardActionListener.java +++ b/java/src/org/futo/inputmethod/keyboard/KeyboardActionListener.java @@ -20,6 +20,11 @@ import org.futo.inputmethod.latin.common.InputPointers; public interface KeyboardActionListener { + public static final int SWIPE_ACTION_LEFT = -1; + public static final int SWIPE_ACTION_RIGHT = 1; + public static final int SWIPE_ACTION_UP = -2; + public static final int SWIPE_ACTION_DOWN = 2; + /** * Called when the user presses a key. This is sent before the {@link #onCodeInput} is called. * For keys that repeat, this is only called once. @@ -106,6 +111,7 @@ public interface KeyboardActionListener { public void onUpWithDeletePointerActive(); public void onUpWithPointerActive(); public void onSwipeLanguage(int direction); + public void onSwipeAction(int direction); public void onMovingCursorLockEvent(boolean canMoveCursor); public static final KeyboardActionListener EMPTY_LISTENER = new Adapter(); @@ -144,6 +150,8 @@ public void onUpWithPointerActive() {} @Override public void onSwipeLanguage(int direction) {} @Override + public void onSwipeAction(int direction) {} + @Override public void onMovingCursorLockEvent(boolean canMoveCursor) {} } } diff --git a/java/src/org/futo/inputmethod/keyboard/PointerTracker.java b/java/src/org/futo/inputmethod/keyboard/PointerTracker.java index e09c182dc5..e2f0065df7 100644 --- a/java/src/org/futo/inputmethod/keyboard/PointerTracker.java +++ b/java/src/org/futo/inputmethod/keyboard/PointerTracker.java @@ -90,6 +90,9 @@ public PointerTrackerParams(final TypedArray mainKeyboardViewAttr) { private static PointerTrackerParams sParams; private static final int sPointerStep = (int)(16.0 * Resources.getSystem().getDisplayMetrics().density); private static final int sPointerBigStep = (int)(32.0 * Resources.getSystem().getDisplayMetrics().density); + private static final int sPointerSwipeActionStep = (int)(18.0 * Resources.getSystem().getDisplayMetrics().density); + private static final float SWIPE_ACTION_HORIZONTAL_DOMINANCE_RATIO = 1.0f; + private static final float SWIPE_ACTION_VERTICAL_DOMINANCE_RATIO = 0.70f; private static final int sPointerHugeStep = Integer.min( (int)(128.0 * Resources.getSystem().getDisplayMetrics().density), Resources.getSystem().getDisplayMetrics().widthPixels * 3 / 2 @@ -150,6 +153,7 @@ public PointerTrackerParams(final TypedArray mainKeyboardViewAttr) { private boolean mStartedOnFastLongPress; private boolean mCursorMoved = false; private boolean mSpacebarLongPressed = false; + private boolean mSwipeActionTriggered = false; // true if keyboard layout has been changed. private boolean mKeyboardLayoutHasBeenChanged; @@ -745,8 +749,14 @@ private void onDownEventInternal(final int x, final int y, final long eventTime) mStartTime = System.currentTimeMillis(); mStartedOnFastLongPress = key.isFastLongPress(); mSpacebarLongPressed = false; + mSwipeActionTriggered = false; - mIsSlidingCursor = key.getCode() == Constants.CODE_DELETE || key.getCode() == Constants.CODE_SPACE; + final boolean swipeActionsMode = + Settings.getInstance().getCurrent().mGestureActionsEnabled; + + mIsSlidingCursor = key.getCode() == Constants.CODE_DELETE + || key.getCode() == Constants.CODE_SPACE + || swipeActionsMode; mIsFlickingKey = !mIsSlidingCursor && key.getHasFlick(); mFlickDirection = key.flickDirection(0, 0); mCurrentKey = key; @@ -954,6 +964,50 @@ private void onMoveEventInternal(final int x, final int y, final long eventTime) final SettingsValues settingsValues = Settings.getInstance().getCurrent(); + if (mIsSlidingCursor && oldKey != null + && settingsValues.mGestureActionsEnabled) { + final int pointerStep = sPointerSwipeActionStep; + final int swipeIgnoreTime = settingsValues.mKeyLongpressTimeout + / MULTIPLIER_FOR_LONG_PRESS_TIMEOUT_IN_SLIDING_INPUT; + final int dx = x - mStartX; + final int dy = y - mStartY; + final long swipeDistanceSquared = (long)dx * dx + (long)dy * dy; + final long swipeStepSquared = (long)pointerStep * pointerStep; + + if (!mSwipeActionTriggered + && mStartTime + swipeIgnoreTime < System.currentTimeMillis() + && swipeDistanceSquared >= swipeStepSquared) { + sTimerProxy.cancelKeyTimersOf(this); + mCursorMoved = true; + mSwipeActionTriggered = true; + + final int absDx = Math.abs(dx); + final int absDy = Math.abs(dy); + final float horizontalScore = absDx + - absDy * SWIPE_ACTION_HORIZONTAL_DOMINANCE_RATIO; + final float verticalScore = absDy + - absDx * SWIPE_ACTION_VERTICAL_DOMINANCE_RATIO; + final boolean isHorizontalSwipe = horizontalScore >= 0.0f; + final boolean isVerticalSwipe = verticalScore >= 0.0f; + + if ((isHorizontalSwipe && !isVerticalSwipe) + || (isHorizontalSwipe == isVerticalSwipe + && horizontalScore >= verticalScore)) { + sListener.onSwipeAction(dx > 0 + ? KeyboardActionListener.SWIPE_ACTION_RIGHT + : KeyboardActionListener.SWIPE_ACTION_LEFT); + } else { + sListener.onSwipeAction(dy < 0 + ? KeyboardActionListener.SWIPE_ACTION_UP + : KeyboardActionListener.SWIPE_ACTION_DOWN); + } + } + + mLastX = x; + mLastY = y; + return; + } + if (mIsSlidingCursor && oldKey != null && oldKey.getCode() == Constants.CODE_SPACE) { int pointerStep = sPointerStep; if(settingsValues.mSpacebarMode == Settings.SPACEBAR_MODE_SWIPE_LANGUAGE && !mSpacebarLongPressed) { diff --git a/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java b/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java index 49bd237883..911f0a44f3 100644 --- a/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java +++ b/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java @@ -648,6 +648,13 @@ public void onSwipeLanguage(int direction) { Subtypes.INSTANCE.switchToNextLanguage(mInputMethodService, direction); } + @Override + public void onSwipeAction(int direction) { + mImeManager.getActiveIME( + mSettings.getCurrent() + ).onSwipeAction(direction); + } + @Override public void onMovingCursorLockEvent(boolean canMoveCursor) { if(canMoveCursor) { diff --git a/java/src/org/futo/inputmethod/latin/settings/Settings.java b/java/src/org/futo/inputmethod/latin/settings/Settings.java index 61ac200881..c20fcbe395 100644 --- a/java/src/org/futo/inputmethod/latin/settings/Settings.java +++ b/java/src/org/futo/inputmethod/latin/settings/Settings.java @@ -85,7 +85,11 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang public static final String PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY = "pref_key_preview_popup_dismiss_delay"; public static final String PREF_BIGRAM_PREDICTIONS = "next_word_prediction"; - public static final String PREF_GESTURE_INPUT = "gesture_input"; + public static final String PREF_GESTURE_INPUT_MODE = "pref_gesture_input_mode"; + private static final String PREF_GESTURE_INPUT_LEGACY = "gesture_input"; + public static final int GESTURE_INPUT_MODE_TYPING = 0; + public static final int GESTURE_INPUT_MODE_ACTIONS = 1; + public static final int GESTURE_INPUT_MODE_NONE = 2; public static final String PREF_VIBRATION_DURATION_SETTINGS = "pref_vibration_duration_settings"; public static final String PREF_KEYPRESS_SOUND_VOLUME = "pref_keypress_sound_volume"; @@ -286,10 +290,24 @@ public static boolean readFromBuildConfigIfGestureInputEnabled(final Resources r return res.getBoolean(R.bool.config_gesture_input_enabled_by_build_config); } - public static boolean readGestureInputEnabled(final SharedPreferences prefs, + public static int readGestureInputMode(final SharedPreferences prefs, final Resources res) { - return readFromBuildConfigIfGestureInputEnabled(res) - && prefs.getBoolean(PREF_GESTURE_INPUT, true); + if (prefs.contains(PREF_GESTURE_INPUT_MODE)) { + final int mode = prefs.getInt(PREF_GESTURE_INPUT_MODE, GESTURE_INPUT_MODE_TYPING); + if (mode == GESTURE_INPUT_MODE_TYPING + || mode == GESTURE_INPUT_MODE_ACTIONS + || mode == GESTURE_INPUT_MODE_NONE) { + return mode; + } + } + + if (prefs.contains(PREF_GESTURE_INPUT_LEGACY)) { + return prefs.getBoolean(PREF_GESTURE_INPUT_LEGACY, true) + ? GESTURE_INPUT_MODE_TYPING + : GESTURE_INPUT_MODE_NONE; + } + + return GESTURE_INPUT_MODE_TYPING; } public static boolean readFromBuildConfigIfToShowKeyPreviewPopupOption(final Resources res) { diff --git a/java/src/org/futo/inputmethod/latin/settings/SettingsValues.java b/java/src/org/futo/inputmethod/latin/settings/SettingsValues.java index 8c25837adb..9c0ff4d807 100644 --- a/java/src/org/futo/inputmethod/latin/settings/SettingsValues.java +++ b/java/src/org/futo/inputmethod/latin/settings/SettingsValues.java @@ -81,7 +81,9 @@ public class SettingsValues { // Use bigrams to predict the next word when there is no input for it yet public final boolean mBigramPredictionEnabled; public final boolean mTransformerPredictionEnabled; + public final int mGestureInputMode; public final boolean mGestureInputEnabled; + public final boolean mGestureActionsEnabled; public final boolean mGestureTrailEnabled; public final boolean mGestureFloatingPreviewTextEnabled; public final boolean mSlidingKeyInputPreviewEnabled; @@ -216,7 +218,13 @@ public SettingsValues(final Context context, final SharedPreferences prefs, fina mAutoCorrectionThreshold = readAutoCorrectionThreshold(res, autoCorrectionThresholdRawValue); mPlausibilityThreshold = Settings.readPlausibilityThreshold(res); - mGestureInputEnabled = Settings.readGestureInputEnabled(prefs, res); + mGestureInputMode = Settings.readGestureInputMode(prefs, res); + final boolean gestureInputAllowedByBuild = + Settings.readFromBuildConfigIfGestureInputEnabled(res); + mGestureInputEnabled = gestureInputAllowedByBuild + && mGestureInputMode == Settings.GESTURE_INPUT_MODE_TYPING; + mGestureActionsEnabled = gestureInputAllowedByBuild + && mGestureInputMode == Settings.GESTURE_INPUT_MODE_ACTIONS; mGestureTrailEnabled = prefs.getBoolean(Settings.PREF_GESTURE_PREVIEW_TRAIL, true); mCloudSyncEnabled = prefs.getBoolean(LocalSettingsConstants.PREF_ENABLE_CLOUD_SYNC, false); mAccount = prefs.getString(LocalSettingsConstants.PREF_ACCOUNT_NAME, diff --git a/java/src/org/futo/inputmethod/latin/uix/settings/Components.kt b/java/src/org/futo/inputmethod/latin/uix/settings/Components.kt index d9c83ee05e..8aa1b7fcc2 100644 --- a/java/src/org/futo/inputmethod/latin/uix/settings/Components.kt +++ b/java/src/org/futo/inputmethod/latin/uix/settings/Components.kt @@ -1,9 +1,6 @@ package org.futo.inputmethod.latin.uix.settings -import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.expandVertically -import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -41,6 +38,8 @@ import androidx.compose.material.icons.filled.ArrowForward import androidx.compose.material.icons.filled.Send import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.Icon +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.RadioButton @@ -124,7 +123,7 @@ fun ScreenTitle(title: String, showBack: Boolean = false, navController: NavHost } Text(title, style = Typography.Heading.Medium, modifier = Modifier .align(CenterVertically) - .padding(0.dp, 16.dp)) + .padding(top = 16.dp, bottom = 10.dp)) } } @@ -137,7 +136,7 @@ fun ScreenTitleWithIcon(title: String, painter: Painter) { Spacer(modifier = Modifier.width(18.dp)) Text(title, style = Typography.Heading.Medium, modifier = Modifier .align(CenterVertically) - .padding(0.dp, 16.dp)) + .padding(top = 16.dp, bottom = 10.dp)) } } @@ -439,22 +438,29 @@ fun SettingToggleSharedPrefs( @Composable fun SettingRadio( - title: String, + title: String? = null, options: List, optionNames: List, setting: DataStoreItem, + optionSubtitles: List? = null, + compact: Boolean = false, hints: List<@Composable () -> Unit>? = null, ) { - ScreenTitle(title, showBack = false) + if (!title.isNullOrBlank()) { + ScreenTitle(title, showBack = false) + } Column { options.zip(optionNames).forEachIndexed { i, it -> - SettingItem(title = it.second, onClick = { setting.setValue(it.first) }, icon = { + val subtitle = optionSubtitles?.getOrNull(i) + SettingItem(title = it.second, subtitle = subtitle, onClick = { setting.setValue(it.first) }, icon = { RadioButton(selected = setting.value == it.first, onClick = null) }, modifier = Modifier.clearAndSetSemantics { - this.text = AnnotatedString(it.second) + this.text = AnnotatedString( + if (subtitle.isNullOrBlank()) it.second else "${it.second}. $subtitle" + ) this.role = Role.RadioButton this.selected = setting.value == it.first - }) { + }, compact = compact) { hints?.getOrNull(i)?.let { it() } } } @@ -816,26 +822,22 @@ fun DropDownPicker( ) { var expanded by remember { mutableStateOf(false) } - - SpacedColumn(4.dp, modifier = modifier.semantics { - role = Role.DropdownList - }) { + Box(modifier = modifier.semantics { role = Role.DropdownList }) { Row( - Modifier.fillMaxWidth().background( - MaterialTheme.colorScheme.surfaceContainerHighest, DropDownShape - ).border( - if(expanded) { 2.dp } else { 1.dp }, - MaterialTheme.colorScheme.outline, - DropDownShape - ).heightIn(min = 44.dp).clip(DropDownShape).clickable { - expanded = !expanded - }.padding(16.dp).semantics { - // TODO: Localization - stateDescription = if(expanded) "Expanded" else "Collapsed" - role = Role.DropdownList - } + Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.surfaceContainerHighest, DropDownShape) + .border(1.dp, MaterialTheme.colorScheme.outline, DropDownShape) + .heightIn(min = 44.dp) + .clip(DropDownShape) + .clickable { expanded = !expanded } + .padding(16.dp) + .semantics { + stateDescription = if (expanded) "Expanded" else "Collapsed" + role = Role.DropdownList + } ) { - if(selection != null) { + if (selection != null) { Text( text = getDisplayName(selection), style = Typography.Body.Regular, @@ -849,54 +851,46 @@ fun DropDownPicker( RotatingChevronIcon(expanded, tint = MaterialTheme.colorScheme.onSurfaceVariant) } - AnimatedVisibility(expanded, enter = expandVertically(), exit = shrinkVertically()) { - val scrollState = rememberScrollState() - Column(Modifier.let { - if(scrollableOptions) { - it.verticalScroll(scrollState) - } else { - it - } - }) { - Spacer(Modifier.height(9.dp)) - Column( - Modifier.fillMaxWidth().background( - MaterialTheme.colorScheme.surfaceContainerHighest, DropDownShape - ).border( - 1.dp, - MaterialTheme.colorScheme.outline, - DropDownShape - ).clip(DropDownShape) - ) { - options.forEach { - Box( - Modifier.fillMaxWidth().heightIn(min = 44.dp).background( - if(selection == it) { - LocalKeyboardScheme.current.onSurfaceTransparent - } else { - Color.Transparent - } - ).clickable { - onSet(it) - expanded = false - }.padding(16.dp).semantics { - selected = selection == it - role = Role.DropdownList + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + modifier = if (scrollableOptions) { + Modifier.heightIn(max = 280.dp) + } else { + Modifier + } + ) { + val selectedBackground = LocalKeyboardScheme.current.onSurfaceTransparent + for (option in options) { + DropdownMenuItem( + text = { + Text( + getDisplayName(option), + style = Typography.Body.Regular, + color = if (selection == option) { + MaterialTheme.colorScheme.onSurface + } else { + MaterialTheme.colorScheme.onSurfaceVariant } - ) { - Text( - getDisplayName(it), - style = Typography.Body.Regular, - color = if(selection == it) { - MaterialTheme.colorScheme.onSurface - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, - modifier = Modifier.align(Alignment.CenterStart) - ) + ) + }, + onClick = { + onSet(option) + expanded = false + }, + modifier = Modifier + .background( + if (selection == option) { + selectedBackground + } else { + Color.Transparent + } + ) + .semantics { + selected = selection == option + role = Role.DropdownList } - } - } + ) } } } @@ -979,4 +973,4 @@ fun PreviewPrimarySetting() { "Enable", dataStoreItem = DataStoreItem(false, { error("") }) ) -} \ No newline at end of file +} diff --git a/java/src/org/futo/inputmethod/latin/uix/settings/pages/Typing.kt b/java/src/org/futo/inputmethod/latin/uix/settings/pages/Typing.kt index de8fea13c6..8e0856f858 100644 --- a/java/src/org/futo/inputmethod/latin/uix/settings/pages/Typing.kt +++ b/java/src/org/futo/inputmethod/latin/uix/settings/pages/Typing.kt @@ -797,6 +797,15 @@ val KeyboardSettingsMenu = UserSettingsMenu( ) ) +val SwipeInputSettingsMenu = UserSettingsMenu( + title = R.string.swipe_input_settings_title, + navPath = "swipe", registerNavPath = true, + settings = listOf( + UserSetting(name = R.string.swipe_input_settings_title) { + SwipeAlphaModesSetting() + } + ) +) val TypingSettingsMenu = UserSettingsMenu( title = R.string.typing_settings_title, navPath = "typing", registerNavPath = true, @@ -807,16 +816,6 @@ val TypingSettingsMenu = UserSettingsMenu( AutoSpacesSetting() } ), - userSettingToggleSharedPrefs( - title = R.string.typing_settings_swipe, - subtitle = R.string.typing_settings_swipe_subtitle, - key = Settings.PREF_GESTURE_INPUT, - default = {true}, - icon = { - Icon(painterResource(id = R.drawable.swipe_icon), contentDescription = null, - tint = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.75f)) - } - ), userSettingToggleDataStore( title = R.string.typing_settings_suggest_emojis, subtitle = R.string.typing_settings_suggest_emojis_subtitle, @@ -971,6 +970,33 @@ val TypingSettingsMenu = UserSettingsMenu( ) ) +@Composable +private fun SwipeAlphaModesSetting() { + SettingRadio( + title = stringResource(R.string.swipe_input_settings_title), + options = listOf( + Settings.GESTURE_INPUT_MODE_TYPING, + Settings.GESTURE_INPUT_MODE_ACTIONS, + Settings.GESTURE_INPUT_MODE_NONE + ), + optionNames = listOf( + stringResource(R.string.swipe_input_settings_swipe), + stringResource(R.string.swipe_input_settings_swipe_actions_mode), + stringResource(R.string.swipe_input_settings_swipe_disabled) + ), + optionSubtitles = listOf( + stringResource(R.string.swipe_input_settings_swipe_subtitle), + stringResource(R.string.swipe_input_settings_swipe_actions_mode_subtitle), + stringResource(R.string.swipe_input_settings_swipe_disabled_subtitle) + ), + compact = true, + setting = useSharedPrefsInt( + key = Settings.PREF_GESTURE_INPUT_MODE, + default = Settings.GESTURE_INPUT_MODE_TYPING + ) + ) +} + @Preview(showBackground = true) @Composable fun KeyboardAndTypingScreen(navController: NavHostController = rememberNavController()) { @@ -996,8 +1022,9 @@ fun KeyboardAndTypingScreen(navController: NavHostController = rememberNavContro } KeyboardSettingsMenu.render(showBack = false, showTitle = false) + SwipeInputSettingsMenu.render(showBack = false, showTitle = false) TypingSettingsMenu.render(showBack = false) BottomSpacer() } -} \ No newline at end of file +}