diff --git a/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java b/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java index 427b88e6b..24769c26d 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; @@ -215,6 +216,14 @@ public static int consumeGestureSeedCodepoint() { private boolean mShortcutBottomRowSwipeAllowed = false; private boolean mInShortcutRowSwipe = false; private static boolean sInShortcutRowSwipe = false; + // 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; @@ -990,6 +999,8 @@ private void onDownEventInternal(final int x, final int y, final long eventTime) mShortcutTopRowSwipeAllowed = isShortcutRowSource(key, true); mShortcutBottomRowSwipeAllowed = isShortcutRowSource(key, false); mInShortcutRowSwipe = false; + mSwipeUpStartKey = null; + mSwipeUpHoldArmed = false; mKeyboardLayoutHasBeenChanged = false; mIsTrackingForActionDisabled = false; resetKeySelectionByDraggingFinger(); @@ -1013,6 +1024,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; } } @@ -1100,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. @@ -1598,6 +1615,59 @@ eventTime, getActivePointerTrackerCount(), graceMs, this, } } + /** + * 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 void maybeArmOrRefreshSwipeUpHold(final int x, final int y) { + if (!Settings.getValues().mSwipeUpSymbol || mShortcutTopRowSwipeAllowed + || mSwipeUpStartKey == null || isShowingPopupKeysPanel()) { + return; + } + 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; + } + // 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; + } + } + @Override public void cancelTrackingForAction() { if (isShowingPopupKeysPanel()) { @@ -1612,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/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..dbd42bf24 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 + 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.