From 6af95db26b1f318b220d73dc58f6d5ff5c0bd51f Mon Sep 17 00:00:00 2001 From: Stavros Korokithakis Date: Wed, 24 Jun 2026 20:25:01 +0300 Subject: [PATCH] feat: Add swipe-to-delete-word functionality --- java/res/values/strings-uix.xml | 3 ++ .../futo/inputmethod/engine/IMEInterface.kt | 1 + .../general/ActionInputTransactionIME.kt | 1 + .../inputmethod/engine/general/ChineseIME.kt | 1 + .../inputmethod/engine/general/GeneralIME.kt | 9 +++++ .../inputmethod/engine/general/JapaneseIME.kt | 2 + .../keyboard/KeyboardActionListener.java | 3 ++ .../inputmethod/keyboard/PointerTracker.java | 40 ++++++++++++++++++- .../inputmethod/latin/LatinIMELegacy.java | 7 ++++ .../inputmethod/latin/settings/Settings.java | 1 + .../latin/settings/SettingsValues.java | 4 ++ .../latin/uix/settings/pages/Swipe.kt | 9 +++++ 12 files changed, 80 insertions(+), 1 deletion(-) diff --git a/java/res/values/strings-uix.xml b/java/res/values/strings-uix.xml index aa146b7b89..a390a7cdd7 100644 --- a/java/res/values/strings-uix.xml +++ b/java/res/values/strings-uix.xml @@ -538,6 +538,9 @@ Enable swipe typing Allow swiping from key to key to write words. + Swipe to delete word + Swipe left on the keyboard to delete the last word. Only works when swipe typing is off. + Increase sensitivity Make swipe trigger more often diff --git a/java/src/org/futo/inputmethod/engine/IMEInterface.kt b/java/src/org/futo/inputmethod/engine/IMEInterface.kt index b52598e041..6831cc274e 100644 --- a/java/src/org/futo/inputmethod/engine/IMEInterface.kt +++ b/java/src/org/futo/inputmethod/engine/IMEInterface.kt @@ -83,6 +83,7 @@ interface IMEInterface { fun onMovePointer(steps: Int, stepOverWords: Boolean, select: Boolean?) fun onMoveDeletePointer(steps: Int) + fun onSelectWordLeft() fun onUpWithDeletePointerActive() fun onUpWithPointerActive() fun onSwipeLanguage(direction: Int) diff --git a/java/src/org/futo/inputmethod/engine/general/ActionInputTransactionIME.kt b/java/src/org/futo/inputmethod/engine/general/ActionInputTransactionIME.kt index 5906d48f95..0f38eddb2c 100644 --- a/java/src/org/futo/inputmethod/engine/general/ActionInputTransactionIME.kt +++ b/java/src/org/futo/inputmethod/engine/general/ActionInputTransactionIME.kt @@ -63,6 +63,7 @@ class ActionInputTransactionIME(val helper: IMEHelper) : IMEInterface, ActionInp override fun onCustomRequest(requestCode: Int): Boolean = false override fun onMovePointer(steps: Int, stepOverWords: Boolean, select: Boolean?) {} override fun onMoveDeletePointer(steps: Int) {} + override fun onSelectWordLeft() {} override fun onUpWithDeletePointerActive() {} override fun onUpWithPointerActive() {} override fun onSwipeLanguage(direction: Int) {} diff --git a/java/src/org/futo/inputmethod/engine/general/ChineseIME.kt b/java/src/org/futo/inputmethod/engine/general/ChineseIME.kt index fcfe2a17e5..53b9565088 100644 --- a/java/src/org/futo/inputmethod/engine/general/ChineseIME.kt +++ b/java/src/org/futo/inputmethod/engine/general/ChineseIME.kt @@ -849,6 +849,7 @@ class ChineseIME(val helper: IMEHelper) : IMEInterface, SuggestionStripViewAcces } connect?.setSelection(lBound, currentMovingCursor) } + override fun onSelectWordLeft() {} override fun onUpWithDeletePointerActive() { anchorOutOfDate = true val selection: CharSequence? = connect?.getSelectedText(0) diff --git a/java/src/org/futo/inputmethod/engine/general/GeneralIME.kt b/java/src/org/futo/inputmethod/engine/general/GeneralIME.kt index 06370ccc0b..ba9c0a0ad9 100644 --- a/java/src/org/futo/inputmethod/engine/general/GeneralIME.kt +++ b/java/src/org/futo/inputmethod/engine/general/GeneralIME.kt @@ -672,6 +672,15 @@ class GeneralIME(val helper: IMEHelper) : IMEInterface, WordLearner, SuggestionS } } + override fun onSelectWordLeft() { + // Select exactly one word to the left, regardless of the backspace-mode setting. + // Used by the "left swipe anywhere to delete one word" gesture. + setNeutralSuggestionStrip() + if (inputLogic.mConnection.hasCursorPosition()) { + inputLogic.cursorLeft(-1, true, true) + } + } + override fun onUpWithDeletePointerActive() { if (inputLogic.mConnection.hasSelection()) { val selection: CharSequence? = inputLogic.mConnection.getSelectedText(0) diff --git a/java/src/org/futo/inputmethod/engine/general/JapaneseIME.kt b/java/src/org/futo/inputmethod/engine/general/JapaneseIME.kt index 726d8daba5..244d0a0b64 100644 --- a/java/src/org/futo/inputmethod/engine/general/JapaneseIME.kt +++ b/java/src/org/futo/inputmethod/engine/general/JapaneseIME.kt @@ -1195,6 +1195,8 @@ class JapaneseIME(val helper: IMEHelper) : IMEInterface { } + override fun onSelectWordLeft() {} + override fun onUpWithDeletePointerActive() { } diff --git a/java/src/org/futo/inputmethod/keyboard/KeyboardActionListener.java b/java/src/org/futo/inputmethod/keyboard/KeyboardActionListener.java index 948fba78d3..53b360629e 100644 --- a/java/src/org/futo/inputmethod/keyboard/KeyboardActionListener.java +++ b/java/src/org/futo/inputmethod/keyboard/KeyboardActionListener.java @@ -103,6 +103,7 @@ public interface KeyboardActionListener { public void onMovePointer(int steps); public void onMoveDeletePointer(int steps); + public void onSelectWordLeft(); public void onUpWithDeletePointerActive(); public void onUpWithPointerActive(); public void onSwipeLanguage(int direction); @@ -138,6 +139,8 @@ public void onMovePointer(int steps) {} @Override public void onMoveDeletePointer(int steps) {} @Override + public void onSelectWordLeft() {} + @Override public void onUpWithDeletePointerActive() {} @Override public void onUpWithPointerActive() {} diff --git a/java/src/org/futo/inputmethod/keyboard/PointerTracker.java b/java/src/org/futo/inputmethod/keyboard/PointerTracker.java index 3f4cb6e3ec..50fe379df9 100644 --- a/java/src/org/futo/inputmethod/keyboard/PointerTracker.java +++ b/java/src/org/futo/inputmethod/keyboard/PointerTracker.java @@ -156,6 +156,7 @@ public static void setStateHint(StateHint stateHint) { private long mStartTime; private boolean mStartedOnFastLongPress; private boolean mCursorMoved = false; + private boolean mIsLeftSwipeDeleteWord = false; private boolean mSpacebarLongPressed = false; // true if keyboard layout has been changed. @@ -761,6 +762,7 @@ private void onDownEventInternal(final int x, final int y, final long eventTime) mStartedOnFastLongPress = key.isFastLongPress(); mSpacebarLongPressed = false; + mIsLeftSwipeDeleteWord = false; mIsSlidingCursor = key.getCode() == Constants.CODE_DELETE || key.getCode() == Constants.CODE_SPACE; mIsFlickingKey = !mIsSlidingCursor && key.getHasFlick(); mFlickDirection = key.flickDirection(0, 0); @@ -1021,6 +1023,37 @@ private void onMoveEventInternal(final int x, final int y, final long eventTime) return; } + // Left swipe anywhere on the alphabet keyboard deletes exactly one word to the left. + // This only fires when glide/swipe typing is off, the stroke starts on a regular key + // (not Delete or Space), and the motion is predominantly horizontal and leftward. + // Mirror direction for RTL: in RTL, swiping right (toward line start) triggers the + // delete, so we negate dx before checking. + if (mIsLeftSwipeDeleteWord) { + // Already latched: consume remaining moves silently, no normal handling. + mLastX = x; + mLastY = y; + return; + } + + if (!sInGesture && !settingsValues.mGestureInputEnabled + && !mIsSlidingCursor + && settingsValues.mDeleteWordSwipeEnabled + && mKeyboard != null && mKeyboard.mId.isAlphabetKeyboard()) { + final int dx = x - mStartX; + final int dy = y - mStartY; + final int threshold = sPointerBigStep; + final int adjustedDx = settingsValues.mIsRTL ? -dx : dx; + if (adjustedDx <= -threshold && Math.abs(dx) > 2 * Math.abs(dy)) { + mIsLeftSwipeDeleteWord = true; + mCursorMoved = true; + sListener.onSelectWordLeft(); + mLastX = x; + mLastY = y; + return; + } + // Non-qualifying move: fall through to normal drag/flick handling below. + } + if(mIsFlickingKey && oldKey != null) { final Direction prevDirection = mFlickDirection; mFlickDirection = oldKey.flickDirection(x - mStartX, y - mStartY); @@ -1132,11 +1165,14 @@ private void onUpEventInternal(final int x, final int y, final long eventTime) { if(mCursorMoved && currentKey != null && currentKey.getCode() == Constants.CODE_DELETE) { sListener.onUpWithDeletePointerActive(); + } else if(mIsLeftSwipeDeleteWord) { + sListener.onUpWithDeletePointerActive(); + mIsLeftSwipeDeleteWord = false; } else if(mCursorMoved) { sListener.onUpWithPointerActive(); } - if(mIsFlickingKey && currentKey != null) { + if(mIsFlickingKey && currentKey != null && !mCursorMoved) { final Key flickedKey = currentKey.flick(x - mStartX, y - mStartY); detectAndSendKey(flickedKey, mKeyX, mKeyY, eventTime); @@ -1192,6 +1228,7 @@ public void cancelTrackingForAction() { return; } mIsTrackingForActionDisabled = true; + mIsLeftSwipeDeleteWord = false; } public boolean isInOperation() { @@ -1284,6 +1321,7 @@ private void onCancelEventInternal() { setReleasedKeyGraphics(mCurrentKey, true /* withAnimation */); resetKeySelectionByDraggingFinger(); dismissMoreKeysPanel(); + mIsLeftSwipeDeleteWord = false; } private boolean isMajorEnoughMoveToBeOnNewKey(final int x, final int y, final long eventTime, diff --git a/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java b/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java index c64d065790..f32e327e6a 100644 --- a/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java +++ b/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java @@ -630,6 +630,13 @@ public void onMoveDeletePointer(int steps) { ).onMoveDeletePointer(steps); } + @Override + public void onSelectWordLeft() { + mImeManager.getActiveIME( + mSettings.getCurrent() + ).onSelectWordLeft(); + } + @Override public void onUpWithDeletePointerActive() { mImeManager.getActiveIME( diff --git a/java/src/org/futo/inputmethod/latin/settings/Settings.java b/java/src/org/futo/inputmethod/latin/settings/Settings.java index 055c166a6c..93b3ae5142 100644 --- a/java/src/org/futo/inputmethod/latin/settings/Settings.java +++ b/java/src/org/futo/inputmethod/latin/settings/Settings.java @@ -90,6 +90,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang 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_SENSITIVITY = "gesture_input_sensitivity"; + public static final String PREF_DELETE_WORD_SWIPE = "delete_word_swipe"; public static final String PREF_VIBRATION_DURATION_SETTINGS = "pref_vibration_duration_settings"; public static final String PREF_KEYPRESS_SOUND_VOLUME = "pref_keypress_sound_volume"; diff --git a/java/src/org/futo/inputmethod/latin/settings/SettingsValues.java b/java/src/org/futo/inputmethod/latin/settings/SettingsValues.java index 30803dc90a..931b62f877 100644 --- a/java/src/org/futo/inputmethod/latin/settings/SettingsValues.java +++ b/java/src/org/futo/inputmethod/latin/settings/SettingsValues.java @@ -86,6 +86,7 @@ public class SettingsValues { public final boolean mTransformerPredictionEnabled; public final boolean mGestureInputEnabled; public final boolean mGestureInputSensitive; + public final boolean mDeleteWordSwipeEnabled; public final boolean mGestureTrailEnabled; public final boolean mGestureFloatingPreviewTextEnabled; public final boolean mSlidingKeyInputPreviewEnabled; @@ -233,6 +234,7 @@ public SettingsValues(final Context context, final SharedPreferences prefs, fina mPlausibilityThreshold = Settings.readPlausibilityThreshold(res); mGestureInputEnabled = Settings.readGestureInputEnabled(prefs, res); mGestureInputSensitive = prefs.getBoolean(Settings.PREF_GESTURE_INPUT_SENSITIVITY, false); + mDeleteWordSwipeEnabled = prefs.getBoolean(Settings.PREF_DELETE_WORD_SWIPE, true); 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, @@ -496,6 +498,8 @@ public String dump() { sb.append("" + mTransformerPredictionEnabled); sb.append("\n mGestureInputEnabled = "); sb.append("" + mGestureInputEnabled); + sb.append("\n mDeleteWordSwipeEnabled = "); + sb.append("" + mDeleteWordSwipeEnabled); sb.append("\n mGestureTrailEnabled = "); sb.append("" + mGestureTrailEnabled); sb.append("\n mGestureFloatingPreviewTextEnabled = "); diff --git a/java/src/org/futo/inputmethod/latin/uix/settings/pages/Swipe.kt b/java/src/org/futo/inputmethod/latin/uix/settings/pages/Swipe.kt index bbc4547f9e..26a19e8b33 100644 --- a/java/src/org/futo/inputmethod/latin/uix/settings/pages/Swipe.kt +++ b/java/src/org/futo/inputmethod/latin/uix/settings/pages/Swipe.kt @@ -38,6 +38,7 @@ import org.futo.inputmethod.latin.uix.settings.UserSetting import org.futo.inputmethod.latin.uix.settings.UserSettingsMenu import org.futo.inputmethod.latin.uix.settings.useDataStore import org.futo.inputmethod.latin.uix.settings.useDataStoreValue +import org.futo.inputmethod.latin.uix.settings.useSharedPrefsBool import org.futo.inputmethod.latin.uix.settings.userSettingDecorationOnly import org.futo.inputmethod.latin.uix.settings.userSettingNavigationItem import org.futo.inputmethod.latin.uix.settings.userSettingToggleSharedPrefs @@ -97,6 +98,14 @@ val SwipeMenu = UserSettingsMenu( default = {true}, ), + userSettingToggleSharedPrefs( + title = R.string.swipe_settings_delete_word_swipe, + subtitle = R.string.swipe_settings_delete_word_swipe_subtitle, + key = Settings.PREF_DELETE_WORD_SWIPE, + default = {true}, + disabled = { useSharedPrefsBool(Settings.PREF_GESTURE_INPUT, true).value }, + ), + userSettingToggleSharedPrefs( title = R.string.swipe_settings_sensitive_swipe, subtitle = R.string.swipe_settings_sensitive_swipe_subtitle,