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..6c221323e 100644 --- a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java +++ b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java @@ -1200,40 +1200,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 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..0dd8647b9 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,7 @@ 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_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..75edd6b6c 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,9 @@ 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"; // 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..ae1e4e17f 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,7 @@ public class SettingsValues { public final boolean mCombiningAutocorrectOnAutospace; public final int mCombiningTapExtraMs; public final boolean mCombiningAutospaceOnlyAfterGesture; + public final boolean mSpacingDeferGraceSpace; // Raw string value: "keep_alternatives" | "next_word" | "alternatives_then_next_word" public final String mCombiningAutospaceSuggestions; public final boolean mCombiningBackspaceDeletesGestureWord; @@ -382,6 +383,9 @@ 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); 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..9469bddc4 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,7 @@ 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) } if (nonNormalSpacing) { add(Settings.PREF_MULTIPART_FULL_WORD_SUGGESTIONS) @@ -146,6 +147,11 @@ 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_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..9948cff76 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -295,6 +295,10 @@ 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. + + 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. 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..24fb0054b 100644 --- a/app/src/test/java/helium314/keyboard/settings/SettingsContainerTest.kt +++ b/app/src/test/java/helium314/keyboard/settings/SettingsContainerTest.kt @@ -60,6 +60,12 @@ 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 twoThumbLowLevelBackspaceSettingIsHiddenFromSearchRegistry() { assertNull(container[Settings.PREF_COMBINING_BACKSPACE_DELETES_GESTURE_WORD])