Skip to content

Fix capitalization and delete swipe behavior - #672

Merged
LeanBitLab merged 1 commit into
LeanBitLab:mainfrom
Zokii0:fix/caps-and-delete-swipe
Oct 14, 2026
Merged

LeanBitLab merged 1 commit into
LeanBitLab:mainfrom
Zokii0:fix/caps-and-delete-swipe

Conversation

@Zokii0

@Zokii0 Zokii0 commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

This change addresses keyboard capitalization during rapid typing and improves vertical delete-swipe selection.

Changes:

  • Avoid re-detecting a key when only the one-shot shift state has changed.
  • Move delete-swipe selection vertically using editor line navigation when supported.
  • Read the editor's current selection to update delete-swipe boundaries.

@Zokii0

Zokii0 commented Oct 13, 2026

Copy link
Copy Markdown
Contributor Author

LeanType: two independent fixes

This package contains two separate changes. They touch different files and do not depend on each other,
so they can be applied together or split into two PRs.

# Change Files
1 First letter typed in lower case when typing very fast keyboard/PointerTracker.kt
2 Delete-swipe: vertical movement selects whole visual lines keyboard/KeyboardActionListenerImpl.kt, latin/RichInputConnection.kt

All paths are relative to app/src/main/java/helium314/keyboard/. Both changes were written against main and apply
unchanged to beta-429-6 (the affected files are identical in both). Neither change has been compiled or tested on a
device (no Android toolchain was available), so please test both before merging.


Part 1: First letter in lower case when typing fast

Symptom

The keyboard shows the auto-capitalized (shifted) layout at the start of a sentence/field. If the user types fast
enough that the second key is touched before the first finger is released (overlapping touches / rollover),
the first letter is committed in lower case even though the keyboard was displaying capitals.
Reproducible only with fast typing; no deleting is involved (see the screen recording in #467).

Root cause

Two existing mechanisms interact badly:

  1. KeyboardState.onPressKey() (unchanged): when a non-shift key is pressed with more than one pointer active,
    in alphabet mode, the one-shot shift is dropped (setShifted(ShiftMode.UNSHIFT) if the state is
    automatic-shifted, or manual-shifted with the shift key released). So the second finger touching down switches the
    keyboard from ELEMENT_ALPHABET_AUTOMATIC_SHIFTED to ELEMENT_ALPHABET, while the first finger is still down.
  2. PointerTracker.detectAndSendKey() (on release): if mKeyboardLayoutHasBeenChanged is set and the key is not a
    modifier, the key is re-detected at the touch coordinates on the current keyboard. This is meant for layout
    switches (symbols, chording, ...). Here the "current keyboard" is already the unshifted one, so the first key
    (a shifted A) is replaced by the unshifted a at the same position.

mKeyboardLayoutHasBeenChanged is set in setKeyDetectorInner() for every tracker whenever the keyboard
changes, including changes caused by another pointer after this pointer went down.

Fix

Only PointerTracker.kt is changed (app/src/main/java/helium314/keyboard/keyboard/PointerTracker.kt).

  • New field mOnlyShiftStateHasChanged. It tracks whether all keyboard changes since the layout-changed flag was last
    reset were only "one-shot shift dropped on the same alphabet layout".
  • New helper isOnlyShiftStateChange(oldKb, newKb): true if the new keyboard is ELEMENT_ALPHABET and the old one
    was ELEMENT_ALPHABET_AUTOMATIC_SHIFTED or ELEMENT_ALPHABET_MANUAL_SHIFTED, with the same subtype and height.
    The opposite direction (e.g. chording with the shift key: unshifted -> manual shifted) is deliberately not
    matched, so existing behavior there is unchanged.
  • setKeyDetectorInner(): updates mOnlyShiftStateHasChanged each time the keyboard changes.
  • onDownEventInternal() and callListenerOnPressAndCheckKeyboardLayoutChange(): reset mOnlyShiftStateHasChanged = true
    next to the existing resets of mKeyboardLayoutHasBeenChanged.
  • detectAndSendKey(): the key is re-detected only if
    mKeyboardLayoutHasBeenChanged && !mOnlyShiftStateHasChanged && !key.isModifier().

Behavior after the fix

  • First finger down on shifted H, second finger touches e before H is released: H is committed as H
    (the key object it was pressed on), e is committed as e. The existing "second finger resets the one-shot shift"
    logic is untouched, so the second letter is still lower case.
  • The own-press path (re-detection right after this pointer's own press changed the layout) is unchanged, which is what
    makes the second key lower case.
  • Layout switches (symbols/numpad/alpha, language switch, chording with shift) still re-detect the key as before.

Notes / testing

  • I could not build or run the app in my environment (no Gradle/Android SDK, no network), so the change is
    untested at runtime; please verify on a device: type fast at the start of a sentence in a new note, also with
    manual one-shot shift, caps-lock, and with ?123 / shift chording.
  • The same file is identical in main and beta-429-6 before the change, so the patch applies to both.
  • Related observation from the issue discussion (point 31): the caps not triggering is not related to backspace;
    it only depends on overlapping touches.

Part 2: Delete-swipe selects whole visual lines

Problem

When swiping on the delete key and moving up (to select lines above), the selection start lands in the
middle of a line instead of one visual line above. See the attached screen recording: after each upward
step the selected block begins at an arbitrary word in the middle of the previous line, and the result
does not match what the text field shows as lines.

Cause

KeyboardActionListenerImpl.getLineBoundariesBackwards() does not know the real layout of the text field.
It splits the text on \n and then cuts every longer segment into chunks of roughly 40 characters
(breaking at whitespace). These guessed boundaries rarely match the real soft-wrapped lines (field width,
font size, etc.), and the first boundary is just the current cursor position, so the first step up only
selects the part of the current line before the cursor.

Expected behavior

Each upward step extends the selection to exactly one visual line above, keeping the same column
(same as pressing Shift+Up). Each downward step shrinks it by one line (never beyond the original cursor).

Fix

Let the editor do the vertical movement instead of guessing:

  1. Before a vertical step, set a reversed selection: anchor = right end (original cursor),
    extent = current left end.
  2. Send Shift+DPAD_UP / Shift+DPAD_DOWN (same events already used for the arrow keys in selection mode).
  3. Read the real selection back with a new helper RichInputConnection.getEditorSelection()
    (synchronous getExtractedText).
  4. Clamp the new left end to the original cursor, restore a normal-order selection, and re-base the
    horizontal char/word state on the new left end, so combined horizontal + vertical swiping still works.

Fallback: if the editor does not return extracted text, or does not keep the reversed selection
(anchor != original cursor), the selection is restored and the old boundary approximation is used.

D-pad toolbar key

No change needed. ARROW_UP/ARROW_DOWN already send real DPAD_UP/DPAD_DOWN events
(with Shift in selection mode), so the editor already moves exactly one visual line.

Files changed

  • keyboard/KeyboardActionListenerImpl.kt: onMoveDeletePointer() + new moveDeleteSelectionByEditorLines()
  • latin/RichInputConnection.kt: new getEditorSelection()

Notes

Not compiled or tested on a device (no Android toolchain available when writing this).
Please test in: a native EditText, a web text field (Chrome/Brave), and a field with very long soft-wrapped lines,
including swiping up, then down, then left/right in word-by-word mode.
Same patch applies to main and beta-429-6 (the two files are identical in both).

@LeanBitLab
LeanBitLab merged commit 800f89c into LeanBitLab:main Oct 14, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants