From 3d0b384785a65dccd01e9ffec36181ca392657c9 Mon Sep 17 00:00:00 2001
From: djurcola <59278980+djurcola@users.noreply.github.com>
Date: Sun, 2 Aug 2026 14:12:02 +1000
Subject: [PATCH 1/2] feat: add keyboard swipe word deletion
---
java/res/values/strings-uix.xml | 3 +
.../futo/inputmethod/engine/IMEInterface.kt | 1 +
.../inputmethod/engine/general/GeneralIME.kt | 28 ++++
.../keyboard/KeyboardActionListener.java | 3 +
.../inputmethod/keyboard/PointerTracker.java | 53 ++++++-
.../internal/SurfaceSwipeDeleteDetector.java | 52 +++++++
.../inputmethod/latin/LatinIMELegacy.java | 7 +
.../inputmethod/latin/settings/Settings.java | 2 +
.../latin/settings/SettingsValues.java | 2 +
.../latin/uix/settings/pages/Swipe.kt | 7 +
.../SurfaceSwipeDeleteDetectorTest.java | 143 ++++++++++++++++++
11 files changed, 300 insertions(+), 1 deletion(-)
create mode 100644 java/src/org/futo/inputmethod/keyboard/internal/SurfaceSwipeDeleteDetector.java
create mode 100644 src/test/java/org/futo/inputmethod/keyboard/internal/SurfaceSwipeDeleteDetectorTest.java
diff --git a/java/res/values/strings-uix.xml b/java/res/values/strings-uix.xml
index d69782d46d..a0e37a1c47 100644
--- a/java/res/values/strings-uix.xml
+++ b/java/res/values/strings-uix.xml
@@ -562,6 +562,9 @@
Shortcuts
Configure spacebar and delete key gestures in long-press settings
+ Swipe to delete words
+ Swipe left on the keyboard to delete the previous word.
+
Show unsupported settingsā¦
Use legacy swipe algorithm
The legacy algorithm is very inaccurate and not recommended.
diff --git a/java/src/org/futo/inputmethod/engine/IMEInterface.kt b/java/src/org/futo/inputmethod/engine/IMEInterface.kt
index b52598e041..03896f706a 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 onSurfaceSwipeDelete(steps: Int) {}
fun onUpWithDeletePointerActive()
fun onUpWithPointerActive()
fun onSwipeLanguage(direction: Int)
diff --git a/java/src/org/futo/inputmethod/engine/general/GeneralIME.kt b/java/src/org/futo/inputmethod/engine/general/GeneralIME.kt
index 06370ccc0b..8cc8de28e0 100644
--- a/java/src/org/futo/inputmethod/engine/general/GeneralIME.kt
+++ b/java/src/org/futo/inputmethod/engine/general/GeneralIME.kt
@@ -672,6 +672,34 @@ class GeneralIME(val helper: IMEHelper) : IMEInterface, WordLearner, SuggestionS
}
}
+ override fun onSurfaceSwipeDelete(steps: Int) {
+ setNeutralSuggestionStrip()
+ if (inputLogic.mConnection.hasCursorPosition()) {
+ val count = kotlin.math.abs(steps)
+ repeat(count) {
+ if (steps < 0) {
+ inputLogic.cursorLeft(-1, true, true)
+ } else {
+ inputLogic.cursorRight(1, true, true)
+ }
+ }
+ } else {
+ var remaining = steps
+ while (remaining < 0) {
+ onEvent(
+ Event.createSoftwareKeypressEvent(
+ Event.NOT_A_CODE_POINT,
+ Constants.CODE_DELETE,
+ Constants.NOT_A_COORDINATE,
+ Constants.NOT_A_COORDINATE,
+ false
+ )
+ )
+ remaining++
+ }
+ }
+ }
+
override fun onUpWithDeletePointerActive() {
if (inputLogic.mConnection.hasSelection()) {
val selection: CharSequence? = inputLogic.mConnection.getSelectedText(0)
diff --git a/java/src/org/futo/inputmethod/keyboard/KeyboardActionListener.java b/java/src/org/futo/inputmethod/keyboard/KeyboardActionListener.java
index 813426429e..f5581853cf 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 onSurfaceSwipeDelete(int steps);
public void onUpWithDeletePointerActive();
public void onUpWithPointerActive();
public void onMovingCursorLockEvent(boolean canMoveCursor);
@@ -140,6 +141,8 @@ public void onMovePointer(int steps) {}
@Override
public void onMoveDeletePointer(int steps) {}
@Override
+ public void onSurfaceSwipeDelete(int steps) {}
+ @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 6469fe1ad2..4d9868883e 100644
--- a/java/src/org/futo/inputmethod/keyboard/PointerTracker.java
+++ b/java/src/org/futo/inputmethod/keyboard/PointerTracker.java
@@ -34,6 +34,7 @@
import org.futo.inputmethod.keyboard.internal.GestureStrokeRecognitionParams;
import org.futo.inputmethod.keyboard.internal.KeyboardState;
import org.futo.inputmethod.keyboard.internal.PointerTrackerQueue;
+import org.futo.inputmethod.keyboard.internal.SurfaceSwipeDeleteDetector;
import org.futo.inputmethod.keyboard.internal.TimerProxy;
import org.futo.inputmethod.keyboard.internal.TypingTimeRecorder;
import org.futo.inputmethod.latin.R;
@@ -159,6 +160,10 @@ public static void setStateHint(StateHint stateHint) {
private boolean mProgressReported = false;
private boolean mSpacebarLongPressed = false;
+ private boolean mIsSurfaceSwiping = false;
+ private final SurfaceSwipeDeleteDetector mSurfaceSwipeDeleteDetector =
+ new SurfaceSwipeDeleteDetector();
+
// true if keyboard layout has been changed.
private boolean mKeyboardLayoutHasBeenChanged;
@@ -730,6 +735,8 @@ private void onDownEventInternal(final int x, final int y, final long eventTime)
|| mKeyDetector.alwaysAllowsKeySelectionByDraggingFinger();
mKeyboardLayoutHasBeenChanged = false;
mIsTrackingForActionDisabled = false;
+ mIsSurfaceSwiping = false;
+ mSurfaceSwipeDeleteDetector.cancel();
resetKeySelectionByDraggingFinger();
if (key != null) {
// This onPress call may have changed keyboard layout. Those cases are detected at
@@ -766,6 +773,17 @@ private void onDownEventInternal(final int x, final int y, final long eventTime)
mIsFlickingKey = !mIsSlidingCursor && key.getHasFlick();
mFlickDirection = key.flickDirection(0, 0);
mCurrentKey = key;
+
+ if (!mIsSlidingCursor && !mIsFlickingKey && !key.isModifier()) {
+ mIsSurfaceSwiping = true;
+ mSurfaceSwipeDeleteDetector.start(x, sPointerBigStep);
+ }
+ } else {
+ mStartX = x;
+ mStartY = y;
+ mStartTime = System.currentTimeMillis();
+ mIsSurfaceSwiping = true;
+ mSurfaceSwipeDeleteDetector.start(x, sPointerBigStep);
}
}
@@ -1029,6 +1047,33 @@ private void onMoveEventInternal(final int x, final int y, final long eventTime)
return;
}
+ if (!sInGesture && mIsSurfaceSwiping
+ && getActivePointerTrackerCount() == 1
+ && settingsValues.mSurfaceSwipeDeleteEnabled) {
+ final int swipeIgnoreTime = settingsValues.mKeyLongpressTimeout
+ / MULTIPLIER_FOR_LONG_PRESS_TIMEOUT_IN_SLIDING_INPUT;
+ final boolean pastDeadTime =
+ mStartTime + swipeIgnoreTime < System.currentTimeMillis();
+ if (mSurfaceSwipeDeleteDetector.isActive() || pastDeadTime) {
+ final int steps = mSurfaceSwipeDeleteDetector.onMove(x, settingsValues.mIsRTL);
+ if (steps != 0) {
+ if (oldKey != null) {
+ sTimerProxy.cancelKeyTimersOf(this);
+ setReleasedKeyGraphics(oldKey, true /* withAnimation */);
+ mCurrentKey = null;
+ mIsDetectingGesture = false;
+ }
+ mCursorMoved = true;
+ sListener.onSurfaceSwipeDelete(steps);
+ }
+ if (mSurfaceSwipeDeleteDetector.isActive()) {
+ mLastX = x;
+ mLastY = y;
+ return;
+ }
+ }
+ }
+
if(mIsFlickingKey && oldKey != null) {
final Direction prevDirection = mFlickDirection;
mFlickDirection = oldKey.flickDirection(x - mStartX, y - mStartY);
@@ -1142,7 +1187,11 @@ private void onUpEventInternal(final int x, final int y, final long eventTime) {
sListener.onSwipeLanguageReleased();
mProgressReported = false;
}
- if(mCursorMoved && currentKey != null && currentKey.getCode() == Constants.CODE_DELETE) {
+ if(mSurfaceSwipeDeleteDetector.isActive()) {
+ sListener.onUpWithDeletePointerActive();
+ mSurfaceSwipeDeleteDetector.cancel();
+ mIsSurfaceSwiping = false;
+ } else if(mCursorMoved && currentKey != null && currentKey.getCode() == Constants.CODE_DELETE) {
sListener.onUpWithDeletePointerActive();
} else if(mCursorMoved) {
sListener.onUpWithPointerActive();
@@ -1296,6 +1345,8 @@ private void onCancelEventInternal() {
setReleasedKeyGraphics(mCurrentKey, true /* withAnimation */);
resetKeySelectionByDraggingFinger();
dismissMoreKeysPanel();
+ mSurfaceSwipeDeleteDetector.cancel();
+ mIsSurfaceSwiping = false;
}
private boolean isMajorEnoughMoveToBeOnNewKey(final int x, final int y, final long eventTime,
diff --git a/java/src/org/futo/inputmethod/keyboard/internal/SurfaceSwipeDeleteDetector.java b/java/src/org/futo/inputmethod/keyboard/internal/SurfaceSwipeDeleteDetector.java
new file mode 100644
index 0000000000..264bafe525
--- /dev/null
+++ b/java/src/org/futo/inputmethod/keyboard/internal/SurfaceSwipeDeleteDetector.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.futo.inputmethod.keyboard.internal;
+
+public final class SurfaceSwipeDeleteDetector {
+ private int mStep;
+ private int mStartX;
+ private boolean mActive;
+
+ public void start(final int startX, final int step) {
+ mStartX = startX;
+ mStep = step > 0 ? step : 1;
+ mActive = false;
+ }
+
+ public boolean isActive() {
+ return mActive;
+ }
+
+ public void cancel() {
+ mActive = false;
+ }
+
+ public int onMove(final int x, final boolean isRTL) {
+ final int steps = (x - mStartX) / mStep;
+ if (!mActive) {
+ if (steps >= 0) {
+ return 0;
+ }
+ mActive = true;
+ }
+ if (steps == 0) {
+ return 0;
+ }
+ mStartX += steps * mStep;
+ return isRTL ? -steps : steps;
+ }
+}
diff --git a/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java b/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java
index 7807c3dc84..25b893e026 100644
--- a/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java
+++ b/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java
@@ -639,6 +639,13 @@ public void onMoveDeletePointer(int steps) {
).onMoveDeletePointer(steps);
}
+ @Override
+ public void onSurfaceSwipeDelete(int steps) {
+ mImeManager.getActiveIME(
+ mSettings.getCurrent()
+ ).onSurfaceSwipeDelete(steps);
+ }
+
@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..c0b3b9d19f 100644
--- a/java/src/org/futo/inputmethod/latin/settings/Settings.java
+++ b/java/src/org/futo/inputmethod/latin/settings/Settings.java
@@ -137,6 +137,8 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
public static final int SPACEBAR_MODE_CURSOR = 1;
public static final int SPACEBAR_MODE_LANGUAGE = 2;
+ public static final String PREF_SURFACE_SWIPE_DELETE = "pref_surface_swipe_delete";
+
public static final String PREF_BACKSPACE_MODE_HOLD = "pref_backspace_mode_hold";
public static final String PREF_BACKSPACE_MODE = "pref_backspace_mode";
public static final int BACKSPACE_MODE_CHARACTERS = 0; // Long-press backspace and swipe backspace removes just characters
diff --git a/java/src/org/futo/inputmethod/latin/settings/SettingsValues.java b/java/src/org/futo/inputmethod/latin/settings/SettingsValues.java
index 30803dc90a..5c6a27df51 100644
--- a/java/src/org/futo/inputmethod/latin/settings/SettingsValues.java
+++ b/java/src/org/futo/inputmethod/latin/settings/SettingsValues.java
@@ -110,6 +110,7 @@ public class SettingsValues {
public final int mSpacebarHoldMode;
public final int mBackspaceMode;
public final int mBackspaceModeHold;
+ public final boolean mSurfaceSwipeDeleteEnabled;
public final int mNumberRowMode;
public final int mAltSpacesMode;
@@ -212,6 +213,7 @@ public SettingsValues(final Context context, final SharedPreferences prefs, fina
mBackspaceMode = prefs.getInt(Settings.PREF_BACKSPACE_MODE, Settings.BACKSPACE_MODE_CHARACTERS);
mBackspaceModeHold = prefs.getInt(Settings.PREF_BACKSPACE_MODE_HOLD, mBackspaceMode);
+ mSurfaceSwipeDeleteEnabled = prefs.getBoolean(Settings.PREF_SURFACE_SWIPE_DELETE, true);
mNumberRowMode = mIsNumberRowEnabledByUser ?
prefs.getInt(Settings.PREF_NUMBER_ROW_MODE, Settings.NUMBER_ROW_MODE_DEFAULT)
: Settings.NUMBER_ROW_MODE_DEFAULT;
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 54a44ec7dc..7d6790aa35 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
@@ -232,6 +232,13 @@ val SwipeMenu = UserSettingsMenu(
default = {false},
),
+ userSettingToggleSharedPrefs(
+ title = R.string.swipe_settings_surface_swipe_delete,
+ subtitle = R.string.swipe_settings_surface_swipe_delete_subtitle,
+ key = Settings.PREF_SURFACE_SWIPE_DELETE,
+ default = {true},
+ ),
+
// KASROZ is primarily for English and the menu isn't translated, so it's hidden if user
// doesn't have English layout
UserSetting(R.string.swipe_settings_kasroz, subtitle = R.string.swipe_settings_kasroz_subtitle, visibilityCheck = {
diff --git a/src/test/java/org/futo/inputmethod/keyboard/internal/SurfaceSwipeDeleteDetectorTest.java b/src/test/java/org/futo/inputmethod/keyboard/internal/SurfaceSwipeDeleteDetectorTest.java
new file mode 100644
index 0000000000..adb5d67e0c
--- /dev/null
+++ b/src/test/java/org/futo/inputmethod/keyboard/internal/SurfaceSwipeDeleteDetectorTest.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.futo.inputmethod.keyboard.internal;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class SurfaceSwipeDeleteDetectorTest {
+ private static final int STEP = 100;
+ private static final int START_X = 1000;
+
+ private SurfaceSwipeDeleteDetector mDetector;
+
+ @Before
+ public void setUp() {
+ mDetector = new SurfaceSwipeDeleteDetector();
+ mDetector.start(START_X, STEP);
+ }
+
+ @Test
+ public void rightwardSwipeNeverInitiates() {
+ assertEquals(0, mDetector.onMove(START_X + 350, false));
+ assertFalse(mDetector.isActive());
+ assertEquals(0, mDetector.onMove(START_X + 50, false));
+ assertFalse(mDetector.isActive());
+ }
+
+ @Test
+ public void movementBelowStepDoesNothing() {
+ assertEquals(0, mDetector.onMove(START_X - (STEP - 1), false));
+ assertFalse(mDetector.isActive());
+ }
+
+ @Test
+ public void singleLeftStepDeletesOneWord() {
+ assertEquals(-1, mDetector.onMove(START_X - STEP, false));
+ assertTrue(mDetector.isActive());
+ }
+
+ @Test
+ public void multipleLeftStepsDeleteMultipleWords() {
+ assertEquals(-3, mDetector.onMove(START_X - 3 * STEP, false));
+ assertTrue(mDetector.isActive());
+ }
+
+ @Test
+ public void stepsAreIncremental() {
+ assertEquals(-1, mDetector.onMove(START_X - STEP, false));
+ assertEquals(-1, mDetector.onMove(START_X - 2 * STEP, false));
+ assertEquals(0, mDetector.onMove(START_X - 2 * STEP - (STEP - 1), false));
+ assertEquals(-1, mDetector.onMove(START_X - 3 * STEP, false));
+ }
+
+ @Test
+ public void rtlFlipsDirection() {
+ assertEquals(1, mDetector.onMove(START_X - STEP, true));
+ assertTrue(mDetector.isActive());
+ }
+
+ @Test
+ public void movingBackRightReleasesSelection() {
+ assertEquals(-2, mDetector.onMove(START_X - 2 * STEP, false));
+ assertEquals(1, mDetector.onMove(START_X - STEP, false));
+ assertTrue(mDetector.isActive());
+ }
+
+ @Test
+ public void cancelStopsGesture() {
+ mDetector.onMove(START_X - STEP, false);
+ assertTrue(mDetector.isActive());
+ mDetector.cancel();
+ assertFalse(mDetector.isActive());
+ assertEquals(0, mDetector.onMove(START_X + STEP, false));
+ }
+
+ @Test
+ public void startResetsState() {
+ mDetector.onMove(START_X - STEP, false);
+ assertTrue(mDetector.isActive());
+ mDetector.start(500, STEP);
+ assertFalse(mDetector.isActive());
+ assertEquals(-1, mDetector.onMove(500 - STEP, false));
+ }
+
+ @Test
+ public void nonPositiveStepIsGuarded() {
+ mDetector.start(START_X, 0);
+ assertEquals(-1, mDetector.onMove(START_X - 1, false));
+ assertTrue(mDetector.isActive());
+ }
+
+ @Test
+ public void keyPressJitterDoesNotActivate() {
+ for (int dx = -5; dx <= 5; dx++) {
+ assertEquals(0, mDetector.onMove(START_X + dx, false));
+ }
+ assertFalse(mDetector.isActive());
+ }
+
+ @Test
+ public void rightwardDriftFromKeyDoesNotActivate() {
+ assertEquals(0, mDetector.onMove(START_X + 200, false));
+ assertEquals(0, mDetector.onMove(START_X + 100, false));
+ assertFalse(mDetector.isActive());
+ }
+
+ @Test
+ public void leftSwipeFromKeyActivatesAfterFullStep() {
+ assertEquals(0, mDetector.onMove(START_X - (STEP - 1), false));
+ assertFalse(mDetector.isActive());
+ assertEquals(-1, mDetector.onMove(START_X - STEP, false));
+ assertTrue(mDetector.isActive());
+ }
+
+ @Test
+ public void restartFromNewKeyResetsOrigin() {
+ assertEquals(-1, mDetector.onMove(START_X - STEP, false));
+ assertTrue(mDetector.isActive());
+ mDetector.start(500, STEP);
+ assertFalse(mDetector.isActive());
+ assertEquals(0, mDetector.onMove(500 - (STEP - 1), false));
+ assertEquals(-1, mDetector.onMove(500 - STEP, false));
+ assertTrue(mDetector.isActive());
+ }
+}
From 44421c66705227ca03d5e56dbd68e08bac5ae405 Mon Sep 17 00:00:00 2001
From: djurcola <59278980+djurcola@users.noreply.github.com>
Date: Sun, 2 Aug 2026 16:45:10 +1000
Subject: [PATCH 2/2] feat: require hold to adjust swipe word deletion
---
java/res/values/strings-uix.xml | 2 +-
.../inputmethod/keyboard/PointerTracker.java | 9 +-
.../internal/SurfaceSwipeDeleteDetector.java | 42 +++++-
.../SurfaceSwipeDeleteDetectorTest.java | 121 +++++++++++++-----
4 files changed, 136 insertions(+), 38 deletions(-)
diff --git a/java/res/values/strings-uix.xml b/java/res/values/strings-uix.xml
index a0e37a1c47..babfa5c542 100644
--- a/java/res/values/strings-uix.xml
+++ b/java/res/values/strings-uix.xml
@@ -563,7 +563,7 @@
Configure spacebar and delete key gestures in long-press settings
Swipe to delete words
- Swipe left on the keyboard to delete the previous word.
+ Swipe left on the keyboard to delete the previous word. Keep holding after the swipe and move left or right to select more or fewer words before releasing.
Show unsupported settingsā¦
Use legacy swipe algorithm
diff --git a/java/src/org/futo/inputmethod/keyboard/PointerTracker.java b/java/src/org/futo/inputmethod/keyboard/PointerTracker.java
index 4d9868883e..19ee59520c 100644
--- a/java/src/org/futo/inputmethod/keyboard/PointerTracker.java
+++ b/java/src/org/futo/inputmethod/keyboard/PointerTracker.java
@@ -725,6 +725,8 @@ private boolean areTwoKeysCompatibleFollowingLayoutChange(final Key a, final Key
}
private void onDownEventInternal(final int x, final int y, final long eventTime) {
+ final int surfaceSwipeHoldTimeout =
+ Settings.getInstance().getCurrent().mKeyLongpressTimeout;
Key key = onDownKey(x, y, eventTime);
// Key selection by dragging finger is allowed when 1) key selection by dragging finger is
// enabled by configuration, 2) this pointer starts dragging from modifier key, or 3) this
@@ -776,14 +778,14 @@ private void onDownEventInternal(final int x, final int y, final long eventTime)
if (!mIsSlidingCursor && !mIsFlickingKey && !key.isModifier()) {
mIsSurfaceSwiping = true;
- mSurfaceSwipeDeleteDetector.start(x, sPointerBigStep);
+ mSurfaceSwipeDeleteDetector.start(x, sPointerBigStep, surfaceSwipeHoldTimeout);
}
} else {
mStartX = x;
mStartY = y;
mStartTime = System.currentTimeMillis();
mIsSurfaceSwiping = true;
- mSurfaceSwipeDeleteDetector.start(x, sPointerBigStep);
+ mSurfaceSwipeDeleteDetector.start(x, sPointerBigStep, surfaceSwipeHoldTimeout);
}
}
@@ -1055,7 +1057,8 @@ && getActivePointerTrackerCount() == 1
final boolean pastDeadTime =
mStartTime + swipeIgnoreTime < System.currentTimeMillis();
if (mSurfaceSwipeDeleteDetector.isActive() || pastDeadTime) {
- final int steps = mSurfaceSwipeDeleteDetector.onMove(x, settingsValues.mIsRTL);
+ final int steps =
+ mSurfaceSwipeDeleteDetector.onMove(x, settingsValues.mIsRTL, eventTime);
if (steps != 0) {
if (oldKey != null) {
sTimerProxy.cancelKeyTimersOf(this);
diff --git a/java/src/org/futo/inputmethod/keyboard/internal/SurfaceSwipeDeleteDetector.java b/java/src/org/futo/inputmethod/keyboard/internal/SurfaceSwipeDeleteDetector.java
index 264bafe525..fe61ea06c7 100644
--- a/java/src/org/futo/inputmethod/keyboard/internal/SurfaceSwipeDeleteDetector.java
+++ b/java/src/org/futo/inputmethod/keyboard/internal/SurfaceSwipeDeleteDetector.java
@@ -20,33 +20,67 @@ public final class SurfaceSwipeDeleteDetector {
private int mStep;
private int mStartX;
private boolean mActive;
+ private int mSelectedWords;
+ private long mHoldTimeout;
+ private long mActivationTime;
+ private boolean mAdjustmentEnabled;
- public void start(final int startX, final int step) {
+ public void start(final int startX, final int step, final long holdTimeout) {
mStartX = startX;
mStep = step > 0 ? step : 1;
+ mHoldTimeout = holdTimeout;
mActive = false;
+ mSelectedWords = 0;
+ mAdjustmentEnabled = false;
}
public boolean isActive() {
return mActive;
}
+ public int getSelectedWords() {
+ return mSelectedWords;
+ }
+
public void cancel() {
mActive = false;
+ mSelectedWords = 0;
+ mAdjustmentEnabled = false;
}
- public int onMove(final int x, final boolean isRTL) {
- final int steps = (x - mStartX) / mStep;
+ public int onMove(final int x, final boolean isRTL, final long eventTime) {
if (!mActive) {
+ final int steps = (x - mStartX) / mStep;
if (steps >= 0) {
return 0;
}
mActive = true;
+ mSelectedWords = 1;
+ mActivationTime = eventTime;
+ mAdjustmentEnabled = false;
+ mStartX = x;
+ return isRTL ? 1 : -1;
}
+ if (!mAdjustmentEnabled) {
+ if (eventTime - mActivationTime < mHoldTimeout) {
+ return 0;
+ }
+ mAdjustmentEnabled = true;
+ mStartX = x;
+ }
+ final int steps = (x - mStartX) / mStep;
if (steps == 0) {
return 0;
}
+ final int rawDelta = isRTL ? -steps : steps;
+ final int wordDelta = isRTL ? rawDelta : -rawDelta;
+ final int selectedWords = Math.max(0, mSelectedWords + wordDelta);
+ final int appliedDelta = selectedWords - mSelectedWords;
+ mSelectedWords = selectedWords;
mStartX += steps * mStep;
- return isRTL ? -steps : steps;
+ if (appliedDelta == 0) {
+ return 0;
+ }
+ return isRTL ? appliedDelta : -appliedDelta;
}
}
diff --git a/src/test/java/org/futo/inputmethod/keyboard/internal/SurfaceSwipeDeleteDetectorTest.java b/src/test/java/org/futo/inputmethod/keyboard/internal/SurfaceSwipeDeleteDetectorTest.java
index adb5d67e0c..32d25e271d 100644
--- a/src/test/java/org/futo/inputmethod/keyboard/internal/SurfaceSwipeDeleteDetectorTest.java
+++ b/src/test/java/org/futo/inputmethod/keyboard/internal/SurfaceSwipeDeleteDetectorTest.java
@@ -26,118 +26,179 @@
public class SurfaceSwipeDeleteDetectorTest {
private static final int STEP = 100;
private static final int START_X = 1000;
+ private static final long HOLD_TIMEOUT = 500;
private SurfaceSwipeDeleteDetector mDetector;
@Before
public void setUp() {
mDetector = new SurfaceSwipeDeleteDetector();
- mDetector.start(START_X, STEP);
+ mDetector.start(START_X, STEP, HOLD_TIMEOUT);
}
@Test
public void rightwardSwipeNeverInitiates() {
- assertEquals(0, mDetector.onMove(START_X + 350, false));
+ assertEquals(0, mDetector.onMove(START_X + 350, false, 0));
assertFalse(mDetector.isActive());
- assertEquals(0, mDetector.onMove(START_X + 50, false));
+ assertEquals(0, mDetector.onMove(START_X + 50, false, 0));
assertFalse(mDetector.isActive());
}
@Test
public void movementBelowStepDoesNothing() {
- assertEquals(0, mDetector.onMove(START_X - (STEP - 1), false));
+ assertEquals(0, mDetector.onMove(START_X - (STEP - 1), false, 0));
assertFalse(mDetector.isActive());
}
@Test
public void singleLeftStepDeletesOneWord() {
- assertEquals(-1, mDetector.onMove(START_X - STEP, false));
+ assertEquals(-1, mDetector.onMove(START_X - STEP, false, 0));
assertTrue(mDetector.isActive());
}
@Test
- public void multipleLeftStepsDeleteMultipleWords() {
- assertEquals(-3, mDetector.onMove(START_X - 3 * STEP, false));
+ public void longLeftSwipeStillSelectsExactlyOneWord() {
+ assertEquals(-1, mDetector.onMove(START_X - 3 * STEP, false, 0));
assertTrue(mDetector.isActive());
+ assertEquals(1, mDetector.getSelectedWords());
+ }
+
+ @Test
+ public void immediateReleaseAfterLongSwipeKeepsOneWord() {
+ assertEquals(-1, mDetector.onMove(START_X - 5 * STEP, false, 0));
+ assertEquals(0, mDetector.onMove(START_X - 8 * STEP, false, HOLD_TIMEOUT - 1));
+ assertEquals(1, mDetector.getSelectedWords());
+ }
+
+ @Test
+ public void noAdjustmentBeforeHold() {
+ assertEquals(-1, mDetector.onMove(START_X - STEP, false, 0));
+ assertEquals(0, mDetector.onMove(START_X - 4 * STEP, false, HOLD_TIMEOUT - 1));
+ assertEquals(0, mDetector.onMove(START_X + 2 * STEP, false, HOLD_TIMEOUT - 1));
+ assertEquals(1, mDetector.getSelectedWords());
+ }
+
+ @Test
+ public void adjustmentOnlyAfterHoldWithRebaseline() {
+ assertEquals(-1, mDetector.onMove(START_X - 3 * STEP, false, 0));
+ final int driftX = START_X - 6 * STEP;
+ assertEquals(0, mDetector.onMove(driftX, false, HOLD_TIMEOUT - 1));
+ assertEquals(1, mDetector.getSelectedWords());
+ assertEquals(0, mDetector.onMove(driftX, false, HOLD_TIMEOUT));
+ assertEquals(1, mDetector.getSelectedWords());
+ assertEquals(-1, mDetector.onMove(driftX - STEP, false, HOLD_TIMEOUT));
+ assertEquals(2, mDetector.getSelectedWords());
+ assertEquals(-1, mDetector.onMove(driftX - 2 * STEP, false, HOLD_TIMEOUT));
+ assertEquals(3, mDetector.getSelectedWords());
+ assertEquals(1, mDetector.onMove(driftX - STEP, false, HOLD_TIMEOUT));
+ assertEquals(2, mDetector.getSelectedWords());
+ }
+
+ @Test
+ public void adjustmentClampsAtZeroWords() {
+ assertEquals(-1, mDetector.onMove(START_X - STEP, false, 0));
+ final int origin = START_X - STEP;
+ assertEquals(0, mDetector.onMove(origin, false, HOLD_TIMEOUT));
+ assertEquals(-1, mDetector.onMove(origin - STEP, false, HOLD_TIMEOUT));
+ assertEquals(2, mDetector.getSelectedWords());
+ assertEquals(2, mDetector.onMove(origin + 5 * STEP, false, HOLD_TIMEOUT));
+ assertEquals(0, mDetector.getSelectedWords());
+ assertEquals(0, mDetector.onMove(origin + 8 * STEP, false, HOLD_TIMEOUT));
+ assertTrue(mDetector.isActive());
+ }
+
+ @Test
+ public void rtlAdjustsInMirrorDirection() {
+ assertEquals(1, mDetector.onMove(START_X - STEP, true, 0));
+ assertEquals(1, mDetector.getSelectedWords());
+ final int origin = START_X - STEP;
+ assertEquals(0, mDetector.onMove(origin, true, HOLD_TIMEOUT));
+ assertEquals(1, mDetector.onMove(origin - STEP, true, HOLD_TIMEOUT));
+ assertEquals(2, mDetector.getSelectedWords());
+ assertEquals(-2, mDetector.onMove(origin + 5 * STEP, true, HOLD_TIMEOUT));
+ assertEquals(0, mDetector.getSelectedWords());
}
@Test
public void stepsAreIncremental() {
- assertEquals(-1, mDetector.onMove(START_X - STEP, false));
- assertEquals(-1, mDetector.onMove(START_X - 2 * STEP, false));
- assertEquals(0, mDetector.onMove(START_X - 2 * STEP - (STEP - 1), false));
- assertEquals(-1, mDetector.onMove(START_X - 3 * STEP, false));
+ assertEquals(-1, mDetector.onMove(START_X - STEP, false, 0));
+ assertEquals(0, mDetector.onMove(START_X - STEP, false, HOLD_TIMEOUT));
+ assertEquals(-1, mDetector.onMove(START_X - 2 * STEP, false, HOLD_TIMEOUT));
+ assertEquals(0, mDetector.onMove(START_X - 2 * STEP - (STEP - 1), false, HOLD_TIMEOUT));
+ assertEquals(-1, mDetector.onMove(START_X - 3 * STEP, false, HOLD_TIMEOUT));
}
@Test
public void rtlFlipsDirection() {
- assertEquals(1, mDetector.onMove(START_X - STEP, true));
+ assertEquals(1, mDetector.onMove(START_X - STEP, true, 0));
assertTrue(mDetector.isActive());
}
@Test
public void movingBackRightReleasesSelection() {
- assertEquals(-2, mDetector.onMove(START_X - 2 * STEP, false));
- assertEquals(1, mDetector.onMove(START_X - STEP, false));
+ assertEquals(-1, mDetector.onMove(START_X - 2 * STEP, false, 0));
+ assertEquals(0, mDetector.onMove(START_X - 2 * STEP, false, HOLD_TIMEOUT));
+ assertEquals(1, mDetector.onMove(START_X - STEP, false, HOLD_TIMEOUT));
assertTrue(mDetector.isActive());
+ assertEquals(0, mDetector.getSelectedWords());
}
@Test
public void cancelStopsGesture() {
- mDetector.onMove(START_X - STEP, false);
+ mDetector.onMove(START_X - STEP, false, 0);
assertTrue(mDetector.isActive());
mDetector.cancel();
assertFalse(mDetector.isActive());
- assertEquals(0, mDetector.onMove(START_X + STEP, false));
+ assertEquals(0, mDetector.onMove(START_X + STEP, false, 0));
}
@Test
public void startResetsState() {
- mDetector.onMove(START_X - STEP, false);
+ mDetector.onMove(START_X - STEP, false, 0);
assertTrue(mDetector.isActive());
- mDetector.start(500, STEP);
+ mDetector.start(500, STEP, HOLD_TIMEOUT);
assertFalse(mDetector.isActive());
- assertEquals(-1, mDetector.onMove(500 - STEP, false));
+ assertEquals(-1, mDetector.onMove(500 - STEP, false, 0));
}
@Test
public void nonPositiveStepIsGuarded() {
- mDetector.start(START_X, 0);
- assertEquals(-1, mDetector.onMove(START_X - 1, false));
+ mDetector.start(START_X, 0, HOLD_TIMEOUT);
+ assertEquals(-1, mDetector.onMove(START_X - 1, false, 0));
assertTrue(mDetector.isActive());
}
@Test
public void keyPressJitterDoesNotActivate() {
for (int dx = -5; dx <= 5; dx++) {
- assertEquals(0, mDetector.onMove(START_X + dx, false));
+ assertEquals(0, mDetector.onMove(START_X + dx, false, 0));
}
assertFalse(mDetector.isActive());
}
@Test
public void rightwardDriftFromKeyDoesNotActivate() {
- assertEquals(0, mDetector.onMove(START_X + 200, false));
- assertEquals(0, mDetector.onMove(START_X + 100, false));
+ assertEquals(0, mDetector.onMove(START_X + 200, false, 0));
+ assertEquals(0, mDetector.onMove(START_X + 100, false, 0));
assertFalse(mDetector.isActive());
}
@Test
public void leftSwipeFromKeyActivatesAfterFullStep() {
- assertEquals(0, mDetector.onMove(START_X - (STEP - 1), false));
+ assertEquals(0, mDetector.onMove(START_X - (STEP - 1), false, 0));
assertFalse(mDetector.isActive());
- assertEquals(-1, mDetector.onMove(START_X - STEP, false));
+ assertEquals(-1, mDetector.onMove(START_X - STEP, false, 0));
assertTrue(mDetector.isActive());
}
@Test
public void restartFromNewKeyResetsOrigin() {
- assertEquals(-1, mDetector.onMove(START_X - STEP, false));
+ assertEquals(-1, mDetector.onMove(START_X - STEP, false, 0));
assertTrue(mDetector.isActive());
- mDetector.start(500, STEP);
+ mDetector.start(500, STEP, HOLD_TIMEOUT);
assertFalse(mDetector.isActive());
- assertEquals(0, mDetector.onMove(500 - (STEP - 1), false));
- assertEquals(-1, mDetector.onMove(500 - STEP, false));
+ assertEquals(0, mDetector.onMove(500 - (STEP - 1), false, 0));
+ assertEquals(-1, mDetector.onMove(500 - STEP, false, 0));
assertTrue(mDetector.isActive());
}
}