Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ public final class InputLogic {
// signal-driven grace + two-gate Assisted-tier logic.
private boolean mSpacingComplete; // typed word is a real dictionary word
private float mSpacingPrefixRichScore; // fraction of candidates that are completions [0..1]
// #24 last gate-decision snapshot — null when the Assisted tier is off (default).
// Read by A11y / TraceRecorder / replay harness; never drives commit behaviour yet.
@Nullable
private SpacingGateDecision mLastSpacingGateDecision;
private final Suggest mSuggest;
private final DictionaryFacilitator mDictionaryFacilitator;
private SingleDictionaryFacilitator mEmojiDictionaryFacilitator;
Expand Down Expand Up @@ -990,7 +994,14 @@ private void enterCombiningMode(final SettingsValues settingsValues, final boole
// separators / cursor-front recompositions there's nothing to auto-commit, and arming
// the timer would draw a spurious progress bar.
if (!mWordComposer.isComposingWord()) return;
final int graceMs = baseGraceMs + Math.max(0, settingsValues.mCombiningTapExtraMs);
final int graceMs;
if (settingsValues.mSpacingSignalDrivenGrace) {
// #24: vary the grace duration by the per-keystroke word-state signals.
graceMs = signalDrivenGraceMs(baseGraceMs, settingsValues.mSpacingCompleteBonusMs,
settingsValues.mSpacingPrefixPenaltyMs, mSpacingComplete, mSpacingPrefixRichScore);
} else {
graceMs = baseGraceMs + Math.max(0, settingsValues.mCombiningTapExtraMs);
}
cancelCombiningTimerOnly();
mInCombiningMode = true;
// #14 "only auto-finish swiped words": still ENTER combining mode (so a following swipe
Expand Down Expand Up @@ -1407,6 +1418,15 @@ public void setSuggestedWords(final SuggestedWords suggestedWords) {
final SpacingSignals spacingSignals = computeSpacingSignals(suggestedWords);
mSpacingComplete = spacingSignals.complete;
mSpacingPrefixRichScore = spacingSignals.prefixRichScore;
// #24 gate-decision: evaluate and snapshot whenever the Assisted tier is on.
// Stored for trace/A11y consumption; does NOT drive any commit behaviour yet.
final SettingsValues sv = Settings.getInstance().getCurrent();
if (sv != null && sv.mSpacingAssistedTier) {
mLastSpacingGateDecision = SpacingGateDecision.evaluate(
true, mSpacingComplete, mSpacingPrefixRichScore, sv.mSpacingLowThreshold);
} else {
mLastSpacingGateDecision = null;
}
final boolean newAutoCorrectionIndicator = suggestedWords.mWillAutoCorrect;

// Put a blue underline to a word in TextView which will be auto-corrected.
Expand Down Expand Up @@ -1461,6 +1481,31 @@ static SpacingSignals computeSpacingSignals(final SuggestedWords suggestedWords)
return new SpacingSignals(complete, (float) completions / n);
}

private static final int SIGNAL_GRACE_MIN_MS = 100;
private static final int SIGNAL_GRACE_MAX_MS = 3000;

/**
* #24 signal-driven grace duration: a confident complete word commits sooner (subtract
* {@code completeBonus}), while an extendable prefix-rich stem waits longer (add
* {@code prefixPenalty} scaled by the score), clamped to a sane range. Pure for testability.
*/
static int signalDrivenGraceMs(final int baseMs, final int completeBonusMs,
final int prefixPenaltyMs, final boolean complete, final float prefixRichScore) {
final int ms = baseMs - (complete ? completeBonusMs : 0)
+ Math.round(prefixPenaltyMs * prefixRichScore);
return Math.max(SIGNAL_GRACE_MIN_MS, Math.min(SIGNAL_GRACE_MAX_MS, ms));
}

/**
* Returns the last gate-decision snapshot (for A11y / TraceRecorder / replay-harness
* consumption), or {@code null} when the Assisted tier is disabled (the default).
* Never drives commit behaviour on its own.
*/
@Nullable
public SpacingGateDecision getLastSpacingGateDecision() {
return mLastSpacingGateDecision;
}

/**
* Handle a consumed event.
* <p>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// SPDX-License-Identifier: GPL-3.0-only
package helium314.keyboard.latin.inputlogic

/**
* Pure two-gate decision model for #24 "Assisted" spacing-policy tier.
*
* Stateless and Android-free: every input is explicit, so the whole thing is unit-testable
* on the JVM without a running InputLogic. Production wiring into [InputLogic.enterCombiningMode]
* is intentionally deferred — this file only provides the model + trace carrier.
*
* Gate table (see docs/SPACING_POLICY.md §5):
*
* | Gate | Condition | Behaviour |
* |------------|-------------------------------------------------|------------------------|
* | A — INSTANT | complete && prefixRichScore ≤ lowThreshold | commit immediately |
* | B — PAUSE | complete && prefixRichScore > lowThreshold | wait for inter-word pause |
* | NONE | !complete (or policy disabled) | fall back to grace timer |
*/
object SpacingGateModel {

/**
* Which gate (if any) the current word falls into.
*
* - [NONE] — policy disabled, or word not complete; fall back to signal-driven grace timer.
* - [INSTANT] — Gate A: word is finished and prefix-richness is low → commit immediately.
* - [PAUSE] — Gate B: word is finished but prefix-richness is high → wait for the inter-word
* pause threshold before committing.
*/
enum class Gate { NONE, INSTANT, PAUSE }

/**
* Decide which gate applies.
*
* @param policyEnabled master "Assisted tier" switch (default off — see
* [helium314.keyboard.latin.settings.Settings.PREF_SPACING_ASSISTED_TIER])
* @param complete the typed word is a real dictionary word (not user-typed-only)
* @param prefixRichScore fraction of suggestion candidates that are completions [0..1]
* @param lowThreshold Gate-A ceiling: score ≤ this triggers [Gate.INSTANT]
* (see [helium314.keyboard.latin.settings.Settings.PREF_SPACING_LOW_THRESHOLD])
* @return the applicable gate, or [Gate.NONE] when conditions are not met
*/
@JvmStatic
fun decide(
policyEnabled: Boolean,
complete: Boolean,
prefixRichScore: Float,
lowThreshold: Float,
): Gate {
if (!policyEnabled || !complete) return Gate.NONE
return if (prefixRichScore <= lowThreshold) Gate.INSTANT else Gate.PAUSE
}
}

/**
* Immutable snapshot of the most recent gate evaluation, held by [InputLogic] as
* `mLastSpacingGateDecision` for A11y / TraceRecorder / replay-harness consumption.
*
* The field is `@Nullable` so callers that have the Assisted tier disabled pay nothing —
* no allocation until the policy is actually on. Use [SpacingGateDecision.update] to
* produce a fresh snapshot cheaply.
*/
data class SpacingGateDecision(
/** The gate that fired (or [SpacingGateModel.Gate.NONE]). */
val gate: SpacingGateModel.Gate,
/** The `complete` signal at decision time. */
val complete: Boolean,
/** The `prefixRichScore` signal at decision time [0..1]. */
val prefixRichScore: Float,
/** The `lowThreshold` used at decision time. */
val lowThreshold: Float,
) {
companion object {
/**
* Convenience factory: evaluate [SpacingGateModel.decide] and wrap the result.
* Called from [InputLogic.setSuggestedWords] when the Assisted tier is enabled.
*/
@JvmStatic
fun evaluate(
policyEnabled: Boolean,
complete: Boolean,
prefixRichScore: Float,
lowThreshold: Float,
): SpacingGateDecision = SpacingGateDecision(
gate = SpacingGateModel.decide(policyEnabled, complete, prefixRichScore, lowThreshold),
complete = complete,
prefixRichScore = prefixRichScore,
lowThreshold = lowThreshold,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,13 @@ object Defaults {
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_SPACING_SIGNAL_DRIVEN_GRACE = false
const val PREF_SPACING_COMPLETE_BONUS_MS = 200 // complete word commits this much sooner
const val PREF_SPACING_PREFIX_PENALTY_MS = 400 // max extra wait when fully prefix-rich
// #24 two-gate Assisted-tier: master switch (default off) + Gate-A low-threshold.
// 0.1f is conservative: only clearly non-extendable words (very few completions) get Gate A.
const val PREF_SPACING_ASSISTED_TIER = false
const val PREF_SPACING_LOW_THRESHOLD = 0.1f
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
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/java/helium314/keyboard/latin/settings/Settings.java
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,16 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
// #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";
// #14/#24 signal-driven grace: vary the grace-timer duration by the per-keystroke word-state
// signals (complete / prefix-richness) instead of a fixed value. Experimental, default off.
public static final String PREF_SPACING_SIGNAL_DRIVEN_GRACE = "spacing_signal_driven_grace";
public static final String PREF_SPACING_COMPLETE_BONUS_MS = "spacing_complete_bonus_ms";
public static final String PREF_SPACING_PREFIX_PENALTY_MS = "spacing_prefix_penalty_ms";
// #24 two-gate Assisted-tier: master on/off + Gate-A low-threshold. Both experimental,
// default off / conservative. The gate decision model is in SpacingGateModel.kt; production
// wiring into enterCombiningMode is deferred to the next phase.
public static final String PREF_SPACING_ASSISTED_TIER = "spacing_assisted_tier";
public static final String PREF_SPACING_LOW_THRESHOLD = "spacing_low_threshold";
// 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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,12 @@ public class SettingsValues {
public final boolean mCombiningAutospaceOnlyAfterGesture;
public final boolean mSpacingDeferGraceSpace;
public final boolean mCombiningGraceOnlyAfterGesture;
public final boolean mSpacingSignalDrivenGrace;
public final int mSpacingCompleteBonusMs;
public final int mSpacingPrefixPenaltyMs;
// #24 two-gate Assisted-tier (experimental, default off).
public final boolean mSpacingAssistedTier;
public final float mSpacingLowThreshold;
// Raw string value: "keep_alternatives" | "next_word" | "alternatives_then_next_word"
public final String mCombiningAutospaceSuggestions;
public final boolean mCombiningBackspaceDeletesGestureWord;
Expand Down Expand Up @@ -390,6 +396,17 @@ public SettingsValues(final Context context, final SharedPreferences prefs, fina
mCombiningGraceOnlyAfterGesture = prefs.getBoolean(
Settings.PREF_COMBINING_GRACE_ONLY_AFTER_GESTURE,
Defaults.PREF_COMBINING_GRACE_ONLY_AFTER_GESTURE);
mSpacingSignalDrivenGrace = prefs.getBoolean(
Settings.PREF_SPACING_SIGNAL_DRIVEN_GRACE,
Defaults.PREF_SPACING_SIGNAL_DRIVEN_GRACE);
mSpacingCompleteBonusMs = prefs.getInt(Settings.PREF_SPACING_COMPLETE_BONUS_MS,
Defaults.PREF_SPACING_COMPLETE_BONUS_MS);
mSpacingPrefixPenaltyMs = prefs.getInt(Settings.PREF_SPACING_PREFIX_PENALTY_MS,
Defaults.PREF_SPACING_PREFIX_PENALTY_MS);
mSpacingAssistedTier = prefs.getBoolean(Settings.PREF_SPACING_ASSISTED_TIER,
Defaults.PREF_SPACING_ASSISTED_TIER);
mSpacingLowThreshold = prefs.getFloat(Settings.PREF_SPACING_LOW_THRESHOLD,
Defaults.PREF_SPACING_LOW_THRESHOLD);
mCombiningAutospaceSuggestions = prefs.getString(Settings.PREF_COMBINING_AUTOSPACE_SUGGESTIONS,
Defaults.PREF_COMBINING_AUTOSPACE_SUGGESTIONS);
final boolean nonNormalTwoThumbSpacing = mGestureManualSpacing || mCombiningGraceMs > 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ fun TwoThumbTypingScreen(
add(Settings.PREF_COMBINING_AUTOSPACE_SUGGESTIONS)
add(Settings.PREF_SPACING_DEFER_GRACE_SPACE)
add(Settings.PREF_COMBINING_GRACE_ONLY_AFTER_GESTURE)
add(Settings.PREF_SPACING_SIGNAL_DRIVEN_GRACE)
add(Settings.PREF_SPACING_COMPLETE_BONUS_MS)
add(Settings.PREF_SPACING_PREFIX_PENALTY_MS)
}
if (nonNormalSpacing) {
add(Settings.PREF_MULTIPART_FULL_WORD_SUGGESTIONS)
Expand Down Expand Up @@ -158,6 +161,44 @@ fun createTwoThumbTypingSettings(context: Context) = listOf(
R.string.combining_grace_only_after_gesture_summary) {
SwitchPreference(it, Defaults.PREF_COMBINING_GRACE_ONLY_AFTER_GESTURE)
},
Setting(context, Settings.PREF_SPACING_SIGNAL_DRIVEN_GRACE,
R.string.spacing_signal_driven_grace, R.string.spacing_signal_driven_grace_summary) {
SwitchPreference(it, Defaults.PREF_SPACING_SIGNAL_DRIVEN_GRACE)
},
Setting(context, Settings.PREF_SPACING_COMPLETE_BONUS_MS,
R.string.spacing_complete_bonus, R.string.spacing_complete_bonus_summary) { def ->
SliderPreference(
name = def.title,
key = def.key,
default = Defaults.PREF_SPACING_COMPLETE_BONUS_MS,
range = 0f..1000f,
description = { stringResource(R.string.abbreviation_unit_milliseconds, it.toString()) }
)
},
Setting(context, Settings.PREF_SPACING_PREFIX_PENALTY_MS,
R.string.spacing_prefix_penalty, R.string.spacing_prefix_penalty_summary) { def ->
SliderPreference(
name = def.title,
key = def.key,
default = Defaults.PREF_SPACING_PREFIX_PENALTY_MS,
range = 0f..1500f,
description = { stringResource(R.string.abbreviation_unit_milliseconds, it.toString()) }
)
},
Setting(context, Settings.PREF_SPACING_ASSISTED_TIER,
R.string.spacing_assisted_tier, R.string.spacing_assisted_tier_summary) {
SwitchPreference(it, Defaults.PREF_SPACING_ASSISTED_TIER)
},
Setting(context, Settings.PREF_SPACING_LOW_THRESHOLD,
R.string.spacing_low_threshold, R.string.spacing_low_threshold_summary) { def ->
SliderPreference(
name = def.title,
key = def.key,
default = Defaults.PREF_SPACING_LOW_THRESHOLD,
range = 0f..1f,
description = { value -> "%.2f".format(value) }
)
},
Setting(context, Settings.PREF_COMBINING_AUTOSPACE_SUGGESTIONS,
R.string.combining_autospace_suggestions, R.string.combining_autospace_suggestions_summary) { def ->
val items = listOf(
Expand Down
12 changes: 12 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,18 @@
<string name="combining_grace_only_after_gesture">Only auto-finish swiped words</string>
<!-- Description for combining_grace_only_after_gesture. -->
<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>
<!-- #24 signal-driven grace toggle + its two tuning sliders (experimental). -->
<string name="spacing_signal_driven_grace">Adapt pause to the word (experimental)</string>
<string name="spacing_signal_driven_grace_summary">Vary the auto-finish pause by what you\'re typing: a finished dictionary word commits sooner, while a stem that many longer words start with waits longer. Tune the two amounts below.</string>
<string name="spacing_complete_bonus">Finished-word speed-up</string>
<string name="spacing_complete_bonus_summary">How much sooner a complete dictionary word auto-finishes.</string>
<string name="spacing_prefix_penalty">Extendable-stem patience</string>
<string name="spacing_prefix_penalty_summary">Extra wait when many longer words start with what you\'ve typed (so you can keep going).</string>
<!-- #24 two-gate Assisted-tier master switch + Gate-A low-threshold slider. -->
<string name="spacing_assisted_tier">Assisted auto-commit (experimental)</string>
<string name="spacing_assisted_tier_summary">Instantly commit clearly finished words (Gate A) and hold extendable stems until you pause (Gate B). Default off \u2014 requires the pause timer above to be active.</string>
<string name="spacing_low_threshold">Finished-word certainty</string>
<string name="spacing_low_threshold_summary">Words whose prefix-richness score is at or below this value get instant auto-commit (Gate A). Lower = more selective (fewer instant commits). Default 0.10.</string>
<!-- Title of the toggle that makes the first backspace after a gesture delete the whole word. -->
<string name="combining_backspace_deletes_gesture_word">Backspace deletes last swipe</string>
<!-- Description for combining_backspace_deletes_gesture_word. -->
Expand Down
Loading
Loading