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])