diff --git a/java/res/values/strings-uix.xml b/java/res/values/strings-uix.xml
index d69782d46d..babfa5c542 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. 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
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..19ee59520c 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;
@@ -720,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
@@ -730,6 +737,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 +775,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, surfaceSwipeHoldTimeout);
+ }
+ } else {
+ mStartX = x;
+ mStartY = y;
+ mStartTime = System.currentTimeMillis();
+ mIsSurfaceSwiping = true;
+ mSurfaceSwipeDeleteDetector.start(x, sPointerBigStep, surfaceSwipeHoldTimeout);
}
}
@@ -1029,6 +1049,34 @@ 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, eventTime);
+ 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 +1190,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 +1348,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..fe61ea06c7
--- /dev/null
+++ b/java/src/org/futo/inputmethod/keyboard/internal/SurfaceSwipeDeleteDetector.java
@@ -0,0 +1,86 @@
+/*
+ * 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;
+ private int mSelectedWords;
+ private long mHoldTimeout;
+ private long mActivationTime;
+ private boolean mAdjustmentEnabled;
+
+ 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 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;
+ if (appliedDelta == 0) {
+ return 0;
+ }
+ return isRTL ? appliedDelta : -appliedDelta;
+ }
+}
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..32d25e271d
--- /dev/null
+++ b/src/test/java/org/futo/inputmethod/keyboard/internal/SurfaceSwipeDeleteDetectorTest.java
@@ -0,0 +1,204 @@
+/*
+ * 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 static final long HOLD_TIMEOUT = 500;
+
+ private SurfaceSwipeDeleteDetector mDetector;
+
+ @Before
+ public void setUp() {
+ mDetector = new SurfaceSwipeDeleteDetector();
+ mDetector.start(START_X, STEP, HOLD_TIMEOUT);
+ }
+
+ @Test
+ public void rightwardSwipeNeverInitiates() {
+ assertEquals(0, mDetector.onMove(START_X + 350, false, 0));
+ assertFalse(mDetector.isActive());
+ 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, 0));
+ assertFalse(mDetector.isActive());
+ }
+
+ @Test
+ public void singleLeftStepDeletesOneWord() {
+ assertEquals(-1, mDetector.onMove(START_X - STEP, false, 0));
+ assertTrue(mDetector.isActive());
+ }
+
+ @Test
+ 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, 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, 0));
+ assertTrue(mDetector.isActive());
+ }
+
+ @Test
+ public void movingBackRightReleasesSelection() {
+ 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, 0);
+ assertTrue(mDetector.isActive());
+ mDetector.cancel();
+ assertFalse(mDetector.isActive());
+ assertEquals(0, mDetector.onMove(START_X + STEP, false, 0));
+ }
+
+ @Test
+ public void startResetsState() {
+ mDetector.onMove(START_X - STEP, false, 0);
+ assertTrue(mDetector.isActive());
+ mDetector.start(500, STEP, HOLD_TIMEOUT);
+ assertFalse(mDetector.isActive());
+ assertEquals(-1, mDetector.onMove(500 - STEP, false, 0));
+ }
+
+ @Test
+ public void nonPositiveStepIsGuarded() {
+ 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, 0));
+ }
+ assertFalse(mDetector.isActive());
+ }
+
+ @Test
+ public void rightwardDriftFromKeyDoesNotActivate() {
+ 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, 0));
+ assertFalse(mDetector.isActive());
+ assertEquals(-1, mDetector.onMove(START_X - STEP, false, 0));
+ assertTrue(mDetector.isActive());
+ }
+
+ @Test
+ public void restartFromNewKeyResetsOrigin() {
+ assertEquals(-1, mDetector.onMove(START_X - STEP, false, 0));
+ assertTrue(mDetector.isActive());
+ mDetector.start(500, STEP, HOLD_TIMEOUT);
+ assertFalse(mDetector.isActive());
+ assertEquals(0, mDetector.onMove(500 - (STEP - 1), false, 0));
+ assertEquals(-1, mDetector.onMove(500 - STEP, false, 0));
+ assertTrue(mDetector.isActive());
+ }
+}