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
+}