From 28d536ec601e3ad0723ee0e4630dd382f078eb8c Mon Sep 17 00:00:00 2001 From: Asaf Mahlev Date: Sun, 7 Jun 2026 11:07:27 +0300 Subject: [PATCH 1/4] feat(symbols): swipe-up on a key emits its first popup symbol (#32, MVP) Quick swipe-up on a non-top-row key emits that key's primary popup-key (the symbol you'd get from long-press) without the long-press delay. Reuses the shortcut-row vertical-swipe gate's threshold (>=~10dp up, |dY|>|dX|), guarded to non-top rows (mShortcutTopRowSwipeAllowed) so it doesn't collide with the top-row shortcut up-swipe; bottom-row down-swipe shortcut unchanged. Sets mIsDetectingGesture=false and cancelTrackingForAction so glide is unaffected and the base letter isn't typed. Emits via the standard listener (onCodeInput / onTextInput for multi-codepoint or outputText popups). No-op if the key has no popup. --- .../keyboard/keyboard/PointerTracker.java | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java b/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java index 427b88e6b..605817959 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java +++ b/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java @@ -29,6 +29,7 @@ import helium314.keyboard.keyboard.internal.PointerTrackerQueue; import helium314.keyboard.keyboard.internal.TimerProxy; import helium314.keyboard.keyboard.internal.TypingTimeRecorder; +import helium314.keyboard.keyboard.internal.PopupKeySpec; import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode; import helium314.keyboard.latin.R; import helium314.keyboard.latin.common.Constants; @@ -1100,6 +1101,9 @@ private void onMoveEvent(final int x, final int y, final long eventTime, final M if (tryStartShortcutRowSwipe(x, y, eventTime)) { return; } + if (tryStartSwipeUpSymbol(x, y)) { + return; + } if (sGestureEnabler.shouldHandleGesture() && me != null) { // Add historical points to gesture path. @@ -1400,6 +1404,58 @@ private boolean tryStartShortcutRowSwipe(final int x, final int y, final long ev return true; } + /** + * Swipe-up on a non-top-row key: emit the key's first popup symbol (same output as tapping + * that popup key after long-press), then cancel tracking so the base letter is not typed. + * Top-row keys are excluded because their up-swipe opens the shortcut-row popup, which is + * handled by {@link #tryStartShortcutRowSwipe}. Glide typing is unaffected because we set + * {@code mIsDetectingGesture = false} (same as the shortcut-row path) and return before the + * gesture accumulation code runs. + */ + private boolean tryStartSwipeUpSymbol(final int x, final int y) { + // Skip if the top-row shortcut swipe is allowed for this key (handled elsewhere), + // or if any conflicting state is active. + if (mShortcutTopRowSwipeAllowed || mInShortcutRowSwipe || sInShortcutRowSwipe + || isShowingPopupKeysPanel() || sInGesture || sInKeySwipe + || mCurrentKey == null) { + return false; + } + // Must be a clear upward swipe: sufficient vertical travel, more vertical than horizontal. + final int dX = x - mStartX; + final int dY = y - mStartY; + if (dY > -sPointerStep || abs(dY) <= abs(dX)) { + return false; + } + // Key must have at least one popup key spec. + final PopupKeySpec[] popupKeys = mCurrentKey.getPopupKeys(); + if (popupKeys == null || popupKeys.length == 0 || popupKeys[0] == null) { + return false; + } + final PopupKeySpec spec = popupKeys[0]; + sTimerProxy.cancelKeyTimersOf(this); + mIsDetectingGesture = false; + setReleasedKeyGraphics(mCurrentKey, true); + // Emit the popup symbol, mirroring the callListenerOnCodeInput logic but without + // proximity correction (symbols do not need it). + if (spec.mCode == KeyCode.MULTIPLE_CODE_POINTS) { + // Multi-codepoint output (e.g. uppercase Eszett "SS"): emit as text. + sListener.onTextInput(spec.mOutputText); + } else if (spec.mCode != KeyCode.NOT_SPECIFIED) { + sListener.onCodeInput(spec.mCode, Constants.NOT_A_COORDINATE, + Constants.NOT_A_COORDINATE, false); + } else if (spec.mOutputText != null) { + // Fallback: spec has no code but has output text. + sListener.onTextInput(spec.mOutputText); + } else { + // No usable output — treat as no-op; let normal handling continue. + return false; + } + // Prevent the base letter from being typed on finger-up. + cancelTrackingForAction(); + return true; + } + + private void onMoveEventInternal(final int x, final int y, final long eventTime) { final Key oldKey = mCurrentKey; From f7e18aabd8bdeb65217e40162569d7cb918b5bde Mon Sep 17 00:00:00 2001 From: Asaf Mahlev Date: Sun, 7 Jun 2026 11:37:58 +0300 Subject: [PATCH 2/4] rework(symbols): decide swipe-up-symbol on release, never preempt glide (#32) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first version decided 'symbol' on the first 10dp of upward motion in onMoveEvent and killed gesture detection there, so it preempted every upward glide. Reworked: - Decision moved to finger-UP (onUpEventInternal), placed AFTER the sInGesture path returns — so a real glide is handled by the gesture path and never reaches the symbol check. Gesture detection is never touched during the move. - Fires only for a deliberate up-flick: >= 2 steps up, more vertical than horizontal, drift <= one key width, on a key that has a popup. Emits the start key's first popup symbol instead of the base letter. - Gated behind new opt-in pref PREF_SWIPE_UP_SYMBOL (default OFF), in Two-Thumb Typing settings, so glide users are unaffected unless they enable it. - Captures the down key + position (mSwipeUpStart*) since mStartX/Y get adjusted during slider moves. Removed the broken onMove path. --- .../keyboard/keyboard/PointerTracker.java | 104 +++++++++--------- .../keyboard/latin/settings/Defaults.kt | 1 + .../keyboard/latin/settings/Settings.java | 1 + .../latin/settings/SettingsValues.java | 3 + .../settings/screens/TwoThumbTypingScreen.kt | 5 + app/src/main/res/values/strings.xml | 2 + 6 files changed, 61 insertions(+), 55 deletions(-) diff --git a/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java b/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java index 605817959..7fdff5104 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java +++ b/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java @@ -216,6 +216,11 @@ public static int consumeGestureSeedCodepoint() { private boolean mShortcutBottomRowSwipeAllowed = false; private boolean mInShortcutRowSwipe = false; private static boolean sInShortcutRowSwipe = false; + // Swipe-up-to-symbol (#32): captured at finger-down so the up-flick decision (made on release, + // only when no glide engaged) can read the original key + start position. + private Key mSwipeUpStartKey = null; + private int mSwipeUpStartX; + private int mSwipeUpStartY; // Touchpad mode for cursor control public static boolean sPersistentTouchpadModeActive = false; @@ -991,6 +996,7 @@ private void onDownEventInternal(final int x, final int y, final long eventTime) mShortcutTopRowSwipeAllowed = isShortcutRowSource(key, true); mShortcutBottomRowSwipeAllowed = isShortcutRowSource(key, false); mInShortcutRowSwipe = false; + mSwipeUpStartKey = null; mKeyboardLayoutHasBeenChanged = false; mIsTrackingForActionDisabled = false; resetKeySelectionByDraggingFinger(); @@ -1014,6 +1020,9 @@ private void onDownEventInternal(final int x, final int y, final long eventTime) mStartX = x; mStartY = y; mStartTime = System.currentTimeMillis(); + mSwipeUpStartKey = key; + mSwipeUpStartX = x; + mSwipeUpStartY = y; } } @@ -1101,9 +1110,6 @@ private void onMoveEvent(final int x, final int y, final long eventTime, final M if (tryStartShortcutRowSwipe(x, y, eventTime)) { return; } - if (tryStartSwipeUpSymbol(x, y)) { - return; - } if (sGestureEnabler.shouldHandleGesture() && me != null) { // Add historical points to gesture path. @@ -1404,58 +1410,6 @@ private boolean tryStartShortcutRowSwipe(final int x, final int y, final long ev return true; } - /** - * Swipe-up on a non-top-row key: emit the key's first popup symbol (same output as tapping - * that popup key after long-press), then cancel tracking so the base letter is not typed. - * Top-row keys are excluded because their up-swipe opens the shortcut-row popup, which is - * handled by {@link #tryStartShortcutRowSwipe}. Glide typing is unaffected because we set - * {@code mIsDetectingGesture = false} (same as the shortcut-row path) and return before the - * gesture accumulation code runs. - */ - private boolean tryStartSwipeUpSymbol(final int x, final int y) { - // Skip if the top-row shortcut swipe is allowed for this key (handled elsewhere), - // or if any conflicting state is active. - if (mShortcutTopRowSwipeAllowed || mInShortcutRowSwipe || sInShortcutRowSwipe - || isShowingPopupKeysPanel() || sInGesture || sInKeySwipe - || mCurrentKey == null) { - return false; - } - // Must be a clear upward swipe: sufficient vertical travel, more vertical than horizontal. - final int dX = x - mStartX; - final int dY = y - mStartY; - if (dY > -sPointerStep || abs(dY) <= abs(dX)) { - return false; - } - // Key must have at least one popup key spec. - final PopupKeySpec[] popupKeys = mCurrentKey.getPopupKeys(); - if (popupKeys == null || popupKeys.length == 0 || popupKeys[0] == null) { - return false; - } - final PopupKeySpec spec = popupKeys[0]; - sTimerProxy.cancelKeyTimersOf(this); - mIsDetectingGesture = false; - setReleasedKeyGraphics(mCurrentKey, true); - // Emit the popup symbol, mirroring the callListenerOnCodeInput logic but without - // proximity correction (symbols do not need it). - if (spec.mCode == KeyCode.MULTIPLE_CODE_POINTS) { - // Multi-codepoint output (e.g. uppercase Eszett "SS"): emit as text. - sListener.onTextInput(spec.mOutputText); - } else if (spec.mCode != KeyCode.NOT_SPECIFIED) { - sListener.onCodeInput(spec.mCode, Constants.NOT_A_COORDINATE, - Constants.NOT_A_COORDINATE, false); - } else if (spec.mOutputText != null) { - // Fallback: spec has no code but has output text. - sListener.onTextInput(spec.mOutputText); - } else { - // No usable output — treat as no-op; let normal handling continue. - return false; - } - // Prevent the base letter from being typed on finger-up. - cancelTrackingForAction(); - return true; - } - - private void onMoveEventInternal(final int x, final int y, final long eventTime) { final Key oldKey = mCurrentKey; @@ -1634,6 +1588,11 @@ eventTime, getActivePointerTrackerCount(), graceMs, this, && (currentKey.getCode() == currentRepeatingKeyCode) && !isInDraggingFinger) { return; } + // Swipe-up-to-symbol (#32): we only get here if no glide engaged (the sInGesture path + // returns above), so this never preempts gliding. Decide on release. + if (tryEmitSwipeUpSymbol(x, y)) { + return; + } detectAndSendKey(currentKey, mKeyX, mKeyY, eventTime); // Combining-mode seeding: remember the last letter tap so a follow-up gesture can // seed its first pointer event with this position and time. Only letter taps qualify @@ -1654,6 +1613,41 @@ eventTime, getActivePointerTrackerCount(), graceMs, this, } } + /** + * Swipe-up-to-symbol (#32, opt-in): on finger release, if the stroke was a clear, mostly-vertical + * UP flick that stayed over the key it started on, emit that key's first popup symbol instead of + * the base letter. Called only from the single-key release path (after the gesture path has + * returned), so a real glide never reaches here and is never preempted. Returns true if a symbol + * was emitted (caller then skips the base-letter commit). + */ + private boolean tryEmitSwipeUpSymbol(final int upX, final int upY) { + if (!Settings.getValues().mSwipeUpSymbol) return false; + // Top-row up-swipe opens the shortcut-row popup; leave that to tryStartShortcutRowSwipe. + if (mShortcutTopRowSwipeAllowed) return false; + final Key startKey = mSwipeUpStartKey; + if (startKey == null) return false; + final PopupKeySpec[] popupKeys = startKey.getPopupKeys(); + if (popupKeys == null || popupKeys.length == 0 || popupKeys[0] == null) return false; + // Require a deliberate upward flick (>= 2 steps up), more vertical than horizontal, that did + // not drift more than one key width sideways (i.e. stayed over the start key's column). + final int dX = upX - mSwipeUpStartX; + final int dY = upY - mSwipeUpStartY; + if (dY > -2 * sPointerStep || abs(dY) <= abs(dX) || abs(dX) > startKey.getWidth()) { + return false; + } + final PopupKeySpec spec = popupKeys[0]; + if (spec.mCode == KeyCode.MULTIPLE_CODE_POINTS) { + sListener.onTextInput(spec.mOutputText); + } else if (spec.mCode != KeyCode.NOT_SPECIFIED) { + sListener.onCodeInput(spec.mCode, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false); + } else if (spec.mOutputText != null) { + sListener.onTextInput(spec.mOutputText); + } else { + return false; + } + return true; + } + @Override public void cancelTrackingForAction() { if (isShowingPopupKeysPanel()) { diff --git a/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt b/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt index ea0459f9a..34bf1b2eb 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt +++ b/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt @@ -195,6 +195,7 @@ object Defaults { const val PREF_ADD_TO_PERSONAL_DICTIONARY = true const val PREF_FLAG_UNKNOWN_WORDS = true const val PREF_GRADUATED_TRUST = true + const val PREF_SWIPE_UP_SYMBOL = false @JvmField val PREF_NAVBAR_COLOR = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q const val PREF_NARROW_KEY_GAPS = true diff --git a/app/src/main/java/helium314/keyboard/latin/settings/Settings.java b/app/src/main/java/helium314/keyboard/latin/settings/Settings.java index 57a785c9b..9fe4c8b1f 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/Settings.java +++ b/app/src/main/java/helium314/keyboard/latin/settings/Settings.java @@ -229,6 +229,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang public static final String PREF_ADD_TO_PERSONAL_DICTIONARY = "add_to_personal_dictionary"; public static final String PREF_FLAG_UNKNOWN_WORDS = "flag_unknown_words"; public static final String PREF_GRADUATED_TRUST = "graduated_trust"; + public static final String PREF_SWIPE_UP_SYMBOL = "swipe_up_symbol"; public static final String PREF_NAVBAR_COLOR = "navbar_color"; public static final String PREF_NARROW_KEY_GAPS = "narrow_key_gaps"; public static final String PREF_NARROW_KEY_GAPS_LEVEL = "narrow_key_gaps_level"; diff --git a/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java b/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java index cdb694c7f..7ca341c8e 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java +++ b/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java @@ -156,6 +156,7 @@ public class SettingsValues { public final boolean mAddToPersonalDictionary; public final boolean mFlagUnknownWords; public final boolean mGraduatedTrust; + public final boolean mSwipeUpSymbol; public final boolean mUseContactsDictionary; public final boolean mUseAppsDictionary; public final boolean mCustomNavBarColor; @@ -477,6 +478,8 @@ public SettingsValues(final Context context, final SharedPreferences prefs, fina Defaults.PREF_FLAG_UNKNOWN_WORDS); mGraduatedTrust = prefs.getBoolean(Settings.PREF_GRADUATED_TRUST, Defaults.PREF_GRADUATED_TRUST); + mSwipeUpSymbol = prefs.getBoolean(Settings.PREF_SWIPE_UP_SYMBOL, + Defaults.PREF_SWIPE_UP_SYMBOL); mUseContactsDictionary = SettingsValues.readUseContactsEnabled(prefs, context); mUseAppsDictionary = prefs.getBoolean(Settings.PREF_USE_APPS, Defaults.PREF_USE_APPS); mCustomNavBarColor = prefs.getBoolean(Settings.PREF_NAVBAR_COLOR, Defaults.PREF_NAVBAR_COLOR); diff --git a/app/src/main/java/helium314/keyboard/settings/screens/TwoThumbTypingScreen.kt b/app/src/main/java/helium314/keyboard/settings/screens/TwoThumbTypingScreen.kt index 160a0c62e..a86e5c33d 100644 --- a/app/src/main/java/helium314/keyboard/settings/screens/TwoThumbTypingScreen.kt +++ b/app/src/main/java/helium314/keyboard/settings/screens/TwoThumbTypingScreen.kt @@ -84,6 +84,7 @@ fun TwoThumbTypingScreen( if (dualThumbHinting) { add(Settings.PREF_GESTURE_DUAL_THUMB_MIDLINE_PCT) } + add(Settings.PREF_SWIPE_UP_SYMBOL) add(R.string.settings_category_two_thumb_typing_troubleshooting) add(Settings.PREF_GESTURE_DEBUG_DRAW_POINTS) @@ -206,6 +207,10 @@ fun createTwoThumbTypingSettings(context: Context) = listOf( R.string.gesture_debug_accumulate_fragments, R.string.gesture_debug_accumulate_fragments_summary) { SwitchPreference(it, Defaults.PREF_GESTURE_DEBUG_ACCUMULATE_FRAGMENTS) }, + Setting(context, Settings.PREF_SWIPE_UP_SYMBOL, + R.string.swipe_up_symbol, R.string.swipe_up_symbol_summary) { + SwitchPreference(it, Defaults.PREF_SWIPE_UP_SYMBOL) + }, ) private const val SPACING_MODE_NORMAL = "normal" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index af45c4520..34dc59d21 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -326,6 +326,8 @@ Delete whole word Improve two-thumb recognition (experimental) Adds synthetic hints for the recognizer when both thumbs are used. It may help two-thumb gestures, but can hurt accuracy if the hand split is wrong. + Swipe up for symbols (experimental) + A quick flick up on a key inserts its popup symbol (no long-press). Decided on release and only when no glide happened, so swipe typing is unaffected. Keep insight across word parts Keep the trail visible across the parts of one word, clearing when the next word starts. Turn off to clear at each new swipe. From c4111c035a11c0b22270048bb84e6cb0f6b9d520 Mon Sep 17 00:00:00 2001 From: Asaf Mahlev Date: Sun, 7 Jun 2026 11:46:54 +0300 Subject: [PATCH 3/4] fix(symbols): swipe-up flick now overrides the gesture (decide before commit) (#32) Previous rework placed the check AFTER the sInGesture path, so an up-flick that engaged the gesture recognizer (most flicks do) was treated as a glide and the symbol never fired. Move the check BEFORE the gesture commit: a clear vertical up-flick (>=2 steps up, |dY|>|dX|, drift <= one key width) cancels any in-progress batch (cancelBatchInput) and emits the symbol. Horizontal glides aren't flicks -> fall through untouched -> glide still works. --- .../keyboard/keyboard/PointerTracker.java | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java b/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java index 7fdff5104..58e987e1f 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java +++ b/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java @@ -1542,6 +1542,12 @@ private void onUpEventInternal(final int x, final int y, final long eventTime) { } } + // Swipe-up-to-symbol (#32): decide on release BEFORE the gesture commit. A clear vertical + // up-flick emits the start key's popup symbol (cancelling any in-progress gesture); a + // horizontal glide is not a flick, falls through (returns false), and types normally. + if (tryEmitSwipeUpSymbol(x, y)) { + return; + } if (sInGesture) { if (currentKey != null) { callListenerOnRelease(currentKey, currentKey.getCode(), true); @@ -1588,11 +1594,6 @@ eventTime, getActivePointerTrackerCount(), graceMs, this, && (currentKey.getCode() == currentRepeatingKeyCode) && !isInDraggingFinger) { return; } - // Swipe-up-to-symbol (#32): we only get here if no glide engaged (the sInGesture path - // returns above), so this never preempts gliding. Decide on release. - if (tryEmitSwipeUpSymbol(x, y)) { - return; - } detectAndSendKey(currentKey, mKeyX, mKeyY, eventTime); // Combining-mode seeding: remember the last letter tap so a follow-up gesture can // seed its first pointer event with this position and time. Only letter taps qualify @@ -1622,6 +1623,7 @@ eventTime, getActivePointerTrackerCount(), graceMs, this, */ private boolean tryEmitSwipeUpSymbol(final int upX, final int upY) { if (!Settings.getValues().mSwipeUpSymbol) return false; + if (mIsTrackingForActionDisabled) return false; // Top-row up-swipe opens the shortcut-row popup; leave that to tryStartShortcutRowSwipe. if (mShortcutTopRowSwipeAllowed) return false; final Key startKey = mSwipeUpStartKey; @@ -1635,6 +1637,11 @@ private boolean tryEmitSwipeUpSymbol(final int upX, final int upY) { if (dY > -2 * sPointerStep || abs(dY) <= abs(dX) || abs(dX) > startKey.getWidth()) { return false; } + // A clear vertical flick wins over the gesture recognizer: cancel any in-progress batch so + // the (garbage) gesture isn't committed, then emit the symbol. + if (sInGesture) { + cancelBatchInput(); + } final PopupKeySpec spec = popupKeys[0]; if (spec.mCode == KeyCode.MULTIPLE_CODE_POINTS) { sListener.onTextInput(spec.mOutputText); From 16c8aa5c73d40567ccaaa89d64c6465a4d821921 Mon Sep 17 00:00:00 2001 From: Asaf Mahlev Date: Sun, 7 Jun 2026 12:40:00 +0300 Subject: [PATCH 4/4] rework(symbols): swipe-up + HOLD opens the key's symbol popup (#32) The 'buy' case proved net-displacement can't tell an up-flick from an upward glide (b->u->y is net-up). The only honest discriminator on the middle rows is a HOLD: a glide is continuous, a deliberate symbol-access is up-then-pause. - On an upward flick from the start key, arm a hold timer; every subsequent move restarts it, so it only fires once the finger STILLS (a continuous glide never lets it fire). Does NOT consume the move event, so gesture detection runs in parallel and gliding (incl. 'buy') is unaffected. - On fire (via onLongPressed), open the START key's popup-keys panel like long-press (drag to pick), cancelling any in-progress gesture. - Replaces the on-release emit. Still opt-in (PREF_SWIPE_UP_SYMBOL, default off). --- .../keyboard/keyboard/PointerTracker.java | 101 ++++++++++-------- app/src/main/res/values/strings.xml | 4 +- 2 files changed, 61 insertions(+), 44 deletions(-) diff --git a/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java b/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java index 58e987e1f..24769c26d 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java +++ b/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java @@ -216,11 +216,14 @@ public static int consumeGestureSeedCodepoint() { private boolean mShortcutBottomRowSwipeAllowed = false; private boolean mInShortcutRowSwipe = false; private static boolean sInShortcutRowSwipe = false; - // Swipe-up-to-symbol (#32): captured at finger-down so the up-flick decision (made on release, - // only when no glide engaged) can read the original key + start position. + // Swipe-up + hold to open a key's symbol popup (#32). mSwipeUpStart* captured at finger-down so a + // later up-flick + hold can target the original key. mSwipeUpHoldArmed: an up-flick is in progress + // and the hold timer is running (it fires once the finger stills; a continuous glide never lets it). private Key mSwipeUpStartKey = null; private int mSwipeUpStartX; private int mSwipeUpStartY; + private boolean mSwipeUpHoldArmed = false; + private static final int SWIPE_UP_HOLD_MS = 120; // Touchpad mode for cursor control public static boolean sPersistentTouchpadModeActive = false; @@ -997,6 +1000,7 @@ private void onDownEventInternal(final int x, final int y, final long eventTime) mShortcutBottomRowSwipeAllowed = isShortcutRowSource(key, false); mInShortcutRowSwipe = false; mSwipeUpStartKey = null; + mSwipeUpHoldArmed = false; mKeyboardLayoutHasBeenChanged = false; mIsTrackingForActionDisabled = false; resetKeySelectionByDraggingFinger(); @@ -1110,6 +1114,9 @@ private void onMoveEvent(final int x, final int y, final long eventTime, final M if (tryStartShortcutRowSwipe(x, y, eventTime)) { return; } + // Swipe-up + hold (#32): arm/refresh the hold timer. Does not consume the event, so gesture + // detection continues — a real glide just keeps the timer from ever firing. + maybeArmOrRefreshSwipeUpHold(x, y); if (sGestureEnabler.shouldHandleGesture() && me != null) { // Add historical points to gesture path. @@ -1542,12 +1549,6 @@ private void onUpEventInternal(final int x, final int y, final long eventTime) { } } - // Swipe-up-to-symbol (#32): decide on release BEFORE the gesture commit. A clear vertical - // up-flick emits the start key's popup symbol (cancelling any in-progress gesture); a - // horizontal glide is not a flick, falls through (returns false), and types normally. - if (tryEmitSwipeUpSymbol(x, y)) { - return; - } if (sInGesture) { if (currentKey != null) { callListenerOnRelease(currentKey, currentKey.getCode(), true); @@ -1615,44 +1616,56 @@ eventTime, getActivePointerTrackerCount(), graceMs, this, } /** - * Swipe-up-to-symbol (#32, opt-in): on finger release, if the stroke was a clear, mostly-vertical - * UP flick that stayed over the key it started on, emit that key's first popup symbol instead of - * the base letter. Called only from the single-key release path (after the gesture path has - * returned), so a real glide never reaches here and is never preempted. Returns true if a symbol - * was emitted (caller then skips the base-letter commit). + * Swipe-up + hold (#32, opt-in): while a swipe-up is armed, called on every move to (re)start the + * hold timer, so it only fires after the finger has been still for SWIPE_UP_HOLD_MS. A continuous + * glide keeps moving and never lets it fire; a deliberate flick-up-then-pause does. Arms on a + * clear upward flick from the start key. Does NOT consume the event — gesture detection proceeds + * in parallel, so gliding (even an upward word like "buy") is unaffected. */ - private boolean tryEmitSwipeUpSymbol(final int upX, final int upY) { - if (!Settings.getValues().mSwipeUpSymbol) return false; - if (mIsTrackingForActionDisabled) return false; - // Top-row up-swipe opens the shortcut-row popup; leave that to tryStartShortcutRowSwipe. - if (mShortcutTopRowSwipeAllowed) return false; - final Key startKey = mSwipeUpStartKey; - if (startKey == null) return false; - final PopupKeySpec[] popupKeys = startKey.getPopupKeys(); - if (popupKeys == null || popupKeys.length == 0 || popupKeys[0] == null) return false; - // Require a deliberate upward flick (>= 2 steps up), more vertical than horizontal, that did - // not drift more than one key width sideways (i.e. stayed over the start key's column). - final int dX = upX - mSwipeUpStartX; - final int dY = upY - mSwipeUpStartY; - if (dY > -2 * sPointerStep || abs(dY) <= abs(dX) || abs(dX) > startKey.getWidth()) { - return false; + private void maybeArmOrRefreshSwipeUpHold(final int x, final int y) { + if (!Settings.getValues().mSwipeUpSymbol || mShortcutTopRowSwipeAllowed + || mSwipeUpStartKey == null || isShowingPopupKeysPanel()) { + return; } - // A clear vertical flick wins over the gesture recognizer: cancel any in-progress batch so - // the (garbage) gesture isn't committed, then emit the symbol. - if (sInGesture) { - cancelBatchInput(); + final PopupKeySpec[] popupKeys = mSwipeUpStartKey.getPopupKeys(); + if (popupKeys == null || popupKeys.length == 0 || popupKeys[0] == null) return; + if (mSwipeUpHoldArmed) { + // Finger still moving — restart the timer so it only fires once the finger stills. + sTimerProxy.cancelLongPressTimersOf(this); + sTimerProxy.startLongPressTimerOf(this, SWIPE_UP_HOLD_MS); + return; } - final PopupKeySpec spec = popupKeys[0]; - if (spec.mCode == KeyCode.MULTIPLE_CODE_POINTS) { - sListener.onTextInput(spec.mOutputText); - } else if (spec.mCode != KeyCode.NOT_SPECIFIED) { - sListener.onCodeInput(spec.mCode, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false); - } else if (spec.mOutputText != null) { - sListener.onTextInput(spec.mOutputText); - } else { - return false; + // Arm on a clear upward flick from the start key (more vertical than horizontal, little drift). + final int dX = x - mSwipeUpStartX; + final int dY = y - mSwipeUpStartY; + if (dY <= -2 * sPointerStep && abs(dY) > abs(dX) && abs(dX) <= mSwipeUpStartKey.getWidth()) { + mSwipeUpHoldArmed = true; + sTimerProxy.cancelLongPressTimersOf(this); + sTimerProxy.startLongPressTimerOf(this, SWIPE_UP_HOLD_MS); + } + } + + /** + * Fired by the hold timer (via onLongPressed) once a swipe-up has been held still: open the START + * key's popup-keys panel (like long-press), cancelling any in-progress glide so it isn't committed. + */ + private void showSwipeUpSymbolPopup() { + mSwipeUpHoldArmed = false; + if (isShowingPopupKeysPanel()) return; + final Key key = mSwipeUpStartKey; + if (key == null || key.getPopupKeys() == null) return; + if (sInGesture) cancelBatchInput(); + setReleasedKeyGraphics(key, false); + final PopupKeysPanel panel = sDrawingProxy.showPopupKeysKeyboard(key, this); + if (panel == null) return; + final int translatedX = panel.translateX(mLastX); + final int translatedY = panel.translateY(mLastY); + panel.onDownEvent(translatedX, translatedY, mPointerId, SystemClock.uptimeMillis()); + mPopupKeysPanel = panel; + if (mKeySwipeAllowed) { + mKeySwipeAllowed = false; + sInKeySwipe = false; } - return true; } @Override @@ -1669,6 +1682,10 @@ public boolean isInOperation() { public void onLongPressed() { sTimerProxy.cancelLongPressTimersOf(this); + if (mSwipeUpHoldArmed) { + showSwipeUpSymbolPopup(); + return; + } if (isShowingPopupKeysPanel()) { return; } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 34dc59d21..dbd42bf24 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -326,8 +326,8 @@ Delete whole word Improve two-thumb recognition (experimental) Adds synthetic hints for the recognizer when both thumbs are used. It may help two-thumb gestures, but can hurt accuracy if the hand split is wrong. - Swipe up for symbols (experimental) - A quick flick up on a key inserts its popup symbol (no long-press). Decided on release and only when no glide happened, so swipe typing is unaffected. + Swipe up + hold for symbols (experimental) + Swipe up on a key and pause briefly to open its symbol popup (drag to pick, release to take the highlighted one). A continuous glide never triggers it, so swipe typing is unaffected. Keep insight across word parts Keep the trail visible across the parts of one word, clearing when the next word starts. Turn off to clear at each new swipe.