Skip to content

Commit 2950dce

Browse files
authored
Merge pull request #91 from AsafMah/dev
Release 3.9.1
2 parents ec9d36d + b9cd182 commit 2950dce

12 files changed

Lines changed: 284 additions & 38 deletions

File tree

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,26 @@ and this project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
1414
1515
## [Unreleased]
1616

17+
## [3.9.1] - 2026-06-11
18+
19+
### Fixed
20+
- **Erratic capitalization in two-thumb grace mode.** After the grace timer auto-committed a
21+
word, the shift/auto-caps state wasn't refreshed, so the next word's capitalization came out
22+
wrong (dropped sentence caps, or mid-word capitals). (#14)
23+
24+
### Changed
25+
- **Tapped words no longer auto-finish by default** in two-thumb grace mode — the new
26+
"Only auto-finish swiped words" option defaults on, so a word you tap out stays open until you
27+
press space or pick a suggestion (fixes tapped shortcuts/corrections firing early). Only swiped
28+
words auto-commit on a pause. (#14)
29+
- Reworded the two easily-confused spacing toggles: **"Only auto-space after swipes"** (the
30+
trailing space) vs **"Only auto-finish swiped words"** (whether the word commits at all). (#14)
31+
32+
### Added
33+
- **Experimental: defer grace-mode space** (`PREF_SPACING_DEFER_GRACE_SPACE`, default off) — routes
34+
the two-thumb grace auto-commit space through the same deferred mechanism as the default swipe
35+
path. (#23)
36+
1737
## [3.9.0] - 2026-06-10
1838

1939
### Added

app/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ android {
2222
applicationId = "com.asafmah.leantypedual"
2323
minSdk = 21
2424
targetSdk = 35
25-
versionCode = 3900
26-
versionName = "3.9.0"
25+
versionCode = 3910
26+
versionName = "3.9.1"
2727

2828
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
2929

app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java

Lines changed: 64 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -988,12 +988,21 @@ private void enterCombiningMode(final SettingsValues settingsValues, final boole
988988
final int graceMs = baseGraceMs + Math.max(0, settingsValues.mCombiningTapExtraMs);
989989
cancelCombiningTimerOnly();
990990
mInCombiningMode = true;
991+
// #14 "only auto-finish swiped words": still ENTER combining mode (so a following swipe
992+
// can extend this word), but DON'T arm the auto-commit timer for a pure tap word — it
993+
// stays open until the user commits. A tap-then-swipe still arms: the gesture re-enters
994+
// here with fromTap=false and the fragment present, so it arms then.
995+
final boolean armTimer = !(fromTap && settingsValues.mCombiningGraceOnlyAfterGesture
996+
&& !mCombiningWordHasGestureFragment && !mWordComposer.isBatchMode());
991997
final long startTime = SystemClock.uptimeMillis();
992-
mPendingCombiningCommit = () -> onCombiningGraceExpired();
993-
mCombiningHandler.postDelayed(mPendingCombiningCommit, graceMs);
998+
if (armTimer) {
999+
mPendingCombiningCommit = () -> onCombiningGraceExpired();
1000+
mCombiningHandler.postDelayed(mPendingCombiningCommit, graceMs);
1001+
}
9941002
final MainKeyboardView kv = KeyboardSwitcher.getInstance().getMainKeyboardView();
9951003
if (kv != null) {
996-
final boolean showAutospaceIndicator = settingsValues.shouldInsertSpacesAutomatically()
1004+
final boolean showAutospaceIndicator = armTimer
1005+
&& settingsValues.shouldInsertSpacesAutomatically()
9971006
&& settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces
9981007
&& (!settingsValues.mCombiningAutospaceOnlyAfterGesture
9991008
|| mCombiningWordHasGestureFragment)
@@ -1200,40 +1209,54 @@ private void onCombiningGraceExpired() {
12001209
} else {
12011210
commitTyped(sv, LastComposedWord.NOT_A_SEPARATOR);
12021211
}
1203-
// Track whether the helper actually wrote a space (skipped for URL / e-mail / phantom).
1204-
final int beforeSpace = mConnection.getExpectedSelectionEnd();
1205-
if (!sv.mCombiningAutospaceOnlyAfterGesture || wordHadGestureFragment) {
1206-
insertAutomaticSpaceIfOptionsAndTextAllow(sv);
1212+
// #23 (PREF_SPACING_DEFER_GRACE_SPACE): defer the grace-mode space through PHANTOM
1213+
// instead of writing it eagerly, so it materializes on the NEXT input via the same path
1214+
// as the default gesture word — URL/e-mail/punctuation gates + backspace-reversibility
1215+
// are applied at materialization time, with no eager space to patch.
1216+
final boolean autospaceInserted;
1217+
if (sv.mSpacingDeferGraceSpace) {
1218+
if (!sv.mCombiningAutospaceOnlyAfterGesture || wordHadGestureFragment) {
1219+
// Arm the deferred space; the PHANTOM consumer (handleNonSeparatorEvent /
1220+
// handleSeparatorEvent) writes or suppresses it on the next input.
1221+
mSpaceState = SpaceState.PHANTOM;
1222+
} else {
1223+
clearOneShotSpaceActionAndNotifyIfChanged();
1224+
mSpaceState = SpaceState.NONE;
1225+
}
1226+
// No eager write: the cursor-delta accounting below treats this as "no space".
1227+
autospaceInserted = false;
1228+
mAutospaceJustWritten = false;
12071229
} else {
1208-
clearOneShotSpaceActionAndNotifyIfChanged();
1230+
// Eager path (default). Track whether the helper actually wrote a space (skipped for
1231+
// URL / e-mail / phantom).
1232+
final int beforeSpace = mConnection.getExpectedSelectionEnd();
1233+
if (!sv.mCombiningAutospaceOnlyAfterGesture || wordHadGestureFragment) {
1234+
insertAutomaticSpaceIfOptionsAndTextAllow(sv);
1235+
} else {
1236+
clearOneShotSpaceActionAndNotifyIfChanged();
1237+
}
1238+
autospaceInserted = mConnection.getExpectedSelectionEnd() > beforeSpace;
1239+
// If we DID insert an autospace, fix up mLastComposedWord so revertCommit (backspace +
1240+
// PREF_BACKSPACE_REVERTS_AUTOCORRECT) deletes the space along with the word. Without
1241+
// this the revert's `deleteLength = cancelLength + separatorLength` only deletes the
1242+
// word, and the DEBUG assertion (last cancelLength chars equals committedWord) throws.
1243+
if (autospaceInserted && mLastComposedWord != null
1244+
&& mLastComposedWord != LastComposedWord.NOT_A_COMPOSED_WORD
1245+
&& Constants.STRING_SPACE.equals(mLastComposedWord.mSeparatorString) == false) {
1246+
mLastComposedWord = new LastComposedWord(
1247+
mLastComposedWord.mEvents,
1248+
mLastComposedWord.mInputPointers,
1249+
mLastComposedWord.mTypedWord,
1250+
mLastComposedWord.mCommittedWord,
1251+
Constants.STRING_SPACE,
1252+
mLastComposedWord.mNgramContext,
1253+
mLastComposedWord.mCapitalizedMode);
1254+
}
1255+
// Don't set PHANTOM here — we already wrote the space; PHANTOM would make the next
1256+
// letter insert a second one.
1257+
mAutospaceJustWritten = autospaceInserted;
1258+
mSpaceState = SpaceState.NONE;
12091259
}
1210-
final boolean autospaceInserted = mConnection.getExpectedSelectionEnd() > beforeSpace;
1211-
// If we DID insert an autospace, fix up mLastComposedWord so revertCommit (backspace +
1212-
// PREF_BACKSPACE_REVERTS_AUTOCORRECT) deletes the space along with the word. Without
1213-
// this the existing revert code's `deleteLength = cancelLength + separatorLength`
1214-
// would only delete the word, and in DEBUG builds the bundled assertion against
1215-
// `getTextBeforeCursor(...).subSequence(0, cancelLength) equals committedWord` throws
1216-
// because the last cancelLength chars now include the trailing space, not the word.
1217-
if (autospaceInserted && mLastComposedWord != null
1218-
&& mLastComposedWord != LastComposedWord.NOT_A_COMPOSED_WORD
1219-
&& Constants.STRING_SPACE.equals(mLastComposedWord.mSeparatorString) == false) {
1220-
mLastComposedWord = new LastComposedWord(
1221-
mLastComposedWord.mEvents,
1222-
mLastComposedWord.mInputPointers,
1223-
mLastComposedWord.mTypedWord,
1224-
mLastComposedWord.mCommittedWord,
1225-
Constants.STRING_SPACE,
1226-
mLastComposedWord.mNgramContext,
1227-
mLastComposedWord.mCapitalizedMode);
1228-
}
1229-
// Don't set PHANTOM here — we already wrote the space to the editor. PHANTOM would
1230-
// make the next letter call insertAutomaticSpaceIfOptionsAndTextAllow AGAIN (see
1231-
// handleNonSeparatorEvent line ~1760), giving a double space. Instead, set a
1232-
// dedicated one-shot flag that handleSeparatorEvent uses to strip the autospace if
1233-
// a punctuation character follows. The flag is cleared by enterCombiningMode (next
1234-
// input took over), cancelCombiningMode (backspace etc), or once consumed.
1235-
mAutospaceJustWritten = autospaceInserted;
1236-
mSpaceState = SpaceState.NONE;
12371260
mConnection.endBatchEdit();
12381261
final int cursorAfter = mConnection.getExpectedSelectionEnd();
12391262
// The commit doesn't move the cursor for the composing text itself (it was already
@@ -1295,6 +1318,12 @@ private void onCombiningGraceExpired() {
12951318
mBackspaceUnits.setCommitted(writtenChars, committedFragments);
12961319
}
12971320
// "keep_alternatives" — fall through, do nothing.
1321+
// #14 bug fix: this commit ran on the async grace timer, OFF the normal onCodeInput path
1322+
// that refreshes the shift state after a commit. Without this, the next word's auto-caps
1323+
// is stale — auto-caps gets dropped after a grace auto-commit and capitalization comes out
1324+
// erratic. Mirror the gesture-commit path's requestUpdatingShiftState.
1325+
KeyboardSwitcher.getInstance().requestUpdatingShiftState(
1326+
getCurrentAutoCapsState(sv), getCurrentRecapitalizeState());
12981327
}
12991328

13001329
/**

app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,8 @@ object Defaults {
149149
const val PREF_COMBINING_AUTOCORRECT_ON_AUTOSPACE = true
150150
const val PREF_COMBINING_TAP_EXTRA_MS = 250
151151
const val PREF_COMBINING_AUTOSPACE_ONLY_AFTER_GESTURE = false
152+
const val PREF_SPACING_DEFER_GRACE_SPACE = false
153+
const val PREF_COMBINING_GRACE_ONLY_AFTER_GESTURE = true // default on: tapped words shouldn't auto-finish
152154
const val PREF_COMBINING_AUTOSPACE_SUGGESTIONS = "alternatives_then_next_word"
153155
const val PREF_COMBINING_BACKSPACE_DELETES_GESTURE_WORD = true
154156
const val PREF_COMBINING_BACKSPACE_DELETES_COMPOSING_TEXT = true

app/src/main/java/helium314/keyboard/latin/settings/Settings.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,12 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
157157
// was a tap (peck-typists need more headroom than swipers between letters). 0 = no extra.
158158
public static final String PREF_COMBINING_TAP_EXTRA_MS = "combining_tap_extra_ms";
159159
public static final String PREF_COMBINING_AUTOSPACE_ONLY_AFTER_GESTURE = "combining_autospace_only_after_gesture";
160+
// #23: route the two-thumb grace-mode auto-commit space through the deferred PHANTOM
161+
// mechanism (like the default gesture path) instead of writing it eagerly. Experimental.
162+
public static final String PREF_SPACING_DEFER_GRACE_SPACE = "spacing_defer_grace_space";
163+
// #14: when on, the combining grace timer only auto-commits words that include a swipe —
164+
// pure tap-typed words are never auto-finished by the timer. Experimental, default off.
165+
public static final String PREF_COMBINING_GRACE_ONLY_AFTER_GESTURE = "combining_grace_only_after_gesture";
160166
// What the suggestion strip shows after the combining grace timer auto-commits a word.
161167
// Values: "keep_alternatives" (1) | "next_word" (2, default) | "alternatives_then_next_word" (3).
162168
public static final String PREF_COMBINING_AUTOSPACE_SUGGESTIONS = "combining_autospace_suggestions";

app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ public class SettingsValues {
137137
public final boolean mCombiningAutocorrectOnAutospace;
138138
public final int mCombiningTapExtraMs;
139139
public final boolean mCombiningAutospaceOnlyAfterGesture;
140+
public final boolean mSpacingDeferGraceSpace;
141+
public final boolean mCombiningGraceOnlyAfterGesture;
140142
// Raw string value: "keep_alternatives" | "next_word" | "alternatives_then_next_word"
141143
public final String mCombiningAutospaceSuggestions;
142144
public final boolean mCombiningBackspaceDeletesGestureWord;
@@ -382,6 +384,12 @@ public SettingsValues(final Context context, final SharedPreferences prefs, fina
382384
mCombiningAutospaceOnlyAfterGesture = prefs.getBoolean(
383385
Settings.PREF_COMBINING_AUTOSPACE_ONLY_AFTER_GESTURE,
384386
Defaults.PREF_COMBINING_AUTOSPACE_ONLY_AFTER_GESTURE);
387+
mSpacingDeferGraceSpace = prefs.getBoolean(
388+
Settings.PREF_SPACING_DEFER_GRACE_SPACE,
389+
Defaults.PREF_SPACING_DEFER_GRACE_SPACE);
390+
mCombiningGraceOnlyAfterGesture = prefs.getBoolean(
391+
Settings.PREF_COMBINING_GRACE_ONLY_AFTER_GESTURE,
392+
Defaults.PREF_COMBINING_GRACE_ONLY_AFTER_GESTURE);
385393
mCombiningAutospaceSuggestions = prefs.getString(Settings.PREF_COMBINING_AUTOSPACE_SUGGESTIONS,
386394
Defaults.PREF_COMBINING_AUTOSPACE_SUGGESTIONS);
387395
final boolean nonNormalTwoThumbSpacing = mGestureManualSpacing || mCombiningGraceMs > 0;

app/src/main/java/helium314/keyboard/settings/screens/TwoThumbTypingScreen.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ fun TwoThumbTypingScreen(
6969
add(Settings.PREF_COMBINING_AUTOSPACE_ONLY_AFTER_GESTURE)
7070
add(Settings.PREF_COMBINING_AUTOCORRECT_ON_AUTOSPACE)
7171
add(Settings.PREF_COMBINING_AUTOSPACE_SUGGESTIONS)
72+
add(Settings.PREF_SPACING_DEFER_GRACE_SPACE)
73+
add(Settings.PREF_COMBINING_GRACE_ONLY_AFTER_GESTURE)
7274
}
7375
if (nonNormalSpacing) {
7476
add(Settings.PREF_MULTIPART_FULL_WORD_SUGGESTIONS)
@@ -146,6 +148,16 @@ fun createTwoThumbTypingSettings(context: Context) = listOf(
146148
R.string.combining_autospace_only_after_gesture_summary) {
147149
SwitchPreference(it, Defaults.PREF_COMBINING_AUTOSPACE_ONLY_AFTER_GESTURE)
148150
},
151+
Setting(context, Settings.PREF_SPACING_DEFER_GRACE_SPACE,
152+
R.string.spacing_defer_grace_space,
153+
R.string.spacing_defer_grace_space_summary) {
154+
SwitchPreference(it, Defaults.PREF_SPACING_DEFER_GRACE_SPACE)
155+
},
156+
Setting(context, Settings.PREF_COMBINING_GRACE_ONLY_AFTER_GESTURE,
157+
R.string.combining_grace_only_after_gesture,
158+
R.string.combining_grace_only_after_gesture_summary) {
159+
SwitchPreference(it, Defaults.PREF_COMBINING_GRACE_ONLY_AFTER_GESTURE)
160+
},
149161
Setting(context, Settings.PREF_COMBINING_AUTOSPACE_SUGGESTIONS,
150162
R.string.combining_autospace_suggestions, R.string.combining_autospace_suggestions_summary) { def ->
151163
val items = listOf(

app/src/main/res/values/strings.xml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,15 @@
294294
<string name="two_thumb_tap_autospace_grace">Extra autospace delay after taps</string>
295295
<string name="two_thumb_tap_autospace_grace_summary">Add this much extra time when the last input was a tapped letter, so tap-then-swipe combinations are easier to continue.</string>
296296
<string name="combining_autospace_only_after_gesture">Only auto-space after swipes</string>
297-
<string name="combining_autospace_only_after_gesture_summary">When enabled, tap-only words are committed without an automatic space. Words that include a swipe still auto-space.</string>
297+
<string name="combining_autospace_only_after_gesture_summary">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.</string>
298+
<!-- Title of the experimental toggle that defers the two-thumb grace auto-commit space. -->
299+
<string name="spacing_defer_grace_space">Defer grace space (experimental)</string>
300+
<!-- Description for spacing_defer_grace_space. -->
301+
<string name="spacing_defer_grace_space_summary">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.</string>
302+
<!-- Title of the toggle that limits the grace auto-commit timer to swiped words. -->
303+
<string name="combining_grace_only_after_gesture">Only auto-finish swiped words</string>
304+
<!-- Description for combining_grace_only_after_gesture. -->
305+
<string name="combining_grace_only_after_gesture_summary">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).</string>
298306
<!-- Title of the toggle that makes the first backspace after a gesture delete the whole word. -->
299307
<string name="combining_backspace_deletes_gesture_word">Backspace deletes last swipe</string>
300308
<!-- Description for combining_backspace_deletes_gesture_word. -->

app/src/test/java/helium314/keyboard/latin/InputLogicTest.kt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,37 @@ class InputLogicTest {
362362
assertEquals("hello ", textBeforeCursor)
363363
}
364364

365+
@Test fun deferredGraceSpaceMaterializesOnNextInput() {
366+
// #23: with PREF_SPACING_DEFER_GRACE_SPACE on, the grace commit does NOT write the space
367+
// eagerly (the default path gives "hello "); it arms PHANTOM so the space appears on the
368+
// next input instead.
369+
reset()
370+
latinIME.prefs().edit {
371+
putInt(Settings.PREF_COMBINING_GRACE_MS, 1000)
372+
putBoolean(Settings.PREF_SPACING_DEFER_GRACE_SPACE, true)
373+
}
374+
gestureInput("hello")
375+
expireCombiningGrace()
376+
assertEquals("hello", textBeforeCursor) // deferred: no trailing space yet
377+
chainInput("world")
378+
assertEquals("hello world", textBeforeCursor) // materialized on the next letter
379+
}
380+
381+
@Test fun deferredGraceCommitIsBackspaceReversible() {
382+
// The deferred commit leaves no eager space to orphan; the first backspace deletes the
383+
// gesture word cleanly (PREF_COMBINING_BACKSPACE_DELETES_GESTURE_WORD default on).
384+
reset()
385+
latinIME.prefs().edit {
386+
putInt(Settings.PREF_COMBINING_GRACE_MS, 1000)
387+
putBoolean(Settings.PREF_SPACING_DEFER_GRACE_SPACE, true)
388+
}
389+
gestureInput("hello")
390+
expireCombiningGrace()
391+
assertEquals("hello", textBeforeCursor)
392+
functionalKeyPress(KeyCode.DELETE)
393+
assertEquals("", textBeforeCursor)
394+
}
395+
365396
@Test fun tapThenGestureCombiningWordStillAutospacesWhenGestureGateEnabled() {
366397
reset()
367398
latinIME.prefs().edit {

app/src/test/java/helium314/keyboard/settings/SettingsContainerTest.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,18 @@ class SettingsContainerTest {
6060
container[Settings.PREF_COMBINING_AUTOSPACE_ONLY_AFTER_GESTURE]?.key)
6161
}
6262

63+
@Test
64+
fun spacingDeferGraceSpaceSettingIsRegistered() {
65+
assertEquals(Settings.PREF_SPACING_DEFER_GRACE_SPACE,
66+
container[Settings.PREF_SPACING_DEFER_GRACE_SPACE]?.key)
67+
}
68+
69+
@Test
70+
fun combiningGraceOnlyAfterGestureSettingIsRegistered() {
71+
assertEquals(Settings.PREF_COMBINING_GRACE_ONLY_AFTER_GESTURE,
72+
container[Settings.PREF_COMBINING_GRACE_ONLY_AFTER_GESTURE]?.key)
73+
}
74+
6375
@Test
6476
fun twoThumbLowLevelBackspaceSettingIsHiddenFromSearchRegistry() {
6577
assertNull(container[Settings.PREF_COMBINING_BACKSPACE_DELETES_GESTURE_WORD])

0 commit comments

Comments
 (0)