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 @@ -48,6 +48,7 @@
import helium314.keyboard.keyboard.internal.PopupKeySpec;
import helium314.keyboard.keyboard.internal.NonDistinctMultitouchHelper;
import helium314.keyboard.keyboard.internal.SlidingKeyInputDrawingPreview;
import helium314.keyboard.keyboard.internal.SpacingInsightDrawingPreview;
import helium314.keyboard.keyboard.internal.TimerHandler;
import helium314.keyboard.keyboard.internal.KeyboardIconsSet;
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode;
Expand Down Expand Up @@ -126,6 +127,8 @@ public final class MainKeyboardView extends KeyboardView implements DrawingProxy
private final SlidingKeyInputDrawingPreview mSlidingKeyInputDrawingPreview;
// Debug overlay for two-thumb point hinting (#2.1), toggled by PREF_GESTURE_DEBUG_DRAW_POINTS.
private final GestureDebugPointsDrawingPreview mGestureDebugPointsDrawingPreview;
// Spacing-policy signal readout (#A11), co-gated by PREF_GESTURE_DEBUG_DRAW_POINTS.
private final SpacingInsightDrawingPreview mSpacingInsightDrawingPreview;

// Key preview
private final KeyPreviewDrawParams mKeyPreviewDrawParams;
Expand Down Expand Up @@ -233,6 +236,8 @@ public MainKeyboardView(final Context context, final AttributeSet attrs, final i
// Debug overlay last so it draws ON TOP of the gesture trail / floating preview.
mGestureDebugPointsDrawingPreview = new GestureDebugPointsDrawingPreview();
mGestureDebugPointsDrawingPreview.setDrawingView(drawingPreviewPlacerView);
mSpacingInsightDrawingPreview = new SpacingInsightDrawingPreview();
mSpacingInsightDrawingPreview.setDrawingView(drawingPreviewPlacerView);
mainKeyboardViewAttr.recycle();

mDrawingPreviewPlacerView = drawingPreviewPlacerView;
Expand Down Expand Up @@ -518,8 +523,9 @@ private void setGesturePreviewMode(final boolean isGestureTrailEnabled,
mGestureTrailsDrawingPreview.setPreviewEnabled(isGestureTrailEnabled);
// The debug overlay tracks its own pref and is independent of the user-visible trail —
// enable the preview whenever the pref is on so the drawing pass actually runs.
mGestureDebugPointsDrawingPreview.setPreviewEnabled(
Settings.getValues().mGestureDebugDrawPoints);
final boolean debugEnabled = Settings.getValues().mGestureDebugDrawPoints;
mGestureDebugPointsDrawingPreview.setPreviewEnabled(debugEnabled);
mSpacingInsightDrawingPreview.setPreviewEnabled(debugEnabled);
}

public void showGestureFloatingPreviewText(@NonNull final SuggestedWords suggestedWords,
Expand Down Expand Up @@ -588,6 +594,16 @@ public void setGestureCommitPending(final boolean pending) {
mGestureFloatingTextDrawingPreview.setCommitPending(pending);
}

// Implements {@link DrawingProxy#setSpacingInsight} (#A11). The readout is co-gated by
// PREF_GESTURE_DEBUG_DRAW_POINTS so there are no new settings to expose.
@Override
public void setSpacingInsight(final boolean complete, final float prefixRichScore,
final int graceMs, @Nullable final String gate) {
if (!Settings.getValues().mGestureDebugDrawPoints) return;
locatePreviewPlacerView();
mSpacingInsightDrawingPreview.update(complete, prefixRichScore, graceMs, gate);
}

// Note that this method is called from a non-UI thread.
@SuppressWarnings("static-method")
public void setMainDictionaryAvailability(final boolean mainDictionaryAvailable) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,22 @@ void setGestureDebugPoints(@NonNull helium314.keyboard.latin.common.InputPointer
* normal commit / cancel / continuation.
*/
void setGestureCommitPending(boolean pending);

/**
* Push a spacing-policy signal snapshot to the debug overlay (#A11), gated behind
* {@code PREF_GESTURE_DEBUG_DRAW_POINTS}. Called from {@code InputLogic} each time the
* combining-mode timer is armed or cleared.
*
* <p>Implementations must guard on the debug-draw setting internally; calling with
* the setting off must be a cheap no-op.
*
* @param complete whether the current typed stem is a dictionary word
* @param prefixRichScore fraction of suggestions that are prefix-completions [0..1]
* @param graceMs resolved grace duration (ms); {@code <= 0} clears the overlay
* @param gate active gate label; pass {@code null} to use the default
* ({@value SpacingInsightDrawingPreview#GATE_TIMER}). The two-gate
* branch passes its own label here.
*/
void setSpacingInsight(boolean complete, float prefixRichScore, int graceMs,
@Nullable String gate);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
/*
* SPDX-License-Identifier: GPL-3.0-only
*/

package helium314.keyboard.keyboard.internal;

import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.os.SystemClock;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import helium314.keyboard.keyboard.PointerTracker;
import helium314.keyboard.latin.common.CoordinateUtils;

/**
* Debug overlay (#A11) that shows the spacing-policy signals for the most recent combining-mode
* arm, drawn in the bottom-left corner of the keyboard area on top of all other previews.
*
* <p>Gated behind {@code PREF_GESTURE_DEBUG_DRAW_POINTS}; no new preference required.
*
* <p>Displayed as decision-first, human-readable text:
* <ul>
* <li>{@code FAST 300ms} – finished word, shorter timer</li>
* <li>{@code WAIT 620ms} – many continuations, longer timer</li>
* <li>{@code INSTANT} / {@code PAUSE} – Assisted-tier gate labels once enabled</li>
* </ul>
*
* <p>The snapshot string is built once per signal update (in {@link #update}), not per draw
* frame, so the drawing path is allocation-free.
*
* <p>Integration point for the gate branch: pass a non-null {@code gate} string to
* {@link #update} (e.g. {@code "timer"}, {@code "two-gate"}) or call {@link #updateGate} to
* re-stamp only the gate label without resetting the other signals.
*/
public final class SpacingInsightDrawingPreview extends AbstractDrawingPreview {

private static final float TEXT_SIZE_SP = 11f; // scaled in setKeyboardViewGeometry
private static final float PADDING_PX = 8f;
private static final long LINGER_AFTER_CLEAR = 1800L; // keep visible long enough to read
private static final int BG_COLOR = 0xDD1A1A2E; // dark navy, ~87% opaque
private static final int TEXT_COLOR = 0xFFFFFFFF; // primary line
private static final int LABEL_COLOR = 0xFFD0E8FF; // detail line
/** Gate label used when the caller passes {@code null}. */
public static final String GATE_TIMER = "timer";
/** Sentinel gate label for "no gate / idle". */
public static final String GATE_NONE = "none";

private final Paint mBgPaint = new Paint();
private final Paint mTextPaint = new Paint();
private final Paint mLabelPaint = new Paint();

// Keyboard area from setKeyboardViewGeometry – used for corner positioning.
private int mKeyboardWidth;
private int mKeyboardHeight;

// Raw signal fields retained so gate branch can call updateGate() without re-supplying all.
private boolean mComplete;
private float mPrefixRichScore;
private int mGraceMs;
@Nullable private String mGate;

/** Primary/detail readout lines; {@code null} primary means overlay draws nothing. */
@Nullable private String mPrimary;
@Nullable private String mDetail;
private long mVisibleUntilMs;

public SpacingInsightDrawingPreview() {
mBgPaint.setStyle(Paint.Style.FILL);
mBgPaint.setColor(BG_COLOR);

mTextPaint.setAntiAlias(true);
mTextPaint.setTypeface(Typeface.MONOSPACE);
mTextPaint.setColor(TEXT_COLOR);
mTextPaint.setTextSize(TEXT_SIZE_SP * 2.5f); // rough default; refined in setKeyboardViewGeometry

mLabelPaint.setAntiAlias(true);
mLabelPaint.setTypeface(Typeface.MONOSPACE);
mLabelPaint.setColor(LABEL_COLOR);
mLabelPaint.setTextSize(TEXT_SIZE_SP * 2.5f);
}

@Override
public void setKeyboardViewGeometry(@NonNull final int[] originCoords,
final int width, final int height) {
super.setKeyboardViewGeometry(originCoords, width, height);
mKeyboardWidth = width;
mKeyboardHeight = height;
// Scale text relative to keyboard height so it stays readable at any DPI.
final float textPx = Math.max(24f, height * 0.045f);
mTextPaint.setTextSize(textPx);
mLabelPaint.setTextSize(textPx);
}

/**
* Push a new signal snapshot. Call from {@code InputLogic} each time the combining-mode
* timer is armed. Call with {@code graceMs <= 0} to clear (e.g. on commit/cancel).
*
* @param complete whether the current stem is a dictionary word
* @param prefixRichScore fraction of suggestions that are prefix-completions [0..1]
* @param graceMs resolved grace duration (ms); {@code <= 0} clears the overlay
* @param gate active gate label; {@code null} renders as {@value #GATE_TIMER}
*/
public void update(final boolean complete, final float prefixRichScore,
final int graceMs, @Nullable final String gate) {
if (graceMs <= 0) {
// Don't disappear immediately on commit/cancel — leave the last decision readable.
if (mPrimary != null) {
mVisibleUntilMs = SystemClock.uptimeMillis() + LINGER_AFTER_CLEAR;
invalidateDrawingView();
}
return;
}
mComplete = complete;
mPrefixRichScore = prefixRichScore;
mGraceMs = graceMs;
mGate = gate;
buildSnapshot(complete, prefixRichScore, graceMs, gate);
mVisibleUntilMs = SystemClock.uptimeMillis() + Math.max(LINGER_AFTER_CLEAR, graceMs + 800L);
invalidateDrawingView();
}

/**
* Re-stamp only the gate label on the current snapshot without resetting the signal
* fields. No-op if there is no active snapshot (no live combining-mode arm).
*
* <p>This is the integration hook for the gate branch: call it as soon as the gate
* decision is made to update the readout without waiting for the next keystroke.
*
* @param gate new gate label; {@code null} falls back to {@value #GATE_TIMER}
*/
public void updateGate(@Nullable final String gate) {
if (mPrimary == null) return;
mGate = gate;
buildSnapshot(mComplete, mPrefixRichScore, mGraceMs, gate);
mVisibleUntilMs = SystemClock.uptimeMillis() + Math.max(LINGER_AFTER_CLEAR, mGraceMs + 800L);
invalidateDrawingView();
}

private void buildSnapshot(final boolean complete, final float prefixRichScore,
final int graceMs, @Nullable final String gate) {
final int prefixPct = Math.round(prefixRichScore * 100f);
final String gateLabel = gate == null ? GATE_TIMER : gate;

if ("instant".equals(gateLabel)) {
mPrimary = "INSTANT";
mDetail = "finished word + low prefix";
} else if ("pause".equals(gateLabel)) {
mPrimary = "WAIT " + graceMs + "ms";
mDetail = "many continuations · px " + prefixPct + "%";
} else if (complete) {
mPrimary = "FAST " + graceMs + "ms";
mDetail = "finished word · px " + prefixPct + "%";
} else if (prefixRichScore >= 0.50f) {
mPrimary = "WAIT " + graceMs + "ms";
mDetail = "many continuations · px " + prefixPct + "%";
} else {
mPrimary = "TIMER " + graceMs + "ms";
mDetail = "not complete · px " + prefixPct + "%";
}
}

@Override
public void drawPreview(@NonNull final Canvas canvas) {
if (!isPreviewEnabled()) return;
final String primary = mPrimary;
if (primary == null) return;
if (SystemClock.uptimeMillis() > mVisibleUntilMs) {
mPrimary = null;
mDetail = null;
return;
}
final String detail = mDetail == null ? "" : mDetail;

final float textH = mTextPaint.getTextSize();
final float lineGap = Math.max(2f, textH * 0.18f);
final float primaryW = mTextPaint.measureText(primary);
final float detailW = mLabelPaint.measureText(detail);
final float boxW = Math.min(mKeyboardWidth - PADDING_PX * 2f,
Math.max(primaryW, detailW) + PADDING_PX * 2f);
final float boxH = textH * 2f + lineGap + PADDING_PX * 2f;

// Position: bottom-left of the keyboard area, inset enough to avoid clipping.
final float left = PADDING_PX;
final float top = Math.max(PADDING_PX, mKeyboardHeight - boxH - PADDING_PX);
final float right = left + boxW;
final float bottom = top + boxH;

canvas.drawRoundRect(left, top, right, bottom, 8f, 8f, mBgPaint);
canvas.drawText(primary, left + PADDING_PX, top + PADDING_PX + textH, mTextPaint);
canvas.drawText(detail, left + PADDING_PX,
top + PADDING_PX + textH * 2f + lineGap, mLabelPaint);
}

@Override
public void onDeallocateMemory() {
mPrimary = null;
mDetail = null;
}

@Override
public void setPreviewPosition(@NonNull final PointerTracker tracker) {
// Position is fixed (bottom-left of keyboard) — no tracker tracking needed.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -990,7 +990,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 All @@ -1014,6 +1021,8 @@ private void enterCombiningMode(final SettingsValues settingsValues, final boole
&& !mSuppressAutospaceForForceNextSpace;
kv.setCombiningMode(showAutospaceIndicator, startTime, graceMs,
true /* compositionActiveForDebug */);
// #A11: push spacing-policy signals to the debug overlay.
kv.setSpacingInsight(mSpacingComplete, mSpacingPrefixRichScore, graceMs, null);
}
}

Expand Down Expand Up @@ -1137,7 +1146,10 @@ void cancelCombiningMode() {
if (mInCombiningMode) {
mInCombiningMode = false;
final MainKeyboardView kv = KeyboardSwitcher.getInstance().getMainKeyboardView();
if (kv != null) kv.setCombiningMode(false, 0L, 0);
if (kv != null) {
kv.setCombiningMode(false, 0L, 0);
kv.setSpacingInsight(false, 0f, 0, null); // clear the #A11 readout
}
}
}

Expand Down Expand Up @@ -1191,7 +1203,10 @@ private void onCombiningGraceExpired() {
mPendingCombiningCommit = null;
mInCombiningMode = false;
final MainKeyboardView kv = KeyboardSwitcher.getInstance().getMainKeyboardView();
if (kv != null) kv.setCombiningMode(false, 0L, 0);
if (kv != null) {
kv.setCombiningMode(false, 0L, 0);
kv.setSpacingInsight(false, 0f, 0, null); // clear the #A11 readout
}
final SettingsValues sv = Settings.getInstance().getCurrent();
if (!mWordComposer.isComposingWord()) return;
// Capture whether the word being committed by this timer came from a gesture. We
Expand Down Expand Up @@ -1461,6 +1476,21 @@ 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));
}

/**
* Handle a consumed event.
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,9 @@ 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
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
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,11 @@ 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";
// 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
Loading
Loading