diff --git a/CHANGELOG.md b/CHANGELOG.md index 956fc56e3..4c551bbc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,26 @@ and this project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html) ## [Unreleased] +## [3.9.1] - 2026-06-11 + +### Fixed +- **Erratic capitalization in two-thumb grace mode.** After the grace timer auto-committed a + word, the shift/auto-caps state wasn't refreshed, so the next word's capitalization came out + wrong (dropped sentence caps, or mid-word capitals). (#14) + +### Changed +- **Tapped words no longer auto-finish by default** in two-thumb grace mode — the new + "Only auto-finish swiped words" option defaults on, so a word you tap out stays open until you + press space or pick a suggestion (fixes tapped shortcuts/corrections firing early). Only swiped + words auto-commit on a pause. (#14) +- Reworded the two easily-confused spacing toggles: **"Only auto-space after swipes"** (the + trailing space) vs **"Only auto-finish swiped words"** (whether the word commits at all). (#14) + +### Added +- **Experimental: defer grace-mode space** (`PREF_SPACING_DEFER_GRACE_SPACE`, default off) — routes + the two-thumb grace auto-commit space through the same deferred mechanism as the default swipe + path. (#23) + ## [3.9.0] - 2026-06-10 ### Added diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0ac2eef83..38fa7f669 100755 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -22,8 +22,8 @@ android { applicationId = "com.asafmah.leantypedual" minSdk = 21 targetSdk = 35 - versionCode = 3900 - versionName = "3.9.0" + versionCode = 3910 + versionName = "3.9.1" proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 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 b4cec9e3e..4ac354a4d 100644 --- a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java +++ b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java @@ -988,12 +988,21 @@ private void enterCombiningMode(final SettingsValues settingsValues, final boole final int graceMs = baseGraceMs + Math.max(0, settingsValues.mCombiningTapExtraMs); cancelCombiningTimerOnly(); mInCombiningMode = true; + // #14 "only auto-finish swiped words": still ENTER combining mode (so a following swipe + // can extend this word), but DON'T arm the auto-commit timer for a pure tap word — it + // stays open until the user commits. A tap-then-swipe still arms: the gesture re-enters + // here with fromTap=false and the fragment present, so it arms then. + final boolean armTimer = !(fromTap && settingsValues.mCombiningGraceOnlyAfterGesture + && !mCombiningWordHasGestureFragment && !mWordComposer.isBatchMode()); final long startTime = SystemClock.uptimeMillis(); - mPendingCombiningCommit = () -> onCombiningGraceExpired(); - mCombiningHandler.postDelayed(mPendingCombiningCommit, graceMs); + if (armTimer) { + mPendingCombiningCommit = () -> onCombiningGraceExpired(); + mCombiningHandler.postDelayed(mPendingCombiningCommit, graceMs); + } final MainKeyboardView kv = KeyboardSwitcher.getInstance().getMainKeyboardView(); if (kv != null) { - final boolean showAutospaceIndicator = settingsValues.shouldInsertSpacesAutomatically() + final boolean showAutospaceIndicator = armTimer + && settingsValues.shouldInsertSpacesAutomatically() && settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces && (!settingsValues.mCombiningAutospaceOnlyAfterGesture || mCombiningWordHasGestureFragment) @@ -1200,40 +1209,54 @@ private void onCombiningGraceExpired() { } else { commitTyped(sv, LastComposedWord.NOT_A_SEPARATOR); } - // Track whether the helper actually wrote a space (skipped for URL / e-mail / phantom). - final int beforeSpace = mConnection.getExpectedSelectionEnd(); - if (!sv.mCombiningAutospaceOnlyAfterGesture || wordHadGestureFragment) { - insertAutomaticSpaceIfOptionsAndTextAllow(sv); + // #23 (PREF_SPACING_DEFER_GRACE_SPACE): defer the grace-mode space through PHANTOM + // instead of writing it eagerly, so it materializes on the NEXT input via the same path + // as the default gesture word — URL/e-mail/punctuation gates + backspace-reversibility + // are applied at materialization time, with no eager space to patch. + final boolean autospaceInserted; + if (sv.mSpacingDeferGraceSpace) { + if (!sv.mCombiningAutospaceOnlyAfterGesture || wordHadGestureFragment) { + // Arm the deferred space; the PHANTOM consumer (handleNonSeparatorEvent / + // handleSeparatorEvent) writes or suppresses it on the next input. + mSpaceState = SpaceState.PHANTOM; + } else { + clearOneShotSpaceActionAndNotifyIfChanged(); + mSpaceState = SpaceState.NONE; + } + // No eager write: the cursor-delta accounting below treats this as "no space". + autospaceInserted = false; + mAutospaceJustWritten = false; } else { - clearOneShotSpaceActionAndNotifyIfChanged(); + // Eager path (default). Track whether the helper actually wrote a space (skipped for + // URL / e-mail / phantom). + final int beforeSpace = mConnection.getExpectedSelectionEnd(); + if (!sv.mCombiningAutospaceOnlyAfterGesture || wordHadGestureFragment) { + insertAutomaticSpaceIfOptionsAndTextAllow(sv); + } else { + clearOneShotSpaceActionAndNotifyIfChanged(); + } + 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 + // this the revert's `deleteLength = cancelLength + separatorLength` only deletes the + // word, and the DEBUG assertion (last cancelLength chars equals committedWord) throws. + if (autospaceInserted && mLastComposedWord != null + && mLastComposedWord != LastComposedWord.NOT_A_COMPOSED_WORD + && Constants.STRING_SPACE.equals(mLastComposedWord.mSeparatorString) == false) { + mLastComposedWord = new LastComposedWord( + mLastComposedWord.mEvents, + mLastComposedWord.mInputPointers, + mLastComposedWord.mTypedWord, + mLastComposedWord.mCommittedWord, + Constants.STRING_SPACE, + mLastComposedWord.mNgramContext, + mLastComposedWord.mCapitalizedMode); + } + // Don't set PHANTOM here — we already wrote the space; PHANTOM would make the next + // letter insert a second one. + mAutospaceJustWritten = autospaceInserted; + mSpaceState = SpaceState.NONE; } - 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 - // this the existing revert code's `deleteLength = cancelLength + separatorLength` - // would only delete the word, and in DEBUG builds the bundled assertion against - // `getTextBeforeCursor(...).subSequence(0, cancelLength) equals committedWord` throws - // because the last cancelLength chars now include the trailing space, not the word. - if (autospaceInserted && mLastComposedWord != null - && mLastComposedWord != LastComposedWord.NOT_A_COMPOSED_WORD - && Constants.STRING_SPACE.equals(mLastComposedWord.mSeparatorString) == false) { - mLastComposedWord = new LastComposedWord( - mLastComposedWord.mEvents, - mLastComposedWord.mInputPointers, - mLastComposedWord.mTypedWord, - mLastComposedWord.mCommittedWord, - Constants.STRING_SPACE, - mLastComposedWord.mNgramContext, - mLastComposedWord.mCapitalizedMode); - } - // Don't set PHANTOM here — we already wrote the space to the editor. PHANTOM would - // make the next letter call insertAutomaticSpaceIfOptionsAndTextAllow AGAIN (see - // handleNonSeparatorEvent line ~1760), giving a double space. Instead, set a - // dedicated one-shot flag that handleSeparatorEvent uses to strip the autospace if - // a punctuation character follows. The flag is cleared by enterCombiningMode (next - // input took over), cancelCombiningMode (backspace etc), or once consumed. - mAutospaceJustWritten = autospaceInserted; - mSpaceState = SpaceState.NONE; mConnection.endBatchEdit(); final int cursorAfter = mConnection.getExpectedSelectionEnd(); // The commit doesn't move the cursor for the composing text itself (it was already @@ -1295,6 +1318,12 @@ private void onCombiningGraceExpired() { mBackspaceUnits.setCommitted(writtenChars, committedFragments); } // "keep_alternatives" — fall through, do nothing. + // #14 bug fix: this commit ran on the async grace timer, OFF the normal onCodeInput path + // that refreshes the shift state after a commit. Without this, the next word's auto-caps + // is stale — auto-caps gets dropped after a grace auto-commit and capitalization comes out + // erratic. Mirror the gesture-commit path's requestUpdatingShiftState. + KeyboardSwitcher.getInstance().requestUpdatingShiftState( + getCurrentAutoCapsState(sv), getCurrentRecapitalizeState()); } /** 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 280eade97..c0e875464 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt +++ b/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt @@ -149,6 +149,8 @@ object Defaults { 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_SPACING_DEFER_GRACE_SPACE = false + const val PREF_COMBINING_GRACE_ONLY_AFTER_GESTURE = true // default on: tapped words shouldn't auto-finish 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 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 664ea8194..cf89a5379 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/Settings.java +++ b/app/src/main/java/helium314/keyboard/latin/settings/Settings.java @@ -157,6 +157,12 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang // 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"; + // #23: route the two-thumb grace-mode auto-commit space through the deferred PHANTOM + // mechanism (like the default gesture path) instead of writing it eagerly. Experimental. + public static final String PREF_SPACING_DEFER_GRACE_SPACE = "spacing_defer_grace_space"; + // #14: when on, the combining grace timer only auto-commits words that include a swipe — + // pure tap-typed words are never auto-finished by the timer. Experimental, default off. + public static final String PREF_COMBINING_GRACE_ONLY_AFTER_GESTURE = "combining_grace_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 72a9c257c..95efd7756 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java +++ b/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java @@ -137,6 +137,8 @@ public class SettingsValues { public final boolean mCombiningAutocorrectOnAutospace; public final int mCombiningTapExtraMs; public final boolean mCombiningAutospaceOnlyAfterGesture; + public final boolean mSpacingDeferGraceSpace; + public final boolean mCombiningGraceOnlyAfterGesture; // Raw string value: "keep_alternatives" | "next_word" | "alternatives_then_next_word" public final String mCombiningAutospaceSuggestions; public final boolean mCombiningBackspaceDeletesGestureWord; @@ -382,6 +384,12 @@ public SettingsValues(final Context context, final SharedPreferences prefs, fina mCombiningAutospaceOnlyAfterGesture = prefs.getBoolean( Settings.PREF_COMBINING_AUTOSPACE_ONLY_AFTER_GESTURE, Defaults.PREF_COMBINING_AUTOSPACE_ONLY_AFTER_GESTURE); + mSpacingDeferGraceSpace = prefs.getBoolean( + Settings.PREF_SPACING_DEFER_GRACE_SPACE, + Defaults.PREF_SPACING_DEFER_GRACE_SPACE); + mCombiningGraceOnlyAfterGesture = prefs.getBoolean( + Settings.PREF_COMBINING_GRACE_ONLY_AFTER_GESTURE, + Defaults.PREF_COMBINING_GRACE_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 160a0c62e..e73389788 100644 --- a/app/src/main/java/helium314/keyboard/settings/screens/TwoThumbTypingScreen.kt +++ b/app/src/main/java/helium314/keyboard/settings/screens/TwoThumbTypingScreen.kt @@ -69,6 +69,8 @@ fun TwoThumbTypingScreen( add(Settings.PREF_COMBINING_AUTOSPACE_ONLY_AFTER_GESTURE) add(Settings.PREF_COMBINING_AUTOCORRECT_ON_AUTOSPACE) add(Settings.PREF_COMBINING_AUTOSPACE_SUGGESTIONS) + add(Settings.PREF_SPACING_DEFER_GRACE_SPACE) + add(Settings.PREF_COMBINING_GRACE_ONLY_AFTER_GESTURE) } if (nonNormalSpacing) { add(Settings.PREF_MULTIPART_FULL_WORD_SUGGESTIONS) @@ -146,6 +148,16 @@ fun createTwoThumbTypingSettings(context: Context) = listOf( R.string.combining_autospace_only_after_gesture_summary) { SwitchPreference(it, Defaults.PREF_COMBINING_AUTOSPACE_ONLY_AFTER_GESTURE) }, + Setting(context, Settings.PREF_SPACING_DEFER_GRACE_SPACE, + R.string.spacing_defer_grace_space, + R.string.spacing_defer_grace_space_summary) { + SwitchPreference(it, Defaults.PREF_SPACING_DEFER_GRACE_SPACE) + }, + Setting(context, Settings.PREF_COMBINING_GRACE_ONLY_AFTER_GESTURE, + R.string.combining_grace_only_after_gesture, + R.string.combining_grace_only_after_gesture_summary) { + SwitchPreference(it, Defaults.PREF_COMBINING_GRACE_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 7568f00af..82e563c1c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -294,7 +294,15 @@ 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. + Controls the automatic space only \u2014 not whether a word commits. When on, a tap-only word still commits but without an auto-space; swiped words still get one. + + Defer grace space (experimental) + + Route the two-thumb grace auto-commit space through the deferred mechanism (like the default swipe path) instead of writing it immediately. The space appears on your next input and stays backspace-reversible. + + Only auto-finish swiped words + + The pause timer auto-commits a word only when it includes a swipe. Words you tap out stay open until you press space or pick a suggestion, so tapped shortcuts and corrections won\'t fire early. On by default \u2014 this controls whether the word commits (the auto-space option above only controls the trailing 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 c5fdd54cc..d3f2accea 100644 --- a/app/src/test/java/helium314/keyboard/latin/InputLogicTest.kt +++ b/app/src/test/java/helium314/keyboard/latin/InputLogicTest.kt @@ -362,6 +362,37 @@ class InputLogicTest { assertEquals("hello ", textBeforeCursor) } + @Test fun deferredGraceSpaceMaterializesOnNextInput() { + // #23: with PREF_SPACING_DEFER_GRACE_SPACE on, the grace commit does NOT write the space + // eagerly (the default path gives "hello "); it arms PHANTOM so the space appears on the + // next input instead. + reset() + latinIME.prefs().edit { + putInt(Settings.PREF_COMBINING_GRACE_MS, 1000) + putBoolean(Settings.PREF_SPACING_DEFER_GRACE_SPACE, true) + } + gestureInput("hello") + expireCombiningGrace() + assertEquals("hello", textBeforeCursor) // deferred: no trailing space yet + chainInput("world") + assertEquals("hello world", textBeforeCursor) // materialized on the next letter + } + + @Test fun deferredGraceCommitIsBackspaceReversible() { + // The deferred commit leaves no eager space to orphan; the first backspace deletes the + // gesture word cleanly (PREF_COMBINING_BACKSPACE_DELETES_GESTURE_WORD default on). + reset() + latinIME.prefs().edit { + putInt(Settings.PREF_COMBINING_GRACE_MS, 1000) + putBoolean(Settings.PREF_SPACING_DEFER_GRACE_SPACE, true) + } + gestureInput("hello") + expireCombiningGrace() + assertEquals("hello", textBeforeCursor) + functionalKeyPress(KeyCode.DELETE) + assertEquals("", textBeforeCursor) + } + @Test fun tapThenGestureCombiningWordStillAutospacesWhenGestureGateEnabled() { 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 a51cfb673..30451afb7 100644 --- a/app/src/test/java/helium314/keyboard/settings/SettingsContainerTest.kt +++ b/app/src/test/java/helium314/keyboard/settings/SettingsContainerTest.kt @@ -60,6 +60,18 @@ class SettingsContainerTest { container[Settings.PREF_COMBINING_AUTOSPACE_ONLY_AFTER_GESTURE]?.key) } + @Test + fun spacingDeferGraceSpaceSettingIsRegistered() { + assertEquals(Settings.PREF_SPACING_DEFER_GRACE_SPACE, + container[Settings.PREF_SPACING_DEFER_GRACE_SPACE]?.key) + } + + @Test + fun combiningGraceOnlyAfterGestureSettingIsRegistered() { + assertEquals(Settings.PREF_COMBINING_GRACE_ONLY_AFTER_GESTURE, + container[Settings.PREF_COMBINING_GRACE_ONLY_AFTER_GESTURE]?.key) + } + @Test fun twoThumbLowLevelBackspaceSettingIsHiddenFromSearchRegistry() { assertNull(container[Settings.PREF_COMBINING_BACKSPACE_DELETES_GESTURE_WORD]) diff --git a/docs/SPACING_POLICY.md b/docs/SPACING_POLICY.md new file mode 100644 index 000000000..534e0f177 --- /dev/null +++ b/docs/SPACING_POLICY.md @@ -0,0 +1,114 @@ +# Spacing Policy + +**Status:** design spec. Epic [#14]; implementation tracked in [#23] (B6b), [#24] (B6a), [#25] (B6c/C1), [#26] (tuning panel). + +Goal: rethink autospace/grace as a **per-word spacing *policy*** driven by word-state signals already computed every keystroke — not a fixed timer — with an opt-in "Assisted" tier, decoupled commit-from-space, and adaptive per-posture cadence. Feel-driven: every knob is a live, on-device-tunable experimental setting; **never hardcode a feel decision**. + +--- + +## 1. The problem + +Two different mechanisms do the same job inconsistently, and grace is blind to whether a word is *finished*: + +- **Default gesture path** uses **`SpaceState.PHANTOM`**: the space is *deferred* and materialized on the next input, so it adapts to connectors / URLs / punctuation and stays backspace-reversible. +- **Combining-grace path** writes the space **eagerly** in `onCombiningGraceExpired` (`InputLogic.java` ~1139–1156: `insertAutomaticSpaceIfOptionsAndTextAllow(sv)` + `mAutospaceJustWritten`). +- Grace is a **fixed timer** (`mCombiningGraceMs`). It can't tell a finished word ("I") from an extendable stem, so finished words still need a space tap while stems get committed too eagerly. + +## 2. Principles + +- **Signals over timers.** Decisions key off per-keystroke word state, at **zero extra native cost**. +- **One deferred-space mechanism** (PHANTOM) for every commit path. +- **Opt-in / default-off**, conservative defaults. +- **Backspace-reversible**, and the policy **never fires mid multipart / live-converge fragment**. +- **Feel-driven**: knobs are live experimental settings (§7); no hardcoded feel calls; validate on-device. + +## 3. Current state (the seam) + +- `SpaceState` values: `NONE`, `DOUBLE`, `SWAP_PUNCTUATION`, `WEAK`, `PHANTOM` (`inputlogic/SpaceState.java`). PHANTOM = the deferred-space promise consumed on next input. +- `OneShotSpaceAction` (`inputlogic/OneShotSpaceAction.kt`): `JOIN_NEXT`, `FORCE_NEXT_SPACE`. +- PHANTOM consumers that materialize/suppress the space on next input: `handleNonSeparatorEvent` (~373 / ~2003), `handleSeparatorEvent` (~2207). +- The combining grace timer: scheduled in `enterCombiningMode`, cleared in `cancelCombiningMode`, fires `onCombiningGraceExpired` (~1139–1156), which currently commits **and eagerly spaces**. +- Backspace-revert of a committed word reads `mLastComposedWord.mSeparatorString` (patched ~1148–1156). + +## 4. Signals (free — from `mSuggestedWords`, computed every keystroke) + +``` +complete = mTypedWordValid && mTypedWordInfo.mSourceDict != DICTIONARY_USER_TYPED +prefixRichScore = (# of KIND_COMPLETION candidates) / (total candidates) // normalized [0..1] +graceMs = clamp(base − completeBonus·complete + prefixPenalty·prefixRichScore, min, max) +``` + +- `complete`: the typed word is a real dictionary word (not a user-typed-only string). +- **`prefixRichScore`** is a **normalized score** (completions ÷ total), not a raw count — more stable across dictionaries of different sizes. High = lots of longer words start with this stem (keep open); low = little left to extend to (safe to commit). + +## 5. The "Assisted" tier — two gates + +A confident, complete word auto-commits with the space **deferred** (PHANTOM). It fires through **either** gate, mapping onto the two signals: + +| Gate | Condition | Behaviour | +|---|---|---| +| **A — instant** | `complete` **and** `prefixRichScore ≤ lowThreshold` | Commit immediately (nothing plausible left to extend to: "I", "the", a finished gesture). Space deferred. | +| **B — pause** | `complete` **and** `prefixRichScore > lowThreshold` | Hold; commit only after an inter-word **pause** (the adaptive threshold, §6) — the stem is extendable, give the user time. Space deferred. | +| _neither_ | not `complete` | Stay open; fall back to the signal-driven `graceMs` timer. | + +So finished words feel instant; extendable-but-complete words wait until you stop. Subsumes the old "short-word auto-commit" idea, but dictionary-driven. + +**Guards (hard):** only above the confidence floor; **never** while a multipart / live-converge fragment is mid-flight; the deferred space keeps the whole thing backspace-reversible. + +## 6. Adaptive per-posture cadence ([#25]) + +The Gate-B pause threshold is **learned**, not fixed: a running percentile of the user's real inter-word pause distribution. **Separate baselines per posture** (one-handed / two-handed), keyed off the existing one-handed toggle (`KeyboardSwitcher.setOneHandedModeEnabled` ~535–547, today layout-only — timing is greenfield) so switching posture **loads the stored baseline instantly** instead of slowly re-adapting. Far-key reach stays handled by the existing layout shrink + swipe shortcuts (C2), not here. + +## 7. Tuning & insight ([#26]) + +- **For users:** a small set of **discrete tiers** — `Off` / `Conservative` / `Assisted` / `Aggressive` — that pick coherent knob bundles. +- **Behind an "experimental" expander:** the raw **sliders** — `base` grace, `completeBonus`, `prefixPenalty`, `lowThreshold`, Gate-B pause percentile — for on-device tuning. +- **Live:** changes take effect with **no restart**; defaults conservative; the whole policy **default-off**. +- **Fuller A11 typing-insight overlay** (paired): per word, surface *why* it committed or stayed open — `complete`, `prefixRichScore`, the resolved `graceMs`, and **which gate fired** (A / B / none). This is the feedback loop that makes the sliders tunable by feel. + +## 8. Sequencing + +| Phase | Issue | What | Why here | +|---|---|---|---| +| 1 | [#23] | Route the grace commit through PHANTOM (remove the eager write) | Structurally correct regardless of feel knobs — the foundation. | +| 2 | [#26] | Live tuning-panel infra + the fuller A11 insight overlay | Force-multiplier: makes Phase 3 tunable on-device and shows *why*. | +| 3 | [#24] | Signal-driven `graceMs` + the two-gate Assisted tier | The meat; tuned via Phase 2 + on-device playtest. | +| 4 | [#25] | Adaptive per-posture cadence | The learning layer on top. | + +(#23 is "first" per the epic; #26 is slotted **second** because Phase 3 is feel-critical and un-tunable without it.) + +## 9. Phase 1 (#23) implementation notes — small seam, deliberate test rewrite + +- **Seam:** in `onCombiningGraceExpired`, replace `insertAutomaticSpaceIfOptionsAndTextAllow(sv)` + `mAutospaceJustWritten` with `mSpaceState = SpaceState.PHANTOM`. Existing PHANTOM consumers then materialize/suppress on next input (already solved for the default path). +- **Move** the `mLastComposedWord.mSeparatorString` backspace-revert patch (~1148–1156) to **space-materialization time**. +- **Test-contract rewrite (not optional):** ~8 `InputLogicTest` cases lock in the *eager* space — `expireCombiningGrace(); assertEquals("hello ", textBeforeCursor)` at ~259/276/357/372/461. Each must be rewritten to assert **deferred-then-materialized** (`"hello"` after expiry, `"hello "` only after the next letter/separator). Add explicit commit-then-backspace and commit-then-punctuation coverage. +- **Risk:** this changes locked-in behaviour contracts. Build it **behind the experimental flag** and validate with **on-device playtesting** — not a blind autonomous change. + +## 10. Settings (new keys, all experimental / default-off) + +Follow the 5-file pattern (`Settings.java` / `Defaults.kt` / `SettingsValues.java` / `strings.xml` / a settings screen) + a `SettingsContainerTest` case each. + +| Key (proposed) | Type | Meaning | +|---|---|---| +| `PREF_SPACING_POLICY_TIER` | enum | Off / Conservative / Assisted / Aggressive | +| `PREF_SPACING_GRACE_BASE_MS` | int | `base` in the graceMs formula | +| `PREF_SPACING_COMPLETE_BONUS_MS` | int | `completeBonus` | +| `PREF_SPACING_PREFIX_PENALTY_MS` | int | `prefixPenalty` | +| `PREF_SPACING_LOW_THRESHOLD` | float | Gate-A `lowThreshold` on `prefixRichScore` | +| `PREF_SPACING_PAUSE_PERCENTILE` | int | Gate-B adaptive-pause percentile | + +(Tier selection writes a coherent bundle into the sliders; the experimental expander exposes the sliders directly. Reuse `MultiSliderPreference.kt`.) + +## 11. Testing strategy + +- **Unit-testable (JVM):** the `graceMs` formula and gate selection are deterministic given a `mSuggestedWords` snapshot — table-test them directly. The Phase 1 deferred-space contract is covered by the rewritten grace tests + new backspace-revert cases. +- **On-device only (feel):** the actual cadence, the tier defaults, and the adaptive percentile. These ride the #26 panel + the A11 overlay; do **not** assert specific timings in tests. +- The native gesture-replay harness ([#78]) and the trace recorder ([#20]) provide fixtures for regression-checking that the policy doesn't break recognition. + +[#14]: https://github.com/AsafMah/LeanType/issues/14 +[#20]: https://github.com/AsafMah/LeanType/issues/20 +[#23]: https://github.com/AsafMah/LeanType/issues/23 +[#24]: https://github.com/AsafMah/LeanType/issues/24 +[#25]: https://github.com/AsafMah/LeanType/issues/25 +[#26]: https://github.com/AsafMah/LeanType/issues/26 +[#78]: https://github.com/AsafMah/LeanType/issues/78 diff --git a/fastlane/metadata/android/en-US/changelogs/3910.txt b/fastlane/metadata/android/en-US/changelogs/3910.txt new file mode 100644 index 000000000..4f97d6f82 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/3910.txt @@ -0,0 +1,4 @@ +- Two-thumb: fixed capitalization going wrong after the grace timer auto-finishes a word (dropped sentence capitals / stray mid-word capitals) +- Two-thumb: words you tap out no longer auto-finish on a pause by default — they stay open until you press space or pick a suggestion, so tapped shortcuts and corrections don't fire early (only swiped words auto-finish). Toggle under Two-thumb settings. +- Clearer labels for the two spacing options ("Only auto-space after swipes" vs "Only auto-finish swiped words") +- Experimental (off by default): defer the two-thumb grace space like the default swipe path