From 657b05a70d53dc3dcb8a43f38444f01a9bb712be Mon Sep 17 00:00:00 2001 From: lurebat Date: Sat, 23 May 2026 09:12:48 +0300 Subject: [PATCH 01/17] Reorganize two-thumb typing settings Group the screen around user-facing behavior, remove unimplemented controls from the visible/searchable settings registry, and rename confusing options with clearer labels. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../settings/screens/TwoThumbTypingScreen.kt | 94 ++++++------------- app/src/main/res/values/strings.xml | 26 +++-- dev-log.md | 44 +++++++++ 3 files changed, 93 insertions(+), 71 deletions(-) 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 f1641c1a9..66a89b5e1 100644 --- a/app/src/main/java/helium314/keyboard/settings/screens/TwoThumbTypingScreen.kt +++ b/app/src/main/java/helium314/keyboard/settings/screens/TwoThumbTypingScreen.kt @@ -25,18 +25,6 @@ import helium314.keyboard.settings.preferences.SliderPreference import helium314.keyboard.settings.preferences.SwitchPreference import helium314.keyboard.settings.previewDark -/** - * Dedicated screen for the experimental two-thumb typing features (HeliBoard issue #291). - * Every option here defaults to off / 0 — turning the whole screen into a no-op until the - * user opts in. Subsections group features around the user-facing behaviour (manual vs. - * autospace-grace, tap+swipe interactions, point hinting, debug overlay) rather than around - * the underlying code paths. - * - * The screen is gated on {@code JniUtils.sHaveGestureLib} + {@code PREF_GESTURE_INPUT}: with - * either of those off, none of the toggles have any effect, so we surface a single hint row - * and skip everything else. That keeps the experimental options out of the user's way until - * they're actually relevant. - */ @Composable fun TwoThumbTypingScreen( onClickBack: () -> Unit, @@ -50,63 +38,56 @@ fun TwoThumbTypingScreen( val hasGestureLib = JniUtils.sHaveGestureLib val gestureEnabled = hasGestureLib && prefs.getBoolean(Settings.PREF_GESTURE_INPUT, Defaults.PREF_GESTURE_INPUT) + val combiningGraceMs = prefs.getInt(Settings.PREF_COMBINING_GRACE_MS, Defaults.PREF_COMBINING_GRACE_MS) + val combiningEnabled = combiningGraceMs > 0 + val multiPartEnabled = prefs.getBoolean( + Settings.PREF_MULTIPART_AUTO_EXTEND_IN_COMBINING, + Defaults.PREF_MULTIPART_AUTO_EXTEND_IN_COMBINING, + ) + val manualSpacing = prefs.getBoolean(Settings.PREF_GESTURE_MANUAL_SPACING, Defaults.PREF_GESTURE_MANUAL_SPACING) + val tapDuringSwipe = prefs.getBoolean(Settings.PREF_GESTURE_TAP_DURING_SWIPE, Defaults.PREF_GESTURE_TAP_DURING_SWIPE) + val dualThumbHinting = prefs.getBoolean(Settings.PREF_GESTURE_DUAL_THUMB_HINTING, Defaults.PREF_GESTURE_DUAL_THUMB_HINTING) val items = buildList { if (!gestureEnabled) { - // Nothing useful to show — every toggle is a no-op until gesture typing is on. - // We deliberately don't even show the section headers; the user lands on an empty - // screen with just the search box, which is the clearest signal "you need to enable - // gesture typing first". + add(R.string.two_thumb_typing_requires_gesture) return@buildList } - // --- Combining mode (unified replacement for autospace grace + tap promotion + flash) --- - add(R.string.settings_category_two_thumb_typing_spacing) + add(R.string.settings_category_two_thumb_typing_words) add(Settings.PREF_COMBINING_GRACE_MS) - if (prefs.getInt(Settings.PREF_COMBINING_GRACE_MS, Defaults.PREF_COMBINING_GRACE_MS) > 0) { + if (combiningEnabled) { add(Settings.PREF_COMBINING_TAP_EXTRA_MS) - add(Settings.PREF_COMBINING_AUTOCORRECT_ON_AUTOSPACE) - add(Settings.PREF_COMBINING_AUTOSPACE_SUGGESTIONS) - add(Settings.PREF_COMBINING_BACKSPACE_DELETES_GESTURE_WORD) - // Multi-part word composition (only meaningful when combining grace > 0). add(Settings.PREF_MULTIPART_AUTO_EXTEND_IN_COMBINING) - if (prefs.getBoolean(Settings.PREF_MULTIPART_AUTO_EXTEND_IN_COMBINING, - Defaults.PREF_MULTIPART_AUTO_EXTEND_IN_COMBINING)) { + if (multiPartEnabled) { add(Settings.PREF_MULTIPART_FULL_WORD_SUGGESTIONS) add(Settings.PREF_MULTIPART_TAP_SEED_GESTURE) - add(Settings.PREF_MULTIPART_JOIN_KEY_MODE) - // Show fragment-backspace alongside multi-part: backspace popping the last - // joined fragment is the natural "undo a bad join" gesture. add(Settings.PREF_GESTURE_FRAGMENT_BACKSPACE) } + add(Settings.PREF_COMBINING_BACKSPACE_DELETES_GESTURE_WORD) + add(Settings.PREF_COMBINING_AUTOCORRECT_ON_AUTOSPACE) + add(Settings.PREF_COMBINING_AUTOSPACE_SUGGESTIONS) } + + add(R.string.settings_category_two_thumb_typing_manual) add(Settings.PREF_GESTURE_MANUAL_SPACING) - val manualSpacing = prefs.getBoolean(Settings.PREF_GESTURE_MANUAL_SPACING, Defaults.PREF_GESTURE_MANUAL_SPACING) - if (manualSpacing) { - // Sub-option of manual spacing too. Multi-part also shows it above; we don't - // double-add since SettingsContainer dedupes by key. + if (manualSpacing && !(combiningEnabled && multiPartEnabled)) { add(Settings.PREF_GESTURE_FRAGMENT_BACKSPACE) } - // --- Tap / swipe interaction tweaks (#1.3) --- - add(R.string.settings_category_two_thumb_typing_taps) + add(R.string.settings_category_two_thumb_typing_two_finger) add(Settings.PREF_GESTURE_TAP_DURING_SWIPE) - if (prefs.getBoolean(Settings.PREF_GESTURE_TAP_DURING_SWIPE, Defaults.PREF_GESTURE_TAP_DURING_SWIPE)) { + if (tapDuringSwipe) { add(Settings.PREF_GESTURE_TAP_AS_SWIPE_WINDOW_MS) } - // --- Layout-side (#2.3): apostrophe key for contractions. Layout edit still owed to - // the user; the toggle is here so it's discoverable once that's in place. --- - add(R.string.settings_category_two_thumb_typing_layout) - add(Settings.PREF_GESTURE_APOSTROPHE_KEY) - - // --- Point hinting + debug overlay (#2.1). The midline only matters when hinting is - // on, so it's nested under the toggle. --- - add(R.string.settings_category_two_thumb_typing_hinting) + add(R.string.settings_category_two_thumb_typing_recognition) add(Settings.PREF_GESTURE_DUAL_THUMB_HINTING) - if (prefs.getBoolean(Settings.PREF_GESTURE_DUAL_THUMB_HINTING, Defaults.PREF_GESTURE_DUAL_THUMB_HINTING)) { + if (dualThumbHinting) { add(Settings.PREF_GESTURE_DUAL_THUMB_MIDLINE_PCT) } + + add(R.string.settings_category_two_thumb_typing_troubleshooting) add(Settings.PREF_GESTURE_DEBUG_DRAW_POINTS) } @@ -125,7 +106,7 @@ fun TwoThumbTypingScreen( */ fun createTwoThumbTypingSettings(context: Context) = listOf( Setting(context, Settings.PREF_COMBINING_GRACE_MS, - R.string.combining_grace, R.string.combining_grace_summary) { def -> + R.string.two_thumb_combine_grace, R.string.two_thumb_combine_grace_summary) { def -> SliderPreference( name = def.title, key = def.key, @@ -156,7 +137,7 @@ fun createTwoThumbTypingSettings(context: Context) = listOf( SwitchPreference(it, Defaults.PREF_COMBINING_BACKSPACE_DELETES_GESTURE_WORD) }, Setting(context, Settings.PREF_COMBINING_TAP_EXTRA_MS, - R.string.combining_tap_extra, R.string.combining_tap_extra_summary) { def -> + R.string.two_thumb_tap_extra, R.string.two_thumb_tap_extra_summary) { def -> SliderPreference( name = def.title, key = def.key, @@ -173,7 +154,7 @@ fun createTwoThumbTypingSettings(context: Context) = listOf( SwitchPreference(it, Defaults.PREF_GESTURE_MANUAL_SPACING) }, Setting(context, Settings.PREF_MULTIPART_AUTO_EXTEND_IN_COMBINING, - R.string.multipart_auto_extend_in_combining, R.string.multipart_auto_extend_in_combining_summary) { + R.string.two_thumb_multi_part_words, R.string.two_thumb_multi_part_words_summary) { SwitchPreference(it, Defaults.PREF_MULTIPART_AUTO_EXTEND_IN_COMBINING) }, Setting(context, Settings.PREF_MULTIPART_FULL_WORD_SUGGESTIONS, @@ -181,28 +162,19 @@ fun createTwoThumbTypingSettings(context: Context) = listOf( SwitchPreference(it, Defaults.PREF_MULTIPART_FULL_WORD_SUGGESTIONS) }, Setting(context, Settings.PREF_MULTIPART_TAP_SEED_GESTURE, - R.string.multipart_tap_seed_gesture, R.string.multipart_tap_seed_gesture_summary) { + R.string.two_thumb_typed_prefix_swipe, R.string.two_thumb_typed_prefix_swipe_summary) { SwitchPreference(it, Defaults.PREF_MULTIPART_TAP_SEED_GESTURE) }, - Setting(context, Settings.PREF_MULTIPART_JOIN_KEY_MODE, - R.string.multipart_join_key_mode, R.string.multipart_join_key_mode_summary) { def -> - val items = listOf( - stringResource(R.string.multipart_join_key_mode_off) to "off", - stringResource(R.string.multipart_join_key_mode_longpress_space) to "longpress_space", - stringResource(R.string.multipart_join_key_mode_dedicated_key) to "dedicated_key", - ) - ListPreference(def, items, Defaults.PREF_MULTIPART_JOIN_KEY_MODE) - }, Setting(context, Settings.PREF_GESTURE_FRAGMENT_BACKSPACE, R.string.gesture_fragment_backspace, R.string.gesture_fragment_backspace_summary) { SwitchPreference(it, Defaults.PREF_GESTURE_FRAGMENT_BACKSPACE) }, Setting(context, Settings.PREF_GESTURE_TAP_DURING_SWIPE, - R.string.gesture_tap_during_swipe, R.string.gesture_tap_during_swipe_summary) { + R.string.two_thumb_tap_during_swipe, R.string.two_thumb_tap_during_swipe_summary) { SwitchPreference(it, Defaults.PREF_GESTURE_TAP_DURING_SWIPE) }, Setting(context, Settings.PREF_GESTURE_TAP_AS_SWIPE_WINDOW_MS, - R.string.gesture_tap_as_swipe_window, R.string.gesture_tap_as_swipe_window_summary) { def -> + R.string.two_thumb_tap_during_swipe_duration, R.string.two_thumb_tap_during_swipe_duration_summary) { def -> SliderPreference( name = def.title, key = def.key, @@ -211,10 +183,6 @@ fun createTwoThumbTypingSettings(context: Context) = listOf( description = { stringResource(R.string.abbreviation_unit_milliseconds, it.toString()) } ) }, - Setting(context, Settings.PREF_GESTURE_APOSTROPHE_KEY, - R.string.gesture_apostrophe_key, R.string.gesture_apostrophe_key_summary) { - SwitchPreference(it, Defaults.PREF_GESTURE_APOSTROPHE_KEY) - }, Setting(context, Settings.PREF_GESTURE_DUAL_THUMB_HINTING, R.string.gesture_dual_thumb_hinting, R.string.gesture_dual_thumb_hinting_summary) { SwitchPreference(it, Defaults.PREF_GESTURE_DUAL_THUMB_HINTING) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f4abc9954..e355c85c7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -211,14 +211,12 @@ Two-thumb typing - - Spacing & word commit - - Tap + swipe interactions - - Layout extras - - Recognition & debug + Enable gesture typing first + Build words from taps and swipes + Manual spacing mode + Two-finger input + Recognition tuning + Troubleshooting Manual spacing @@ -235,10 +233,14 @@ Tap during swipe A quick tap with another finger while swiping adds that letter to the same word + Tap letters while swiping + While one thumb is swiping, a quick tap from the other thumb can add that letter to the same gesture word. Max tap duration during swipe Taps shorter than this — while another finger is swiping — are merged into the swipe. Longer presses type the letter normally. + Maximum tap length + Taps shorter than this can join the active swipe; longer presses type the letter normally. Tap-then-swipe window @@ -265,12 +267,16 @@ Combining grace period After every tap or swipe, wait this long for a follow-up before committing the word. While waiting, the spacebar shows a countdown. + Wait for next input + After a tap or swipe, wait this long before committing the word. A follow-up tap or swipe during the countdown extends the same word. Autocorrect on auto-space When the grace timer commits a word with an automatic space, also apply autocorrection Extra grace after a tap + More time after taps + Add extra waiting time after a tapped letter, since taps are usually slower than swipe fragments. Backspace deletes last swipe @@ -281,10 +287,14 @@ Concatenate fragments into one word While the combining grace timer is running, the next swipe or tap extends the current composing word instead of starting a new one (tech+nology → technology, tech+y → Techy). + Join word parts + During the countdown, the next tap or swipe extends the current word instead of starting a new one (tech + nology → technology). Suggestions for full composing word During multi-part composition, the suggestion strip shows alternatives for the entire word so far, not just the most recent fragment. Seed swipe from typed prefix When you type some letters and then swipe while combining mode is armed, treat the swipe as a continuation of the typed prefix. + Swipe continues typed prefix + If you type a few letters and then swipe during the countdown, treat the swipe as the rest of that word. Explicit "join next" modifier Force the next input to extend the current word regardless of timing. Off diff --git a/dev-log.md b/dev-log.md index 3ae29ea94..1591dc180 100644 --- a/dev-log.md +++ b/dev-log.md @@ -232,3 +232,47 @@ Review comment r3289683508 on PR #3 noted that `createToolbarStateBackground()` ### Open Questions / Next Steps - If toolbar state updates ever move off the main thread, add `@Volatile` or synchronization to the cache variable. +--- + +## 2026-05-23 — Reorganize two-thumb typing settings + +### Context +The two-thumb typing settings screen mixed current user-facing features, legacy/manual-spacing controls, debugging options, and future/unimplemented toggles. Some settings were confusingly named after implementation details, while `gesture_apostrophe_key` and `multipart_join_key_mode` were exposed even though they are not wired to runtime behavior. + +### Actions Taken +- Created branch `copilot/organize-two-thumb-settings` from `origin/main`. +- Reorganized `TwoThumbTypingScreen.kt` into clearer user-facing sections: + - Build words from taps and swipes + - Manual spacing mode + - Two-finger input + - Recognition tuning + - Troubleshooting +- Renamed visible labels/summaries for the main combining grace, tap extra time, multi-part joining, typed-prefix swipe continuation, and tap-during-swipe options. +- Removed unimplemented/dead settings from the screen and global search registry: + - `PREF_GESTURE_APOSTROPHE_KEY` + - `PREF_MULTIPART_JOIN_KEY_MODE` +- Kept existing preference keys/defaults/runtime reads in place for compatibility; only the user-facing settings registry was changed. +- Built `:app:assembleStandardDebug` and ran `SettingsContainerTest`. + +### Decisions Made +- Kept the existing runtime behavior unchanged and only reorganized/renamed the UI. +- Left legacy manual spacing visible but moved it into its own advanced-feeling section so it is not confused with the recommended combining-mode flow. +- Left debug point drawing visible under **Troubleshooting** rather than mixing it with recognition settings. +- Did not remove old strings yet, because other docs/translations may still refer to them and keeping them is safer than a broad cleanup. + +### Manual Tests — Two-thumb Settings Organization + +| # | Steps | Expected Result | +|---|---|---| +| 1 | Open **Settings → Two-thumb typing** with gesture typing disabled. | Screen shows a simple “Enable gesture typing first” hint instead of confusing no-op toggles. | +| 2 | Enable gesture typing, then open **Two-thumb typing**. | Settings are grouped into the new user-facing sections. | +| 3 | Set **Wait for next input** to 0 ms. | Advanced combining options are hidden. | +| 4 | Set **Wait for next input** above 0 ms. | Follow-up timing, word joining, backspace, autocorrect, and suggestion options appear. | +| 5 | Disable **Join word parts**. | Sub-options for full-word suggestions, typed-prefix swipe, and fragment backspace are hidden unless manual spacing needs fragment backspace. | +| 6 | Enable **Manual spacing**. | Manual-spacing-related fragment backspace appears when not already shown under word joining. | +| 7 | Enable **Tap letters while swiping**. | **Maximum tap length** appears. | +| 8 | Enable **Two-thumb point hinting**. | **Left/right hand split** appears. | +| 9 | Search settings for “apostrophe” or “join next modifier”. | The removed unimplemented controls do not appear. | + +### Open Questions / Next Steps +- User should review the new wording on-device and decide whether debug point drawing should remain visible or move behind the separate debug settings screen later. From 6cdb730616019012f79b4d4fd5ae9bd1ac2419e7 Mon Sep 17 00:00:00 2001 From: lurebat Date: Sat, 23 May 2026 10:02:45 +0300 Subject: [PATCH 02/17] Use mode selectors for two-thumb settings Replace low-level two-thumb controls with user-facing spacing and backspace behavior selectors, derive multipart internals from non-normal spacing, and add whole-word backspace handling. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../keyboard/latin/inputlogic/InputLogic.java | 6 + .../keyboard/latin/settings/Defaults.kt | 2 +- .../latin/settings/SettingsValues.java | 7 +- .../keyboard/settings/SettingsContainer.kt | 2 + .../settings/screens/TwoThumbTypingScreen.kt | 179 +++++++++++++----- app/src/main/res/values/strings.xml | 21 +- dev-log.md | 40 ++++ 7 files changed, 199 insertions(+), 58 deletions(-) diff --git a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java index c531cba49..425a6772a 100644 --- a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java +++ b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java @@ -2269,6 +2269,12 @@ private void handleBackspaceEvent(final Event event, final InputTransaction inpu Constants.EVENT_REJECTION); } StatsUtils.onBackspaceWordDelete(rejectedSuggestion.length()); + } else if ((inputTransaction.getSettingsValues().mGestureManualSpacing + || inputTransaction.getSettingsValues().mCombiningGraceMs > 0) + && inputTransaction.getSettingsValues().mCombiningBackspaceDeletesGestureWord) { + final int wordLength = mWordComposer.getTypedWord().length(); + mWordComposer.reset(); + StatsUtils.onBackspaceWordDelete(wordLength); } else { mWordComposer.applyProcessedEvent(event); StatsUtils.onBackspacePressed(1); 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 4f7ed1340..54080e8d5 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt +++ b/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt @@ -140,7 +140,7 @@ object Defaults { const val PREF_COMBINING_GRACE_MS = 0 const val PREF_COMBINING_AUTOCORRECT_ON_AUTOSPACE = true const val PREF_COMBINING_TAP_EXTRA_MS = 250 - const val PREF_COMBINING_AUTOSPACE_SUGGESTIONS = "next_word" + const val PREF_COMBINING_AUTOSPACE_SUGGESTIONS = "alternatives_then_next_word" const val PREF_COMBINING_BACKSPACE_DELETES_GESTURE_WORD = true // Multi-part word composition (this branch). All default-on except the explicit join key. const val PREF_MULTIPART_AUTO_EXTEND_IN_COMBINING = true 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 95a12386b..413980c91 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java +++ b/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java @@ -357,16 +357,17 @@ public SettingsValues(final Context context, final SharedPreferences prefs, fina Defaults.PREF_COMBINING_TAP_EXTRA_MS); mCombiningAutospaceSuggestions = prefs.getString(Settings.PREF_COMBINING_AUTOSPACE_SUGGESTIONS, Defaults.PREF_COMBINING_AUTOSPACE_SUGGESTIONS); + final boolean nonNormalTwoThumbSpacing = mGestureManualSpacing || mCombiningGraceMs > 0; mCombiningBackspaceDeletesGestureWord = prefs.getBoolean( Settings.PREF_COMBINING_BACKSPACE_DELETES_GESTURE_WORD, Defaults.PREF_COMBINING_BACKSPACE_DELETES_GESTURE_WORD); - mMultipartAutoExtendInCombining = prefs.getBoolean( + mMultipartAutoExtendInCombining = nonNormalTwoThumbSpacing || prefs.getBoolean( Settings.PREF_MULTIPART_AUTO_EXTEND_IN_COMBINING, Defaults.PREF_MULTIPART_AUTO_EXTEND_IN_COMBINING); - mMultipartFullWordSuggestions = prefs.getBoolean( + mMultipartFullWordSuggestions = nonNormalTwoThumbSpacing || prefs.getBoolean( Settings.PREF_MULTIPART_FULL_WORD_SUGGESTIONS, Defaults.PREF_MULTIPART_FULL_WORD_SUGGESTIONS); - mMultipartTapSeedGesture = prefs.getBoolean( + mMultipartTapSeedGesture = nonNormalTwoThumbSpacing || prefs.getBoolean( Settings.PREF_MULTIPART_TAP_SEED_GESTURE, Defaults.PREF_MULTIPART_TAP_SEED_GESTURE); mMultipartJoinKeyMode = prefs.getString(Settings.PREF_MULTIPART_JOIN_KEY_MODE, diff --git a/app/src/main/java/helium314/keyboard/settings/SettingsContainer.kt b/app/src/main/java/helium314/keyboard/settings/SettingsContainer.kt index fffd5e436..ca72e4685 100644 --- a/app/src/main/java/helium314/keyboard/settings/SettingsContainer.kt +++ b/app/src/main/java/helium314/keyboard/settings/SettingsContainer.kt @@ -92,6 +92,8 @@ object SettingsWithoutKey { const val BACKUP_RESTORE = "backup_restore" const val DEBUG_SETTINGS = "screen_debug" const val LOAD_GESTURE_LIB = "load_gesture_library" + const val TWO_THUMB_SPACING_MODE = "two_thumb_spacing_mode" + const val TWO_THUMB_BACKSPACE_BEHAVIOR = "two_thumb_backspace_behavior" const val BACKGROUND_IMAGE = "background_image" const val BACKGROUND_IMAGE_LANDSCAPE = "background_image_landscape" const val CUSTOM_FONT = "custom_font" 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 66a89b5e1..0e71d077e 100644 --- a/app/src/main/java/helium314/keyboard/settings/screens/TwoThumbTypingScreen.kt +++ b/app/src/main/java/helium314/keyboard/settings/screens/TwoThumbTypingScreen.kt @@ -2,12 +2,18 @@ package helium314.keyboard.settings.screens import android.content.Context +import androidx.compose.material3.Text import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.core.content.edit import helium314.keyboard.latin.R import helium314.keyboard.latin.settings.Defaults import helium314.keyboard.latin.settings.Settings @@ -18,9 +24,12 @@ import helium314.keyboard.latin.utils.prefs import helium314.keyboard.settings.SearchSettingsScreen import helium314.keyboard.settings.Setting import helium314.keyboard.settings.SettingsActivity +import helium314.keyboard.settings.SettingsWithoutKey import helium314.keyboard.settings.Theme +import helium314.keyboard.settings.dialogs.ListPickerDialog import helium314.keyboard.settings.initPreview import helium314.keyboard.settings.preferences.ListPreference +import helium314.keyboard.settings.preferences.Preference import helium314.keyboard.settings.preferences.SliderPreference import helium314.keyboard.settings.preferences.SwitchPreference import helium314.keyboard.settings.previewDark @@ -38,13 +47,9 @@ fun TwoThumbTypingScreen( val hasGestureLib = JniUtils.sHaveGestureLib val gestureEnabled = hasGestureLib && prefs.getBoolean(Settings.PREF_GESTURE_INPUT, Defaults.PREF_GESTURE_INPUT) - val combiningGraceMs = prefs.getInt(Settings.PREF_COMBINING_GRACE_MS, Defaults.PREF_COMBINING_GRACE_MS) - val combiningEnabled = combiningGraceMs > 0 - val multiPartEnabled = prefs.getBoolean( - Settings.PREF_MULTIPART_AUTO_EXTEND_IN_COMBINING, - Defaults.PREF_MULTIPART_AUTO_EXTEND_IN_COMBINING, - ) - val manualSpacing = prefs.getBoolean(Settings.PREF_GESTURE_MANUAL_SPACING, Defaults.PREF_GESTURE_MANUAL_SPACING) + val spacingMode = currentSpacingMode(prefs) + val autospaceMode = spacingMode == SPACING_MODE_AUTOSPACE + val nonNormalSpacing = spacingMode != SPACING_MODE_NORMAL val tapDuringSwipe = prefs.getBoolean(Settings.PREF_GESTURE_TAP_DURING_SWIPE, Defaults.PREF_GESTURE_TAP_DURING_SWIPE) val dualThumbHinting = prefs.getBoolean(Settings.PREF_GESTURE_DUAL_THUMB_HINTING, Defaults.PREF_GESTURE_DUAL_THUMB_HINTING) @@ -55,24 +60,15 @@ fun TwoThumbTypingScreen( } add(R.string.settings_category_two_thumb_typing_words) - add(Settings.PREF_COMBINING_GRACE_MS) - if (combiningEnabled) { + add(SettingsWithoutKey.TWO_THUMB_SPACING_MODE) + if (autospaceMode) { + add(Settings.PREF_COMBINING_GRACE_MS) add(Settings.PREF_COMBINING_TAP_EXTRA_MS) - add(Settings.PREF_MULTIPART_AUTO_EXTEND_IN_COMBINING) - if (multiPartEnabled) { - add(Settings.PREF_MULTIPART_FULL_WORD_SUGGESTIONS) - add(Settings.PREF_MULTIPART_TAP_SEED_GESTURE) - add(Settings.PREF_GESTURE_FRAGMENT_BACKSPACE) - } - add(Settings.PREF_COMBINING_BACKSPACE_DELETES_GESTURE_WORD) add(Settings.PREF_COMBINING_AUTOCORRECT_ON_AUTOSPACE) add(Settings.PREF_COMBINING_AUTOSPACE_SUGGESTIONS) } - - add(R.string.settings_category_two_thumb_typing_manual) - add(Settings.PREF_GESTURE_MANUAL_SPACING) - if (manualSpacing && !(combiningEnabled && multiPartEnabled)) { - add(Settings.PREF_GESTURE_FRAGMENT_BACKSPACE) + if (nonNormalSpacing) { + add(SettingsWithoutKey.TWO_THUMB_BACKSPACE_BEHAVIOR) } add(R.string.settings_category_two_thumb_typing_two_finger) @@ -98,15 +94,13 @@ fun TwoThumbTypingScreen( ) } -/** - * Factory for all two-thumb typing {@link Setting} entries. Registered with - * {@link helium314.keyboard.settings.SettingsContainer} so the entries are picked up by the - * screen above AND remain searchable globally. Moved out of {@link #createGestureTypingSettings} - * so the parent gesture screen stays uncluttered. - */ fun createTwoThumbTypingSettings(context: Context) = listOf( + Setting(context, SettingsWithoutKey.TWO_THUMB_SPACING_MODE, + R.string.two_thumb_spacing_mode, R.string.two_thumb_spacing_mode_summary) { + TwoThumbSpacingModePreference(it) + }, Setting(context, Settings.PREF_COMBINING_GRACE_MS, - R.string.two_thumb_combine_grace, R.string.two_thumb_combine_grace_summary) { def -> + R.string.two_thumb_autospace_duration, R.string.two_thumb_autospace_duration_summary) { def -> SliderPreference( name = def.title, key = def.key, @@ -137,7 +131,7 @@ fun createTwoThumbTypingSettings(context: Context) = listOf( SwitchPreference(it, Defaults.PREF_COMBINING_BACKSPACE_DELETES_GESTURE_WORD) }, Setting(context, Settings.PREF_COMBINING_TAP_EXTRA_MS, - R.string.two_thumb_tap_extra, R.string.two_thumb_tap_extra_summary) { def -> + R.string.two_thumb_tap_autospace_grace, R.string.two_thumb_tap_autospace_grace_summary) { def -> SliderPreference( name = def.title, key = def.key, @@ -149,25 +143,9 @@ fun createTwoThumbTypingSettings(context: Context) = listOf( } ) }, - Setting(context, Settings.PREF_GESTURE_MANUAL_SPACING, - R.string.gesture_manual_spacing, R.string.gesture_manual_spacing_summary) { - SwitchPreference(it, Defaults.PREF_GESTURE_MANUAL_SPACING) - }, - Setting(context, Settings.PREF_MULTIPART_AUTO_EXTEND_IN_COMBINING, - R.string.two_thumb_multi_part_words, R.string.two_thumb_multi_part_words_summary) { - SwitchPreference(it, Defaults.PREF_MULTIPART_AUTO_EXTEND_IN_COMBINING) - }, - Setting(context, Settings.PREF_MULTIPART_FULL_WORD_SUGGESTIONS, - R.string.multipart_full_word_suggestions, R.string.multipart_full_word_suggestions_summary) { - SwitchPreference(it, Defaults.PREF_MULTIPART_FULL_WORD_SUGGESTIONS) - }, - Setting(context, Settings.PREF_MULTIPART_TAP_SEED_GESTURE, - R.string.two_thumb_typed_prefix_swipe, R.string.two_thumb_typed_prefix_swipe_summary) { - SwitchPreference(it, Defaults.PREF_MULTIPART_TAP_SEED_GESTURE) - }, - Setting(context, Settings.PREF_GESTURE_FRAGMENT_BACKSPACE, - R.string.gesture_fragment_backspace, R.string.gesture_fragment_backspace_summary) { - SwitchPreference(it, Defaults.PREF_GESTURE_FRAGMENT_BACKSPACE) + Setting(context, SettingsWithoutKey.TWO_THUMB_BACKSPACE_BEHAVIOR, + R.string.two_thumb_backspace_behavior, R.string.two_thumb_backspace_behavior_summary) { + TwoThumbBackspaceBehaviorPreference(it) }, Setting(context, Settings.PREF_GESTURE_TAP_DURING_SWIPE, R.string.two_thumb_tap_during_swipe, R.string.two_thumb_tap_during_swipe_summary) { @@ -184,7 +162,7 @@ fun createTwoThumbTypingSettings(context: Context) = listOf( ) }, Setting(context, Settings.PREF_GESTURE_DUAL_THUMB_HINTING, - R.string.gesture_dual_thumb_hinting, R.string.gesture_dual_thumb_hinting_summary) { + R.string.two_thumb_point_hinting, R.string.two_thumb_point_hinting_summary) { SwitchPreference(it, Defaults.PREF_GESTURE_DUAL_THUMB_HINTING) }, Setting(context, Settings.PREF_GESTURE_DUAL_THUMB_MIDLINE_PCT, R.string.gesture_dual_thumb_midline) { def -> @@ -202,6 +180,109 @@ fun createTwoThumbTypingSettings(context: Context) = listOf( }, ) +private const val SPACING_MODE_NORMAL = "normal" +private const val SPACING_MODE_MANUAL = "manual" +private const val SPACING_MODE_AUTOSPACE = "autospace" +private const val DEFAULT_AUTOSPACE_GRACE_MS = 500 + +private const val BACKSPACE_NORMAL = "normal" +private const val BACKSPACE_FRAGMENT = "fragment" +private const val BACKSPACE_WORD = "word" + +private fun currentSpacingMode(prefs: android.content.SharedPreferences): String = when { + prefs.getBoolean(Settings.PREF_GESTURE_MANUAL_SPACING, Defaults.PREF_GESTURE_MANUAL_SPACING) -> SPACING_MODE_MANUAL + prefs.getInt(Settings.PREF_COMBINING_GRACE_MS, Defaults.PREF_COMBINING_GRACE_MS) > 0 -> SPACING_MODE_AUTOSPACE + else -> SPACING_MODE_NORMAL +} + +@Composable +private fun TwoThumbSpacingModePreference(setting: Setting) { + val prefs = LocalContext.current.prefs() + var showDialog by rememberSaveable { mutableStateOf(false) } + val items = listOf( + stringResource(R.string.two_thumb_spacing_mode_normal) to SPACING_MODE_NORMAL, + stringResource(R.string.two_thumb_spacing_mode_manual) to SPACING_MODE_MANUAL, + stringResource(R.string.two_thumb_spacing_mode_autospace) to SPACING_MODE_AUTOSPACE, + ) + val selected = currentSpacingMode(prefs) + Preference( + name = setting.title, + description = items.first { it.second == selected }.first, + onClick = { showDialog = true }, + ) + if (showDialog) { + ListPickerDialog( + onDismissRequest = { showDialog = false }, + items = items, + onItemSelected = { item -> + val mode = item.second + prefs.edit { + putBoolean(Settings.PREF_GESTURE_MANUAL_SPACING, mode == SPACING_MODE_MANUAL) + when (mode) { + SPACING_MODE_NORMAL, SPACING_MODE_MANUAL -> putInt(Settings.PREF_COMBINING_GRACE_MS, 0) + SPACING_MODE_AUTOSPACE -> if (prefs.getInt(Settings.PREF_COMBINING_GRACE_MS, 0) <= 0) { + putInt(Settings.PREF_COMBINING_GRACE_MS, DEFAULT_AUTOSPACE_GRACE_MS) + } + } + } + }, + selectedItem = items.first { it.second == selected }, + title = { Text(setting.title) }, + getItemName = { it.first }, + ) + } +} + +private fun currentBackspaceBehavior(prefs: android.content.SharedPreferences): String = when { + prefs.getBoolean(Settings.PREF_COMBINING_BACKSPACE_DELETES_GESTURE_WORD, + Defaults.PREF_COMBINING_BACKSPACE_DELETES_GESTURE_WORD) -> BACKSPACE_WORD + prefs.getBoolean(Settings.PREF_GESTURE_FRAGMENT_BACKSPACE, + Defaults.PREF_GESTURE_FRAGMENT_BACKSPACE) -> BACKSPACE_FRAGMENT + else -> BACKSPACE_NORMAL +} + +@Composable +private fun TwoThumbBackspaceBehaviorPreference(setting: Setting) { + val prefs = LocalContext.current.prefs() + var showDialog by rememberSaveable { mutableStateOf(false) } + val items = listOf( + stringResource(R.string.two_thumb_backspace_normal) to BACKSPACE_NORMAL, + stringResource(R.string.two_thumb_backspace_fragment) to BACKSPACE_FRAGMENT, + stringResource(R.string.two_thumb_backspace_word) to BACKSPACE_WORD, + ) + val selected = currentBackspaceBehavior(prefs) + Preference( + name = setting.title, + description = items.first { it.second == selected }.first, + onClick = { showDialog = true }, + ) + if (showDialog) { + ListPickerDialog( + onDismissRequest = { showDialog = false }, + items = items, + onItemSelected = { item -> + when (item.second) { + BACKSPACE_NORMAL -> prefs.edit { + putBoolean(Settings.PREF_GESTURE_FRAGMENT_BACKSPACE, false) + putBoolean(Settings.PREF_COMBINING_BACKSPACE_DELETES_GESTURE_WORD, false) + } + BACKSPACE_FRAGMENT -> prefs.edit { + putBoolean(Settings.PREF_GESTURE_FRAGMENT_BACKSPACE, true) + putBoolean(Settings.PREF_COMBINING_BACKSPACE_DELETES_GESTURE_WORD, false) + } + BACKSPACE_WORD -> prefs.edit { + putBoolean(Settings.PREF_GESTURE_FRAGMENT_BACKSPACE, false) + putBoolean(Settings.PREF_COMBINING_BACKSPACE_DELETES_GESTURE_WORD, true) + } + } + }, + selectedItem = items.first { it.second == selected }, + title = { Text(setting.title) }, + getItemName = { it.first }, + ) + } +} + @Preview @Composable private fun Preview() { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e355c85c7..1cdc1cd0f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -213,10 +213,14 @@ Two-thumb typing Enable gesture typing first Build words from taps and swipes - Manual spacing mode Two-finger input Recognition tuning Troubleshooting + Spacing mode + Choose how swipe/tap word parts are committed and spaced. + Normal spacing + Manual spacing + Auto-space after a delay Manual spacing @@ -267,16 +271,16 @@ Combining grace period After every tap or swipe, wait this long for a follow-up before committing the word. While waiting, the spacebar shows a countdown. - Wait for next input - After a tap or swipe, wait this long before committing the word. A follow-up tap or swipe during the countdown extends the same word. + Duration until autospace + After a tap or swipe, wait this long before committing the word and inserting a space. A follow-up during the countdown extends the same word. Autocorrect on auto-space When the grace timer commits a word with an automatic space, also apply autocorrection Extra grace after a tap - More time after taps - Add extra waiting time after a tapped letter, since taps are usually slower than swipe fragments. + Extra autospace delay after taps + Add this much extra time when the last input was a tapped letter, so tap-then-swipe combinations are easier to continue. Backspace deletes last swipe @@ -295,11 +299,18 @@ When you type some letters and then swipe while combining mode is armed, treat the swipe as a continuation of the typed prefix. Swipe continues typed prefix If you type a few letters and then swipe during the countdown, treat the swipe as the rest of that word. + Backspace behavior + Choose what backspace removes while using manual spacing or delayed autospace. + Delete one character + Delete last word part + Delete whole word Explicit "join next" modifier Force the next input to extend the current word regardless of timing. Off Long-press spacebar Dedicated key in symbols layer + 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. After auto-space, show… diff --git a/dev-log.md b/dev-log.md index 1591dc180..1e3d54015 100644 --- a/dev-log.md +++ b/dev-log.md @@ -276,3 +276,43 @@ The two-thumb typing settings screen mixed current user-facing features, legacy/ ### Open Questions / Next Steps - User should review the new wording on-device and decide whether debug point drawing should remain visible or move behind the separate debug settings screen later. + +--- + +## 2026-05-23 — Convert two-thumb settings to mode selectors + +### Context +After trying the first reorganization, the user clarified the desired structure: a single spacing-mode selector should drive normal/manual/autospace behavior, backspace should be a single behavior selector, and implementation details like multi-part joining should be enabled automatically instead of exposed as separate confusing toggles. + +### Actions Taken +- Added a synthetic **Spacing mode** radio/list setting with Normal spacing, Manual spacing, and Auto-space after a delay. +- Added a synthetic **Backspace behavior** radio/list setting shown only for manual/autospace modes. +- Mapped the spacing selector onto the existing `PREF_GESTURE_MANUAL_SPACING` and `PREF_COMBINING_GRACE_MS` runtime preferences. +- Mapped backspace behavior onto existing fragment-backspace and whole-word-backspace preferences. +- Added whole composing-word deletion for the new **Delete whole word** behavior when manual spacing or autospace mode is active. +- Forced multi-part internals (join word parts, full-word suggestions, typed-prefix continuation) on whenever non-normal spacing is active, removing those implementation toggles from the user-facing screen. +- Changed the default after-autospace suggestion behavior to **Alternatives, then next word on space**. +- Renamed autospace timing and tap timing labels to clearer user-facing wording. +- Rebuilt `:app:assembleStandardDebug` and ran `SettingsContainerTest`. + +### Decisions Made +- Kept underlying preference keys for compatibility but made the UI present modes instead of exposing each low-level boolean. +- Defaulted the autospace mode transition to a 500 ms grace when switching from Normal/Manual to Autospace. +- Left **Tap letters while swiping** as a separate opt-in feature for now because it is a distinct runtime path; only the timing label/summary was clarified. +- Left **Improve two-thumb recognition** off by default and described the possible tradeoff, because the midline can hurt recognition if configured poorly. + +### Manual Tests — Two-thumb Mode Selectors + +| # | Steps | Expected Result | +|---|---|---| +| 1 | Open **Settings → Two-thumb typing**. | First control is **Spacing mode** with Normal, Manual, and Auto-space choices. | +| 2 | Select **Normal spacing**. | Autospace duration/backspace behavior controls are hidden; typing behaves normally. | +| 3 | Select **Manual spacing**. | Backspace behavior appears; no autospace duration controls appear. | +| 4 | Select **Auto-space after a delay**. | Duration, tap delay, autocorrect, after-autospace suggestions, and backspace behavior appear. | +| 5 | In Auto-space mode, check **After auto-space, show…** default. | New default is **Alternatives, then next-word on space** unless an older saved value exists. | +| 6 | Try each backspace behavior in Manual/Autospace modes. | Normal deletes characters, last part removes the latest fragment, whole word removes the composing/last swiped word. | +| 7 | Search settings for “join word parts”, “typed prefix”, or “full composing”. | These implementation toggles no longer appear separately. | + +### Open Questions / Next Steps +- User should confirm whether **Tap letters while swiping** should remain user-facing or be folded into the spacing mode later. +- Debug overlay expansion (different colors/shapes for fragments, taps, fingers, start/end) is still future work. From fc132a3a258ec61672166133d93d37ffea9538de Mon Sep 17 00:00:00 2001 From: lurebat Date: Sat, 23 May 2026 10:33:23 +0300 Subject: [PATCH 03/17] Remove obsolete tap-during-swipe setting Drop the old tap-during-swipe preference and suppression path now that combining and multi-part word composition cover the useful tap/swipe behavior without the flag. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Agents.md | 2 +- .../keyboard/keyboard/PointerTracker.java | 23 ------------------- .../keyboard/latin/settings/Defaults.kt | 2 -- .../keyboard/latin/settings/Settings.java | 2 -- .../latin/settings/SettingsValues.java | 6 ----- .../settings/screens/TwoThumbTypingScreen.kt | 21 ----------------- app/src/main/res/values/strings.xml | 15 +----------- dev-log.md | 23 ++++++++++++++++++- docs/FEATURES.md | 4 +--- docs/TWO_THUMB_TYPING_INTERNALS.md | 8 +++---- 10 files changed, 28 insertions(+), 78 deletions(-) diff --git a/Agents.md b/Agents.md index a3a51df45..64efac4a1 100644 --- a/Agents.md +++ b/Agents.md @@ -14,7 +14,7 @@ | Offline AI | ONNX Runtime with T5-based grammar-correction models (Offline build only) | | Custom AI Keys | 10 toolbar keys with user-defined prompts and hashtag personas | | Floating Keyboard | Detachable draggable overlay window | -| Two-thumb Typing | Nintype-style combining mode, tap-during-swipe, manual spacing, autospace grace | +| Two-thumb Typing | Nintype-style combining mode, manual spacing, autospace grace, point hinting | | UI / UX | Squircle key backgrounds, Split toolbar, Incognito icon, Clipboard search & undo, Screenshot suggestion, Emoji search | | Build flavors | `standard` (AI + internet), `offline` (ONNX, no internet), `offlinelite` (no AI at all) | diff --git a/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java b/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java index 9459a9e9a..1ac904c79 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java +++ b/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java @@ -129,12 +129,6 @@ public static void switchTo(DrawingProxy drawingProxy) { private boolean mIsDetectingGesture = false; // per PointerTracker. private static boolean sInGesture = false; - // True for this pointer when its down-event happened while another pointer's batch gesture - // was already in progress (sInGesture==true) and the pref PREF_GESTURE_TAP_DURING_SWIPE is on. - // Used in onUpEventInternal to suppress an accidental tap-keystroke if the parent gesture - // committed between this pointer's down and up. - private boolean mIsTapDuringSwipe = false; - // ---- Combining-mode tap seeding ------------------------------------------------------ // When the user taps a letter and within the combining-grace window starts a swipe, the // pure concat path ("s" + recognizer-of-"ilo") produces unreliable results because the @@ -930,12 +924,6 @@ private void onDownEvent(final int x, final int y, final long eventTime, sTypingTimeRecorder.getLastLetterTypingTime(), getActivePointerTrackerCount()); mGestureStrokeDrawingPoints.onDownEvent( seedX, seedY, mBatchInputArbiter.getElapsedTimeSinceFirstDown(seedTime)); - // Two-thumb typing: remember whether this pointer started while another pointer's - // batch gesture was already in progress. Used in onUpEventInternal to suppress a - // stray tap-keystroke if the parent gesture commits before this pointer lifts. - mIsTapDuringSwipe = sInGesture - && sv.mGestureTapDuringSwipe - && key != null && Character.isLetter(key.getCode()); } } @@ -1394,8 +1382,6 @@ private void onUpEventInternal(final int x, final int y, final long eventTime) { sTimerProxy.cancelKeyTimersOf(this); final boolean isInDraggingFinger = mIsInDraggingFinger; final boolean isInSlidingKeyInput = mIsInSlidingKeyInput; - final boolean wasTapDuringSwipe = mIsTapDuringSwipe; - mIsTapDuringSwipe = false; resetKeySelectionByDraggingFinger(); mIsDetectingGesture = false; final Key currentKey = mCurrentKey; @@ -1489,15 +1475,6 @@ eventTime, getActivePointerTrackerCount(), graceMs, this, && (currentKey.getCode() == currentRepeatingKeyCode) && !isInDraggingFinger) { return; } - // Two-thumb typing (#1.3): if this pointer went down as a child of an in-progress - // batch gesture but the parent gesture committed before our up-event, suppress the - // tap-keystroke so we don't append a stray letter to the just-committed word. - // Long-press taps (longer than PREF_GESTURE_TAP_AS_SWIPE_WINDOW_MS) fall through to - // normal behaviour so the user can still type characters after a gesture commits. - if (wasTapDuringSwipe && !sInGesture - && eventTime - mDownTime <= Settings.getValues().mGestureTapAsSwipeWindowMs) { - 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 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 54080e8d5..2cadfc31d 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt +++ b/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt @@ -128,8 +128,6 @@ object Defaults { const val PREF_GESTURE_MANUAL_SPACING = false const val PREF_GESTURE_FRAGMENT_BACKSPACE = true const val PREF_GESTURE_AUTOSPACE_GRACE_MS = 0 - const val PREF_GESTURE_TAP_DURING_SWIPE = false - const val PREF_GESTURE_TAP_AS_SWIPE_WINDOW_MS = 60 const val PREF_GESTURE_TAP_PROMOTION_MS = 0 const val PREF_GESTURE_DUAL_THUMB_HINTING = false const val PREF_GESTURE_DUAL_THUMB_MIDLINE_PCT = 50 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 96c8d1840..91f2113b9 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/Settings.java +++ b/app/src/main/java/helium314/keyboard/latin/settings/Settings.java @@ -135,8 +135,6 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang public static final String PREF_GESTURE_MANUAL_SPACING = "gesture_manual_spacing"; public static final String PREF_GESTURE_FRAGMENT_BACKSPACE = "gesture_fragment_backspace"; public static final String PREF_GESTURE_AUTOSPACE_GRACE_MS = "gesture_autospace_grace_ms"; - public static final String PREF_GESTURE_TAP_DURING_SWIPE = "gesture_tap_during_swipe"; - public static final String PREF_GESTURE_TAP_AS_SWIPE_WINDOW_MS = "gesture_tap_as_swipe_window_ms"; public static final String PREF_GESTURE_TAP_PROMOTION_MS = "gesture_tap_promotion_ms"; public static final String PREF_GESTURE_DUAL_THUMB_HINTING = "gesture_dual_thumb_hinting"; public static final String PREF_GESTURE_DUAL_THUMB_MIDLINE_PCT = "gesture_dual_thumb_midline_pct"; 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 413980c91..680b5b3ec 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java +++ b/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java @@ -119,8 +119,6 @@ public class SettingsValues { public final boolean mGestureManualSpacing; public final boolean mGestureFragmentBackspace; public final int mGestureAutospaceGraceMs; - public final boolean mGestureTapDuringSwipe; - public final int mGestureTapAsSwipeWindowMs; public final int mGestureTapPromotionMs; public final boolean mGestureDualThumbHinting; public final int mGestureDualThumbMidlinePct; @@ -332,10 +330,6 @@ public SettingsValues(final Context context, final SharedPreferences prefs, fina Defaults.PREF_GESTURE_FRAGMENT_BACKSPACE); mGestureAutospaceGraceMs = prefs.getInt(Settings.PREF_GESTURE_AUTOSPACE_GRACE_MS, Defaults.PREF_GESTURE_AUTOSPACE_GRACE_MS); - mGestureTapDuringSwipe = prefs.getBoolean(Settings.PREF_GESTURE_TAP_DURING_SWIPE, - Defaults.PREF_GESTURE_TAP_DURING_SWIPE); - mGestureTapAsSwipeWindowMs = prefs.getInt(Settings.PREF_GESTURE_TAP_AS_SWIPE_WINDOW_MS, - Defaults.PREF_GESTURE_TAP_AS_SWIPE_WINDOW_MS); mGestureTapPromotionMs = prefs.getInt(Settings.PREF_GESTURE_TAP_PROMOTION_MS, Defaults.PREF_GESTURE_TAP_PROMOTION_MS); mGestureDualThumbHinting = prefs.getBoolean(Settings.PREF_GESTURE_DUAL_THUMB_HINTING, 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 0e71d077e..06cd7cfdd 100644 --- a/app/src/main/java/helium314/keyboard/settings/screens/TwoThumbTypingScreen.kt +++ b/app/src/main/java/helium314/keyboard/settings/screens/TwoThumbTypingScreen.kt @@ -50,7 +50,6 @@ fun TwoThumbTypingScreen( val spacingMode = currentSpacingMode(prefs) val autospaceMode = spacingMode == SPACING_MODE_AUTOSPACE val nonNormalSpacing = spacingMode != SPACING_MODE_NORMAL - val tapDuringSwipe = prefs.getBoolean(Settings.PREF_GESTURE_TAP_DURING_SWIPE, Defaults.PREF_GESTURE_TAP_DURING_SWIPE) val dualThumbHinting = prefs.getBoolean(Settings.PREF_GESTURE_DUAL_THUMB_HINTING, Defaults.PREF_GESTURE_DUAL_THUMB_HINTING) val items = buildList { @@ -71,12 +70,6 @@ fun TwoThumbTypingScreen( add(SettingsWithoutKey.TWO_THUMB_BACKSPACE_BEHAVIOR) } - add(R.string.settings_category_two_thumb_typing_two_finger) - add(Settings.PREF_GESTURE_TAP_DURING_SWIPE) - if (tapDuringSwipe) { - add(Settings.PREF_GESTURE_TAP_AS_SWIPE_WINDOW_MS) - } - add(R.string.settings_category_two_thumb_typing_recognition) add(Settings.PREF_GESTURE_DUAL_THUMB_HINTING) if (dualThumbHinting) { @@ -147,20 +140,6 @@ fun createTwoThumbTypingSettings(context: Context) = listOf( R.string.two_thumb_backspace_behavior, R.string.two_thumb_backspace_behavior_summary) { TwoThumbBackspaceBehaviorPreference(it) }, - Setting(context, Settings.PREF_GESTURE_TAP_DURING_SWIPE, - R.string.two_thumb_tap_during_swipe, R.string.two_thumb_tap_during_swipe_summary) { - SwitchPreference(it, Defaults.PREF_GESTURE_TAP_DURING_SWIPE) - }, - Setting(context, Settings.PREF_GESTURE_TAP_AS_SWIPE_WINDOW_MS, - R.string.two_thumb_tap_during_swipe_duration, R.string.two_thumb_tap_during_swipe_duration_summary) { def -> - SliderPreference( - name = def.title, - key = def.key, - default = Defaults.PREF_GESTURE_TAP_AS_SWIPE_WINDOW_MS, - range = 0f..200f, - description = { stringResource(R.string.abbreviation_unit_milliseconds, it.toString()) } - ) - }, Setting(context, Settings.PREF_GESTURE_DUAL_THUMB_HINTING, R.string.two_thumb_point_hinting, R.string.two_thumb_point_hinting_summary) { SwitchPreference(it, Defaults.PREF_GESTURE_DUAL_THUMB_HINTING) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1cdc1cd0f..34bc4259b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -36,7 +36,7 @@ Preferences Gesture Typing - + Two-thumb typing (experimental) Experimental tweaks for typing with two thumbs at once @@ -213,7 +213,6 @@ Two-thumb typing Enable gesture typing first Build words from taps and swipes - Two-finger input Recognition tuning Troubleshooting Spacing mode @@ -233,18 +232,6 @@ Autospace grace period Off - - Tap during swipe - - A quick tap with another finger while swiping adds that letter to the same word - Tap letters while swiping - While one thumb is swiping, a quick tap from the other thumb can add that letter to the same gesture word. - - Max tap duration during swipe - - Taps shorter than this — while another finger is swiping — are merged into the swipe. Longer presses type the letter normally. - Maximum tap length - Taps shorter than this can join the active swipe; longer presses type the letter normally. Tap-then-swipe window diff --git a/dev-log.md b/dev-log.md index 1e3d54015..a21037cab 100644 --- a/dev-log.md +++ b/dev-log.md @@ -247,7 +247,7 @@ The two-thumb typing settings screen mixed current user-facing features, legacy/ - Two-finger input - Recognition tuning - Troubleshooting -- Renamed visible labels/summaries for the main combining grace, tap extra time, multi-part joining, typed-prefix swipe continuation, and tap-during-swipe options. +- Renamed visible labels/summaries for the main combining grace, tap extra time, multi-part joining, and typed-prefix swipe continuation options. - Removed unimplemented/dead settings from the screen and global search registry: - `PREF_GESTURE_APOSTROPHE_KEY` - `PREF_MULTIPART_JOIN_KEY_MODE` @@ -316,3 +316,24 @@ After trying the first reorganization, the user clarified the desired structure: ### Open Questions / Next Steps - User should confirm whether **Tap letters while swiping** should remain user-facing or be folded into the spacing mode later. - Debug overlay expansion (different colors/shapes for fragments, taps, fingers, start/end) is still future work. + +--- + +## 2026-05-23 — Remove obsolete tap-during-swipe setting + +### Context +The user tested several tap/swipe word combinations and found they worked without the old tap-during-swipe flag. The newer combining/multi-part word system appears to cover the useful behavior, while the old flag only suppressed quick child taps and added confusion. + +### Actions Taken +- Removed the tap-during-swipe setting and timing setting from the two-thumb settings screen and global settings registry. +- Removed the corresponding `Settings` constants, defaults, `SettingsValues` fields, and `PointerTracker` suppression path. +- Updated docs and user-facing references so the removed setting is no longer advertised. +- Built `:app:compileStandardDebugKotlin`. + +### Decisions Made +- Kept combining/multi-part tap seeding intact; only the obsolete simultaneous tap suppression flag was removed. +- Left the observed “giraffe” gesture-recognition issue as a separate investigation target, likely around gesture recognition/hinting rather than settings UI. + +### Open Questions / Next Steps +- Investigate why some `giraffe` attempts stop swipe detection after the second letter. +- Investigate why first attempts often recognize unrelated long words before learning improves. diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 72a87b272..3de0892d4 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -236,12 +236,10 @@ LeanType ships a set of opt-in tweaks for typing with two thumbs at once — mix > Side effect: when Manual spacing is on, the **Autospace before/after gesture typing** toggles under **Settings → Text correction → Space** are hidden — they become runtime no-ops. -### Tap + swipe interactions +### Combining taps and swipes | Pref | Behaviour | | :--- | :--- | -| **Tap during swipe** (`gesture_tap_during_swipe`) | When ON, a quick tap with one finger while the other is mid-swipe is treated as part of the same word — fixes the "stray letter committed before the gesture finishes" pain when typing with two thumbs simultaneously. | -| **Tap-as-swipe window** (`gesture_tap_as_swipe_window_ms`, 0–200 ms) | Only visible when Tap during swipe is on. How quickly the tap must complete to count as part of the swipe. Default 60 ms. Long-presses fall through to normal behaviour so you can still type a letter after a gesture commits. | | **Tap-then-swipe window** (`gesture_tap_promotion_ms`, 0–200 ms) | When > 0, lets you **tap one or more letters** then immediately **swipe** the rest, getting one merged word. Example: tap `p`, tap `a`, swipe `ul` → `paul`. Set to 0 to disable. The window is measured against the *last letter input* — chains grow naturally as long as each new tap arrives within the window of the previous one. | ### Visual feedback diff --git a/docs/TWO_THUMB_TYPING_INTERNALS.md b/docs/TWO_THUMB_TYPING_INTERNALS.md index f340df09e..fed0bd438 100644 --- a/docs/TWO_THUMB_TYPING_INTERNALS.md +++ b/docs/TWO_THUMB_TYPING_INTERNALS.md @@ -35,7 +35,7 @@ The PR landed in four architectural waves. The current state reflects wave 4 — | Wave | Commits | What it built | Status | | ---: | :--- | :--- | :--- | -| 1 | `ea078058`–`13db794b` | Scaffolding + per-feature prefs (manual spacing, autospace grace, tap-during-swipe, tap-promotion, fragment backspace, apostrophe key, dual-thumb hinting, debug overlay) | Foundation; mostly still used | +| 1 | `ea078058`–`13db794b` | Scaffolding + per-feature prefs (manual spacing, autospace grace, tap-promotion, fragment backspace, apostrophe key, dual-thumb hinting, debug overlay) | Foundation; mostly still used | | 2 | `72acb111`, `37f45e67` | Tap-promotion refactor (drop deferred multi-tap chain in `PointerTracker`, move "extend" decision to `InputLogic`) + visual flash on autospace | Flash later replaced by progress bar | | 3 | `b921c58f`–`f53a8e97` | **Unified combining-mode state machine** (replaces wave-1/2's split grace + tap-promotion + flash) | Current production design | | 4 | `3330dfd4`–`f2d66c3d` | Toolbar toggles (AUTOSPACE / AUTO_CAP / FORCE_AUTO_CAP), forward-delete keycode, daily-driver fixes (PHANTOM after auto-commit, auto-cap gesture results) | Current | @@ -643,8 +643,6 @@ the raw key-event path. Useful for the user-imported "power" symbol layout | `PREF_COMBINING_BACKSPACE_DELETES_GESTURE_WORD` | true | ✓ | First backspace after a gesture-auto-commit deletes the whole word + space | | `PREF_GESTURE_MANUAL_SPACING` | false | ✓ | Original Nintype-style "never auto-commit" mode (orthogonal to combining grace) | | `PREF_GESTURE_FRAGMENT_BACKSPACE` | false | ✓ | Sub-option of manual spacing — backspace pops one fragment instead of one char | -| `PREF_GESTURE_TAP_DURING_SWIPE` | false | ✓ | When a finger taps mid-swipe of another finger, fold the tap into the swipe | -| `PREF_GESTURE_TAP_AS_SWIPE_WINDOW_MS` | 60 | ✓ | Max tap duration before it stops counting as part of an ongoing swipe | | `PREF_GESTURE_DUAL_THUMB_HINTING` | false | ✓ | Post-process the gesture's points before handing to the recognizer (`DualThumbHinter`) | | `PREF_GESTURE_DUAL_THUMB_MIDLINE_PCT` | 50 | ✓ | Left/right hand split for the hinter (proximity guard) | | `PREF_GESTURE_DEBUG_DRAW_POINTS` | false | ✓ | Overlay raw + synthetic gesture points (`GestureDebugPointsDrawingPreview`) | @@ -678,7 +676,7 @@ the raw key-event path. Useful for the user-imported "power" symbol layout | `latin/settings/Defaults.kt` | Matching defaults (off / 0 for behaviour-changing prefs; `true` for the autocorrect-on-autospace toggle; `250` for tap-extra) | | `latin/settings/SettingsValues.java` | Matching `public final` fields + `prefs.get*` reads in the ctor | | `latin/inputlogic/InputLogic.java` | The combining-mode state machine itself (§3.2–§3.9). Hooks on `handleNonSeparatorEvent`, `onStartBatchInput`, `onUpdateTailBatchInputCompleted`, `handleSeparatorEvent`, `handleBackspaceEvent`, `onPickSuggestionManually`, `resetComposingState`, `onCancelBatchInput`. | -| `keyboard/PointerTracker.java` | Tap-seeding statics + decision at `onDownEvent`; tap-during-swipe pointer marker + suppression at `onUpEventInternal`; `mayEndBatchInput` now always passes `graceMs = 0` so the wave-1 `BatchInputArbiter` grace path is dormant. | +| `keyboard/PointerTracker.java` | Tap-seeding statics + decision at `onDownEvent`; `mayEndBatchInput` now always passes `graceMs = 0` so the wave-1 `BatchInputArbiter` grace path is dormant. | | `keyboard/MainKeyboardView.java` | Removed the flash overlay; added `setCombiningMode` + the spacebar progress bar; new field-set + `ValueAnimator`. | | `keyboard/internal/BatchInputArbiter.java` | Wave-1 added the static grace handler (`sPendingGraceRunnable`, `continuePendingGesture`, `flushGrace`, `isGracePending`). Wave 3 doesn't activate it (we pass `graceMs = 0`) but the API surface remains for the dual-thumb-hinter and dormant pref. | | `keyboard/internal/DrawingProxy.java` | Two interface additions (`clearGestureDebugPoints`, `setGestureCommitPending`) used by the hinter and the wave-1 ellipsis preview. The ellipsis is dormant in wave 3 but the API is left so `PointerTracker`'s defensive-cleanup paths stay intact. | @@ -701,7 +699,7 @@ the raw key-event path. Useful for the user-imported "power" symbol layout ## 8. Known caveats / future work - **Gesture-recognition accuracy with two thumbs** is bounded by the native glide-typing library. The PR's seed + concat trick fixes the common single-thumb-tap-then-swipe case (`"silo"`, `"technology"`) but a simultaneous two-thumb gesture where one thumb taps mid-swipe of the other can still produce odd results — that's where `PREF_GESTURE_DUAL_THUMB_HINTING` and `PREF_GESTURE_DEBUG_DRAW_POINTS` come in, and they remain experimental. -- **Tap-as-swipe** (`PREF_GESTURE_TAP_DURING_SWIPE`) doesn't yet seed the parent stroke with the tap's coordinates — it just suppresses the stray keystroke. Future work could add seeding here too. +- **Simultaneous tap-while-swiping recognition** now relies on combining/multi-part composition and the experimental point hinter rather than a separate suppression preference. Remaining odd recognizer outputs should be investigated in the gesture data / hinting path. - **`alternatives_then_next_word` mode** eats the first space tap to swap the strip. The current implementation doesn't restart the combining-mode timer for that synthetic event (since no composing word exists at that point). Probably correct, but worth keeping an eye on. - **`tryFragmentBackspace`** (manual-spacing sub-feature) was kept from wave 1. It is independent of the combining-mode timer and only fires under manual spacing — no conflict, but it does mean two backspace-pop mechanisms coexist (one for fragments under manual spacing, one for the gesture-committed-whole-word under combining mode). From 6f0d964dbf9079b4885bd4ffaa944600d570795f Mon Sep 17 00:00:00 2001 From: lurebat Date: Sat, 23 May 2026 10:40:10 +0300 Subject: [PATCH 04/17] Enhance gesture debug overlay Color-code raw points by pointer, draw per-finger paths, mark tap-like runs and start/end points, and show synthetic hint points as blue crosses for easier two-thumb diagnosis. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../GestureDebugPointsDrawingPreview.java | 150 ++++++++++++++++-- 1 file changed, 134 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/helium314/keyboard/keyboard/internal/GestureDebugPointsDrawingPreview.java b/app/src/main/java/helium314/keyboard/keyboard/internal/GestureDebugPointsDrawingPreview.java index 5267bf9a1..08c05fd5a 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/internal/GestureDebugPointsDrawingPreview.java +++ b/app/src/main/java/helium314/keyboard/keyboard/internal/GestureDebugPointsDrawingPreview.java @@ -21,11 +21,13 @@ * *

The overlay snapshots the inputs at each batch-end (immediate or grace-deferred) and keeps * them visible until the next batch starts, so the trail is still on screen when the user - * compares it with the suggestion strip. Two colour bands distinguish the streams: + * compares it with the suggestion strip. The overlay distinguishes the streams and gesture + * structure: *

    - *
  • raw samples → small red dots, semi-transparent (60 % opaque),
  • - *
  • synthetic samples added by {@link DualThumbHinter} → slightly larger blue dots, fully - * opaque so they pop against the raw points.
  • + *
  • raw samples and connecting segments are colour-coded by pointer/finger id,
  • + *
  • short tap-like runs are outlined as squares,
  • + *
  • each pointer run has a green start ring and black end marker,
  • + *
  • synthetic samples added by {@link DualThumbHinter} are drawn as blue crosses.
  • *
* *

Everything is single-threaded on the UI / main-looper thread: {@link #updateSnapshot} is @@ -39,26 +41,68 @@ public final class GestureDebugPointsDrawingPreview extends AbstractDrawingPrevi // upstream BatchInputArbiter is free to reset its aggregate without affecting us. private int[] mRawXs; private int[] mRawYs; + private int[] mRawIds; + private int[] mRawTimes; private int[] mSyntheticXs; private int[] mSyntheticYs; + private int[] mSyntheticIds; + private int[] mSyntheticTimes; private final Paint mRawPaint = new Paint(); + private final Paint mLinePaint = new Paint(); private final Paint mSyntheticPaint = new Paint(); + private final Paint mStartPaint = new Paint(); + private final Paint mEndPaint = new Paint(); + private final Paint mTapPaint = new Paint(); /** Radius in pixels for a "raw" sample dot. Picked so dots remain visible at typical DPIs. */ private static final float RAW_RADIUS_PX = 4f; - /** Radius for "synthetic" sample dots — deliberately larger so the diff is obvious. */ - private static final float SYNTHETIC_RADIUS_PX = 7f; + private static final float SYNTHETIC_CROSS_RADIUS_PX = 8f; + private static final float TAP_MARKER_RADIUS_PX = 10f; + private static final float START_MARKER_RADIUS_PX = 9f; + private static final float END_MARKER_RADIUS_PX = 8f; + private static final int TAP_MAX_POINTS = 5; + private static final int TAP_MAX_DURATION_MS = 80; + private static final int[] POINTER_COLORS = { + Color.rgb(244, 67, 54), // red + Color.rgb(76, 175, 80), // green + Color.rgb(255, 152, 0), // orange + Color.rgb(156, 39, 176), // purple + Color.rgb(0, 188, 212), // cyan + Color.rgb(255, 235, 59), // yellow + }; public GestureDebugPointsDrawingPreview() { mRawPaint.setAntiAlias(true); - mRawPaint.setColor(Color.RED); mRawPaint.setAlpha(0x99); // 60 % opaque — raw stream is dense, so visually a wash mRawPaint.setStyle(Paint.Style.FILL); + mLinePaint.setAntiAlias(true); + mLinePaint.setAlpha(0x77); + mLinePaint.setStrokeWidth(3f); + mLinePaint.setStyle(Paint.Style.STROKE); + mSyntheticPaint.setAntiAlias(true); mSyntheticPaint.setColor(Color.BLUE); mSyntheticPaint.setAlpha(0xFF); - mSyntheticPaint.setStyle(Paint.Style.FILL); + mSyntheticPaint.setStrokeWidth(4f); + mSyntheticPaint.setStyle(Paint.Style.STROKE); + + mStartPaint.setAntiAlias(true); + mStartPaint.setColor(Color.rgb(0, 200, 83)); + mStartPaint.setAlpha(0xFF); + mStartPaint.setStrokeWidth(4f); + mStartPaint.setStyle(Paint.Style.STROKE); + + mEndPaint.setAntiAlias(true); + mEndPaint.setColor(Color.BLACK); + mEndPaint.setAlpha(0xCC); + mEndPaint.setStyle(Paint.Style.FILL); + + mTapPaint.setAntiAlias(true); + mTapPaint.setColor(Color.WHITE); + mTapPaint.setAlpha(0xDD); + mTapPaint.setStrokeWidth(3f); + mTapPaint.setStyle(Paint.Style.STROKE); } /** @@ -76,12 +120,18 @@ public GestureDebugPointsDrawingPreview() { public void updateSnapshot(@NonNull final InputPointers raw, final InputPointers synthetic) { mRawXs = copyOfLength(raw.getXCoordinates(), raw.getPointerSize()); mRawYs = copyOfLength(raw.getYCoordinates(), raw.getPointerSize()); + mRawIds = copyOfLength(raw.getPointerIds(), raw.getPointerSize()); + mRawTimes = copyOfLength(raw.getTimes(), raw.getPointerSize()); if (synthetic != null && synthetic.getPointerSize() > 0) { mSyntheticXs = copyOfLength(synthetic.getXCoordinates(), synthetic.getPointerSize()); mSyntheticYs = copyOfLength(synthetic.getYCoordinates(), synthetic.getPointerSize()); + mSyntheticIds = copyOfLength(synthetic.getPointerIds(), synthetic.getPointerSize()); + mSyntheticTimes = copyOfLength(synthetic.getTimes(), synthetic.getPointerSize()); } else { mSyntheticXs = null; mSyntheticYs = null; + mSyntheticIds = null; + mSyntheticTimes = null; } invalidateDrawingView(); } @@ -90,8 +140,12 @@ public void updateSnapshot(@NonNull final InputPointers raw, final InputPointers public void clear() { mRawXs = null; mRawYs = null; + mRawIds = null; + mRawTimes = null; mSyntheticXs = null; mSyntheticYs = null; + mSyntheticIds = null; + mSyntheticTimes = null; invalidateDrawingView(); } @@ -103,9 +157,10 @@ public void onDeallocateMemory() { @Override public void drawPreview(@NonNull final Canvas canvas) { if (!isPreviewEnabled()) return; - // Raw first so synthetic dots draw on top. - drawPoints(canvas, mRawXs, mRawYs, mRawPaint, RAW_RADIUS_PX); - drawPoints(canvas, mSyntheticXs, mSyntheticYs, mSyntheticPaint, SYNTHETIC_RADIUS_PX); + drawPointerLines(canvas); + drawRawPoints(canvas); + drawRunMarkers(canvas); + drawSyntheticPoints(canvas); } @Override @@ -114,15 +169,78 @@ public void setPreviewPosition(@NonNull final PointerTracker tracker) { // a single pointer the way the trail / floating-text previews do. } - private static void drawPoints(final Canvas canvas, final int[] xs, final int[] ys, - final Paint paint, final float radiusPx) { - if (xs == null || ys == null) return; - final int n = Math.min(xs.length, ys.length); + private void drawPointerLines(final Canvas canvas) { + if (mRawXs == null || mRawYs == null || mRawIds == null) return; + final int n = Math.min(Math.min(mRawXs.length, mRawYs.length), mRawIds.length); + for (int i = 1; i < n; i++) { + if (mRawIds[i] != mRawIds[i - 1]) continue; + mLinePaint.setColor(colorForPointer(mRawIds[i])); + canvas.drawLine(mRawXs[i - 1], mRawYs[i - 1], mRawXs[i], mRawYs[i], mLinePaint); + } + } + + private void drawRawPoints(final Canvas canvas) { + if (mRawXs == null || mRawYs == null || mRawIds == null) return; + final int n = Math.min(Math.min(mRawXs.length, mRawYs.length), mRawIds.length); for (int i = 0; i < n; i++) { - canvas.drawCircle(xs[i], ys[i], radiusPx, paint); + mRawPaint.setColor(colorForPointer(mRawIds[i])); + canvas.drawCircle(mRawXs[i], mRawYs[i], RAW_RADIUS_PX, mRawPaint); + } + } + + private void drawRunMarkers(final Canvas canvas) { + if (mRawXs == null || mRawYs == null || mRawIds == null || mRawTimes == null) return; + final int n = Math.min(Math.min(mRawXs.length, mRawYs.length), + Math.min(mRawIds.length, mRawTimes.length)); + int start = 0; + while (start < n) { + int end = start + 1; + while (end < n && mRawIds[end] == mRawIds[start]) { + end++; + } + drawRunMarker(canvas, start, end); + start = end; } } + private void drawRunMarker(final Canvas canvas, final int start, final int end) { + canvas.drawCircle(mRawXs[start], mRawYs[start], START_MARKER_RADIUS_PX, mStartPaint); + canvas.drawRect( + mRawXs[end - 1] - END_MARKER_RADIUS_PX, + mRawYs[end - 1] - END_MARKER_RADIUS_PX, + mRawXs[end - 1] + END_MARKER_RADIUS_PX, + mRawYs[end - 1] + END_MARKER_RADIUS_PX, + mEndPaint); + final int pointCount = end - start; + final int duration = mRawTimes[end - 1] - mRawTimes[start]; + if (pointCount <= TAP_MAX_POINTS && duration <= TAP_MAX_DURATION_MS) { + final int mid = start + pointCount / 2; + canvas.drawRect( + mRawXs[mid] - TAP_MARKER_RADIUS_PX, + mRawYs[mid] - TAP_MARKER_RADIUS_PX, + mRawXs[mid] + TAP_MARKER_RADIUS_PX, + mRawYs[mid] + TAP_MARKER_RADIUS_PX, + mTapPaint); + } + } + + private void drawSyntheticPoints(final Canvas canvas) { + if (mSyntheticXs == null || mSyntheticYs == null) return; + final int n = Math.min(mSyntheticXs.length, mSyntheticYs.length); + for (int i = 0; i < n; i++) { + final int x = mSyntheticXs[i]; + final int y = mSyntheticYs[i]; + canvas.drawLine(x - SYNTHETIC_CROSS_RADIUS_PX, y, x + SYNTHETIC_CROSS_RADIUS_PX, + y, mSyntheticPaint); + canvas.drawLine(x, y - SYNTHETIC_CROSS_RADIUS_PX, x, + y + SYNTHETIC_CROSS_RADIUS_PX, mSyntheticPaint); + } + } + + private static int colorForPointer(final int pointerId) { + return POINTER_COLORS[(pointerId & 0x7fffffff) % POINTER_COLORS.length]; + } + private static int[] copyOfLength(final int[] src, final int length) { if (length <= 0) return null; final int[] copy = new int[length]; From b29c18f5e61c6b03639ebb4417070fa3ea7ad3cf Mon Sep 17 00:00:00 2001 From: lurebat Date: Sat, 23 May 2026 10:49:21 +0300 Subject: [PATCH 05/17] Accumulate debug overlay across word fragments Keep gesture debug fragments visible during combining mode, add typed tap points to the overlay, and clear the overlay when combining mode ends so fragmented words can be inspected end-to-end. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../keyboard/keyboard/MainKeyboardView.java | 10 +- .../keyboard/keyboard/PointerTracker.java | 22 ++++- .../keyboard/internal/DrawingProxy.java | 3 + .../GestureDebugPointsDrawingPreview.java | 91 ++++++++++++------- 4 files changed, 90 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/helium314/keyboard/keyboard/MainKeyboardView.java b/app/src/main/java/helium314/keyboard/keyboard/MainKeyboardView.java index 3a3b20fe0..c573af171 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/MainKeyboardView.java +++ b/app/src/main/java/helium314/keyboard/keyboard/MainKeyboardView.java @@ -338,7 +338,10 @@ public void setCombiningMode(final boolean active, final long startTimeMs, final // clears immediately — invalidateKey() in the animator only refreshes the space // key's bounds, which isn't enough for the keyboard-wide tint. invalidate(); - if (!mCombiningModeActive) return; + if (!mCombiningModeActive) { + mGestureDebugPointsDrawingPreview.clear(); + return; + } if (mSpaceKey == null) return; final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f); animator.setDuration(graceMs); @@ -555,6 +558,11 @@ public void clearGestureDebugPoints() { mGestureDebugPointsDrawingPreview.clear(); } + @Override + public boolean isCombiningModeActiveForDebug() { + return mCombiningModeActive; + } + @Override public void setGestureCommitPending(final boolean pending) { mGestureFloatingTextDrawingPreview.setCommitPending(pending); diff --git a/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java b/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java index 1ac904c79..c3bdb7632 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java +++ b/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java @@ -362,6 +362,18 @@ private static void pushGestureDebugSnapshot( sDrawingProxy.setGestureDebugPoints(raw, syntheticOnly); } + private static void pushTapDebugPoint(final int x, final int y, final int pointerId, + final long eventTime) { + final SettingsValues sv = Settings.getValues(); + if (!sv.mGestureDebugDrawPoints + || (sv.mCombiningGraceMs <= 0 && !sv.mGestureManualSpacing)) { + return; + } + final InputPointers tap = new InputPointers(1); + tap.addPointer(x, y, pointerId, 0); + sDrawingProxy.setGestureDebugPoints(tap, new InputPointers(0)); + } + public static void setKeyboardActionListener(final KeyboardActionListener listener) { sListener = listener; @@ -690,9 +702,12 @@ public void onStartBatchInput() { if (DEBUG_LISTENER) { Log.d(TAG, String.format(Locale.US, "[%d] onStartBatchInput", mPointerId)); } - // Two-thumb typing (#2.1): a fresh gesture starts — wipe the previous batch's debug - // overlay so it doesn't visually mix with the in-flight gesture. - sDrawingProxy.clearGestureDebugPoints(); + // Two-thumb typing debug overlay: clear only when this is a new word. While combining + // mode is active, a fresh gesture is another fragment of the same word and should remain + // visible alongside earlier fragments. + if (!sDrawingProxy.isCombiningModeActiveForDebug()) { + sDrawingProxy.clearGestureDebugPoints(); + } sListener.onStartBatchInput(); dismissAllPopupKeysPanels(); sTimerProxy.cancelLongPressTimersOf(this); @@ -1487,6 +1502,7 @@ eventTime, getActivePointerTrackerCount(), graceMs, this, sLastLetterTapY = mKeyY; sLastLetterTapTime = eventTime; sLastLetterTapCodepoint = code; + pushTapDebugPoint(mKeyX, mKeyY, mPointerId, eventTime); } } if (isInSlidingKeyInput) { diff --git a/app/src/main/java/helium314/keyboard/keyboard/internal/DrawingProxy.java b/app/src/main/java/helium314/keyboard/keyboard/internal/DrawingProxy.java index 72ed64c78..a73f94a5b 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/internal/DrawingProxy.java +++ b/app/src/main/java/helium314/keyboard/keyboard/internal/DrawingProxy.java @@ -82,6 +82,9 @@ void setGestureDebugPoints(@NonNull helium314.keyboard.latin.common.InputPointer /** Clear the debug-points overlay (e.g. on gesture start or cancel). */ void clearGestureDebugPoints(); + /** Whether debug overlay fragments should be preserved across the next gesture start. */ + boolean isCombiningModeActiveForDebug(); + /** * Toggle a "commit pending" visual indicator on the gesture floating preview text * (feature #1.2). Shown during the autospace grace window so the user has a visible cue diff --git a/app/src/main/java/helium314/keyboard/keyboard/internal/GestureDebugPointsDrawingPreview.java b/app/src/main/java/helium314/keyboard/keyboard/internal/GestureDebugPointsDrawingPreview.java index 08c05fd5a..9c42146b8 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/internal/GestureDebugPointsDrawingPreview.java +++ b/app/src/main/java/helium314/keyboard/keyboard/internal/GestureDebugPointsDrawingPreview.java @@ -24,7 +24,7 @@ * compares it with the suggestion strip. The overlay distinguishes the streams and gesture * structure: *

    - *
  • raw samples and connecting segments are colour-coded by pointer/finger id,
  • + *
  • raw samples and connecting segments are colour-coded by word fragment,
  • *
  • short tap-like runs are outlined as squares,
  • *
  • each pointer run has a green start ring and black end marker,
  • *
  • synthetic samples added by {@link DualThumbHinter} are drawn as blue crosses.
  • @@ -43,10 +43,13 @@ public final class GestureDebugPointsDrawingPreview extends AbstractDrawingPrevi private int[] mRawYs; private int[] mRawIds; private int[] mRawTimes; + private int[] mRawFragments; private int[] mSyntheticXs; private int[] mSyntheticYs; private int[] mSyntheticIds; private int[] mSyntheticTimes; + private int[] mSyntheticFragments; + private int mNextFragmentId; private final Paint mRawPaint = new Paint(); private final Paint mLinePaint = new Paint(); @@ -62,7 +65,7 @@ public final class GestureDebugPointsDrawingPreview extends AbstractDrawingPrevi private static final float END_MARKER_RADIUS_PX = 8f; private static final int TAP_MAX_POINTS = 5; private static final int TAP_MAX_DURATION_MS = 80; - private static final int[] POINTER_COLORS = { + private static final int[] FRAGMENT_COLORS = { Color.rgb(244, 67, 54), // red Color.rgb(76, 175, 80), // green Color.rgb(255, 152, 0), // orange @@ -118,20 +121,20 @@ public GestureDebugPointsDrawingPreview() { * {@link InputPointers} or {@code null} (treated identically — nothing blue drawn). */ public void updateSnapshot(@NonNull final InputPointers raw, final InputPointers synthetic) { - mRawXs = copyOfLength(raw.getXCoordinates(), raw.getPointerSize()); - mRawYs = copyOfLength(raw.getYCoordinates(), raw.getPointerSize()); - mRawIds = copyOfLength(raw.getPointerIds(), raw.getPointerSize()); - mRawTimes = copyOfLength(raw.getTimes(), raw.getPointerSize()); + final int fragmentId = mNextFragmentId++; + final int rawSize = raw.getPointerSize(); + mRawXs = append(mRawXs, raw.getXCoordinates(), rawSize); + mRawYs = append(mRawYs, raw.getYCoordinates(), rawSize); + mRawIds = append(mRawIds, raw.getPointerIds(), rawSize); + mRawTimes = append(mRawTimes, raw.getTimes(), rawSize); + mRawFragments = appendFilled(mRawFragments, fragmentId, rawSize); if (synthetic != null && synthetic.getPointerSize() > 0) { - mSyntheticXs = copyOfLength(synthetic.getXCoordinates(), synthetic.getPointerSize()); - mSyntheticYs = copyOfLength(synthetic.getYCoordinates(), synthetic.getPointerSize()); - mSyntheticIds = copyOfLength(synthetic.getPointerIds(), synthetic.getPointerSize()); - mSyntheticTimes = copyOfLength(synthetic.getTimes(), synthetic.getPointerSize()); - } else { - mSyntheticXs = null; - mSyntheticYs = null; - mSyntheticIds = null; - mSyntheticTimes = null; + final int syntheticSize = synthetic.getPointerSize(); + mSyntheticXs = append(mSyntheticXs, synthetic.getXCoordinates(), syntheticSize); + mSyntheticYs = append(mSyntheticYs, synthetic.getYCoordinates(), syntheticSize); + mSyntheticIds = append(mSyntheticIds, synthetic.getPointerIds(), syntheticSize); + mSyntheticTimes = append(mSyntheticTimes, synthetic.getTimes(), syntheticSize); + mSyntheticFragments = appendFilled(mSyntheticFragments, fragmentId, syntheticSize); } invalidateDrawingView(); } @@ -142,10 +145,13 @@ public void clear() { mRawYs = null; mRawIds = null; mRawTimes = null; + mRawFragments = null; mSyntheticXs = null; mSyntheticYs = null; mSyntheticIds = null; mSyntheticTimes = null; + mSyntheticFragments = null; + mNextFragmentId = 0; invalidateDrawingView(); } @@ -170,32 +176,36 @@ public void setPreviewPosition(@NonNull final PointerTracker tracker) { } private void drawPointerLines(final Canvas canvas) { - if (mRawXs == null || mRawYs == null || mRawIds == null) return; - final int n = Math.min(Math.min(mRawXs.length, mRawYs.length), mRawIds.length); + if (mRawXs == null || mRawYs == null || mRawIds == null || mRawFragments == null) return; + final int n = Math.min(Math.min(mRawXs.length, mRawYs.length), + Math.min(mRawIds.length, mRawFragments.length)); for (int i = 1; i < n; i++) { - if (mRawIds[i] != mRawIds[i - 1]) continue; - mLinePaint.setColor(colorForPointer(mRawIds[i])); + if (mRawIds[i] != mRawIds[i - 1] + || mRawFragments[i] != mRawFragments[i - 1]) continue; + mLinePaint.setColor(colorForFragment(mRawFragments[i])); canvas.drawLine(mRawXs[i - 1], mRawYs[i - 1], mRawXs[i], mRawYs[i], mLinePaint); } } private void drawRawPoints(final Canvas canvas) { - if (mRawXs == null || mRawYs == null || mRawIds == null) return; - final int n = Math.min(Math.min(mRawXs.length, mRawYs.length), mRawIds.length); + if (mRawXs == null || mRawYs == null || mRawFragments == null) return; + final int n = Math.min(Math.min(mRawXs.length, mRawYs.length), mRawFragments.length); for (int i = 0; i < n; i++) { - mRawPaint.setColor(colorForPointer(mRawIds[i])); + mRawPaint.setColor(colorForFragment(mRawFragments[i])); canvas.drawCircle(mRawXs[i], mRawYs[i], RAW_RADIUS_PX, mRawPaint); } } private void drawRunMarkers(final Canvas canvas) { - if (mRawXs == null || mRawYs == null || mRawIds == null || mRawTimes == null) return; + if (mRawXs == null || mRawYs == null || mRawIds == null || mRawTimes == null + || mRawFragments == null) return; final int n = Math.min(Math.min(mRawXs.length, mRawYs.length), - Math.min(mRawIds.length, mRawTimes.length)); + Math.min(Math.min(mRawIds.length, mRawTimes.length), mRawFragments.length)); int start = 0; while (start < n) { int end = start + 1; - while (end < n && mRawIds[end] == mRawIds[start]) { + while (end < n && mRawIds[end] == mRawIds[start] + && mRawFragments[end] == mRawFragments[start]) { end++; } drawRunMarker(canvas, start, end); @@ -237,14 +247,31 @@ private void drawSyntheticPoints(final Canvas canvas) { } } - private static int colorForPointer(final int pointerId) { - return POINTER_COLORS[(pointerId & 0x7fffffff) % POINTER_COLORS.length]; + private static int colorForFragment(final int fragmentId) { + return FRAGMENT_COLORS[(fragmentId & 0x7fffffff) % FRAGMENT_COLORS.length]; + } + + private static int[] append(final int[] existing, final int[] src, final int length) { + if (length <= 0) return existing; + final int oldLength = existing == null ? 0 : existing.length; + final int[] result = new int[oldLength + length]; + if (existing != null) { + System.arraycopy(existing, 0, result, 0, oldLength); + } + System.arraycopy(src, 0, result, oldLength, length); + return result; } - private static int[] copyOfLength(final int[] src, final int length) { - if (length <= 0) return null; - final int[] copy = new int[length]; - System.arraycopy(src, 0, copy, 0, length); - return copy; + private static int[] appendFilled(final int[] existing, final int value, final int length) { + if (length <= 0) return existing; + final int oldLength = existing == null ? 0 : existing.length; + final int[] result = new int[oldLength + length]; + if (existing != null) { + System.arraycopy(existing, 0, result, 0, oldLength); + } + for (int i = oldLength; i < result.length; i++) { + result[i] = value; + } + return result; } } From 6705726cd84fdf62a44ee84d4232726181e78916 Mon Sep 17 00:00:00 2001 From: lurebat Date: Sat, 23 May 2026 10:56:13 +0300 Subject: [PATCH 06/17] Keep debug overlay visible after word commit Persist accumulated debug fragments until the next word starts, darken later fragments progressively, and log fragment/tap/clear events while debug points are enabled. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../keyboard/keyboard/MainKeyboardView.java | 5 +- .../keyboard/keyboard/PointerTracker.java | 3 ++ .../GestureDebugPointsDrawingPreview.java | 48 ++++++++++++++----- 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/helium314/keyboard/keyboard/MainKeyboardView.java b/app/src/main/java/helium314/keyboard/keyboard/MainKeyboardView.java index c573af171..7da95ec13 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/MainKeyboardView.java +++ b/app/src/main/java/helium314/keyboard/keyboard/MainKeyboardView.java @@ -338,10 +338,7 @@ public void setCombiningMode(final boolean active, final long startTimeMs, final // clears immediately — invalidateKey() in the animator only refreshes the space // key's bounds, which isn't enough for the keyboard-wide tint. invalidate(); - if (!mCombiningModeActive) { - mGestureDebugPointsDrawingPreview.clear(); - return; - } + if (!mCombiningModeActive) return; if (mSpaceKey == null) return; final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f); animator.setDuration(graceMs); diff --git a/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java b/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java index c3bdb7632..6b218a980 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java +++ b/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java @@ -359,6 +359,8 @@ private static DualThumbHinter.Result applyDualThumbHinting( private static void pushGestureDebugSnapshot( final InputPointers raw, final InputPointers syntheticOnly) { if (!Settings.getValues().mGestureDebugDrawPoints) return; + Log.d(TAG, "debug gesture fragment raw=" + raw.getPointerSize() + + " synthetic=" + syntheticOnly.getPointerSize()); sDrawingProxy.setGestureDebugPoints(raw, syntheticOnly); } @@ -371,6 +373,7 @@ private static void pushTapDebugPoint(final int x, final int y, final int pointe } final InputPointers tap = new InputPointers(1); tap.addPointer(x, y, pointerId, 0); + Log.d(TAG, "debug tap fragment x=" + x + " y=" + y + " pointer=" + pointerId); sDrawingProxy.setGestureDebugPoints(tap, new InputPointers(0)); } diff --git a/app/src/main/java/helium314/keyboard/keyboard/internal/GestureDebugPointsDrawingPreview.java b/app/src/main/java/helium314/keyboard/keyboard/internal/GestureDebugPointsDrawingPreview.java index 9c42146b8..7ace21fa5 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/internal/GestureDebugPointsDrawingPreview.java +++ b/app/src/main/java/helium314/keyboard/keyboard/internal/GestureDebugPointsDrawingPreview.java @@ -12,6 +12,7 @@ import helium314.keyboard.keyboard.PointerTracker; import helium314.keyboard.latin.common.InputPointers; +import helium314.keyboard.latin.utils.Log; /** * Visual debug overlay (feature #2.1). Draws the points the gesture library actually sees, @@ -24,7 +25,8 @@ * compares it with the suggestion strip. The overlay distinguishes the streams and gesture * structure: *
      - *
    • raw samples and connecting segments are colour-coded by word fragment,
    • + *
    • raw samples and connecting segments are colour-coded by word fragment, with later + * fragments drawn darker,
    • *
    • short tap-like runs are outlined as squares,
    • *
    • each pointer run has a green start ring and black end marker,
    • *
    • synthetic samples added by {@link DualThumbHinter} are drawn as blue crosses.
    • @@ -37,6 +39,7 @@ * invariant holds. */ public final class GestureDebugPointsDrawingPreview extends AbstractDrawingPreview { + private static final String TAG = "GestureDebugOverlay"; // Snapshot of the most recent batch's points. Held as primitive int[] copies so the // upstream BatchInputArbiter is free to reset its aggregate without affecting us. private int[] mRawXs; @@ -65,13 +68,13 @@ public final class GestureDebugPointsDrawingPreview extends AbstractDrawingPrevi private static final float END_MARKER_RADIUS_PX = 8f; private static final int TAP_MAX_POINTS = 5; private static final int TAP_MAX_DURATION_MS = 80; - private static final int[] FRAGMENT_COLORS = { - Color.rgb(244, 67, 54), // red - Color.rgb(76, 175, 80), // green - Color.rgb(255, 152, 0), // orange - Color.rgb(156, 39, 176), // purple - Color.rgb(0, 188, 212), // cyan - Color.rgb(255, 235, 59), // yellow + private static final int[] BASE_FRAGMENT_COLORS = { + Color.rgb(244, 67, 54), // red + Color.rgb(33, 150, 243), // blue + Color.rgb(76, 175, 80), // green + Color.rgb(255, 152, 0), // orange + Color.rgb(156, 39, 176), // purple + Color.rgb(0, 188, 212), // cyan }; public GestureDebugPointsDrawingPreview() { @@ -123,13 +126,18 @@ public GestureDebugPointsDrawingPreview() { public void updateSnapshot(@NonNull final InputPointers raw, final InputPointers synthetic) { final int fragmentId = mNextFragmentId++; final int rawSize = raw.getPointerSize(); + final int syntheticSize = synthetic == null ? 0 : synthetic.getPointerSize(); + Log.d(TAG, "fragment=" + fragmentId + + " raw=" + rawSize + + " synthetic=" + syntheticSize + + " totalBefore=" + (mRawXs == null ? 0 : mRawXs.length) + + " tapLike=" + isTapLike(raw)); mRawXs = append(mRawXs, raw.getXCoordinates(), rawSize); mRawYs = append(mRawYs, raw.getYCoordinates(), rawSize); mRawIds = append(mRawIds, raw.getPointerIds(), rawSize); mRawTimes = append(mRawTimes, raw.getTimes(), rawSize); mRawFragments = appendFilled(mRawFragments, fragmentId, rawSize); - if (synthetic != null && synthetic.getPointerSize() > 0) { - final int syntheticSize = synthetic.getPointerSize(); + if (syntheticSize > 0) { mSyntheticXs = append(mSyntheticXs, synthetic.getXCoordinates(), syntheticSize); mSyntheticYs = append(mSyntheticYs, synthetic.getYCoordinates(), syntheticSize); mSyntheticIds = append(mSyntheticIds, synthetic.getPointerIds(), syntheticSize); @@ -141,6 +149,11 @@ public void updateSnapshot(@NonNull final InputPointers raw, final InputPointers /** Drop the overlay (e.g. on gesture cancel or view teardown). */ public void clear() { + if (mRawXs != null || mSyntheticXs != null) { + Log.d(TAG, "clear fragments=" + mNextFragmentId + + " rawTotal=" + (mRawXs == null ? 0 : mRawXs.length) + + " syntheticTotal=" + (mSyntheticXs == null ? 0 : mSyntheticXs.length)); + } mRawXs = null; mRawYs = null; mRawIds = null; @@ -248,7 +261,13 @@ private void drawSyntheticPoints(final Canvas canvas) { } private static int colorForFragment(final int fragmentId) { - return FRAGMENT_COLORS[(fragmentId & 0x7fffffff) % FRAGMENT_COLORS.length]; + final int baseColor = BASE_FRAGMENT_COLORS[(fragmentId & 0x7fffffff) + % BASE_FRAGMENT_COLORS.length]; + final float factor = Math.max(0.35f, 1.0f - 0.12f * fragmentId); + return Color.rgb( + (int)(Color.red(baseColor) * factor), + (int)(Color.green(baseColor) * factor), + (int)(Color.blue(baseColor) * factor)); } private static int[] append(final int[] existing, final int[] src, final int length) { @@ -274,4 +293,11 @@ private static int[] appendFilled(final int[] existing, final int value, final i } return result; } + + private static boolean isTapLike(@NonNull final InputPointers pointers) { + final int size = pointers.getPointerSize(); + if (size == 0 || size > TAP_MAX_POINTS) return false; + final int[] times = pointers.getTimes(); + return times[size - 1] - times[0] <= TAP_MAX_DURATION_MS; + } } From 5a520178b8bbf9cb488eb3ea25a0ac771a862383 Mon Sep 17 00:00:00 2001 From: lurebat Date: Sat, 23 May 2026 11:04:50 +0300 Subject: [PATCH 07/17] Clear debug overlay on next word start Keep accumulated gesture debug fragments visible after commit, clear them on the first tap or swipe after a space boundary, and use stronger fragment color cycling with progressive darkening. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../keyboard/keyboard/MainKeyboardView.java | 5 ++++ .../keyboard/internal/DrawingProxy.java | 3 ++ .../GestureDebugPointsDrawingPreview.java | 29 ++++++++++--------- .../keyboard/latin/inputlogic/InputLogic.java | 16 ++++++++++ 4 files changed, 39 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/helium314/keyboard/keyboard/MainKeyboardView.java b/app/src/main/java/helium314/keyboard/keyboard/MainKeyboardView.java index 7da95ec13..d2931e9ce 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/MainKeyboardView.java +++ b/app/src/main/java/helium314/keyboard/keyboard/MainKeyboardView.java @@ -560,6 +560,11 @@ public boolean isCombiningModeActiveForDebug() { return mCombiningModeActive; } + @Override + public boolean hasGestureDebugPoints() { + return mGestureDebugPointsDrawingPreview.hasSnapshot(); + } + @Override public void setGestureCommitPending(final boolean pending) { mGestureFloatingTextDrawingPreview.setCommitPending(pending); diff --git a/app/src/main/java/helium314/keyboard/keyboard/internal/DrawingProxy.java b/app/src/main/java/helium314/keyboard/keyboard/internal/DrawingProxy.java index a73f94a5b..6738af31f 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/internal/DrawingProxy.java +++ b/app/src/main/java/helium314/keyboard/keyboard/internal/DrawingProxy.java @@ -85,6 +85,9 @@ void setGestureDebugPoints(@NonNull helium314.keyboard.latin.common.InputPointer /** Whether debug overlay fragments should be preserved across the next gesture start. */ boolean isCombiningModeActiveForDebug(); + /** True when there are debug points currently visible on the overlay. */ + boolean hasGestureDebugPoints(); + /** * Toggle a "commit pending" visual indicator on the gesture floating preview text * (feature #1.2). Shown during the autospace grace window so the user has a visible cue diff --git a/app/src/main/java/helium314/keyboard/keyboard/internal/GestureDebugPointsDrawingPreview.java b/app/src/main/java/helium314/keyboard/keyboard/internal/GestureDebugPointsDrawingPreview.java index 7ace21fa5..d89bbdfe9 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/internal/GestureDebugPointsDrawingPreview.java +++ b/app/src/main/java/helium314/keyboard/keyboard/internal/GestureDebugPointsDrawingPreview.java @@ -68,13 +68,13 @@ public final class GestureDebugPointsDrawingPreview extends AbstractDrawingPrevi private static final float END_MARKER_RADIUS_PX = 8f; private static final int TAP_MAX_POINTS = 5; private static final int TAP_MAX_DURATION_MS = 80; - private static final int[] BASE_FRAGMENT_COLORS = { - Color.rgb(244, 67, 54), // red - Color.rgb(33, 150, 243), // blue - Color.rgb(76, 175, 80), // green - Color.rgb(255, 152, 0), // orange - Color.rgb(156, 39, 176), // purple - Color.rgb(0, 188, 212), // cyan + private static final float[][] FRAGMENT_HSV = { + { 0f, 0.95f, 1.00f }, // red + { 210f, 0.90f, 1.00f }, // blue + { 120f, 0.85f, 0.95f }, // green + { 35f, 0.95f, 1.00f }, // orange + { 285f, 0.85f, 0.95f }, // purple + { 180f, 0.90f, 0.95f }, // cyan }; public GestureDebugPointsDrawingPreview() { @@ -168,6 +168,10 @@ public void clear() { invalidateDrawingView(); } + public boolean hasSnapshot() { + return mRawXs != null || mSyntheticXs != null; + } + @Override public void onDeallocateMemory() { clear(); @@ -261,13 +265,10 @@ private void drawSyntheticPoints(final Canvas canvas) { } private static int colorForFragment(final int fragmentId) { - final int baseColor = BASE_FRAGMENT_COLORS[(fragmentId & 0x7fffffff) - % BASE_FRAGMENT_COLORS.length]; - final float factor = Math.max(0.35f, 1.0f - 0.12f * fragmentId); - return Color.rgb( - (int)(Color.red(baseColor) * factor), - (int)(Color.green(baseColor) * factor), - (int)(Color.blue(baseColor) * factor)); + final int positiveId = fragmentId & 0x7fffffff; + final float[] hsv = FRAGMENT_HSV[positiveId % FRAGMENT_HSV.length].clone(); + hsv[2] *= Math.max(0.45f, 1.0f - 0.16f * (positiveId / FRAGMENT_HSV.length)); + return Color.HSVToColor(hsv); } private static int[] append(final int[] existing, final int[] src, final int length) { diff --git a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java index 425a6772a..077560c3e 100644 --- a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java +++ b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java @@ -217,6 +217,17 @@ private void clearOneShotSpaceActionAndNotifyIfChanged() { } } + private void clearGestureDebugOverlayAfterSpaceIfStartingWord(final int spaceState) { + final MainKeyboardView kv = KeyboardSwitcher.getInstance().getMainKeyboardView(); + if (kv == null || !kv.hasGestureDebugPoints()) return; + if (mAutospaceJustWritten + || SpaceState.PHANTOM == spaceState + || SpaceState.WEAK == spaceState + || Constants.CODE_SPACE == mConnection.getCodePointBeforeCursor()) { + kv.clearGestureDebugPoints(); + } + } + private void consumeJoinNextActionAndNotifyIfChanged() { if (OneShotSpaceAction.consumeJoinNext()) { mLatinIME.onOneShotSpaceActionStateChanged(); @@ -625,6 +636,10 @@ public InputTransaction onCodeInput(final SettingsValues settingsValues, final InputTransaction inputTransaction = new InputTransaction(settingsValues, processedEvent, SystemClock.uptimeMillis(), mSpaceState, getActualCapsMode(settingsValues, keyboardShiftMode)); + if (!processedEvent.isFunctionalKeyEvent() + && settingsValues.isWordCodePoint(processedEvent.getCodePoint())) { + clearGestureDebugOverlayAfterSpaceIfStartingWord(inputTransaction.getSpaceState()); + } if (processedEvent.getKeyCode() != KeyCode.DELETE || inputTransaction.getTimestamp() > mLastKeyTime + Constants.LONG_PRESS_MILLISECONDS) { mDeleteCount = 0; @@ -678,6 +693,7 @@ public InputTransaction onCodeInput(final SettingsValues settingsValues, public void onStartBatchInput(final SettingsValues settingsValues, final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) { + clearGestureDebugOverlayAfterSpaceIfStartingWord(mSpaceState); markForceNextSpaceWordStarted(); // Snapshot the keyboard's shift mode BEFORE any state mutates — the shifted indicator // typically auto-clears once the gesture starts moving, so by the time From 9cf4588d105abeb858a12d88b9479073224bb75e Mon Sep 17 00:00:00 2001 From: lurebat Date: Sat, 23 May 2026 11:13:39 +0300 Subject: [PATCH 08/17] Log batch recognition decisions for debug overlay When gesture debug points are enabled, log top candidates, merge state, seed state, and final composed text to make bad multi-part recognition reproducible from logcat. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../keyboard/latin/inputlogic/InputLogic.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java index 077560c3e..1ce573451 100644 --- a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java +++ b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java @@ -3504,6 +3504,19 @@ private boolean textBeforeCursorMayBeUrlOrSimilar(final SettingsValues settingsV public void onUpdateTailBatchInputCompleted(final SettingsValues settingsValues, final SuggestedWords suggestedWords, final KeyboardSwitcher keyboardSwitcher) { String batchInputText = suggestedWords.isEmpty() ? null : suggestedWords.getWord(0); + if (settingsValues.mGestureDebugDrawPoints) { + final StringBuilder candidates = new StringBuilder(); + final int candidateCount = Math.min(suggestedWords.size(), 6); + for (int i = 0; i < candidateCount; i++) { + if (i > 0) candidates.append(" | "); + candidates.append(i).append(':').append(suggestedWords.getWord(i)); + } + Log.d(TAG, "batch candidates size=" + suggestedWords.size() + + " top=" + batchInputText + + " composingBefore=" + mWordComposer.getTypedWord() + + " extendBase=" + mWordComposer.isExtendBatchInputBaseSet() + + " candidates=[" + candidates + "]"); + } // Multi-part word composition (#1.6): when the merged-trail extend-base path was // used, the lib returns the full word but its top pick is sometimes an obscure // hyphenated/mid-cased dictionary entry ("technon-U") that ranks just above the @@ -3578,6 +3591,14 @@ public void onUpdateTailBatchInputCompleted(final SettingsValues settingsValues, mWordComposer.setExtendBatchInputBase(null); final String prevTypedWord = (extendExistingCompose && !usedMergedTrail) ? mWordComposer.getTypedWord() : ""; + if (settingsValues.mGestureDebugDrawPoints) { + Log.d(TAG, "batch merge seed=" + seedCp + + " extendExisting=" + extendExistingCompose + + " combiningExtends=" + combiningExtendsSwipe + + " usedMergedTrail=" + usedMergedTrail + + " prevTyped='" + prevTypedWord + "'" + + " chosen='" + batchInputText + "'"); + } // Auto-capitalize the first letter of a fresh-word gesture when the keyboard is in // auto-shifted / manual-shifted / shift-locked state. The gesture-recognizer always // returns lowercase, so without this fix swiping "Hello" at sentence-start types @@ -3601,6 +3622,9 @@ public void onUpdateTailBatchInputCompleted(final SettingsValues settingsValues, // commit later. mShiftModeAtGestureStart = WordComposer.CAPS_MODE_OFF; final String composedText = prevTypedWord + batchInputText; + if (settingsValues.mGestureDebugDrawPoints) { + Log.d(TAG, "batch composed='" + composedText + "'"); + } // Two-thumb typing (#1.1 + #1.4): never silently prepend an autospace when extending an // existing composing word. {@code mSpaceState} can carry PHANTOM in from prior // punctuation / suggestion-pick paths; clearing it here (without inserting) is the From 4faec4d8cda6f3ea8c7fbc8c15a85dfdc582e089 Mon Sep 17 00:00:00 2001 From: lurebat Date: Sat, 23 May 2026 11:29:50 +0300 Subject: [PATCH 09/17] Treat taps during swipe as word fragments When a second finger lands during an active gesture, keep it pending as a tap fragment unless it moves far enough to become a real gesture; commit pending taps as separate letter fragments so they don't distort the active swipe path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../keyboard/keyboard/PointerTracker.java | 53 ++++++++++++++++++- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java b/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java index 6b218a980..9362e5d3a 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java +++ b/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java @@ -129,6 +129,7 @@ public static void switchTo(DrawingProxy drawingProxy) { private boolean mIsDetectingGesture = false; // per PointerTracker. private static boolean sInGesture = false; + private boolean mPendingTapFragmentDuringGesture = false; // ---- Combining-mode tap seeding ------------------------------------------------------ // When the user taps a letter and within the combining-grace window starts a swipe, the // pure concat path ("s" + recognizer-of-"ilo") produces unreliable results because the @@ -835,6 +836,7 @@ private void onDownEvent(final int x, final int y, final long eventTime, } final Key key = getKeyOn(x, y); + final boolean wasInGestureOnDown = sInGesture; mBogusMoveEventDetector.onActualDownEvent(x, y); if (key != null && key.isModifier()) { if (sInGesture) { @@ -892,7 +894,14 @@ private void onDownEvent(final int x, final int y, final long eventTime, // disabled when the key is repeating. mIsDetectingGesture = (mKeyboard != null) && mKeyboard.mId.isAlphabetKeyboard() && key != null && !key.isModifier() && !mKeySwipeAllowed && !sInKeySwipe; + mPendingTapFragmentDuringGesture = wasInGestureOnDown + && mIsDetectingGesture + && key != null + && Character.isLetter(key.getCode()); if (mIsDetectingGesture) { + if (mPendingTapFragmentDuringGesture) { + return; + } // Combining-mode tap seeding: if the user just tapped a letter within the // (base + tap-extra) combining grace window, prepend that tap's position+time as // the down-event for this gesture. The recognizer sees a continuous stroke from @@ -1029,7 +1038,7 @@ private boolean isSwiper(final int code) { private void onGestureMoveEvent(final int x, final int y, final long eventTime, final boolean isMajorEvent, final Key key) { - if (!mIsDetectingGesture || sInKeySwipe) { + if (!mIsDetectingGesture || mPendingTapFragmentDuringGesture || sInKeySwipe) { return; } final boolean onValidArea = mBatchInputArbiter.addMoveEventPoint( @@ -1069,7 +1078,7 @@ private void onMoveEvent(final int x, final int y, final long eventTime, final M return; } - if (sGestureEnabler.shouldHandleGesture() && me != null) { + if (sGestureEnabler.shouldHandleGesture() && me != null && !mPendingTapFragmentDuringGesture) { // Add historical points to gesture path. final int pointerIndex = me.findPointerIndex(mPointerId); final int historicalSize = me.getHistorySize(); @@ -1322,6 +1331,24 @@ private void onKeySwipe(final int code, final int x, final int y, final long eve private void onMoveEventInternal(final int x, final int y, final long eventTime) { final Key oldKey = mCurrentKey; + if (mPendingTapFragmentDuringGesture) { + final int dX = x - mStartX; + final int dY = y - mStartY; + if (dX * dX + dY * dY < sPointerStep * sPointerStep) { + return; + } + if (DEBUG_MODE || Settings.getValues().mGestureDebugDrawPoints) { + Log.d(TAG, String.format(Locale.US, + "[%d] promote tap-fragment to gesture dX=%d dY=%d", + mPointerId, dX, dY)); + } + mPendingTapFragmentDuringGesture = false; + mBatchInputArbiter.addDownEventPoint(mStartX, mStartY, mDownTime, + sTypingTimeRecorder.getLastLetterTypingTime(), getActivePointerTrackerCount()); + mGestureStrokeDrawingPoints.onDownEvent( + mStartX, mStartY, mBatchInputArbiter.getElapsedTimeSinceFirstDown(mDownTime)); + } + // todo (later): move key swipe stuff to KeyboardActionListener (and finally // extend it) if (mKeySwipeAllowed) { @@ -1409,6 +1436,27 @@ private void onUpEventInternal(final int x, final int y, final long eventTime) { // Release the last pressed key. setReleasedKeyGraphics(currentKey, true); + if (mPendingTapFragmentDuringGesture) { + mPendingTapFragmentDuringGesture = false; + if (!mIsTrackingForActionDisabled && currentKey != null) { + final int code = currentKey.getCode(); + if (code > 0 && Character.isLetter(code)) { + if (Settings.getValues().mGestureDebugDrawPoints) { + Log.d(TAG, String.format(Locale.US, + "[%d] commit tap-fragment during gesture: %s", + mPointerId, Constants.printableCode(code))); + } + callListenerOnCodeInput(currentKey, code, mKeyX, mKeyY, eventTime, false); + sLastLetterTapX = mKeyX; + sLastLetterTapY = mKeyY; + sLastLetterTapTime = eventTime; + sLastLetterTapCodepoint = code; + pushTapDebugPoint(mKeyX, mKeyY, mPointerId, eventTime); + } + } + return; + } + if (mInHorizontalSwipe && currentKey.getCode() == KeyCode.DELETE) { sListener.onUpWithDeletePointerActive(); } @@ -1597,6 +1645,7 @@ private void onCancelEvent(final int x, final int y, final long eventTime) { private void onCancelEventInternal() { sTimerProxy.cancelKeyTimersOf(this); + mPendingTapFragmentDuringGesture = false; setReleasedKeyGraphics(mCurrentKey, true); resetKeySelectionByDraggingFinger(); dismissPopupKeysPanel(); From f3fbe916f327eb320ee2bdd9a6da2bfd24d4c305 Mon Sep 17 00:00:00 2001 From: lurebat Date: Sat, 23 May 2026 11:39:51 +0300 Subject: [PATCH 10/17] Keep in-gesture tap fragments pending on same key Avoid promoting intended tap fragments into gestures because of small finger jitter; promote only after the pending tap leaves its original key. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../helium314/keyboard/keyboard/PointerTracker.java | 13 ++++++++----- 1 file changed, 8 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 9362e5d3a..8f46771ed 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java +++ b/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java @@ -1332,15 +1332,18 @@ private void onMoveEventInternal(final int x, final int y, final long eventTime) final Key oldKey = mCurrentKey; if (mPendingTapFragmentDuringGesture) { - final int dX = x - mStartX; - final int dY = y - mStartY; - if (dX * dX + dY * dY < sPointerStep * sPointerStep) { + final Key keyOnMove = getKeyOn(x, y); + if (keyOnMove == oldKey) { return; } + final int dX = x - mStartX; + final int dY = y - mStartY; if (DEBUG_MODE || Settings.getValues().mGestureDebugDrawPoints) { Log.d(TAG, String.format(Locale.US, - "[%d] promote tap-fragment to gesture dX=%d dY=%d", - mPointerId, dX, dY)); + "[%d] promote tap-fragment to gesture dX=%d dY=%d from=%s to=%s", + mPointerId, dX, dY, + oldKey == null ? "none" : Constants.printableCode(oldKey.getCode()), + keyOnMove == null ? "none" : Constants.printableCode(keyOnMove.getCode()))); } mPendingTapFragmentDuringGesture = false; mBatchInputArbiter.addDownEventPoint(mStartX, mStartY, mDownTime, From d95d61e2f5b1c05bb487bcbe8aec04adf303eaeb Mon Sep 17 00:00:00 2001 From: lurebat Date: Sat, 23 May 2026 15:34:37 +0300 Subject: [PATCH 11/17] Add swipe-gated autospace setting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../keyboard/latin/inputlogic/InputLogic.java | 23 +++++-- .../keyboard/latin/settings/Defaults.kt | 1 + .../keyboard/latin/settings/Settings.java | 1 + .../latin/settings/SettingsValues.java | 4 ++ .../settings/screens/TwoThumbTypingScreen.kt | 6 ++ app/src/main/res/values/strings.xml | 2 + .../keyboard/latin/InputLogicTest.kt | 64 ++++++++++++++++++- .../settings/SettingsContainerTest.kt | 7 ++ dev-log.md | 49 ++++++++++++++ 9 files changed, 150 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java index 1ce573451..f41a68bef 100644 --- a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java +++ b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java @@ -153,6 +153,7 @@ public final class InputLogic { private final Handler mCombiningHandler = new Handler(Looper.getMainLooper()); @Nullable private Runnable mPendingCombiningCommit; private boolean mInCombiningMode; + private boolean mCombiningWordHasGestureFragment; // Keeps track of most recently inserted text (multi-character key) for // reverting @@ -942,6 +943,8 @@ private void enterCombiningMode(final SettingsValues settingsValues, final boole if (kv != null) { final boolean showAutospaceIndicator = settingsValues.shouldInsertSpacesAutomatically() && settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces + && (!settingsValues.mCombiningAutospaceOnlyAfterGesture + || mCombiningWordHasGestureFragment) && !mSuppressAutospaceForForceNextSpace; kv.setCombiningMode(showAutospaceIndicator, startTime, graceMs); } @@ -1015,6 +1018,7 @@ void cancelCombiningMode() { mAutoCommitRevertLength = 0; mLastGestureCommittedLength = 0; mAutospaceJustWritten = false; + mCombiningWordHasGestureFragment = false; if (mInCombiningMode) { mInCombiningMode = false; final MainKeyboardView kv = KeyboardSwitcher.getInstance().getMainKeyboardView(); @@ -1085,7 +1089,7 @@ private void onCombiningGraceExpired() { // Capture whether the word being committed by this timer came from a gesture. We // use this AFTER commit to decide whether to arm the "backspace deletes whole word" // behavior. Tap-only words don't qualify — char-by-char backspace is fine for those. - final boolean wasBatchMode = mWordComposer.isBatchMode(); + final boolean wordHadGestureFragment = mCombiningWordHasGestureFragment || mWordComposer.isBatchMode(); // Snapshot the typed word's length BEFORE commit — the composing text is already in // the editor (added via setComposingTextInternal during the tap/gesture path), so the // commit itself doesn't move the cursor. We need the typed-word length plus whatever @@ -1102,7 +1106,11 @@ private void onCombiningGraceExpired() { } // Track whether the helper actually wrote a space (skipped for URL / e-mail / phantom). final int beforeSpace = mConnection.getExpectedSelectionEnd(); - insertAutomaticSpaceIfOptionsAndTextAllow(sv); + if (!sv.mCombiningAutospaceOnlyAfterGesture || wordHadGestureFragment) { + insertAutomaticSpaceIfOptionsAndTextAllow(sv); + } else { + clearOneShotSpaceActionAndNotifyIfChanged(); + } final boolean autospaceInserted = mConnection.getExpectedSelectionEnd() > beforeSpace; // If we DID insert an autospace, fix up mLastComposedWord so revertCommit (backspace + // PREF_BACKSPACE_REVERTS_AUTOCORRECT) deletes the space along with the word. Without @@ -1155,16 +1163,16 @@ private void onCombiningGraceExpired() { mAutospaceAlternativesPending = false; mAutoCommitRevertLength = 0; final String mode = sv.mCombiningAutospaceSuggestions; - if ("next_word".equals(mode)) { + if (autospaceInserted && "next_word".equals(mode)) { if (mSuggestionStripViewAccessor != null) { setSuggestedWords(SuggestedWords.getEmptyInstance()); } mLatinIME.mHandler.postUpdateSuggestionStrip(SuggestedWords.INPUT_STYLE_NONE); - } else if ("alternatives_then_next_word".equals(mode)) { + } else if (autospaceInserted && "alternatives_then_next_word".equals(mode)) { // Leave the strip alone; arm the swap-on-next-space behavior AND the revert window. mAutospaceAlternativesPending = true; mAutoCommitRevertLength = writtenChars; - } else if ("keep_alternatives".equals(mode)) { + } else if ("keep_alternatives".equals(mode) || !autospaceInserted) { // Arm the revert window so the next suggestion pick replaces the auto-committed // word rather than appending to it. mAutoCommitRevertLength = writtenChars; @@ -1172,7 +1180,7 @@ private void onCombiningGraceExpired() { // Independent of the strip behavior: if the timer just committed a GESTURE word, // arm "first backspace deletes the whole word" — see handleBackspaceEvent. Stays // 0 for tap-only commits, where char-by-char delete is the right behavior. - if (wasBatchMode) { + if (wordHadGestureFragment) { mLastGestureCommittedLength = writtenChars; } // "keep_alternatives" — fall through, do nothing. @@ -3290,6 +3298,7 @@ private void resetComposingState(final boolean alsoResetLastComposedWord) { mWordComposer.reset(); // Two-thumb typing (#1.1): composing word is wiped — drop the matching boundaries. clearFragmentBoundaries(); + mCombiningWordHasGestureFragment = false; // Combining mode is keyed on a composing word existing; if we're wiping it, the // pending timer would commit nothing useful, so cancel. cancelCombiningMode(); @@ -3559,6 +3568,7 @@ public void onUpdateTailBatchInputCompleted(final SettingsValues settingsValues, } } if (batchInputText.isEmpty()) return; + mCombiningWordHasGestureFragment = true; mConnection.beginBatchEdit(); // Two-thumb typing (#1.1 + #1.4): when either manual spacing OR tap-promotion-extend // applies, a follow-up gesture EXTENDS the existing composing word rather than @@ -3867,6 +3877,7 @@ private void commitChosenWord(final SettingsValues settingsValues, final String // Two-thumb typing (#1.1): the composing word is gone — any tracked fragment // boundaries are now stale and would point past the end of the (empty) word. clearFragmentBoundaries(); + mCombiningWordHasGestureFragment = false; if (DebugFlags.DEBUG_ENABLED) { long runTimeMillis = System.currentTimeMillis() - startTimeMillis; Log.d(TAG, "commitChosenWord() : " + runTimeMillis + " ms to run " 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 2cadfc31d..1ae6cff0f 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt +++ b/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt @@ -138,6 +138,7 @@ object Defaults { const val PREF_COMBINING_GRACE_MS = 0 const val PREF_COMBINING_AUTOCORRECT_ON_AUTOSPACE = true const val PREF_COMBINING_TAP_EXTRA_MS = 250 + const val PREF_COMBINING_AUTOSPACE_ONLY_AFTER_GESTURE = false const val PREF_COMBINING_AUTOSPACE_SUGGESTIONS = "alternatives_then_next_word" const val PREF_COMBINING_BACKSPACE_DELETES_GESTURE_WORD = true // Multi-part word composition (this branch). All default-on except the explicit join key. 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 91f2113b9..98a38f8c3 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/Settings.java +++ b/app/src/main/java/helium314/keyboard/latin/settings/Settings.java @@ -149,6 +149,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang // Additional grace added on top of PREF_COMBINING_GRACE_MS when the most recent input // was a tap (peck-typists need more headroom than swipers between letters). 0 = no extra. public static final String PREF_COMBINING_TAP_EXTRA_MS = "combining_tap_extra_ms"; + public static final String PREF_COMBINING_AUTOSPACE_ONLY_AFTER_GESTURE = "combining_autospace_only_after_gesture"; // What the suggestion strip shows after the combining grace timer auto-commits a word. // Values: "keep_alternatives" (1) | "next_word" (2, default) | "alternatives_then_next_word" (3). public static final String PREF_COMBINING_AUTOSPACE_SUGGESTIONS = "combining_autospace_suggestions"; 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 680b5b3ec..41d1ae023 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java +++ b/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java @@ -129,6 +129,7 @@ public class SettingsValues { public final int mCombiningGraceMs; public final boolean mCombiningAutocorrectOnAutospace; public final int mCombiningTapExtraMs; + public final boolean mCombiningAutospaceOnlyAfterGesture; // Raw string value: "keep_alternatives" | "next_word" | "alternatives_then_next_word" public final String mCombiningAutospaceSuggestions; public final boolean mCombiningBackspaceDeletesGestureWord; @@ -349,6 +350,9 @@ public SettingsValues(final Context context, final SharedPreferences prefs, fina Defaults.PREF_COMBINING_AUTOCORRECT_ON_AUTOSPACE); mCombiningTapExtraMs = prefs.getInt(Settings.PREF_COMBINING_TAP_EXTRA_MS, Defaults.PREF_COMBINING_TAP_EXTRA_MS); + mCombiningAutospaceOnlyAfterGesture = prefs.getBoolean( + Settings.PREF_COMBINING_AUTOSPACE_ONLY_AFTER_GESTURE, + Defaults.PREF_COMBINING_AUTOSPACE_ONLY_AFTER_GESTURE); mCombiningAutospaceSuggestions = prefs.getString(Settings.PREF_COMBINING_AUTOSPACE_SUGGESTIONS, Defaults.PREF_COMBINING_AUTOSPACE_SUGGESTIONS); final boolean nonNormalTwoThumbSpacing = mGestureManualSpacing || mCombiningGraceMs > 0; 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 06cd7cfdd..bfb15c537 100644 --- a/app/src/main/java/helium314/keyboard/settings/screens/TwoThumbTypingScreen.kt +++ b/app/src/main/java/helium314/keyboard/settings/screens/TwoThumbTypingScreen.kt @@ -63,6 +63,7 @@ fun TwoThumbTypingScreen( if (autospaceMode) { add(Settings.PREF_COMBINING_GRACE_MS) add(Settings.PREF_COMBINING_TAP_EXTRA_MS) + add(Settings.PREF_COMBINING_AUTOSPACE_ONLY_AFTER_GESTURE) add(Settings.PREF_COMBINING_AUTOCORRECT_ON_AUTOSPACE) add(Settings.PREF_COMBINING_AUTOSPACE_SUGGESTIONS) } @@ -109,6 +110,11 @@ fun createTwoThumbTypingSettings(context: Context) = listOf( R.string.combining_autocorrect_on_autospace, R.string.combining_autocorrect_on_autospace_summary) { SwitchPreference(it, Defaults.PREF_COMBINING_AUTOCORRECT_ON_AUTOSPACE) }, + Setting(context, Settings.PREF_COMBINING_AUTOSPACE_ONLY_AFTER_GESTURE, + R.string.combining_autospace_only_after_gesture, + R.string.combining_autospace_only_after_gesture_summary) { + SwitchPreference(it, Defaults.PREF_COMBINING_AUTOSPACE_ONLY_AFTER_GESTURE) + }, Setting(context, Settings.PREF_COMBINING_AUTOSPACE_SUGGESTIONS, R.string.combining_autospace_suggestions, R.string.combining_autospace_suggestions_summary) { def -> val items = listOf( diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 34bc4259b..7e708c61d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -268,6 +268,8 @@ Extra grace after a tap Extra autospace delay after taps Add this much extra time when the last input was a tapped letter, so tap-then-swipe combinations are easier to continue. + Only auto-space after swipes + When enabled, tap-only words are committed without an automatic space. Words that include a swipe still auto-space. Backspace deletes last swipe diff --git a/app/src/test/java/helium314/keyboard/latin/InputLogicTest.kt b/app/src/test/java/helium314/keyboard/latin/InputLogicTest.kt index 95f3d9326..4e22e4478 100644 --- a/app/src/test/java/helium314/keyboard/latin/InputLogicTest.kt +++ b/app/src/test/java/helium314/keyboard/latin/InputLogicTest.kt @@ -69,6 +69,7 @@ class InputLogicTest { @BeforeTest fun setUp() { + mainKeyboardView = Mockito.mock(MainKeyboardView::class.java) latinIME = Robolectric.setupService(LatinIME::class.java) // start logging only after latinIME is created, avoids showing the stack traces if library is not found ShadowLog.setupLogging() @@ -311,6 +312,66 @@ class InputLogicTest { assertEquals("hello world", textBeforeCursor) } + @Test fun tapOnlyCombiningWordDoesNotAutospaceWhenGestureGateEnabled() { + reset() + latinIME.prefs().edit { + putInt(Settings.PREF_COMBINING_GRACE_MS, 1000) + putBoolean(Settings.PREF_COMBINING_AUTOSPACE_ONLY_AFTER_GESTURE, true) + } + + chainInput("hello") + expireCombiningGrace() + + assertEquals("hello", textBeforeCursor) + assertEquals("", composingText) + + input(' ') + assertEquals("hello ", textBeforeCursor) + } + + @Test fun tapOnlyCombiningWordDoesNotShowAutospaceIndicatorWhenGestureGateEnabled() { + reset() + latinIME.prefs().edit { + putInt(Settings.PREF_COMBINING_GRACE_MS, 1000) + putBoolean(Settings.PREF_COMBINING_AUTOSPACE_ONLY_AFTER_GESTURE, true) + } + + input('h') + + Mockito.verify(mainKeyboardView, Mockito.atLeastOnce()) + .setCombiningMode(Mockito.eq(false), Mockito.anyLong(), Mockito.anyInt()) + Mockito.verify(mainKeyboardView, Mockito.never()) + .setCombiningMode(Mockito.eq(true), Mockito.anyLong(), Mockito.anyInt()) + } + + @Test fun gestureCombiningWordStillAutospacesWhenGestureGateEnabled() { + reset() + latinIME.prefs().edit { + putInt(Settings.PREF_COMBINING_GRACE_MS, 1000) + putBoolean(Settings.PREF_COMBINING_AUTOSPACE_ONLY_AFTER_GESTURE, true) + } + + gestureInput("hello") + expireCombiningGrace() + + assertEquals("hello ", textBeforeCursor) + } + + @Test fun tapThenGestureCombiningWordStillAutospacesWhenGestureGateEnabled() { + reset() + latinIME.prefs().edit { + putInt(Settings.PREF_COMBINING_GRACE_MS, 1000) + putBoolean(Settings.PREF_COMBINING_AUTOSPACE_ONLY_AFTER_GESTURE, true) + putBoolean(Settings.PREF_MULTIPART_AUTO_EXTEND_IN_COMBINING, true) + } + + chainInput("fire") + gestureInput("firetruck") + expireCombiningGrace() + + assertEquals("firetruck ", textBeforeCursor) + } + @Test fun forceAutoCapWorksWhenAutoCapIsOff() { reset() latinIME.prefs().edit { @@ -1066,6 +1127,7 @@ class InputLogicTest { private var currentInputType = InputType.TYPE_CLASS_TEXT private var currentScript = ScriptUtils.SCRIPT_LATIN +private lateinit var mainKeyboardView: MainKeyboardView private val messages = mutableListOf() // for latinIME / ShadowInputMethodService private val delayedMessages = mutableListOf() // for latinIME / ShadowInputMethodService // inputconnection stuff @@ -1283,7 +1345,7 @@ class ShadowHandler { class ShadowKeyboardSwitcher { @Implementation // basically only needed for null check - fun getMainKeyboardView(): MainKeyboardView = Mockito.mock(MainKeyboardView::class.java) + fun getMainKeyboardView(): MainKeyboardView = mainKeyboardView @Implementation // only affects view fun setKeyboard(keyboardId: Int, toggleState: KeyboardSwitcher.KeyboardSwitchState) = Unit diff --git a/app/src/test/java/helium314/keyboard/settings/SettingsContainerTest.kt b/app/src/test/java/helium314/keyboard/settings/SettingsContainerTest.kt index 99f275c1e..ac864d1c6 100644 --- a/app/src/test/java/helium314/keyboard/settings/SettingsContainerTest.kt +++ b/app/src/test/java/helium314/keyboard/settings/SettingsContainerTest.kt @@ -2,6 +2,7 @@ package helium314.keyboard.settings import android.content.Context import androidx.test.core.app.ApplicationProvider +import helium314.keyboard.latin.settings.Settings import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Before @@ -50,4 +51,10 @@ class SettingsContainerTest { } println("Filter performance: ${time / 1_000_000} ms") } + + @Test + fun twoThumbGestureGatedAutospaceSettingIsRegistered() { + assertEquals(Settings.PREF_COMBINING_AUTOSPACE_ONLY_AFTER_GESTURE, + container[Settings.PREF_COMBINING_AUTOSPACE_ONLY_AFTER_GESTURE]?.key) + } } diff --git a/dev-log.md b/dev-log.md index a21037cab..0d187dc8d 100644 --- a/dev-log.md +++ b/dev-log.md @@ -337,3 +337,52 @@ The user tested several tap/swipe word combinations and found they worked withou ### Open Questions / Next Steps - Investigate why some `giraffe` attempts stop swipe detection after the second letter. - Investigate why first attempts often recognize unrelated long words before learning improves. + +## 2026-05-23 — Gate autospace on swiped words + +### Context +The user wanted an opt-in autospace behavior for two-thumb Auto-space mode: tap-only words should commit without inserting an automatic space, while words that include a swipe should keep the existing autospace flow. + +### Actions Taken +- Added `PREF_COMBINING_AUTOSPACE_ONLY_AFTER_GESTURE` across the settings 5-file pattern: `Settings.java`, `Defaults.kt`, `SettingsValues.java`, `strings.xml`, and `TwoThumbTypingScreen.kt`. +- Added the setting to the Auto-space mode group as **Only auto-space after swipes**, defaulting off to preserve existing behavior. +- Updated `InputLogic.java` to track whether the current combining word has received a gesture fragment, including tap-then-swipe words where the composer is later downgraded out of batch mode. +- Suppressed timer-driven autospace for tap-only words when the new setting is enabled, while still committing the word and preserving suggestion-revert behavior. +- Added `InputLogicTest` coverage for tap-only suppression, gesture autospace, tap-then-gesture autospace, and `SettingsContainerTest` coverage for setting registration. +- Built `:app:assembleStandardDebug` and installed the APK on the connected device. + +### Decisions Made +- Kept explicit spaces, punctuation, Join Next, and Force Next Space paths on their existing behavior; the new setting only gates the timer's automatic space after committing a combining word. +- Used a dedicated per-word gesture flag instead of relying only on `WordComposer.isBatchMode()`, because tap-then-swipe composition can unset batch mode after merging fragments. +- When autospace is skipped, kept the suggestion strip in an alternatives/revert-friendly state instead of requesting next-word predictions, since there is no trailing space yet. + +### Manual Tests — Gesture-gated Autospace + +| # | Steps | Expected Result | +|---|---|---| +| 1 | Open **Settings → Two-thumb typing**, select **Auto-space after a delay**, and enable **Only auto-space after swipes**. | Toggle turns on; no crash. | +| 2 | Tap a word letter-by-letter and pause past the autospace delay. | Word commits, but no automatic space is inserted. | +| 3 | Swipe a word and pause past the autospace delay. | Word commits with the normal automatic space. | +| 4 | Tap a prefix, then swipe the rest of the word before the delay expires. | Combined word commits with the normal automatic space. | +| 5 | Disable **Only auto-space after swipes** and repeat a tap-only word. | Existing autospace behavior returns. | + +### Open Questions / Next Steps +- The targeted new unit tests pass. A full `InputLogicTest` run in the standard debug variant still reports unrelated existing failures (`insertLetterIntoWordHangulFails`, `revert autocorrect on delete`). + +--- + +## 2026-05-23 — Hide tap-only autospace indicator + +### Context +After enabling gesture-gated autospace, tap-only words no longer inserted an automatic space, but the first tapped letter still showed the autospace progress bar even though no autospace would happen. + +### Actions Taken +- Updated `InputLogic.enterCombiningMode()` so the spacebar autospace progress indicator only appears when autospace can actually be inserted under the current settings. +- Added regression coverage in `InputLogicTest` verifying tap-only input with **Only auto-space after swipes** enabled arms the combining timer without showing an autospace indicator. +- Rebuilt `:app:assembleStandardDebug`. + +### Decisions Made +- Kept the underlying combining commit timer active for tap-only words; only the autospace visual indicator is hidden until the current word includes a gesture fragment. + +### Open Questions / Next Steps +- APK install was not completed because ADB reported no connected devices. From 872d504e94aa4d8e1fac63609720c7ff9a9f21fb Mon Sep 17 00:00:00 2001 From: lurebat Date: Sat, 23 May 2026 17:23:35 +0300 Subject: [PATCH 12/17] Restore advanced two-thumb toggles Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../keyboard/keyboard/PointerTracker.java | 3 ++- .../keyboard/latin/inputlogic/InputLogic.java | 10 ++++++--- .../keyboard/latin/settings/Defaults.kt | 2 ++ .../keyboard/latin/settings/Settings.java | 2 ++ .../latin/settings/SettingsValues.java | 10 ++++++++- .../settings/screens/TwoThumbTypingScreen.kt | 22 +++++++++++++++++++ app/src/main/res/values/strings.xml | 4 ++++ .../keyboard/latin/InputLogicTest.kt | 19 ++++++++++++++++ .../settings/SettingsContainerTest.kt | 10 +++++++++ dev-log.md | 19 ++++++++++++++++ 10 files changed, 96 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 8f46771ed..d3d96f542 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java +++ b/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java @@ -709,7 +709,8 @@ public void onStartBatchInput() { // Two-thumb typing debug overlay: clear only when this is a new word. While combining // mode is active, a fresh gesture is another fragment of the same word and should remain // visible alongside earlier fragments. - if (!sDrawingProxy.isCombiningModeActiveForDebug()) { + if (!Settings.getValues().mGestureDebugAccumulateFragments + || !sDrawingProxy.isCombiningModeActiveForDebug()) { sDrawingProxy.clearGestureDebugPoints(); } sListener.onStartBatchInput(); diff --git a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java index 677dcce11..817b5fade 100644 --- a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java +++ b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java @@ -2298,9 +2298,13 @@ private void handleBackspaceEvent(final Event event, final InputTransaction inpu || inputTransaction.getSettingsValues().mCombiningGraceMs > 0) && inputTransaction.getSettingsValues().mCombiningBackspaceDeletesGestureWord) { final int wordLength = mWordComposer.getTypedWord().length(); - mConnection.beginBatchEdit(); - mConnection.deleteTextBeforeCursor(wordLength); - mConnection.endBatchEdit(); + if (inputTransaction.getSettingsValues().mCombiningBackspaceDeletesComposingText) { + mConnection.beginBatchEdit(); + mConnection.deleteTextBeforeCursor(wordLength); + mConnection.endBatchEdit(); + } else { + mConnection.finishComposingText(); + } mWordComposer.reset(); deletedWholeComposingWord = true; StatsUtils.onBackspaceWordDelete(wordLength); 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 a48bfed5d..6202a2860 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt +++ b/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt @@ -133,6 +133,7 @@ object Defaults { const val PREF_GESTURE_DUAL_THUMB_HINTING = false const val PREF_GESTURE_DUAL_THUMB_MIDLINE_PCT = 50 const val PREF_GESTURE_DEBUG_DRAW_POINTS = false + const val PREF_GESTURE_DEBUG_ACCUMULATE_FRAGMENTS = true const val PREF_GESTURE_APOSTROPHE_KEY = false const val PREF_AUTOSPACE_VISUAL_HINT = true // Unified combining-mode model (default 0 = off; 500 ms is a comfortable starting point). @@ -142,6 +143,7 @@ object Defaults { const val PREF_COMBINING_AUTOSPACE_ONLY_AFTER_GESTURE = false const val PREF_COMBINING_AUTOSPACE_SUGGESTIONS = "alternatives_then_next_word" const val PREF_COMBINING_BACKSPACE_DELETES_GESTURE_WORD = true + const val PREF_COMBINING_BACKSPACE_DELETES_COMPOSING_TEXT = true // Multi-part word composition (this branch). All default-on except the explicit join key. const val PREF_MULTIPART_AUTO_EXTEND_IN_COMBINING = true const val PREF_MULTIPART_FULL_WORD_SUGGESTIONS = 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 fa2b48f35..5986c0176 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/Settings.java +++ b/app/src/main/java/helium314/keyboard/latin/settings/Settings.java @@ -139,6 +139,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang public static final String PREF_GESTURE_DUAL_THUMB_HINTING = "gesture_dual_thumb_hinting"; public static final String PREF_GESTURE_DUAL_THUMB_MIDLINE_PCT = "gesture_dual_thumb_midline_pct"; public static final String PREF_GESTURE_DEBUG_DRAW_POINTS = "gesture_debug_draw_points"; + public static final String PREF_GESTURE_DEBUG_ACCUMULATE_FRAGMENTS = "gesture_debug_accumulate_fragments"; public static final String PREF_GESTURE_APOSTROPHE_KEY = "gesture_apostrophe_key"; public static final String PREF_AUTOSPACE_VISUAL_HINT = "autospace_visual_hint"; // Unified "combining-mode" model: after every composing-word-extending event (tap OR @@ -157,6 +158,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang // in one keystroke (unless an autocorrect-revert applies — that always goes first). Tap // typing is unaffected; this only kicks in when the just-committed word came from a swipe. public static final String PREF_COMBINING_BACKSPACE_DELETES_GESTURE_WORD = "combining_backspace_deletes_gesture_word"; + public static final String PREF_COMBINING_BACKSPACE_DELETES_COMPOSING_TEXT = "combining_backspace_deletes_composing_text"; // Multi-part word composition: when combining mode is armed (grace timer running), the // next swipe/tap concatenates onto the existing composing word rather than starting a // new one. Allows tech+nology -> technology and tech+y -> Techy without an explicit 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 e86edba5f..81650ec9e 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java +++ b/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java @@ -123,6 +123,7 @@ public class SettingsValues { public final boolean mGestureDualThumbHinting; public final int mGestureDualThumbMidlinePct; public final boolean mGestureDebugDrawPoints; + public final boolean mGestureDebugAccumulateFragments; public final boolean mGestureApostropheKey; public final boolean mAutospaceVisualHint; // Unified combining-mode (replaces gesture-only grace + tap-promotion). Default 0 = off. @@ -133,6 +134,7 @@ public class SettingsValues { // Raw string value: "keep_alternatives" | "next_word" | "alternatives_then_next_word" public final String mCombiningAutospaceSuggestions; public final boolean mCombiningBackspaceDeletesGestureWord; + public final boolean mCombiningBackspaceDeletesComposingText; // Multi-part word composition (this branch). public final boolean mMultipartAutoExtendInCombining; public final boolean mMultipartFullWordSuggestions; @@ -342,6 +344,9 @@ public SettingsValues(final Context context, final SharedPreferences prefs, fina Defaults.PREF_GESTURE_DUAL_THUMB_MIDLINE_PCT); mGestureDebugDrawPoints = prefs.getBoolean(Settings.PREF_GESTURE_DEBUG_DRAW_POINTS, Defaults.PREF_GESTURE_DEBUG_DRAW_POINTS); + mGestureDebugAccumulateFragments = prefs.getBoolean( + Settings.PREF_GESTURE_DEBUG_ACCUMULATE_FRAGMENTS, + Defaults.PREF_GESTURE_DEBUG_ACCUMULATE_FRAGMENTS); mGestureApostropheKey = prefs.getBoolean(Settings.PREF_GESTURE_APOSTROPHE_KEY, Defaults.PREF_GESTURE_APOSTROPHE_KEY); mAutospaceVisualHint = prefs.getBoolean(Settings.PREF_AUTOSPACE_VISUAL_HINT, @@ -362,10 +367,13 @@ public SettingsValues(final Context context, final SharedPreferences prefs, fina mCombiningBackspaceDeletesGestureWord = prefs.getBoolean( Settings.PREF_COMBINING_BACKSPACE_DELETES_GESTURE_WORD, Defaults.PREF_COMBINING_BACKSPACE_DELETES_GESTURE_WORD); + mCombiningBackspaceDeletesComposingText = prefs.getBoolean( + Settings.PREF_COMBINING_BACKSPACE_DELETES_COMPOSING_TEXT, + Defaults.PREF_COMBINING_BACKSPACE_DELETES_COMPOSING_TEXT); mMultipartAutoExtendInCombining = nonNormalTwoThumbSpacing || prefs.getBoolean( Settings.PREF_MULTIPART_AUTO_EXTEND_IN_COMBINING, Defaults.PREF_MULTIPART_AUTO_EXTEND_IN_COMBINING); - mMultipartFullWordSuggestions = nonNormalTwoThumbSpacing || prefs.getBoolean( + mMultipartFullWordSuggestions = prefs.getBoolean( Settings.PREF_MULTIPART_FULL_WORD_SUGGESTIONS, Defaults.PREF_MULTIPART_FULL_WORD_SUGGESTIONS); mMultipartTapSeedGesture = nonNormalTwoThumbSpacing || prefs.getBoolean( 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 ee5ee3d2d..e985f445d 100644 --- a/app/src/main/java/helium314/keyboard/settings/screens/TwoThumbTypingScreen.kt +++ b/app/src/main/java/helium314/keyboard/settings/screens/TwoThumbTypingScreen.kt @@ -55,7 +55,9 @@ fun TwoThumbTypingScreen( val spacingMode = currentSpacingMode(prefs) val autospaceMode = spacingMode == SPACING_MODE_AUTOSPACE val nonNormalSpacing = spacingMode != SPACING_MODE_NORMAL + val backspaceBehavior = currentBackspaceBehavior(prefs) val dualThumbHinting = prefs.getBoolean(Settings.PREF_GESTURE_DUAL_THUMB_HINTING, Defaults.PREF_GESTURE_DUAL_THUMB_HINTING) + val debugDrawPoints = prefs.getBoolean(Settings.PREF_GESTURE_DEBUG_DRAW_POINTS, Defaults.PREF_GESTURE_DEBUG_DRAW_POINTS) val items = buildList { add(R.string.settings_category_two_thumb_typing_words) @@ -68,7 +70,11 @@ fun TwoThumbTypingScreen( add(Settings.PREF_COMBINING_AUTOSPACE_SUGGESTIONS) } if (nonNormalSpacing) { + add(Settings.PREF_MULTIPART_FULL_WORD_SUGGESTIONS) add(SettingsWithoutKey.TWO_THUMB_BACKSPACE_BEHAVIOR) + if (backspaceBehavior == BACKSPACE_WORD) { + add(Settings.PREF_COMBINING_BACKSPACE_DELETES_COMPOSING_TEXT) + } } add(R.string.settings_category_two_thumb_typing_recognition) @@ -79,6 +85,9 @@ fun TwoThumbTypingScreen( add(R.string.settings_category_two_thumb_typing_troubleshooting) add(Settings.PREF_GESTURE_DEBUG_DRAW_POINTS) + if (debugDrawPoints) { + add(Settings.PREF_GESTURE_DEBUG_ACCUMULATE_FRAGMENTS) + } } SearchSettingsScreen( @@ -157,6 +166,15 @@ fun createTwoThumbTypingSettings(context: Context) = listOf( R.string.two_thumb_backspace_behavior, R.string.two_thumb_backspace_behavior_summary) { TwoThumbBackspaceBehaviorPreference(it) }, + Setting(context, Settings.PREF_COMBINING_BACKSPACE_DELETES_COMPOSING_TEXT, + R.string.combining_backspace_deletes_composing_text, + R.string.combining_backspace_deletes_composing_text_summary) { + SwitchPreference(it, Defaults.PREF_COMBINING_BACKSPACE_DELETES_COMPOSING_TEXT) + }, + Setting(context, Settings.PREF_MULTIPART_FULL_WORD_SUGGESTIONS, + R.string.multipart_full_word_suggestions, R.string.multipart_full_word_suggestions_summary) { + SwitchPreference(it, Defaults.PREF_MULTIPART_FULL_WORD_SUGGESTIONS) + }, Setting(context, Settings.PREF_GESTURE_DUAL_THUMB_HINTING, R.string.two_thumb_point_hinting, R.string.two_thumb_point_hinting_summary) { SwitchPreference(it, Defaults.PREF_GESTURE_DUAL_THUMB_HINTING) @@ -174,6 +192,10 @@ fun createTwoThumbTypingSettings(context: Context) = listOf( R.string.gesture_debug_draw_points, R.string.gesture_debug_draw_points_summary) { SwitchPreference(it, Defaults.PREF_GESTURE_DEBUG_DRAW_POINTS) }, + Setting(context, Settings.PREF_GESTURE_DEBUG_ACCUMULATE_FRAGMENTS, + R.string.gesture_debug_accumulate_fragments, R.string.gesture_debug_accumulate_fragments_summary) { + SwitchPreference(it, Defaults.PREF_GESTURE_DEBUG_ACCUMULATE_FRAGMENTS) + }, ) 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 85c1454e1..34af187c2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -274,6 +274,8 @@ Backspace deletes last swipe After a swiped word is auto-committed, the first backspace removes the whole word + space (autocorrect-revert still takes priority). + Delete live composing text + When Backspace behavior is set to whole word, also remove the currently composing text from the editor. Add this much time on top of the base grace when the last input was a tap (peck-typists often need more time between letters than swipers between gestures). @@ -300,6 +302,8 @@ Dedicated key in symbols layer 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. + Accumulate debug fragments + Keep the debug overlay across word fragments and clear it when the next word starts. Turn off to clear at each new swipe. After auto-space, show… diff --git a/app/src/test/java/helium314/keyboard/latin/InputLogicTest.kt b/app/src/test/java/helium314/keyboard/latin/InputLogicTest.kt index 782fa61ef..7ab90f319 100644 --- a/app/src/test/java/helium314/keyboard/latin/InputLogicTest.kt +++ b/app/src/test/java/helium314/keyboard/latin/InputLogicTest.kt @@ -390,6 +390,25 @@ class InputLogicTest { assertEquals("", composingText) } + @Test fun wholeWordBackspaceCanKeepManualSpacingComposingTextInEditor() { + reset() + latinIME.prefs().edit { + putBoolean(Settings.PREF_GESTURE_MANUAL_SPACING, true) + putBoolean(Settings.PREF_GESTURE_FRAGMENT_BACKSPACE, false) + putBoolean(Settings.PREF_COMBINING_BACKSPACE_DELETES_GESTURE_WORD, true) + putBoolean(Settings.PREF_COMBINING_BACKSPACE_DELETES_COMPOSING_TEXT, false) + } + + chainInput("hello") + assertEquals("hello", textBeforeCursor) + assertEquals("hello", composingText) + + functionalKeyPress(KeyCode.DELETE) + + assertEquals("hello", textBeforeCursor) + assertEquals("", composingText) + } + @Test fun forceAutoCapWorksWhenAutoCapIsOff() { reset() latinIME.prefs().edit { diff --git a/app/src/test/java/helium314/keyboard/settings/SettingsContainerTest.kt b/app/src/test/java/helium314/keyboard/settings/SettingsContainerTest.kt index 20d2d6adf..77ff16ce3 100644 --- a/app/src/test/java/helium314/keyboard/settings/SettingsContainerTest.kt +++ b/app/src/test/java/helium314/keyboard/settings/SettingsContainerTest.kt @@ -63,4 +63,14 @@ class SettingsContainerTest { fun twoThumbLowLevelBackspaceSettingIsHiddenFromSearchRegistry() { assertNull(container[Settings.PREF_COMBINING_BACKSPACE_DELETES_GESTURE_WORD]) } + + @Test + fun twoThumbAdvancedTogglesAreRegistered() { + assertEquals(Settings.PREF_COMBINING_BACKSPACE_DELETES_COMPOSING_TEXT, + container[Settings.PREF_COMBINING_BACKSPACE_DELETES_COMPOSING_TEXT]?.key) + assertEquals(Settings.PREF_MULTIPART_FULL_WORD_SUGGESTIONS, + container[Settings.PREF_MULTIPART_FULL_WORD_SUGGESTIONS]?.key) + assertEquals(Settings.PREF_GESTURE_DEBUG_ACCUMULATE_FRAGMENTS, + container[Settings.PREF_GESTURE_DEBUG_ACCUMULATE_FRAGMENTS]?.key) + } } diff --git a/dev-log.md b/dev-log.md index 1314cd974..9fdb25140 100644 --- a/dev-log.md +++ b/dev-log.md @@ -428,3 +428,22 @@ The user asked to return to PR #6, merge current `main`, and resolve the review ### Open Questions / Next Steps - Run targeted tests and push the review fixes to PR #6. + +--- + +## 2026-05-23 — Restore advanced two-thumb toggles + +### Context +After testing the PR branch, the user asked to make the live composing-text deletion and debug overlay accumulation behavior toggleable, and to restore the removed full-word suggestion setting. + +### Actions Taken +- Added a `Delete live composing text` switch shown when Backspace behavior is set to whole-word deletion. +- Added an `Accumulate debug fragments` switch under the gesture debug overlay setting. +- Restored the `Suggestions for full composing word` setting to the two-thumb screen and made the runtime value respect the switch instead of forcing it on for all non-normal spacing modes. +- Added tests covering the new setting registration and the new whole-word backspace toggle behavior. + +### Decisions Made +- Defaults preserve the current PR behavior: live composing text deletion, debug fragment accumulation, and full-word suggestions are all enabled unless the user turns them off. + +### Open Questions / Next Steps +- Run targeted tests and rebuild/install before handing back for device testing. From 639ee32d101a7b6cd3a8fa2a1e13dca96829d570 Mon Sep 17 00:00:00 2001 From: Asaf Mahlev Date: Sat, 23 May 2026 20:00:43 +0300 Subject: [PATCH 13/17] Restore tap during swipe toggle Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../keyboard/keyboard/PointerTracker.java | 5 +++-- .../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 ++ .../settings/SettingsContainerTest.kt | 2 ++ dev-log.md | 20 +++++++++++++++++++ 8 files changed, 37 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java b/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java index d3d96f542..563d4df48 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java +++ b/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java @@ -895,7 +895,9 @@ private void onDownEvent(final int x, final int y, final long eventTime, // disabled when the key is repeating. mIsDetectingGesture = (mKeyboard != null) && mKeyboard.mId.isAlphabetKeyboard() && key != null && !key.isModifier() && !mKeySwipeAllowed && !sInKeySwipe; - mPendingTapFragmentDuringGesture = wasInGestureOnDown + final SettingsValues sv = Settings.getValues(); + mPendingTapFragmentDuringGesture = sv.mGestureTapDuringSwipe + && wasInGestureOnDown && mIsDetectingGesture && key != null && Character.isLetter(key.getCode()); @@ -922,7 +924,6 @@ private void onDownEvent(final int x, final int y, final long eventTime, int seedY = y; long seedTime = eventTime; sCurrentGestureSeedCodepoint = 0; - final SettingsValues sv = Settings.getValues(); // Multi-part composition (#1.6): when the WordComposer extend-base path is // active, it already feeds the lib the full prior-fragment trail with proper // re-timed pointers. The single-point seed here would duplicate context and, 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 6202a2860..fd70ff71e 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt +++ b/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt @@ -130,6 +130,7 @@ object Defaults { const val PREF_GESTURE_FRAGMENT_BACKSPACE = true const val PREF_GESTURE_AUTOSPACE_GRACE_MS = 0 const val PREF_GESTURE_TAP_PROMOTION_MS = 0 + const val PREF_GESTURE_TAP_DURING_SWIPE = true const val PREF_GESTURE_DUAL_THUMB_HINTING = false const val PREF_GESTURE_DUAL_THUMB_MIDLINE_PCT = 50 const val PREF_GESTURE_DEBUG_DRAW_POINTS = false 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 5986c0176..fc585ffd5 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/Settings.java +++ b/app/src/main/java/helium314/keyboard/latin/settings/Settings.java @@ -136,6 +136,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang public static final String PREF_GESTURE_FRAGMENT_BACKSPACE = "gesture_fragment_backspace"; public static final String PREF_GESTURE_AUTOSPACE_GRACE_MS = "gesture_autospace_grace_ms"; public static final String PREF_GESTURE_TAP_PROMOTION_MS = "gesture_tap_promotion_ms"; + public static final String PREF_GESTURE_TAP_DURING_SWIPE = "gesture_tap_during_swipe"; public static final String PREF_GESTURE_DUAL_THUMB_HINTING = "gesture_dual_thumb_hinting"; public static final String PREF_GESTURE_DUAL_THUMB_MIDLINE_PCT = "gesture_dual_thumb_midline_pct"; public static final String PREF_GESTURE_DEBUG_DRAW_POINTS = "gesture_debug_draw_points"; 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 81650ec9e..99f3faca0 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java +++ b/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java @@ -120,6 +120,7 @@ public class SettingsValues { public final boolean mGestureFragmentBackspace; public final int mGestureAutospaceGraceMs; public final int mGestureTapPromotionMs; + public final boolean mGestureTapDuringSwipe; public final boolean mGestureDualThumbHinting; public final int mGestureDualThumbMidlinePct; public final boolean mGestureDebugDrawPoints; @@ -338,6 +339,8 @@ public SettingsValues(final Context context, final SharedPreferences prefs, fina Defaults.PREF_GESTURE_AUTOSPACE_GRACE_MS); mGestureTapPromotionMs = prefs.getInt(Settings.PREF_GESTURE_TAP_PROMOTION_MS, Defaults.PREF_GESTURE_TAP_PROMOTION_MS); + mGestureTapDuringSwipe = prefs.getBoolean(Settings.PREF_GESTURE_TAP_DURING_SWIPE, + Defaults.PREF_GESTURE_TAP_DURING_SWIPE); mGestureDualThumbHinting = prefs.getBoolean(Settings.PREF_GESTURE_DUAL_THUMB_HINTING, Defaults.PREF_GESTURE_DUAL_THUMB_HINTING); mGestureDualThumbMidlinePct = prefs.getInt(Settings.PREF_GESTURE_DUAL_THUMB_MIDLINE_PCT, 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 e985f445d..b5af3d05c 100644 --- a/app/src/main/java/helium314/keyboard/settings/screens/TwoThumbTypingScreen.kt +++ b/app/src/main/java/helium314/keyboard/settings/screens/TwoThumbTypingScreen.kt @@ -62,6 +62,7 @@ fun TwoThumbTypingScreen( val items = buildList { add(R.string.settings_category_two_thumb_typing_words) add(SettingsWithoutKey.TWO_THUMB_SPACING_MODE) + add(Settings.PREF_GESTURE_TAP_DURING_SWIPE) if (autospaceMode) { add(Settings.PREF_COMBINING_GRACE_MS) add(Settings.PREF_COMBINING_TAP_EXTRA_MS) @@ -175,6 +176,10 @@ fun createTwoThumbTypingSettings(context: Context) = listOf( R.string.multipart_full_word_suggestions, R.string.multipart_full_word_suggestions_summary) { SwitchPreference(it, Defaults.PREF_MULTIPART_FULL_WORD_SUGGESTIONS) }, + Setting(context, Settings.PREF_GESTURE_TAP_DURING_SWIPE, + R.string.gesture_tap_during_swipe, R.string.gesture_tap_during_swipe_summary) { + SwitchPreference(it, Defaults.PREF_GESTURE_TAP_DURING_SWIPE) + }, Setting(context, Settings.PREF_GESTURE_DUAL_THUMB_HINTING, R.string.two_thumb_point_hinting, R.string.two_thumb_point_hinting_summary) { SwitchPreference(it, Defaults.PREF_GESTURE_DUAL_THUMB_HINTING) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 34af187c2..da35172c0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -286,6 +286,8 @@ During the countdown, the next tap or swipe extends the current word instead of starting a new one (tech + nology → technology). Suggestions for full composing word During multi-part composition, the suggestion strip shows alternatives for the entire word so far, not just the most recent fragment. + Tap during swipe + A quick tap with another finger while swiping adds that letter to the same word. Seed swipe from typed prefix When you type some letters and then swipe while combining mode is armed, treat the swipe as a continuation of the typed prefix. Swipe continues typed prefix diff --git a/app/src/test/java/helium314/keyboard/settings/SettingsContainerTest.kt b/app/src/test/java/helium314/keyboard/settings/SettingsContainerTest.kt index 77ff16ce3..f2e80cbac 100644 --- a/app/src/test/java/helium314/keyboard/settings/SettingsContainerTest.kt +++ b/app/src/test/java/helium314/keyboard/settings/SettingsContainerTest.kt @@ -70,6 +70,8 @@ class SettingsContainerTest { container[Settings.PREF_COMBINING_BACKSPACE_DELETES_COMPOSING_TEXT]?.key) assertEquals(Settings.PREF_MULTIPART_FULL_WORD_SUGGESTIONS, container[Settings.PREF_MULTIPART_FULL_WORD_SUGGESTIONS]?.key) + assertEquals(Settings.PREF_GESTURE_TAP_DURING_SWIPE, + container[Settings.PREF_GESTURE_TAP_DURING_SWIPE]?.key) assertEquals(Settings.PREF_GESTURE_DEBUG_ACCUMULATE_FRAGMENTS, container[Settings.PREF_GESTURE_DEBUG_ACCUMULATE_FRAGMENTS]?.key) } diff --git a/dev-log.md b/dev-log.md index 9fdb25140..71ba90b81 100644 --- a/dev-log.md +++ b/dev-log.md @@ -447,3 +447,23 @@ After testing the PR branch, the user asked to make the live composing-text dele ### Open Questions / Next Steps - Run targeted tests and rebuild/install before handing back for device testing. + +--- + +## 2026-05-23 — Restore tap-during-swipe toggle + +### Context +The user pointed out that the tap-during-swipe behavior had been made hardcoded, but they had asked for it to remain a toggleable option. + +### Actions Taken +- Restored `PREF_GESTURE_TAP_DURING_SWIPE` as a settings key/default/runtime value. +- Added the `Tap during swipe` switch back to the two-thumb settings screen. +- Gated `PointerTracker`'s pending tap-fragment behavior behind the restored preference. +- Added settings registry coverage for the restored toggle. +- Built and installed the updated standard debug APK on the connected device after fully uninstalling the mismatched existing debug package. + +### Decisions Made +- Defaulted the toggle to enabled so existing PR behavior remains unchanged unless the user turns it off. + +### Open Questions / Next Steps +- Commit and push the PR update. From 8fb49ef179d4d42a03e48fdcfb62282ba13e1278 Mon Sep 17 00:00:00 2001 From: Asaf Mahlev Date: Sat, 23 May 2026 20:10:49 +0300 Subject: [PATCH 14/17] Remove tap during swipe fragments Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../keyboard/keyboard/PointerTracker.java | 59 +------------------ .../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 - .../settings/SettingsContainerTest.kt | 2 - dev-log.md | 19 ++++++ 8 files changed, 22 insertions(+), 70 deletions(-) diff --git a/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java b/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java index 563d4df48..e8f007a98 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java +++ b/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java @@ -129,7 +129,6 @@ public static void switchTo(DrawingProxy drawingProxy) { private boolean mIsDetectingGesture = false; // per PointerTracker. private static boolean sInGesture = false; - private boolean mPendingTapFragmentDuringGesture = false; // ---- Combining-mode tap seeding ------------------------------------------------------ // When the user taps a letter and within the combining-grace window starts a swipe, the // pure concat path ("s" + recognizer-of-"ilo") produces unreliable results because the @@ -837,7 +836,6 @@ private void onDownEvent(final int x, final int y, final long eventTime, } final Key key = getKeyOn(x, y); - final boolean wasInGestureOnDown = sInGesture; mBogusMoveEventDetector.onActualDownEvent(x, y); if (key != null && key.isModifier()) { if (sInGesture) { @@ -895,16 +893,7 @@ private void onDownEvent(final int x, final int y, final long eventTime, // disabled when the key is repeating. mIsDetectingGesture = (mKeyboard != null) && mKeyboard.mId.isAlphabetKeyboard() && key != null && !key.isModifier() && !mKeySwipeAllowed && !sInKeySwipe; - final SettingsValues sv = Settings.getValues(); - mPendingTapFragmentDuringGesture = sv.mGestureTapDuringSwipe - && wasInGestureOnDown - && mIsDetectingGesture - && key != null - && Character.isLetter(key.getCode()); if (mIsDetectingGesture) { - if (mPendingTapFragmentDuringGesture) { - return; - } // Combining-mode tap seeding: if the user just tapped a letter within the // (base + tap-extra) combining grace window, prepend that tap's position+time as // the down-event for this gesture. The recognizer sees a continuous stroke from @@ -924,6 +913,7 @@ private void onDownEvent(final int x, final int y, final long eventTime, int seedY = y; long seedTime = eventTime; sCurrentGestureSeedCodepoint = 0; + final SettingsValues sv = Settings.getValues(); // Multi-part composition (#1.6): when the WordComposer extend-base path is // active, it already feeds the lib the full prior-fragment trail with proper // re-timed pointers. The single-point seed here would duplicate context and, @@ -1040,7 +1030,7 @@ private boolean isSwiper(final int code) { private void onGestureMoveEvent(final int x, final int y, final long eventTime, final boolean isMajorEvent, final Key key) { - if (!mIsDetectingGesture || mPendingTapFragmentDuringGesture || sInKeySwipe) { + if (!mIsDetectingGesture || sInKeySwipe) { return; } final boolean onValidArea = mBatchInputArbiter.addMoveEventPoint( @@ -1080,7 +1070,7 @@ private void onMoveEvent(final int x, final int y, final long eventTime, final M return; } - if (sGestureEnabler.shouldHandleGesture() && me != null && !mPendingTapFragmentDuringGesture) { + if (sGestureEnabler.shouldHandleGesture() && me != null) { // Add historical points to gesture path. final int pointerIndex = me.findPointerIndex(mPointerId); final int historicalSize = me.getHistorySize(); @@ -1333,27 +1323,6 @@ private void onKeySwipe(final int code, final int x, final int y, final long eve private void onMoveEventInternal(final int x, final int y, final long eventTime) { final Key oldKey = mCurrentKey; - if (mPendingTapFragmentDuringGesture) { - final Key keyOnMove = getKeyOn(x, y); - if (keyOnMove == oldKey) { - return; - } - final int dX = x - mStartX; - final int dY = y - mStartY; - if (DEBUG_MODE || Settings.getValues().mGestureDebugDrawPoints) { - Log.d(TAG, String.format(Locale.US, - "[%d] promote tap-fragment to gesture dX=%d dY=%d from=%s to=%s", - mPointerId, dX, dY, - oldKey == null ? "none" : Constants.printableCode(oldKey.getCode()), - keyOnMove == null ? "none" : Constants.printableCode(keyOnMove.getCode()))); - } - mPendingTapFragmentDuringGesture = false; - mBatchInputArbiter.addDownEventPoint(mStartX, mStartY, mDownTime, - sTypingTimeRecorder.getLastLetterTypingTime(), getActivePointerTrackerCount()); - mGestureStrokeDrawingPoints.onDownEvent( - mStartX, mStartY, mBatchInputArbiter.getElapsedTimeSinceFirstDown(mDownTime)); - } - // todo (later): move key swipe stuff to KeyboardActionListener (and finally // extend it) if (mKeySwipeAllowed) { @@ -1441,27 +1410,6 @@ private void onUpEventInternal(final int x, final int y, final long eventTime) { // Release the last pressed key. setReleasedKeyGraphics(currentKey, true); - if (mPendingTapFragmentDuringGesture) { - mPendingTapFragmentDuringGesture = false; - if (!mIsTrackingForActionDisabled && currentKey != null) { - final int code = currentKey.getCode(); - if (code > 0 && Character.isLetter(code)) { - if (Settings.getValues().mGestureDebugDrawPoints) { - Log.d(TAG, String.format(Locale.US, - "[%d] commit tap-fragment during gesture: %s", - mPointerId, Constants.printableCode(code))); - } - callListenerOnCodeInput(currentKey, code, mKeyX, mKeyY, eventTime, false); - sLastLetterTapX = mKeyX; - sLastLetterTapY = mKeyY; - sLastLetterTapTime = eventTime; - sLastLetterTapCodepoint = code; - pushTapDebugPoint(mKeyX, mKeyY, mPointerId, eventTime); - } - } - return; - } - if (mInHorizontalSwipe && currentKey.getCode() == KeyCode.DELETE) { sListener.onUpWithDeletePointerActive(); } @@ -1650,7 +1598,6 @@ private void onCancelEvent(final int x, final int y, final long eventTime) { private void onCancelEventInternal() { sTimerProxy.cancelKeyTimersOf(this); - mPendingTapFragmentDuringGesture = false; setReleasedKeyGraphics(mCurrentKey, true); resetKeySelectionByDraggingFinger(); dismissPopupKeysPanel(); 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 fd70ff71e..6202a2860 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt +++ b/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt @@ -130,7 +130,6 @@ object Defaults { const val PREF_GESTURE_FRAGMENT_BACKSPACE = true const val PREF_GESTURE_AUTOSPACE_GRACE_MS = 0 const val PREF_GESTURE_TAP_PROMOTION_MS = 0 - const val PREF_GESTURE_TAP_DURING_SWIPE = true const val PREF_GESTURE_DUAL_THUMB_HINTING = false const val PREF_GESTURE_DUAL_THUMB_MIDLINE_PCT = 50 const val PREF_GESTURE_DEBUG_DRAW_POINTS = false 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 fc585ffd5..5986c0176 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/Settings.java +++ b/app/src/main/java/helium314/keyboard/latin/settings/Settings.java @@ -136,7 +136,6 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang public static final String PREF_GESTURE_FRAGMENT_BACKSPACE = "gesture_fragment_backspace"; public static final String PREF_GESTURE_AUTOSPACE_GRACE_MS = "gesture_autospace_grace_ms"; public static final String PREF_GESTURE_TAP_PROMOTION_MS = "gesture_tap_promotion_ms"; - public static final String PREF_GESTURE_TAP_DURING_SWIPE = "gesture_tap_during_swipe"; public static final String PREF_GESTURE_DUAL_THUMB_HINTING = "gesture_dual_thumb_hinting"; public static final String PREF_GESTURE_DUAL_THUMB_MIDLINE_PCT = "gesture_dual_thumb_midline_pct"; public static final String PREF_GESTURE_DEBUG_DRAW_POINTS = "gesture_debug_draw_points"; 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 99f3faca0..81650ec9e 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java +++ b/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java @@ -120,7 +120,6 @@ public class SettingsValues { public final boolean mGestureFragmentBackspace; public final int mGestureAutospaceGraceMs; public final int mGestureTapPromotionMs; - public final boolean mGestureTapDuringSwipe; public final boolean mGestureDualThumbHinting; public final int mGestureDualThumbMidlinePct; public final boolean mGestureDebugDrawPoints; @@ -339,8 +338,6 @@ public SettingsValues(final Context context, final SharedPreferences prefs, fina Defaults.PREF_GESTURE_AUTOSPACE_GRACE_MS); mGestureTapPromotionMs = prefs.getInt(Settings.PREF_GESTURE_TAP_PROMOTION_MS, Defaults.PREF_GESTURE_TAP_PROMOTION_MS); - mGestureTapDuringSwipe = prefs.getBoolean(Settings.PREF_GESTURE_TAP_DURING_SWIPE, - Defaults.PREF_GESTURE_TAP_DURING_SWIPE); mGestureDualThumbHinting = prefs.getBoolean(Settings.PREF_GESTURE_DUAL_THUMB_HINTING, Defaults.PREF_GESTURE_DUAL_THUMB_HINTING); mGestureDualThumbMidlinePct = prefs.getInt(Settings.PREF_GESTURE_DUAL_THUMB_MIDLINE_PCT, 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 b5af3d05c..e985f445d 100644 --- a/app/src/main/java/helium314/keyboard/settings/screens/TwoThumbTypingScreen.kt +++ b/app/src/main/java/helium314/keyboard/settings/screens/TwoThumbTypingScreen.kt @@ -62,7 +62,6 @@ fun TwoThumbTypingScreen( val items = buildList { add(R.string.settings_category_two_thumb_typing_words) add(SettingsWithoutKey.TWO_THUMB_SPACING_MODE) - add(Settings.PREF_GESTURE_TAP_DURING_SWIPE) if (autospaceMode) { add(Settings.PREF_COMBINING_GRACE_MS) add(Settings.PREF_COMBINING_TAP_EXTRA_MS) @@ -176,10 +175,6 @@ fun createTwoThumbTypingSettings(context: Context) = listOf( R.string.multipart_full_word_suggestions, R.string.multipart_full_word_suggestions_summary) { SwitchPreference(it, Defaults.PREF_MULTIPART_FULL_WORD_SUGGESTIONS) }, - Setting(context, Settings.PREF_GESTURE_TAP_DURING_SWIPE, - R.string.gesture_tap_during_swipe, R.string.gesture_tap_during_swipe_summary) { - SwitchPreference(it, Defaults.PREF_GESTURE_TAP_DURING_SWIPE) - }, Setting(context, Settings.PREF_GESTURE_DUAL_THUMB_HINTING, R.string.two_thumb_point_hinting, R.string.two_thumb_point_hinting_summary) { SwitchPreference(it, Defaults.PREF_GESTURE_DUAL_THUMB_HINTING) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index da35172c0..34af187c2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -286,8 +286,6 @@ During the countdown, the next tap or swipe extends the current word instead of starting a new one (tech + nology → technology). Suggestions for full composing word During multi-part composition, the suggestion strip shows alternatives for the entire word so far, not just the most recent fragment. - Tap during swipe - A quick tap with another finger while swiping adds that letter to the same word. Seed swipe from typed prefix When you type some letters and then swipe while combining mode is armed, treat the swipe as a continuation of the typed prefix. Swipe continues typed prefix diff --git a/app/src/test/java/helium314/keyboard/settings/SettingsContainerTest.kt b/app/src/test/java/helium314/keyboard/settings/SettingsContainerTest.kt index f2e80cbac..77ff16ce3 100644 --- a/app/src/test/java/helium314/keyboard/settings/SettingsContainerTest.kt +++ b/app/src/test/java/helium314/keyboard/settings/SettingsContainerTest.kt @@ -70,8 +70,6 @@ class SettingsContainerTest { container[Settings.PREF_COMBINING_BACKSPACE_DELETES_COMPOSING_TEXT]?.key) assertEquals(Settings.PREF_MULTIPART_FULL_WORD_SUGGESTIONS, container[Settings.PREF_MULTIPART_FULL_WORD_SUGGESTIONS]?.key) - assertEquals(Settings.PREF_GESTURE_TAP_DURING_SWIPE, - container[Settings.PREF_GESTURE_TAP_DURING_SWIPE]?.key) assertEquals(Settings.PREF_GESTURE_DEBUG_ACCUMULATE_FRAGMENTS, container[Settings.PREF_GESTURE_DEBUG_ACCUMULATE_FRAGMENTS]?.key) } diff --git a/dev-log.md b/dev-log.md index 71ba90b81..0cfe0af92 100644 --- a/dev-log.md +++ b/dev-log.md @@ -467,3 +467,22 @@ The user pointed out that the tap-during-swipe behavior had been made hardcoded, ### Open Questions / Next Steps - Commit and push the PR update. + +--- + +## 2026-05-23 — Remove tap-during-swipe fragments + +### Context +After testing, the user confirmed the tap-during-swipe fragment behavior was causing the typing problem and asked to remove both the implementation and the setting. + +### Actions Taken +- Removed the pending tap-fragment state machine from `PointerTracker`. +- Removed the `Tap during swipe` setting from the two-thumb screen and settings registry coverage. +- Removed the preference key/default/runtime read and the now-unused strings. +- Built and installed the updated standard debug APK on the connected device. + +### Decisions Made +- Kept the rest of two-thumb composing, autospace, backspace, and debug overlay options unchanged. + +### Open Questions / Next Steps +- Commit and push the PR update. From db10de174087e988c8b75e2f65581944c581c53a Mon Sep 17 00:00:00 2001 From: Asaf Mahlev Date: Sat, 23 May 2026 20:29:31 +0300 Subject: [PATCH 15/17] Address two-thumb review followups Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../keyboard/keyboard/MainKeyboardView.java | 9 ++++++++- .../keyboard/keyboard/PointerTracker.java | 6 ++++-- .../keyboard/latin/inputlogic/InputLogic.java | 6 ++++-- .../keyboard/latin/settings/Defaults.kt | 1 + .../keyboard/latin/settings/Settings.java | 1 + .../settings/screens/TwoThumbTypingScreen.kt | 15 +++++++++++--- dev-log.md | 20 +++++++++++++++++++ 7 files changed, 50 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/helium314/keyboard/keyboard/MainKeyboardView.java b/app/src/main/java/helium314/keyboard/keyboard/MainKeyboardView.java index d2931e9ce..3e9c1527a 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/MainKeyboardView.java +++ b/app/src/main/java/helium314/keyboard/keyboard/MainKeyboardView.java @@ -106,6 +106,7 @@ public final class MainKeyboardView extends KeyboardView implements DrawingProxy // ValueAnimator drives invalidations at ~60fps so the bar shrinks smoothly; on cancel // / timer-expiry we set mCombiningModeActive=false and the bar disappears next frame. private boolean mCombiningModeActive = false; + private boolean mCombiningCompositionActiveForDebug = false; private long mCombiningStartTimeMs = 0L; private int mCombiningGraceMs = 0; @Nullable private ValueAnimator mCombiningAnimator; @@ -325,6 +326,11 @@ public void setLanguageOnSpacebarAnimAlpha(final int alpha) { * No-ops when the space key isn't on the current layout (numeric / symbol). */ public void setCombiningMode(final boolean active, final long startTimeMs, final int graceMs) { + setCombiningMode(active, startTimeMs, graceMs, active); + } + + public void setCombiningMode(final boolean active, final long startTimeMs, final int graceMs, + final boolean compositionActiveForDebug) { // Stop any in-flight animator before mutating state — guarantees we don't have an // animator updating mCombiningModeActive=false's invalidation while we set true. if (mCombiningAnimator != null) { @@ -332,6 +338,7 @@ public void setCombiningMode(final boolean active, final long startTimeMs, final mCombiningAnimator = null; } mCombiningModeActive = active && graceMs > 0; + mCombiningCompositionActiveForDebug = compositionActiveForDebug; mCombiningStartTimeMs = startTimeMs; mCombiningGraceMs = graceMs; // Always invalidate the whole view once so the global tint overlay appears or @@ -557,7 +564,7 @@ public void clearGestureDebugPoints() { @Override public boolean isCombiningModeActiveForDebug() { - return mCombiningModeActive; + return mCombiningCompositionActiveForDebug; } @Override diff --git a/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java b/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java index e8f007a98..9a85256ba 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java +++ b/app/src/main/java/helium314/keyboard/keyboard/PointerTracker.java @@ -708,8 +708,10 @@ public void onStartBatchInput() { // Two-thumb typing debug overlay: clear only when this is a new word. While combining // mode is active, a fresh gesture is another fragment of the same word and should remain // visible alongside earlier fragments. - if (!Settings.getValues().mGestureDebugAccumulateFragments - || !sDrawingProxy.isCombiningModeActiveForDebug()) { + final SettingsValues settingsValues = Settings.getValues(); + if (!settingsValues.mGestureDebugAccumulateFragments + || (!sDrawingProxy.isCombiningModeActiveForDebug() + && !settingsValues.mGestureManualSpacing)) { sDrawingProxy.clearGestureDebugPoints(); } sListener.onStartBatchInput(); diff --git a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java index 817b5fade..14a93db93 100644 --- a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java +++ b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java @@ -946,7 +946,8 @@ private void enterCombiningMode(final SettingsValues settingsValues, final boole && (!settingsValues.mCombiningAutospaceOnlyAfterGesture || mCombiningWordHasGestureFragment) && !mSuppressAutospaceForForceNextSpace; - kv.setCombiningMode(showAutospaceIndicator, startTime, graceMs); + kv.setCombiningMode(showAutospaceIndicator, startTime, graceMs, + true /* compositionActiveForDebug */); } } @@ -972,7 +973,8 @@ private void enterJoinNextMode(final SettingsValues settingsValues) { final boolean showAutospaceIndicator = settingsValues.shouldInsertSpacesAutomatically() && settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces; kv.setCombiningMode(showAutospaceIndicator, SystemClock.uptimeMillis(), - Math.max(0, settingsValues.mCombiningGraceMs)); + Math.max(0, settingsValues.mCombiningGraceMs), + true /* compositionActiveForDebug */); } } } 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 6202a2860..8645d3c2e 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt +++ b/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt @@ -138,6 +138,7 @@ object Defaults { const val PREF_AUTOSPACE_VISUAL_HINT = true // Unified combining-mode model (default 0 = off; 500 ms is a comfortable starting point). const val PREF_COMBINING_GRACE_MS = 0 + const val PREF_COMBINING_LAST_AUTOSPACE_GRACE_MS = 500 const val PREF_COMBINING_AUTOCORRECT_ON_AUTOSPACE = true const val PREF_COMBINING_TAP_EXTRA_MS = 250 const val PREF_COMBINING_AUTOSPACE_ONLY_AFTER_GESTURE = false 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 5986c0176..a9a404d04 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/Settings.java +++ b/app/src/main/java/helium314/keyboard/latin/settings/Settings.java @@ -146,6 +146,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang // gesture), wait this many milliseconds; any new tap/gesture within the window EXTENDS // the same composing word, anything else (or expiry) commits + autospaces. 0 disables. public static final String PREF_COMBINING_GRACE_MS = "combining_grace_ms"; + public static final String PREF_COMBINING_LAST_AUTOSPACE_GRACE_MS = "combining_last_autospace_grace_ms"; public static final String PREF_COMBINING_AUTOCORRECT_ON_AUTOSPACE = "combining_autocorrect_on_autospace"; // Additional grace added on top of PREF_COMBINING_GRACE_MS when the most recent input // was a tap (peck-typists need more headroom than swipers between letters). 0 = no extra. 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 e985f445d..e0f2766ae 100644 --- a/app/src/main/java/helium314/keyboard/settings/screens/TwoThumbTypingScreen.kt +++ b/app/src/main/java/helium314/keyboard/settings/screens/TwoThumbTypingScreen.kt @@ -201,7 +201,6 @@ fun createTwoThumbTypingSettings(context: Context) = listOf( private const val SPACING_MODE_NORMAL = "normal" private const val SPACING_MODE_MANUAL = "manual" private const val SPACING_MODE_AUTOSPACE = "autospace" -private const val DEFAULT_AUTOSPACE_GRACE_MS = 500 private const val BACKSPACE_NORMAL = "normal" private const val BACKSPACE_FRAGMENT = "fragment" @@ -237,9 +236,19 @@ private fun TwoThumbSpacingModePreference(setting: Setting) { prefs.edit { putBoolean(Settings.PREF_GESTURE_MANUAL_SPACING, mode == SPACING_MODE_MANUAL) when (mode) { - SPACING_MODE_NORMAL, SPACING_MODE_MANUAL -> putInt(Settings.PREF_COMBINING_GRACE_MS, 0) + SPACING_MODE_NORMAL, SPACING_MODE_MANUAL -> { + val currentGraceMs = prefs.getInt(Settings.PREF_COMBINING_GRACE_MS, 0) + if (currentGraceMs > 0) { + putInt(Settings.PREF_COMBINING_LAST_AUTOSPACE_GRACE_MS, currentGraceMs) + } + putInt(Settings.PREF_COMBINING_GRACE_MS, 0) + } SPACING_MODE_AUTOSPACE -> if (prefs.getInt(Settings.PREF_COMBINING_GRACE_MS, 0) <= 0) { - putInt(Settings.PREF_COMBINING_GRACE_MS, DEFAULT_AUTOSPACE_GRACE_MS) + val restoredGraceMs = prefs.getInt( + Settings.PREF_COMBINING_LAST_AUTOSPACE_GRACE_MS, + Defaults.PREF_COMBINING_LAST_AUTOSPACE_GRACE_MS, + ) + putInt(Settings.PREF_COMBINING_GRACE_MS, restoredGraceMs.coerceAtLeast(1)) } } } diff --git a/dev-log.md b/dev-log.md index 0cfe0af92..c40c8f8d7 100644 --- a/dev-log.md +++ b/dev-log.md @@ -470,6 +470,26 @@ The user pointed out that the tap-during-swipe behavior had been made hardcoded, --- +## 2026-05-23 — Address follow-up PR review comments + +### Context +Copilot review comments on PR #6 flagged that debug overlay accumulation depended on the visual autospace indicator state, and that changing spacing modes discarded the user's configured autospace duration. + +### Actions Taken +- Separated the debug accumulation state in `MainKeyboardView` from the visible spacebar combining indicator. +- Preserved debug overlay accumulation for manual-spacing composition as well as hidden-indicator combining mode. +- Added a hidden last-autospace-duration preference and restored it when switching back to Auto-space mode. +- Ran targeted settings test, built the standard debug APK, and installed it on the connected device. + +### Decisions Made +- Kept the visual indicator behavior unchanged; only the debug overlay preservation decision now uses the dedicated composition/debug state. +- Preserved the user's last positive autospace duration when switching to Normal or Manual spacing, then restored it when Auto-space is selected again. + +### Open Questions / Next Steps +- Commit, push, and resolve the PR review threads. + +--- + ## 2026-05-23 — Remove tap-during-swipe fragments ### Context From a777ced4e4624e096866a4491b0fdb6097eb2199 Mon Sep 17 00:00:00 2001 From: Asaf Mahlev Date: Sat, 23 May 2026 20:32:56 +0300 Subject: [PATCH 16/17] Fix two-thumb gesture enabled gate Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../settings/screens/TwoThumbTypingScreen.kt | 5 +---- dev-log.md | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) 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 e0f2766ae..55a83a688 100644 --- a/app/src/main/java/helium314/keyboard/settings/screens/TwoThumbTypingScreen.kt +++ b/app/src/main/java/helium314/keyboard/settings/screens/TwoThumbTypingScreen.kt @@ -22,7 +22,6 @@ import androidx.core.content.edit import helium314.keyboard.latin.R import helium314.keyboard.latin.settings.Defaults import helium314.keyboard.latin.settings.Settings -import helium314.keyboard.latin.utils.JniUtils import helium314.keyboard.latin.utils.Log import helium314.keyboard.latin.utils.getActivity import helium314.keyboard.latin.utils.prefs @@ -50,8 +49,7 @@ fun TwoThumbTypingScreen( if ((b?.value ?: 0) < 0) Log.v("irrelevant", "stupid way to trigger recomposition on preference change") - val hasGestureLib = JniUtils.sHaveGestureLib - val gestureEnabled = hasGestureLib && prefs.getBoolean(Settings.PREF_GESTURE_INPUT, Defaults.PREF_GESTURE_INPUT) + val gestureEnabled = prefs.getBoolean(Settings.PREF_GESTURE_INPUT, Defaults.PREF_GESTURE_INPUT) val spacingMode = currentSpacingMode(prefs) val autospaceMode = spacingMode == SPACING_MODE_AUTOSPACE val nonNormalSpacing = spacingMode != SPACING_MODE_NORMAL @@ -313,7 +311,6 @@ private fun TwoThumbBackspaceBehaviorPreference(setting: Setting) { @Preview @Composable private fun Preview() { - JniUtils.sHaveGestureLib = true initPreview(LocalContext.current) Theme(previewDark) { Surface { diff --git a/dev-log.md b/dev-log.md index c40c8f8d7..904f41791 100644 --- a/dev-log.md +++ b/dev-log.md @@ -490,6 +490,23 @@ Copilot review comments on PR #6 flagged that debug overlay accumulation depende --- +## 2026-05-23 — Fix two-thumb enabled gate + +### Context +After installing PR #6, the user reported the two-thumb settings screen showed "Enable gesture typing first" even though gesture typing was enabled. + +### Actions Taken +- Changed `TwoThumbTypingScreen.kt` to gate the "Enable gesture typing first" hint only on `PREF_GESTURE_INPUT`, not `JniUtils.sHaveGestureLib`. +- Built and installed the updated standard debug APK on the connected device. + +### Decisions Made +- Kept gesture-library availability checks on the dedicated Gesture typing screen, where the library loader is shown; the two-thumb screen now reflects the actual user-facing enable toggle. + +### Open Questions / Next Steps +- Commit and push the fix. + +--- + ## 2026-05-23 — Remove tap-during-swipe fragments ### Context From 0c7998badc0a285757e74e5ba2c9ece591a9d949 Mon Sep 17 00:00:00 2001 From: Asaf Mahlev Date: Sat, 23 May 2026 20:40:17 +0300 Subject: [PATCH 17/17] Clarify missing gesture library state Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../settings/screens/TwoThumbTypingScreen.kt | 12 ++++++++++-- app/src/main/res/values/strings.xml | 1 + dev-log.md | 19 +++++++++++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) 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 55a83a688..94c29334c 100644 --- a/app/src/main/java/helium314/keyboard/settings/screens/TwoThumbTypingScreen.kt +++ b/app/src/main/java/helium314/keyboard/settings/screens/TwoThumbTypingScreen.kt @@ -22,6 +22,7 @@ import androidx.core.content.edit import helium314.keyboard.latin.R import helium314.keyboard.latin.settings.Defaults import helium314.keyboard.latin.settings.Settings +import helium314.keyboard.latin.utils.JniUtils import helium314.keyboard.latin.utils.Log import helium314.keyboard.latin.utils.getActivity import helium314.keyboard.latin.utils.prefs @@ -49,7 +50,9 @@ fun TwoThumbTypingScreen( if ((b?.value ?: 0) < 0) Log.v("irrelevant", "stupid way to trigger recomposition on preference change") - val gestureEnabled = prefs.getBoolean(Settings.PREF_GESTURE_INPUT, Defaults.PREF_GESTURE_INPUT) + val hasGestureLib = JniUtils.sHaveGestureLib + val gesturePrefEnabled = prefs.getBoolean(Settings.PREF_GESTURE_INPUT, Defaults.PREF_GESTURE_INPUT) + val gestureEnabled = hasGestureLib && gesturePrefEnabled val spacingMode = currentSpacingMode(prefs) val autospaceMode = spacingMode == SPACING_MODE_AUTOSPACE val nonNormalSpacing = spacingMode != SPACING_MODE_NORMAL @@ -102,7 +105,11 @@ fun TwoThumbTypingScreen( shape = MaterialTheme.shapes.medium, ) { Text( - text = stringResource(R.string.two_thumb_typing_requires_gesture), + text = stringResource(if (hasGestureLib) { + R.string.two_thumb_typing_requires_gesture + } else { + R.string.two_thumb_typing_requires_gesture_library + }), modifier = Modifier.padding(16.dp), ) } @@ -311,6 +318,7 @@ private fun TwoThumbBackspaceBehaviorPreference(setting: Setting) { @Preview @Composable private fun Preview() { + JniUtils.sHaveGestureLib = true initPreview(LocalContext.current) Theme(previewDark) { Surface { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 34af187c2..98816d057 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -212,6 +212,7 @@ Two-thumb typing Enable gesture typing first + Load the gesture typing library first Build words from taps and swipes Recognition tuning Troubleshooting diff --git a/dev-log.md b/dev-log.md index 904f41791..e9263c47a 100644 --- a/dev-log.md +++ b/dev-log.md @@ -507,6 +507,25 @@ After installing PR #6, the user reported the two-thumb settings screen showed " --- +## 2026-05-23 — Restore gesture library after reinstall + +### Context +After the debug app was fully uninstalled to fix a signature mismatch, gesture typing stopped working. The reinstall preserved the gesture preference, but the user-supplied native gesture library in app files was gone. + +### Actions Taken +- Confirmed the device app files did not contain `libjni_latinime.so`. +- Downloaded the arm64 OpenBoard `libjni_latinimegoogle.so`, verified its expected SHA-256 checksum, copied it into the app files as `libjni_latinime.so`, and force-stopped the app so it reloads. +- Updated the two-thumb settings gate to distinguish between "gesture toggle is off" and "gesture library is missing". +- Built and installed the updated standard debug APK while preserving the restored gesture library file. + +### Decisions Made +- Kept the library availability gate for the two-thumb screen, but changed the missing-library message so it no longer incorrectly says only "Enable gesture typing first". + +### Open Questions / Next Steps +- Commit and push the corrected screen text. + +--- + ## 2026-05-23 — Remove tap-during-swipe fragments ### Context