From 875eb29215d354bb5fc313e3afa57dea3d6c1248 Mon Sep 17 00:00:00 2001 From: pablohc Date: Sat, 18 Apr 2026 23:12:56 +0200 Subject: [PATCH 1/5] refactor: redesign on-screen keyboard (#1644) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Complete redesign of the on-screen keyboard (used for WiFi password, KOReader, Calibre URLs) with improved layout, navigation, visual style, and new input features: **cursor mode** for text navigation, **password mode** with visibility toggle, and **URL mode** with pre-defined snippets. |**master** | **PR #1644** | |----------|-------------| | image | image | | **master** | **PR #1644** | |----------|-------------| | image | image | | ABC Mode | Symbol Mode | URL Mode | |----------|-------------|----------| | image | image | image | | Cursor Mode | Password Toggle | |-------------|-----------------| | image | image | - Reduced from 13/11/10 columns per row to **10 uniform columns** across all rows - Keyboard now uses **90% of screen width** (was ~66%) - Row 0: Numbers `1-9, 0` with secondary symbols (`!@#$%^&*()`) - Rows 1-3: Standard QWERTY letters - Bottom row: `shift` | `#@!` | `___` | `←` | `OK` - New mode toggle key `#@!` / `abc` switches between letter and symbol layouts - Symbol layout: 4 rows (numbers, inverted symbols, paired symbols, loose symbols) - Covers all 95 printable ASCII characters - **No secondary hints, no long-press** in symbol mode (simple and direct) - SHIFT key remains visible but **disabled** in symbol mode - In `InputType::Url`, the Space key becomes a **URL toggle** button - Activating URL mode replaces the 4 content rows with a **3×3 grid of URL snippets**: - Col 0 (protocols): `https://`, `http://`, `/opds` - Col 1 (hosts/ports): `www.`, `192.168.`, `:8080` - Col 2 (domains): `.com`, `.org`, `.net` - Snippets are inserted as full strings at the cursor position - URL mode **persists** after inserting a snippet (does not auto-deactivate) - Column alignment: col 0 over ABC, col 1 over URL, col 2 over Del - Up/Down navigation maps `bottomCol - 1` / `urlCol + 1` - SHIFT disabled in URL mode - SpecMode (`abc`) exits URL mode back to ABC - SpecSpace (`URL`) toggles URL mode on/off; selection always stays on the URL button - Button styled with `KeyboardKeyType::Mode` for consistent outline - **Enter**: Long-press Up (500ms) while in keyboard mode - **Exit**: Short-press Down while in cursor mode (resets `passwordVisible`, clears toggle position) - **Navigate**: Left/Right move cursor position within text (one position per press, no continuous repeat) - **Visual**: - Keyboard mode: underline cursor (2px line + serifs) - Cursor mode: inverted block cursor (black fill + white character) - Block width adapts to the actual character width under cursor (minimum 6px for narrow chars like space) - Block position includes inter-character kerning offset for correct alignment (calculated via string-difference: `getTextWidth(before+char) - getTextWidth(before) - getTextWidth(char)`) - End-of-text: thin 6px block - Password hidden: 3-part drawing (Part 1 + block + Part 3) to prevent block overflow onto `*` characters - Toggle position: caret ("I") cursor at saved position, `[abc]`/`[***]` label with inverted selection - **Inactive key styling**: - BaseTheme: 2px outline rectangle - LyraTheme: gray filled rounded rectangle (`Color::LightGray`) - **Password toggle position**: in cursor mode (Password only), Hold Right (500ms) enters toggle — caret cursor shows saved position, `[abc]`/`[***]` label becomes selected. Press Confirm to toggle `passwordVisible`. Press Left to restore cursor to saved position. Right from toggle is a no-op. Down from toggle exits to keyboard. - `cursorPos` persists between keyboard and cursor modes - `InputType::Password` enum replaces `bool isPassword` parameter - Text is masked with `*` except for one revealed character: - Keyboard mode: reveals character at `cursorPos - 1` - Cursor mode: no reveal in display text (block cursor draws actual char directly) - **Toggle `[abc]`/`[***]`**: accessible via cursor mode — Hold Right (500ms) enters toggle position, Confirm toggles visibility, Left exits back to cursor. Caret ("I") shown at saved position while in toggle. - `passwordVisible` resets to `false` when exiting cursor mode - **Long-press Del (1.5s)**: clears all text and resets cursor to 0 - Replaced `bool isPassword` constructor parameter with `enum class InputType { Text, Password, Url }` - Callers updated: `WifiSelectionActivity`, `KOReaderSettingsActivity`, `CalibreSettingsActivity` - `"Tips:"` header followed by context-sensitive hints, centered between text field underline and keyboard as a block - ABC mode: `"Hold SELECT for UPPERCASE or secondary char"` (shift ON: `"lowercase"` variant) + `"Hold DEL to clear all text"` (only if text not empty) - ABC + `InputType::Url`: same + `"Press URL for snippets"` - Symbol mode: `"Hold DEL to clear all text"` (only if text not empty) - URL mode: `"Press ABC to exit URL mode"` + `"Hold DEL to clear all text"` (only if text not empty) - Cursor mode: `"Press DOWN to return to keyboard"` - **Phase 1**: `"Hold UP to edit entry"` — shown after 2× DEL press, auto-hides after 4s, positioned below underline - **Phase 2**: `"Press < or > to move cursor"` + dynamic password toggle hint — shown when entering cursor mode, positioned below underline, visible until exit - When `!passwordVisible`: `"Hold > then press [abc] to show password"` - When `passwordVisible`: `"Hold > then press [***] to hide password"` - When in toggle position: `"Press < to return to cursor position"` - Holding Confirm (>500ms) inserts the **alternative character** instead of the primary - Letters: long-press inserts opposite case (e.g., `a`→`A`, `A`→`a`) - Numbers/symbols (row 0): long-press inserts secondary (e.g., `0`→`)`, `)`→`0`) - Only active in ABC mode; disabled in Symbol mode and URL mode - **`InputType::Url`**: Hold SELECT on ABC rows 1+ (letters) returns primary character only (same as short press). Row 0 (symbols) still returns secondary character on Hold SELECT. - Reduced from 3 states (shift/SHIFT/LOCK) to **2 sticky states** (shift/SHIFT) - Shift stays active after typing until manually toggled off - Label: `shift` (off) / `SHIFT` (on) - `enum class SpecialKeyType { Shift, Mode, Space, Del, Ok }` replaces plain `enum` (`SpecShift`, `SpecMode`, etc.) for type safety - All switch cases updated to `SpecialKeyType::*` with `static_cast()` for array indexing - `onExit()` reverted to simple `Activity::onExit()` call (half-refresh removed) - **Bottom row column mapping**: navigating up/down between content rows and bottom row uses `col/2` and `col*2` formulas for consistent positioning (10 cols ↔ 5 cols) - **URL mode column mapping**: `bottomCol - 1` / `urlCol + 1` (3 cols ↔ 5 cols) - **Wrap-around**: row 0 → up → bottom row and bottom row → down → row 0 both apply correct column mapping - **Space key**: underscore-style horizontal line (60% of key width, 3px thick) - **Delete key**: arrow `←` drawn with lines (3px thick) instead of "DEL" text - **Secondary label** (ABC row 0): small hint in top-right corner with separation from primary number - **BaseTheme**: selection uses **inverted fill** (black rect + white text) instead of `[bracket]` markers - **BaseTheme**: text field brackets drawn as **stretchable lines** that adapt to multi-line input (1px normal, 3px cursor mode) - **LyraTheme**: text field uses **fixed-width underline** (16px margins, 8px each side) instead of stretchable line (2px normal, 3px cursor mode) - **Both themes**: special keys (shift, mode, space, del, OK) have bordered/bordered-rounded rectangles - **Font size**: keyboard uses `UI_12_FONT_ID` in both themes (was `UI_10` in Base) - **Key height**: 40px in all themes for better proportions - **Layout unification**: text and password toggle are left-aligned in all themes (`keyboardCenteredText = false` for Lyra/Lyra3Covers) - **`primaryOffset` removed**: dead code eliminated from BaseTheme and LyraTheme `drawKeyboardKey` - `keyboardVerticalOffset`: per-theme vertical adjustment of keyboard position - Base: `-13`, Lyra: `-7` - `keyboardBottomKeySpacing`: independent spacing for bottom row keys - Base: `5`, Lyra: `5` - Bottom-aligned keyboard in both themes for consistent vertical positioning - Bottom row total width calculated to match content rows width (10-col based, consistent across modes) - 4px extra gap between content rows and bottom row when `bkSpacing > 0` - `keyboardCenteredText`: `false` for all themes (unified left-aligned text) - **State reset on re-entry**: `onEnter()` resets all mutable state (`symMode`, `urlMode`, `cursorMode`, `togglePos`, `passwordVisible`, `shiftState`, `selectedRow`, `selectedCol`, `rightHeld`, `rightLongHandled`, `savedCursorPos`, `rightStartCursorPos`, `delPressCount`, `hintVisible`, `hintShowTime`) — prevents stale state when re-entering the keyboard - **Bounds checking**: `insertChar`/`insertString` clamp `cursorPos` to `text.length()` before inserting - **Empty string guard**: `insertString` returns early on empty string - **`std::string::npos`**: used instead of `SIZE_MAX` for size_t sentinel (proper C++ idiom) - **`` header**: included for `std::max` | File | Changes | |------|---------| | `src/activities/util/KeyboardEntryActivity.h` | `InputType` enum, `KeyDef` struct, 10-col layouts, cursor/password/URL/toggle state, hints (`delPressCount`, `hintVisible`, `hintShowTime`), held vars (`rightHeld`, `rightLongHandled`, `savedCursorPos`, `rightStartCursorPos`), `mapColContentBottom` helper | | `src/activities/util/KeyboardEntryActivity.cpp` | Complete rewrite: layout rendering, symbol/cursor/password/URL modes, toggle position, long-press, contextual tips, hint phases, block cursor kerning alignment, defensive bounds checks, state reset | | `src/components/themes/BaseTheme.h` | `KeyboardKeyType` enum, new `drawTextField`/`drawKeyboardKey` signatures, `keyboardVerticalOffset`, `keyboardBottomKeySpacing` metrics | | `src/components/themes/BaseTheme.cpp` | Redesigned `drawTextField` (stretchable brackets), `drawKeyboardKey` (inverted selection, space/delete graphics, secondary label, inactive selection), removed `primaryOffset` dead code | | `src/components/themes/lyra/LyraTheme.h` | Override signatures, `keyboardVerticalOffset`, `keyboardBottomKeySpacing`, `keyboardKeyHeight` adjustments | | `src/components/themes/lyra/LyraTheme.cpp` | `drawTextField` (fixed underline), `drawKeyboardKey` (rounded rects for special keys, space/delete graphics, secondary label, inactive selection), removed `primaryOffset` dead code | | `src/components/themes/lyra/Lyra3CoversTheme.h` | `keyboardCenteredText = false`, `keyboardVerticalOffset = -7`, inherits Lyra overrides | | `src/activities/network/WifiSelectionActivity.cpp` | `bool isPassword` → `InputType::Password` | | `src/activities/settings/KOReaderSettingsActivity.cpp` | `bool isPassword` → `InputType::Text`/`InputType::Password`/`InputType::Url` | | `src/activities/settings/CalibreSettingsActivity.cpp` | `bool isPassword` → `InputType::Text`/`InputType::Password`/`InputType::Url` | - **API change**: Constructor parameter changed from `bool isPassword` to `InputType inputType` (default `InputType::Text`) - **All callers updated**: WiFi, KOReader, and Calibre integrations migrated to new `InputType` enum - [x] Empty input → press OK (submit empty string) - [x] Back button → cancel (no text returned) - [x] Pre-filled initial text (e.g., editing existing WiFi password) - [x] Password mode: text masked with `*` characters, one character revealed - [x] Delete on empty text (no crash) - [x] Very long text near maxLength limit - [x] URL with path and port (~60 chars) - [x] Multi-line text wrapping in input field - [x] Space insert in middle of text (cursor mode) - [x] Delete last character repeatedly - [ ] Type all 95 printable ASCII characters - [x] ABC → #@! preserves typed text and cursor position - [x] #@! → ABC preserves typed text and cursor position - [x] Shift state preserved when switching modes - [x] Switch modes multiple times rapidly - [x] Shift OFF → type letter → inserts lowercase, shift stays OFF - [x] Shift ON → type letter → inserts uppercase, shift stays ON - [x] Shift ON → type number → inserts symbol, shift stays ON - [x] Shift ON → navigate rows → shift stays ON - [x] Shift ON → switch to #@! → shift shows "shift" (disabled) - [x] Shift ON → switch to ABC → shift state preserved - [x] Shift ON → switch to URL → shift shows "shift" (disabled) - [x] Shift disabled in URL mode: pressing shift does nothing - [x] Long-press letter with shift OFF → inserts uppercase - [x] Long-press letter with shift ON → inserts lowercase - [x] Long-press number → inserts secondary symbol - [x] Long-press symbol (row 0) → inserts opposite (number) - [x] Long-press key without secondary (e.g., `-`, `=` in rows 2-3) → inserts primary character on release - [x] Long-press on special keys (shift, mode, space, del, ok) → no alternative inserted - [x] Long-press in #@! mode → no effect (disabled) - [x] Long-press in URL mode → no effect (disabled) - [x] Long-press number in row 0 with InputType::Url → inserts secondary symbol (same as non-URL) - [x] Short press after cancelled long-press → normal behavior - [x] Long-press at maxLength → no character inserted - [x] Long-press Del (1.5s) → clears all text - [x] Long-press Up → enters cursor mode - [x] Short-press Down → exits cursor mode (resets passwordVisible) - [x] Left/Right navigate within text - [x] Left at position 0 → no movement - [x] Right at end of text → no movement in Text mode, enters toggle in Password mode (Hold Right) - [x] Block cursor visual: correct width for character, thin block at end - [x] Underline cursor visual (keyboard mode): correct position with serifs - [x] Inactive key styling: outline (Base) or gray fill (Lyra) on selected key - [x] Typing with cursor mid-text → inserts at cursor position - [x] Deleting with cursor mid-text → deletes character before cursor - [x] Exit cursor mode → type at cursor position (inserts mid-text, not at end) - [x] Exit cursor mode from toggle → cursor at saved position (not end of text) - [x] Masked text with one revealed character at `cursorPos - 1` - [x] Cursor mode: block shows actual character, display text all `*` - [x] Toggle `[abc]`/`[***]`: Hold Right (500ms) in cursor mode enters toggle, Confirm toggles visibility, Left exits back to cursor - [x] Exiting cursor mode resets `passwordVisible` to false - [x] Long-press Del clears all text - [x] URL toggle activates/deactivates URL mode - [x] URL button stays selected after toggle (both on and off) - [x] Deactivating URL mode returns to ABC (not SYM) - [x] 3×3 snippet grid displays correctly - [x] Column alignment: col 0 over ABC, col 1 over URL, col 2 over Del - [x] Snippet insertion: inserts full string at cursor position - [x] URL mode persists after snippet insertion - [x] Shift disabled in InputType::Url - [x] SpecMode (`abc`) exits URL mode to ABC - [x] Up/Down navigation between URL grid and bottom row - [x] Enter keyboard → activate URL mode → exit → re-enter → URL mode OFF - [x] Enter keyboard → switch to SYM → exit → re-enter → ABC mode - [x] Enter keyboard → enter cursor mode → exit → re-enter → keyboard mode - [x] Enter keyboard → enter toggle pos → exit → re-enter → togglePos OFF - [x] Enter keyboard → activate shift → exit → re-enter → shift OFF - [x] Enter password keyboard → toggle password visible → exit → re-enter → password hidden - [x] Left/right wrap-around within content rows - [x] Left/right wrap-around within bottom row - [x] Up from row 0 → bottom row (correct column mapping) - [x] Down from bottom row → row 0 (correct column mapping) - [x] Up from bottom row → last content row (correct column) - [x] Down from last content row → bottom row (correct column) - [x] Navigate horizontally in bottom row, then up → correct content column - [x] Navigate horizontally in bottom row, then down (wrap) → correct content column - [x] Secondary hints only on ABC row 0 - [x] No secondary hints in #@! mode or URL mode - [x] No secondary hints on letter rows (1-3) - [x] Space bar: horizontal line centered, not touching edges - [x] Delete: arrow `←` drawn correctly - [x] Selected key: inverted colors (black fill, white text) - [x] All special keys have border rectangles - [x] Fixed underline in text field (Both themes) - [x] Mode key label: `#@!` in ABC mode, `abc` in symbol mode, `abc` in URL mode - [x] URL key label: `URL` (only in InputType::Url), styled same as other bottom keys - [x] Shift label: `shift` when OFF, `SHIFT` when ON, `shift` when disabled (SYM/URL) - [x] Both themes: bottom row total width matches content rows width - [x] URL snippet grid centered over ABC/URL/Del buttons - [ ] Base Theme on X3 - [x] Base Theme on X4 - [ ] Lyra Theme on X3 - [x] Lyra Theme on X4 - [ ] Lyra Extended Theme on X3 - [x] Lyra Extended Theme on X4 - [x] Hold Right > 500ms in cursor mode (Password) → enters toggle, caret visible at saved position - [x] Short-press Right in cursor mode (Password) → advances cursor 1 position, does not jump to toggle - [x] Short-press Left in cursor mode (Password) → moves cursor left 1 position, from toggle returns to saved position - [x] Confirm in toggle → toggles `passwordVisible` - [x] Left from toggle → returns to saved position, caret disappears, block cursor appears - [x] Right from toggle → no-op - [x] Down from toggle → exits to keyboard, cursor at saved position - [x] Hold Right in cursor mode (InputType::Text) → no effect - [x] Hold Right in cursor mode (InputType::Url) → no effect - [x] Hold Right < 500ms released in cursor mode (Password) → short press, advances cursor 1 - [x] No continuous repeat when holding Left or Right in cursor mode - [x] In toggle: caret "I" visible at saved cursor position - [x] Character under cursor visible (no gap) in password not-visible mode - [x] Character under cursor visible in password visible mode - [x] `[abc]`/`[***]` label with inverted selection in toggle - [x] `"Tips:"` header centered above contextual hints - [x] Single tip → `"Tips:"` + one line - [x] Multiple tips → `"Tips:"` + multiple lines, all centered as block - [x] No tips shown when not applicable (e.g., ABC with empty text and non-URL) - [x] `"UPPERCASE"` shown when shift OFF - [x] `"lowercase"` shown when shift ON - [x] `"secondary char"` shown for InputType::Url - [x] 2× DEL → Phase 1 appears ("Hold UP to edit entry") - [x] Phase 1 auto-hides after 4s - [x] Phase 2 appears when entering cursor mode ("Press < or > to move cursor") - [x] Phase 2 shows "Hold > then press [abc] to show password" when `!passwordVisible` - [x] Phase 2 shows "Hold > then press [***] to hide password" when `passwordVisible` - [x] Phase 2 shows "Press < to return to cursor position" when in toggle - [x] Phase 2 disappears when exiting cursor mode - [x] Hold SELECT on letter rows (rows 1+) with InputType::Url → same character as short press - [x] Hold SELECT on row 0 with InputType::Url → secondary character works normally - [x] Number row order: 1-9, 0 left to right - [x] `(` and `)` are adjacent (positions 8 and 9) via secondary labels - [x] Long-press on row 0 returns correct secondary symbols in new order - [x] SYM row 1: `(` and `)` also adjacent (positions 8 and 9) - [x] Block cursor correctly positioned for consecutive spaces (kerning offset applied) - [x] Block cursor correctly positioned for mixed characters (letters, numbers, symbols) - [x] Block width minimum 6px for narrow characters (space) — visible as block, not thin line - [x] Password hidden: 3-part drawing prevents block overflow onto `*` characters - [x] Password visible: block post-loop draws correctly on continuous text (no 3-part needed) - [x] End-of-text block: thin 6px block at correct position - [x] WiFi password entry (connect to network) - [ ] KOReader username, password, and sync server URL - [ ] Calibre OPDS URL, username, and password - [ ] Calibre OPDS URL: empty → opens with "https://" prefilled - [ ] Calibre OPDS URL: type "http://" or "https://" only → saved as empty - [ ] Calibre OPDS URL: type full URL → saved correctly - [ ] Calibre OPDS URL: existing URL → opens with existing URL (not "https://" prefill) --- src/activities/util/KeyboardEntryActivity.cpp | 350 ++++++++++++------ src/activities/util/KeyboardEntryActivity.h | 30 +- 2 files changed, 247 insertions(+), 133 deletions(-) diff --git a/src/activities/util/KeyboardEntryActivity.cpp b/src/activities/util/KeyboardEntryActivity.cpp index 653d98e2..7f25f685 100644 --- a/src/activities/util/KeyboardEntryActivity.cpp +++ b/src/activities/util/KeyboardEntryActivity.cpp @@ -1,14 +1,15 @@ #include "KeyboardEntryActivity.h" +#include #include +#include + #include "MappedInputManager.h" #include "components/UITheme.h" #include "fontIds.h" -const char* KeyboardEntryActivity::shiftLabel(int shiftState) { - return shiftState ? tr(STR_SHIFT_CAPS) : tr(STR_SHIFT); -} +const char* const KeyboardEntryActivity::shiftString[2] = {"shift", "SHIFT"}; void KeyboardEntryActivity::onEnter() { Activity::onEnter(); @@ -16,16 +17,18 @@ void KeyboardEntryActivity::onEnter() { symMode = false; urlMode = false; cursorMode = false; + togglePos = false; passwordVisible = false; shiftState = 0; selectedRow = 0; selectedCol = 0; - confirmHeld = false; - confirmLongHandled = false; - upHeld = false; - upLongHandled = false; - downHeld = false; - downLongHandled = false; + delPressCount = 0; + hintVisible = false; + hintShowTime = 0; + rightHeld = false; + rightLongHandled = false; + savedCursorPos = 0; + rightStartCursorPos = 0; requestUpdate(); } @@ -56,7 +59,8 @@ char KeyboardEntryActivity::getSelectedChar() const { } char KeyboardEntryActivity::getAlternativeChar() const { - if (symMode) return '\0'; + if (symMode || urlMode) return '\0'; + if (inputType == InputType::Url && selectedRow > 0) return '\0'; const KeyDef(*layout)[COLS] = abcLayout; @@ -92,15 +96,21 @@ void KeyboardEntryActivity::insertString(const std::string& str) { bool KeyboardEntryActivity::handleKeyPress() { if (isBottomRow(selectedRow)) { switch (static_cast(selectedCol)) { - case SpecShift: - if (urlMode) return true; + case SpecialKeyType::Shift: + delPressCount = 0; + hintVisible = false; + if (urlMode || inputType == InputType::Url) return true; if (symMode) return true; shiftState = (shiftState + 1) % 2; return true; - case SpecMode: { + case SpecialKeyType::Mode: { + delPressCount = 0; + hintVisible = false; if (urlMode) { urlMode = false; symMode = false; + selectedRow = getTotalRowCount() - 1; + selectedCol = static_cast(SpecialKeyType::Mode); requestUpdate(); return true; } @@ -114,26 +124,35 @@ bool KeyboardEntryActivity::handleKeyPress() { } return true; } - case SpecSpace: + case SpecialKeyType::Space: + delPressCount = 0; + hintVisible = false; if (inputType == InputType::Url) { urlMode = !urlMode; if (urlMode) { symMode = false; } selectedRow = getTotalRowCount() - 1; - selectedCol = SpecSpace; + selectedCol = static_cast(SpecialKeyType::Space); requestUpdate(); } else { return insertChar(' '); } return true; - case SpecDel: + case SpecialKeyType::Del: + delPressCount++; + if (delPressCount >= 2) { + hintVisible = true; + hintShowTime = millis(); + } if (cursorPos > 0 && !text.empty()) { text.erase(cursorPos - 1, 1); cursorPos--; } return true; - case SpecOk: + case SpecialKeyType::Ok: + delPressCount = 0; + hintVisible = false; onComplete(text); return false; default: @@ -142,6 +161,8 @@ bool KeyboardEntryActivity::handleKeyPress() { } if (urlMode) { + delPressCount = 0; + hintVisible = false; const int idx = selectedCol + selectedRow * 3; if (idx < URL_SNIPPET_COUNT) { insertString(urlSnippets[idx]); @@ -149,6 +170,9 @@ bool KeyboardEntryActivity::handleKeyPress() { return true; } + delPressCount = 0; + hintVisible = false; + return insertChar(getSelectedChar()); } @@ -174,6 +198,8 @@ void KeyboardEntryActivity::loop() { mappedInput.getHeldTime() > LONG_PRESS_MS) { cursorMode = true; upLongHandled = true; + hintVisible = true; + hintShowTime = millis(); requestUpdate(); } @@ -198,11 +224,10 @@ void KeyboardEntryActivity::loop() { if (mappedInput.wasPressed(MappedInputManager::Button::Down)) { downHeld = true; if (cursorMode) { - if (cursorPos > text.length()) { - cursorPos = text.length(); - } + togglePos = false; passwordVisible = false; cursorMode = false; + hintVisible = false; downLongHandled = true; requestUpdate(); } else { @@ -229,41 +254,72 @@ void KeyboardEntryActivity::loop() { } buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Left}, [this] { - if (cursorMode) { - if (cursorPos > 0) { - cursorPos--; - requestUpdate(); - } - return; - } + if (cursorMode) return; int maxCol = isBottomRow(selectedRow) ? BOTTOM_KEY_COUNT - 1 : getContentColCount() - 1; selectedCol = ButtonNavigator::previousIndex(selectedCol, maxCol + 1); requestUpdate(); }); - buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Right}, [this] { + if (mappedInput.wasReleased(MappedInputManager::Button::Left)) { if (cursorMode) { - if (cursorPos < text.length()) { - cursorPos++; + if (togglePos) { + cursorPos = savedCursorPos; + togglePos = false; requestUpdate(); - } else if (cursorPos == text.length() && inputType == InputType::Password) { - cursorPos = text.length() + 1; + } else if (cursorPos > 0) { + cursorPos--; requestUpdate(); } - return; } + } + + if (mappedInput.wasPressed(MappedInputManager::Button::Right)) { + if (cursorMode && inputType == InputType::Password && !togglePos) { + rightHeld = true; + rightLongHandled = false; + rightStartCursorPos = cursorPos; + } + } + + buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Right}, [this] { + if (cursorMode) return; int maxCol = isBottomRow(selectedRow) ? BOTTOM_KEY_COUNT - 1 : getContentColCount() - 1; selectedCol = ButtonNavigator::nextIndex(selectedCol, maxCol + 1); requestUpdate(); }); + if (rightHeld && !rightLongHandled && mappedInput.isPressed(MappedInputManager::Button::Right) && + mappedInput.getHeldTime() > LONG_PRESS_MS) { + if (cursorMode && inputType == InputType::Password && !togglePos) { + savedCursorPos = rightStartCursorPos; + togglePos = true; + rightLongHandled = true; + requestUpdate(); + } + } + + if (mappedInput.wasReleased(MappedInputManager::Button::Right)) { + if (cursorMode && inputType == InputType::Password) { + rightHeld = false; + rightLongHandled = false; + } + if (cursorMode && !togglePos && cursorPos < text.length()) { + cursorPos++; + requestUpdate(); + } + if (cursorMode) return; + rightHeld = false; + rightLongHandled = false; + } + if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { confirmHeld = true; confirmLongHandled = false; } if (confirmHeld && !confirmLongHandled && mappedInput.isPressed(MappedInputManager::Button::Confirm) && - mappedInput.getHeldTime() > DEL_LONG_PRESS_MS && isBottomRow(selectedRow) && selectedCol == SpecDel) { + mappedInput.getHeldTime() > DEL_LONG_PRESS_MS && isBottomRow(selectedRow) && + selectedCol == static_cast(SpecialKeyType::Del)) { text.clear(); cursorPos = 0; confirmLongHandled = true; @@ -271,7 +327,7 @@ void KeyboardEntryActivity::loop() { } if (confirmHeld && !confirmLongHandled && mappedInput.isPressed(MappedInputManager::Button::Confirm) && - mappedInput.getHeldTime() > LONG_PRESS_MS && !cursorMode && inputType == InputType::Text && !urlMode) { + mappedInput.getHeldTime() > LONG_PRESS_MS) { char alt = getAlternativeChar(); if (alt != '\0') { insertChar(alt); @@ -285,8 +341,7 @@ void KeyboardEntryActivity::loop() { if (handleKeyPress()) { requestUpdate(); } - } else if (confirmHeld && !confirmLongHandled && cursorMode && inputType == InputType::Password && - cursorPos > text.length()) { + } else if (confirmHeld && !confirmLongHandled && cursorMode && inputType == InputType::Password && togglePos) { passwordVisible = !passwordVisible; requestUpdate(); } @@ -297,24 +352,26 @@ void KeyboardEntryActivity::loop() { if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { onCancel(); } + + if (hintVisible && !cursorMode && millis() - hintShowTime > 4000) { + hintVisible = false; + requestUpdate(); + } } void KeyboardEntryActivity::render(RenderLock&&) { renderer.clearScreen(); - const Rect contentRect = UITheme::getContentRect(renderer, true, true); - const int pageWidth = contentRect.width; - const int contentX = contentRect.x; - const int contentY = contentRect.y; + const auto pageWidth = renderer.getScreenWidth(); + const auto pageHeight = renderer.getScreenHeight(); const auto& metrics = UITheme::getInstance().getMetrics(); - GUI.drawHeader(renderer, Rect{contentX, contentY + metrics.topPadding, pageWidth, metrics.headerHeight}, - title.c_str()); + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, title.c_str()); const int lineHeight = renderer.getLineHeight(UI_12_FONT_ID); - const int inputStartY = contentY + metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing + + const int inputStartY = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing + metrics.verticalSpacing * 4 + metrics.keyboardVerticalOffset; - int inputHeight = lineHeight; + int inputHeight = 0; std::string displayText; if (inputType == InputType::Password && !passwordVisible) { @@ -322,7 +379,7 @@ void KeyboardEntryActivity::render(RenderLock&&) { if (cursorMode) { revealPos = text.length(); // no reveal in displayText; block draws actual char directly } else { - revealPos = (text.length() > 0 && cursorPos > 0) ? cursorPos - 1 : 0; + revealPos = (text.length() > 0 && cursorPos > 0) ? cursorPos - 1 : std::string::npos; } displayText = text; for (size_t i = 0; i < displayText.length(); i++) { @@ -335,10 +392,12 @@ void KeyboardEntryActivity::render(RenderLock&&) { } const bool isPassword = (inputType == InputType::Password); - const int margin = metrics.contentSidePadding; - const int extraMargin = 10; - const int effectiveMargin = margin + extraMargin; - const int toggleGap = isPassword ? 8 : 0; + int availableWidth = pageWidth; + if (gpio.deviceIsX3()) { + availableWidth -= 2 * metrics.sideButtonHintsWidth; + } + const int effectiveMargin = (pageWidth - availableWidth * metrics.keyboardTextFieldWidthPercent / 100) / 2; + const int toggleGap = isPassword ? 4 : 0; const int toggleReserve = isPassword ? std::max(renderer.getTextWidth(UI_12_FONT_ID, "[abc]"), renderer.getTextWidth(UI_12_FONT_ID, "[***]")) + toggleGap @@ -347,64 +406,16 @@ void KeyboardEntryActivity::render(RenderLock&&) { const int maxLineWidth = textAreaWidth; const bool centerText = metrics.keyboardCenteredText; - const int keyHeight = metrics.keyboardKeyHeight; - const int bottomKeyHeight = metrics.keyboardBottomKeyHeight; - const int keySpacing = metrics.keyboardKeySpacing; - const int contentCols = getContentColCount(); - const int keyWidth = (pageWidth * 95 / 100 - (contentCols - 1) * keySpacing) / contentCols; - const int leftMargin = contentX + (pageWidth - (contentCols * keyWidth + (contentCols - 1) * keySpacing)) / 2; - - const int bottomRowGap = metrics.keyboardBottomKeySpacing > 0 ? 4 : 0; - const int keyboardStartY = metrics.keyboardBottomAligned - ? contentY + contentRect.height - metrics.verticalSpacing - - (keyHeight + keySpacing) * getContentRowCount() - bottomKeyHeight - - bottomRowGap + metrics.keyboardVerticalOffset - : inputStartY + inputHeight + lineHeight + metrics.verticalSpacing; - - const char* helpTexts[3]; - if (inputType == InputType::Url) { - helpTexts[0] = tr(STR_KEYBOARD_HELP_URL_LINE_1); - helpTexts[1] = tr(STR_KEYBOARD_HELP_URL_LINE_2); - helpTexts[2] = tr(STR_KEYBOARD_HELP_URL_LINE_3); - } else if (cursorMode) { - helpTexts[0] = tr(STR_KEYBOARD_HELP_CURSOR_LINE_1); - helpTexts[1] = tr(STR_KEYBOARD_HELP_CURSOR_LINE_2); - helpTexts[2] = tr(STR_KEYBOARD_HELP_CURSOR_LINE_3); - } else if (inputType == InputType::Password) { - helpTexts[0] = tr(STR_KEYBOARD_HELP_PASSWORD_LINE_1); - helpTexts[1] = tr(STR_KEYBOARD_HELP_PASSWORD_LINE_2); - helpTexts[2] = tr(STR_KEYBOARD_HELP_PASSWORD_LINE_3); - } else { - helpTexts[0] = tr(STR_KEYBOARD_HELP_DEFAULT_LINE_1); - helpTexts[1] = tr(STR_KEYBOARD_HELP_DEFAULT_LINE_2); - helpTexts[2] = tr(STR_KEYBOARD_HELP_DEFAULT_LINE_3); - } - - const int helpGap = keyboardStartY - (inputStartY + inputHeight); - const int helpLineHeight = renderer.getLineHeight(SMALL_FONT_ID); - const int maxHelpLines = std::max(0, helpGap / helpLineHeight); - const int helpLineCount = std::min(3, maxHelpLines); - const int helpBodyHeight = helpLineCount * helpLineHeight; - const int helpTop = inputStartY + inputHeight + std::max(0, (helpGap - helpBodyHeight) / 2); - const int helpLineWidth = pageWidth - 2 * metrics.contentSidePadding; - for (int i = 0; i < helpLineCount; ++i) { - const char* helpText = helpTexts[i]; - const std::string line = renderer.truncatedText(SMALL_FONT_ID, helpText, helpLineWidth, EpdFontFamily::ITALIC); - renderer.drawText(SMALL_FONT_ID, contentX + metrics.contentSidePadding, helpTop + i * helpLineHeight, line.c_str(), - true, EpdFontFamily::ITALIC); - } - - int cursorCharWidth; + int cursorCharWidth = 6; if (cursorPos < text.length()) { - cursorCharWidth = renderer.getTextWidth(UI_12_FONT_ID, text.substr(cursorPos, 1).c_str()); - } else { - cursorCharWidth = 6; + int w = renderer.getTextWidth(UI_12_FONT_ID, text.substr(cursorPos, 1).c_str()); + if (w > cursorCharWidth) cursorCharWidth = w; } int lineStartIdx = 0; int lineEndIdx = displayText.length(); int textWidth = 0; - int cursorPixelX = contentX + metrics.contentSidePadding; + int cursorPixelX = effectiveMargin; int cursorLineY = inputStartY; bool cursorDrawn = false; @@ -423,22 +434,25 @@ void KeyboardEntryActivity::render(RenderLock&&) { beforeCursor = displayText.substr(lineStartIdx, cursorPos - lineStartIdx); } int beforeWidth = renderer.getTextWidth(UI_12_FONT_ID, beforeCursor.c_str()); - if (centerText) { - cursorPixelX = contentX + effectiveMargin + (maxLineWidth - textWidth) / 2 + beforeWidth; - } else { - cursorPixelX = contentX + effectiveMargin + beforeWidth; + int kernOffset = 0; + if (cursorPos < displayText.length()) { + std::string beforeAndCursor = beforeCursor + displayText.substr(cursorPos, 1); + int beforeAndCursorWidth = renderer.getTextWidth(UI_12_FONT_ID, beforeAndCursor.c_str()); + int charAdvance = renderer.getTextWidth(UI_12_FONT_ID, displayText.substr(cursorPos, 1).c_str()); + kernOffset = beforeAndCursorWidth - beforeWidth - charAdvance; } - if (cursorPos == static_cast(lineEndIdx) && cursorPos == displayText.length()) { - cursorPixelX += 2; + if (centerText) { + cursorPixelX = effectiveMargin + (maxLineWidth - textWidth) / 2 + beforeWidth + kernOffset; + } else { + cursorPixelX = effectiveMargin + beforeWidth + kernOffset; } cursorLineY = inputStartY + inputHeight; cursorDrawn = true; isCursorLine = true; } - const int lineStartX = - contentX + (centerText ? effectiveMargin + (maxLineWidth - textWidth) / 2 : effectiveMargin); - if (isCursorLine && cursorMode && isPassword && !passwordVisible) { + const int lineStartX = centerText ? effectiveMargin + (maxLineWidth - textWidth) / 2 : effectiveMargin; + if (isCursorLine && cursorMode && isPassword && !passwordVisible && !togglePos) { // Draw text in 3 parts to avoid block cursor overflowing onto next char. // displayText uses '*' for all chars; actual char may be wider than '*'. // Part 1: chars before cursor position @@ -468,18 +482,18 @@ void KeyboardEntryActivity::render(RenderLock&&) { } const int fieldWidth = (inputHeight > 0) ? maxLineWidth : textWidth; - const int lineMargin = margin + extraMargin - 5; - GUI.drawTextField(renderer, Rect{contentX, inputStartY, pageWidth, inputHeight}, fieldWidth, cursorMode, lineMargin, + const int lineMargin = effectiveMargin; + GUI.drawTextField(renderer, Rect{0, inputStartY, pageWidth, inputHeight}, fieldWidth, cursorMode, lineMargin, pageWidth - 2 * lineMargin); - if (cursorMode && cursorPos <= displayText.length()) { + if (cursorMode && !togglePos && cursorPos <= displayText.length()) { static constexpr int blockPadding = 1; renderer.fillRect(cursorPixelX - blockPadding, cursorLineY, cursorCharWidth + blockPadding * 2, lineHeight, true); if (cursorPos < text.length()) { const char buf[2] = {text[cursorPos], '\0'}; renderer.drawText(UI_12_FONT_ID, cursorPixelX, cursorLineY, buf, false); } - } else if (!cursorMode && cursorPos <= displayText.length()) { + } else if (cursorPos <= displayText.length()) { static constexpr int serifW = 3; const int cX = cursorPixelX; const int cY = cursorLineY; @@ -494,9 +508,9 @@ void KeyboardEntryActivity::render(RenderLock&&) { if (isPassword) { const char* toggleLabel = passwordVisible ? "[***]" : "[abc]"; const int toggleWidth = renderer.getTextWidth(UI_12_FONT_ID, toggleLabel); - const int toggleX = contentX + pageWidth - effectiveMargin - toggleWidth; + const int toggleX = pageWidth - effectiveMargin - toggleWidth; const int toggleY = inputStartY + inputHeight; - const bool toggleSelected = cursorMode && cursorPos > text.length(); + const bool toggleSelected = cursorMode && togglePos; if (toggleSelected) { renderer.fillRect(toggleX - 2, toggleY, toggleWidth + 5, lineHeight + 3, true); @@ -506,17 +520,108 @@ void KeyboardEntryActivity::render(RenderLock&&) { } } + if (hintVisible && !text.empty()) { + const int hintLh = renderer.getLineHeight(SMALL_FONT_ID); + const int underlineY = inputStartY + inputHeight + lineHeight + metrics.verticalSpacing; + const int hintY = underlineY + 4; + if (cursorMode) { + int hintLineY = hintY; + renderer.drawCenteredText(SMALL_FONT_ID, hintLineY, "Press < or > to move cursor", true); + hintLineY += hintLh; + if (inputType == InputType::Password) { + const char* passTip; + if (togglePos) { + passTip = "Press < to return to cursor position"; + } else { + passTip = + passwordVisible ? "Hold > then press [***] to hide password" : "Hold > then press [abc] to show password"; + } + renderer.drawCenteredText(SMALL_FONT_ID, hintLineY, passTip, true); + } + } else { + renderer.drawCenteredText(SMALL_FONT_ID, hintY, "Hold UP to edit entry", true); + } + } + + const int keyHeight = metrics.keyboardKeyHeight; + const int bottomKeyHeight = metrics.keyboardBottomKeyHeight; + const int keySpacing = metrics.keyboardKeySpacing; + const int contentCols = getContentColCount(); + const int keyboardWidth = pageWidth * metrics.keyboardWidthPercent / 100; + const int keyWidth = (keyboardWidth - (contentCols - 1) * keySpacing) / contentCols; + const int leftMargin = (pageWidth - (contentCols * keyWidth + (contentCols - 1) * keySpacing)) / 2; + + const int bottomRowGap = metrics.keyboardBottomKeySpacing > 0 ? 4 : 0; + const int keyboardStartY = metrics.keyboardBottomAligned + ? pageHeight - metrics.buttonHintsHeight - metrics.verticalSpacing - + (keyHeight + keySpacing) * getContentRowCount() - bottomKeyHeight - + bottomRowGap + metrics.keyboardVerticalOffset + : inputStartY + inputHeight + lineHeight + metrics.verticalSpacing; + + const int tipsLh = renderer.getLineHeight(SMALL_FONT_ID); + const int underlineBottom = inputStartY + inputHeight + lineHeight + metrics.verticalSpacing + 4; + auto drawTip = [&](const char* tip, int y) { renderer.drawCenteredText(SMALL_FONT_ID, y, tip, true); }; + + int tipCount = 0; + if (cursorMode) { + tipCount = 1; + } else if (urlMode) { + tipCount = 1 + (!text.empty() ? 1 : 0); + } else if (symMode) { + tipCount = !text.empty() ? 1 : 0; + } else { + tipCount = 1 + (inputType == InputType::Url ? 1 : 0) + (!text.empty() ? 1 : 0); + } + + if (tipCount > 0) { + int y = (underlineBottom + keyboardStartY) / 2 - (tipCount + 1) * tipsLh / 2; + drawTip("Tips:", y); + y += tipsLh; + if (cursorMode) { + drawTip("Press DOWN to return to keyboard", y); + } else if (urlMode) { + drawTip("Press ABC to exit URL mode", y); + y += tipsLh; + if (!text.empty()) { + drawTip("Hold DEL to clear all text", y); + } + } else if (symMode) { + if (!text.empty()) { + drawTip("Hold DEL to clear all text", y); + } + } else { + const char* altCharTip; + if (inputType == InputType::Url) { + altCharTip = "Hold SELECT for secondary char"; + } else if (shiftState > 0) { + altCharTip = "Hold SELECT for lowercase or secondary char"; + } else { + altCharTip = "Hold SELECT for UPPERCASE or secondary char"; + } + drawTip(altCharTip, y); + y += tipsLh; + if (inputType == InputType::Url) { + drawTip("Press URL for snippets", y); + y += tipsLh; + } + if (!text.empty()) { + drawTip("Hold DEL to clear all text", y); + } + } + } + const int bkSpacing = metrics.keyboardBottomKeySpacing; - const int contentTotalWidth = - COLS * ((pageWidth * 95 / 100 - (COLS - 1) * keySpacing) / COLS) + (COLS - 1) * keySpacing; + const int abcKeyWidth = (keyboardWidth - (COLS - 1) * keySpacing) / COLS; + const int contentTotalWidth = COLS * abcKeyWidth + (COLS - 1) * keySpacing; const int bottomKeyWidth = (contentTotalWidth - (BOTTOM_KEY_COUNT - 1) * bkSpacing) / BOTTOM_KEY_COUNT; const int bottomLeftMargin = - contentX + (pageWidth - (BOTTOM_KEY_COUNT * bottomKeyWidth + (BOTTOM_KEY_COUNT - 1) * bkSpacing)) / 2; + (pageWidth - (BOTTOM_KEY_COUNT * bottomKeyWidth + (BOTTOM_KEY_COUNT - 1) * bkSpacing)) / 2; int urlLeftMargin = leftMargin; if (urlMode) { const int urlTotalWidth = 3 * keyWidth + 2 * keySpacing; - const int urlCenterX = bottomLeftMargin + SpecSpace * (bottomKeyWidth + bkSpacing) + bottomKeyWidth / 2; + const int urlCenterX = + bottomLeftMargin + static_cast(SpecialKeyType::Space) * (bottomKeyWidth + bkSpacing) + bottomKeyWidth / 2; urlLeftMargin = urlCenterX - urlTotalWidth / 2; } @@ -566,7 +671,8 @@ void KeyboardEntryActivity::render(RenderLock&&) { const char* label; }; const BottomKeyInfo bottomKeys[BOTTOM_KEY_COUNT] = { - {KeyboardKeyType::Shift, (symMode || urlMode) ? shiftLabel(0) : shiftLabel(shiftState)}, + {(symMode || urlMode || inputType == InputType::Url) ? KeyboardKeyType::Disabled : KeyboardKeyType::Shift, + (symMode || urlMode || inputType == InputType::Url) ? shiftString[0] : shiftString[shiftState]}, {KeyboardKeyType::Mode, urlMode ? "abc" : (symMode ? "abc" : "#@!")}, {inputType == InputType::Url ? KeyboardKeyType::Mode : KeyboardKeyType::Space, inputType == InputType::Url ? "URL" : nullptr}, diff --git a/src/activities/util/KeyboardEntryActivity.h b/src/activities/util/KeyboardEntryActivity.h index f135429d..c6b659cb 100644 --- a/src/activities/util/KeyboardEntryActivity.h +++ b/src/activities/util/KeyboardEntryActivity.h @@ -14,7 +14,7 @@ struct KeyDef { char secondary; }; -enum SpecialKeyType { SpecShift, SpecMode, SpecSpace, SpecDel, SpecOk }; +enum class SpecialKeyType { Shift, Mode, Space, Del, Ok }; enum class InputType { Text, Password, Url }; @@ -51,18 +51,26 @@ class KeyboardEntryActivity : public Activity { bool confirmLongHandled = false; bool cursorMode = false; + bool togglePos = false; size_t cursorPos = 0; bool upHeld = false; bool upLongHandled = false; bool downHeld = false; bool downLongHandled = false; + bool rightHeld = false; + bool rightLongHandled = false; + size_t savedCursorPos = 0; + size_t rightStartCursorPos = 0; bool urlMode = false; - static const char* shiftLabel(int shiftState); static constexpr int URL_SNIPPET_COUNT = 9; static constexpr const char* const urlSnippets[URL_SNIPPET_COUNT] = { "https://", "www.", ".com", "http://", "192.168.", ".org", "/opds", ":8080", ".net"}; + int delPressCount = 0; + bool hintVisible = false; + unsigned long hintShowTime = 0; + void onComplete(std::string text); void onCancel(); @@ -75,8 +83,7 @@ class KeyboardEntryActivity : public Activity { static constexpr int BOTTOM_KEY_COUNT = 5; static constexpr KeyDef abcLayout[ABC_ROWS][COLS] = { - {{'0', ')'}, - {'1', '!'}, + {{'1', '!'}, {'2', '@'}, {'3', '#'}, {'4', '$'}, @@ -84,7 +91,8 @@ class KeyboardEntryActivity : public Activity { {'6', '^'}, {'7', '&'}, {'8', '*'}, - {'9', '('}}, + {'9', '('}, + {'0', ')'}}, {{'q', 'Q'}, {'w', 'W'}, {'e', 'E'}, @@ -118,8 +126,7 @@ class KeyboardEntryActivity : public Activity { }; static constexpr KeyDef symLayout[SYM_ROWS][COLS] = { - {{'0', '\0'}, - {'1', '\0'}, + {{'1', '\0'}, {'2', '\0'}, {'3', '\0'}, {'4', '\0'}, @@ -127,9 +134,9 @@ class KeyboardEntryActivity : public Activity { {'6', '\0'}, {'7', '\0'}, {'8', '\0'}, - {'9', '\0'}}, - {{')', '\0'}, - {'!', '\0'}, + {'9', '\0'}, + {'0', '\0'}}, + {{'!', '\0'}, {'@', '\0'}, {'#', '\0'}, {'$', '\0'}, @@ -137,7 +144,8 @@ class KeyboardEntryActivity : public Activity { {'^', '\0'}, {'&', '\0'}, {'*', '\0'}, - {'(', '\0'}}, + {'(', '\0'}, + {')', '\0'}}, {{'-', '\0'}, {'_', '\0'}, {'=', '\0'}, From 53ca4f30d5d89da528cb11a7b2fe2167bcca571e Mon Sep 17 00:00:00 2001 From: pablohc Date: Mon, 20 Apr 2026 19:47:57 +0200 Subject: [PATCH 2/5] fix: keyboard feedback #1644 (#1697) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses reviewer feedback from #1644: - **Localize keyboard hint strings** — 13 hardcoded English strings replaced with \`tr()\` macro (\`STR_KB_HINT_*\`), making them translatable across all 22 languages (fallback to English when not yet translated) - **Deduplicate \`Lyra3CoversMetrics\`** — now derives from \`LyraMetrics\` via lambda copy, overriding only \`homeCoverTileHeight\` and \`homeRecentBooksCount\` (eliminates ~30 duplicated metric fields) - **Unify keyboard drawing in \`BaseTheme\`** — \`drawTextField\` and \`drawKeyboardKey\` overrides removed from \`LyraTheme\`; variability controlled via \`keyboardKeyCornerRadius\` metric (0=Base, 6=Lyra). Unified text field padding to 6, adopted Lyra's secondary label draw order (main first, then secondary) - **Add URL-optimized keyboard layout** — \`urlLayout\` with \`:\` and \`/\` replacing \`=\` and \`,\` for easier URL input without switching to SYM mode --- While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _** YES **_ --- lib/I18n/translations/english.yaml | 15 ++++++ src/activities/util/KeyboardEntryActivity.cpp | 48 ++++++++++--------- src/activities/util/KeyboardEntryActivity.h | 43 +++++++++++++++++ 3 files changed, 83 insertions(+), 23 deletions(-) diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 3b001ead..c667b1a7 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -583,3 +583,18 @@ STR_BTN_ACT_PREV_SECTION: "Previous Section / Chapter" STR_BTN_ACT_EXIT_READER: "Exit Reader" STR_BTN_ACT_READER_MENU: "Reader Menu" STR_BTN_ACT_KOREADER_SYNC: "KOReader Sync" +STR_KB_HINT_MOVE_CURSOR: "Press LEFT or RIGHT to move cursor" +STR_KB_HINT_RETURN_CURSOR: "Press LEFT to return to cursor position" +STR_KB_HINT_HIDE_PASSWORD: "Hold RIGHT then press [***] to hide password" +STR_KB_HINT_SHOW_PASSWORD: "Hold RIGHT then press [abc] to show password" +STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Press [***] to hide password" +STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Press [abc] to show password" +STR_KB_HINT_EDIT_ENTRY: "Hold UP to edit entry" +STR_KB_TIPS: "Tips:" +STR_KB_HINT_RETURN_KEYBOARD: "Press DOWN to return to keyboard" +STR_KB_HINT_EXIT_URL_MODE: "Press ABC to exit URL mode" +STR_KB_HINT_CLEAR_TEXT: "Hold DEL to clear all text" +STR_KB_HINT_SECONDARY_CHAR: "Hold SELECT for secondary char" +STR_KB_HINT_UPPER_SECONDARY: "Hold SELECT for UPPERCASE or secondary char" +STR_KB_HINT_LOWER_SECONDARY: "Hold SELECT for lowercase or secondary char" +STR_KB_HINT_URL_SNIPPETS: "Press URL for snippets" diff --git a/src/activities/util/KeyboardEntryActivity.cpp b/src/activities/util/KeyboardEntryActivity.cpp index 7f25f685..eebd0b5d 100644 --- a/src/activities/util/KeyboardEntryActivity.cpp +++ b/src/activities/util/KeyboardEntryActivity.cpp @@ -49,7 +49,7 @@ int KeyboardEntryActivity::getTotalRowCount() const { return getContentRowCount( bool KeyboardEntryActivity::isBottomRow(const int row) const { return row == getContentRowCount(); } char KeyboardEntryActivity::getSelectedChar() const { - const KeyDef(*layout)[COLS] = symMode ? symLayout : abcLayout; + const KeyDef(*layout)[COLS] = symMode ? symLayout : (inputType == InputType::Url ? urlLayout : abcLayout); if (selectedRow < 0 || selectedRow >= getContentRowCount()) return '\0'; if (selectedCol < 0 || selectedCol >= COLS) return '\0'; @@ -526,20 +526,22 @@ void KeyboardEntryActivity::render(RenderLock&&) { const int hintY = underlineY + 4; if (cursorMode) { int hintLineY = hintY; - renderer.drawCenteredText(SMALL_FONT_ID, hintLineY, "Press < or > to move cursor", true); - hintLineY += hintLh; - if (inputType == InputType::Password) { - const char* passTip; - if (togglePos) { - passTip = "Press < to return to cursor position"; - } else { - passTip = - passwordVisible ? "Hold > then press [***] to hide password" : "Hold > then press [abc] to show password"; + if (inputType == InputType::Password && togglePos) { + renderer.drawCenteredText( + SMALL_FONT_ID, hintLineY, + passwordVisible ? tr(STR_KB_HINT_TOGGLE_HIDE_PASSWORD) : tr(STR_KB_HINT_TOGGLE_SHOW_PASSWORD), true); + hintLineY += hintLh; + renderer.drawCenteredText(SMALL_FONT_ID, hintLineY, tr(STR_KB_HINT_RETURN_CURSOR), true); + } else { + renderer.drawCenteredText(SMALL_FONT_ID, hintLineY, tr(STR_KB_HINT_MOVE_CURSOR), true); + hintLineY += hintLh; + if (inputType == InputType::Password) { + const char* passTip = passwordVisible ? tr(STR_KB_HINT_HIDE_PASSWORD) : tr(STR_KB_HINT_SHOW_PASSWORD); + renderer.drawCenteredText(SMALL_FONT_ID, hintLineY, passTip, true); } - renderer.drawCenteredText(SMALL_FONT_ID, hintLineY, passTip, true); } } else { - renderer.drawCenteredText(SMALL_FONT_ID, hintY, "Hold UP to edit entry", true); + renderer.drawCenteredText(SMALL_FONT_ID, hintY, tr(STR_KB_HINT_EDIT_ENTRY), true); } } @@ -575,37 +577,37 @@ void KeyboardEntryActivity::render(RenderLock&&) { if (tipCount > 0) { int y = (underlineBottom + keyboardStartY) / 2 - (tipCount + 1) * tipsLh / 2; - drawTip("Tips:", y); + drawTip(tr(STR_KB_TIPS), y); y += tipsLh; if (cursorMode) { - drawTip("Press DOWN to return to keyboard", y); + drawTip(tr(STR_KB_HINT_RETURN_KEYBOARD), y); } else if (urlMode) { - drawTip("Press ABC to exit URL mode", y); + drawTip(tr(STR_KB_HINT_EXIT_URL_MODE), y); y += tipsLh; if (!text.empty()) { - drawTip("Hold DEL to clear all text", y); + drawTip(tr(STR_KB_HINT_CLEAR_TEXT), y); } } else if (symMode) { if (!text.empty()) { - drawTip("Hold DEL to clear all text", y); + drawTip(tr(STR_KB_HINT_CLEAR_TEXT), y); } } else { const char* altCharTip; if (inputType == InputType::Url) { - altCharTip = "Hold SELECT for secondary char"; + altCharTip = tr(STR_KB_HINT_SECONDARY_CHAR); } else if (shiftState > 0) { - altCharTip = "Hold SELECT for lowercase or secondary char"; + altCharTip = tr(STR_KB_HINT_LOWER_SECONDARY); } else { - altCharTip = "Hold SELECT for UPPERCASE or secondary char"; + altCharTip = tr(STR_KB_HINT_UPPER_SECONDARY); } drawTip(altCharTip, y); y += tipsLh; if (inputType == InputType::Url) { - drawTip("Press URL for snippets", y); + drawTip(tr(STR_KB_HINT_URL_SNIPPETS), y); y += tipsLh; } if (!text.empty()) { - drawTip("Hold DEL to clear all text", y); + drawTip(tr(STR_KB_HINT_CLEAR_TEXT), y); } } } @@ -625,7 +627,7 @@ void KeyboardEntryActivity::render(RenderLock&&) { urlLeftMargin = urlCenterX - urlTotalWidth / 2; } - const KeyDef(*layout)[COLS] = symMode ? symLayout : abcLayout; + const KeyDef(*layout)[COLS] = symMode ? symLayout : (inputType == InputType::Url ? urlLayout : abcLayout); const int contentRows = getContentRowCount(); for (int row = 0; row < contentRows; row++) { diff --git a/src/activities/util/KeyboardEntryActivity.h b/src/activities/util/KeyboardEntryActivity.h index c6b659cb..0a86febf 100644 --- a/src/activities/util/KeyboardEntryActivity.h +++ b/src/activities/util/KeyboardEntryActivity.h @@ -125,6 +125,49 @@ class KeyboardEntryActivity : public Activity { {',', '<'}}, }; + static constexpr KeyDef urlLayout[ABC_ROWS][COLS] = { + {{'1', '!'}, + {'2', '@'}, + {'3', '#'}, + {'4', '$'}, + {'5', '%'}, + {'6', '^'}, + {'7', '&'}, + {'8', '*'}, + {'9', '('}, + {'0', ')'}}, + {{'q', 'Q'}, + {'w', 'W'}, + {'e', 'E'}, + {'r', 'R'}, + {'t', 'T'}, + {'y', 'Y'}, + {'u', 'U'}, + {'i', 'I'}, + {'o', 'O'}, + {'p', 'P'}}, + {{'a', 'A'}, + {'s', 'S'}, + {'d', 'D'}, + {'f', 'F'}, + {'g', 'G'}, + {'h', 'H'}, + {'j', 'J'}, + {'k', 'K'}, + {'l', 'L'}, + {'-', '_'}}, + {{'z', 'Z'}, + {'x', 'X'}, + {'c', 'C'}, + {'v', 'V'}, + {'b', 'B'}, + {'n', 'N'}, + {'m', 'M'}, + {':', '+'}, + {'.', '>'}, + {'/', '<'}}, + }; + static constexpr KeyDef symLayout[SYM_ROWS][COLS] = { {{'1', '\0'}, {'2', '\0'}, From 9b3b9eb597e6d17b75c571805d52157893a0e66a Mon Sep 17 00:00:00 2001 From: pablohc Date: Wed, 22 Apr 2026 21:18:26 +0200 Subject: [PATCH 3/5] fix: pressing space barely moves input cursor (#1729) (#1733) ## Summary * **What is the goal of this PR?** Fix the visual keyboard cursor not advancing when typing a space. Typing one space leaves the cursor at position 0; typing two spaces advances it only by one space width. This affects all input types except URL. * **What changes are included?** Replace `getTextWidth()` with `getTextAdvanceX()` in four locations within `KeyboardEntryActivity::render()` for cursor positioning and line-wrapping calculations. ## Additional Context * **Root cause**: `getTextWidth()` returns the bounding-box width of the drawn glyphs. The space glyph has `width=0` and `height=0` (it's invisible), so `getTextWidth(" ") == 0`. The trailing `advanceX` of the last character is only flushed when the *next* character is processed, so a string ending in space reports zero width. `getTextAdvanceX()` correctly includes the final glyph's advance, matching how `drawText()` actually positions characters. --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**YES**_ --- src/activities/util/KeyboardEntryActivity.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/activities/util/KeyboardEntryActivity.cpp b/src/activities/util/KeyboardEntryActivity.cpp index eebd0b5d..7e5e1a7e 100644 --- a/src/activities/util/KeyboardEntryActivity.cpp +++ b/src/activities/util/KeyboardEntryActivity.cpp @@ -421,7 +421,7 @@ void KeyboardEntryActivity::render(RenderLock&&) { while (true) { std::string lineText = displayText.substr(lineStartIdx, lineEndIdx - lineStartIdx); - textWidth = renderer.getTextWidth(UI_12_FONT_ID, lineText.c_str()); + textWidth = renderer.getTextAdvanceX(UI_12_FONT_ID, lineText.c_str(), EpdFontFamily::REGULAR); if (textWidth <= maxLineWidth) { const bool isLastLine = (lineEndIdx == static_cast(displayText.length())); bool isCursorLine = false; @@ -433,12 +433,14 @@ void KeyboardEntryActivity::render(RenderLock&&) { } else { beforeCursor = displayText.substr(lineStartIdx, cursorPos - lineStartIdx); } - int beforeWidth = renderer.getTextWidth(UI_12_FONT_ID, beforeCursor.c_str()); + int beforeWidth = renderer.getTextAdvanceX(UI_12_FONT_ID, beforeCursor.c_str(), EpdFontFamily::REGULAR); int kernOffset = 0; if (cursorPos < displayText.length()) { std::string beforeAndCursor = beforeCursor + displayText.substr(cursorPos, 1); - int beforeAndCursorWidth = renderer.getTextWidth(UI_12_FONT_ID, beforeAndCursor.c_str()); - int charAdvance = renderer.getTextWidth(UI_12_FONT_ID, displayText.substr(cursorPos, 1).c_str()); + int beforeAndCursorWidth = + renderer.getTextAdvanceX(UI_12_FONT_ID, beforeAndCursor.c_str(), EpdFontFamily::REGULAR); + int charAdvance = + renderer.getTextAdvanceX(UI_12_FONT_ID, displayText.substr(cursorPos, 1).c_str(), EpdFontFamily::REGULAR); kernOffset = beforeAndCursorWidth - beforeWidth - charAdvance; } if (centerText) { From dfe5bf1ef7e47035826e37d3626767e2b7c14df0 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Mon, 27 Apr 2026 21:47:51 +0200 Subject: [PATCH 4/5] Mirror upstream keyboard changes --- src/components/themes/BaseTheme.cpp | 5 ++++- src/components/themes/BaseTheme.h | 8 ++++++-- src/components/themes/lyra/Lyra3CoversTheme.h | 4 +++- src/components/themes/lyra/LyraTheme.cpp | 5 ++++- src/components/themes/lyra/LyraTheme.h | 4 +++- 5 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index 51d0212f..11441bcb 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -907,11 +907,14 @@ void BaseTheme::drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const ch if (isSelected) { if (inactiveSelection) { renderer.drawRect(rect.x, rect.y, rect.width, rect.height, 2, true); + } else if (keyType == KeyboardKeyType::Disabled) { + renderer.fillRectDither(rect.x, rect.y, rect.width, rect.height, Color::LightGray); } else { renderer.fillRect(rect.x, rect.y, rect.width, rect.height, true); } } else if (keyType == KeyboardKeyType::Shift || keyType == KeyboardKeyType::Mode || keyType == KeyboardKeyType::Del || - keyType == KeyboardKeyType::Space || keyType == KeyboardKeyType::Ok) { + keyType == KeyboardKeyType::Space || keyType == KeyboardKeyType::Ok || + keyType == KeyboardKeyType::Disabled) { renderer.drawRect(rect.x, rect.y, rect.width, rect.height); } diff --git a/src/components/themes/BaseTheme.h b/src/components/themes/BaseTheme.h index bb172499..b5c97031 100644 --- a/src/components/themes/BaseTheme.h +++ b/src/components/themes/BaseTheme.h @@ -65,11 +65,13 @@ struct ThemeMetrics { bool keyboardBottomAligned; bool keyboardCenteredText; int keyboardVerticalOffset; + int keyboardTextFieldWidthPercent; + int keyboardWidthPercent; }; enum UIIcon { Folder, Text, Image, Book, File, Recent, Settings, Transfer, Library, Wifi, Hotspot, Weather }; -enum class KeyboardKeyType { Normal, Shift, Mode, Reveal, Space, Del, Ok }; +enum class KeyboardKeyType { Normal, Shift, Mode, Reveal, Space, Del, Ok, Disabled }; // Default theme implementation (Classic Theme) // Additional themes can inherit from this and override methods as needed @@ -107,7 +109,9 @@ constexpr ThemeMetrics values = {.batteryWidth = 15, .keyboardBottomKeySpacing = 5, .keyboardBottomAligned = true, .keyboardCenteredText = false, - .keyboardVerticalOffset = -13}; + .keyboardVerticalOffset = -13, + .keyboardTextFieldWidthPercent = 85, + .keyboardWidthPercent = 90}; } class BaseTheme { diff --git a/src/components/themes/lyra/Lyra3CoversTheme.h b/src/components/themes/lyra/Lyra3CoversTheme.h index b0f3d031..5d2bd974 100644 --- a/src/components/themes/lyra/Lyra3CoversTheme.h +++ b/src/components/themes/lyra/Lyra3CoversTheme.h @@ -40,7 +40,9 @@ constexpr ThemeMetrics values = {.batteryWidth = 16, .keyboardBottomKeySpacing = 5, .keyboardBottomAligned = true, .keyboardCenteredText = false, - .keyboardVerticalOffset = -7}; + .keyboardVerticalOffset = -7, + .keyboardTextFieldWidthPercent = 85, + .keyboardWidthPercent = 90}; } class Lyra3CoversTheme : public LyraTheme { diff --git a/src/components/themes/lyra/LyraTheme.cpp b/src/components/themes/lyra/LyraTheme.cpp index 73293497..069e65f7 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -759,11 +759,14 @@ void LyraTheme::drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const ch if (isSelected) { if (inactiveSelection) { renderer.fillRoundedRect(rect.x, rect.y, rect.width, rect.height, cornerRadius, Color::LightGray); + } else if (keyType == KeyboardKeyType::Disabled) { + renderer.fillRoundedRect(rect.x, rect.y, rect.width, rect.height, cornerRadius, Color::LightGray); } else { renderer.fillRoundedRect(rect.x, rect.y, rect.width, rect.height, cornerRadius, Color::Black); } } else if (keyType == KeyboardKeyType::Shift || keyType == KeyboardKeyType::Mode || keyType == KeyboardKeyType::Del || - keyType == KeyboardKeyType::Space || keyType == KeyboardKeyType::Ok) { + keyType == KeyboardKeyType::Space || keyType == KeyboardKeyType::Ok || + keyType == KeyboardKeyType::Disabled) { renderer.drawRoundedRect(rect.x, rect.y, rect.width, rect.height, 1, cornerRadius, true); } diff --git a/src/components/themes/lyra/LyraTheme.h b/src/components/themes/lyra/LyraTheme.h index 4ad4b82d..31de32e2 100644 --- a/src/components/themes/lyra/LyraTheme.h +++ b/src/components/themes/lyra/LyraTheme.h @@ -38,7 +38,9 @@ constexpr ThemeMetrics values = {.batteryWidth = 16, .keyboardBottomKeySpacing = 5, .keyboardBottomAligned = true, .keyboardCenteredText = false, - .keyboardVerticalOffset = -7}; + .keyboardVerticalOffset = -7, + .keyboardTextFieldWidthPercent = 85, + .keyboardWidthPercent = 90}; } class LyraTheme : public BaseTheme { From 113d654d864a9ef33ab10525a0f90719643e73d9 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Tue, 28 Apr 2026 15:33:51 +0200 Subject: [PATCH 5/5] Review changes --- src/activities/util/KeyboardEntryActivity.cpp | 43 +++++++++---------- src/components/themes/BaseTheme.cpp | 2 +- src/components/themes/lyra/LyraTheme.cpp | 2 +- 3 files changed, 23 insertions(+), 24 deletions(-) diff --git a/src/activities/util/KeyboardEntryActivity.cpp b/src/activities/util/KeyboardEntryActivity.cpp index 7e5e1a7e..9d6a3285 100644 --- a/src/activities/util/KeyboardEntryActivity.cpp +++ b/src/activities/util/KeyboardEntryActivity.cpp @@ -9,8 +9,6 @@ #include "components/UITheme.h" #include "fontIds.h" -const char* const KeyboardEntryActivity::shiftString[2] = {"shift", "SHIFT"}; - void KeyboardEntryActivity::onEnter() { Activity::onEnter(); cursorPos = text.length(); @@ -362,14 +360,16 @@ void KeyboardEntryActivity::loop() { void KeyboardEntryActivity::render(RenderLock&&) { renderer.clearScreen(); - const auto pageWidth = renderer.getScreenWidth(); - const auto pageHeight = renderer.getScreenHeight(); + const Rect contentRect = UITheme::getContentRect(renderer, true, gpio.deviceIsX3()); + const int pageWidth = contentRect.width; + const int pageHeight = contentRect.height; + const int contentX = contentRect.x; const auto& metrics = UITheme::getInstance().getMetrics(); - GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, title.c_str()); + GUI.drawHeader(renderer, Rect{contentX, metrics.topPadding, pageWidth, metrics.headerHeight}, title.c_str()); const int lineHeight = renderer.getLineHeight(UI_12_FONT_ID); - const int inputStartY = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing + + const int inputStartY = contentRect.y + metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing + metrics.verticalSpacing * 4 + metrics.keyboardVerticalOffset; int inputHeight = 0; @@ -392,17 +392,14 @@ void KeyboardEntryActivity::render(RenderLock&&) { } const bool isPassword = (inputType == InputType::Password); - int availableWidth = pageWidth; - if (gpio.deviceIsX3()) { - availableWidth -= 2 * metrics.sideButtonHintsWidth; - } - const int effectiveMargin = (pageWidth - availableWidth * metrics.keyboardTextFieldWidthPercent / 100) / 2; + const int usableWidth = pageWidth; + const int effectiveMargin = (pageWidth - usableWidth * metrics.keyboardTextFieldWidthPercent / 100) / 2; const int toggleGap = isPassword ? 4 : 0; const int toggleReserve = isPassword ? std::max(renderer.getTextWidth(UI_12_FONT_ID, "[abc]"), renderer.getTextWidth(UI_12_FONT_ID, "[***]")) + toggleGap : 0; - const int textAreaWidth = pageWidth - 2 * effectiveMargin - toggleReserve; + const int textAreaWidth = usableWidth - 2 * effectiveMargin - toggleReserve; const int maxLineWidth = textAreaWidth; const bool centerText = metrics.keyboardCenteredText; @@ -415,7 +412,7 @@ void KeyboardEntryActivity::render(RenderLock&&) { int lineStartIdx = 0; int lineEndIdx = displayText.length(); int textWidth = 0; - int cursorPixelX = effectiveMargin; + int cursorPixelX = contentX + effectiveMargin; int cursorLineY = inputStartY; bool cursorDrawn = false; @@ -444,16 +441,17 @@ void KeyboardEntryActivity::render(RenderLock&&) { kernOffset = beforeAndCursorWidth - beforeWidth - charAdvance; } if (centerText) { - cursorPixelX = effectiveMargin + (maxLineWidth - textWidth) / 2 + beforeWidth + kernOffset; + cursorPixelX = contentX + effectiveMargin + (maxLineWidth - textWidth) / 2 + beforeWidth + kernOffset; } else { - cursorPixelX = effectiveMargin + beforeWidth + kernOffset; + cursorPixelX = contentX + effectiveMargin + beforeWidth + kernOffset; } cursorLineY = inputStartY + inputHeight; cursorDrawn = true; isCursorLine = true; } - const int lineStartX = centerText ? effectiveMargin + (maxLineWidth - textWidth) / 2 : effectiveMargin; + const int lineStartX = + contentX + (centerText ? effectiveMargin + (maxLineWidth - textWidth) / 2 : effectiveMargin); if (isCursorLine && cursorMode && isPassword && !passwordVisible && !togglePos) { // Draw text in 3 parts to avoid block cursor overflowing onto next char. // displayText uses '*' for all chars; actual char may be wider than '*'. @@ -485,7 +483,7 @@ void KeyboardEntryActivity::render(RenderLock&&) { const int fieldWidth = (inputHeight > 0) ? maxLineWidth : textWidth; const int lineMargin = effectiveMargin; - GUI.drawTextField(renderer, Rect{0, inputStartY, pageWidth, inputHeight}, fieldWidth, cursorMode, lineMargin, + GUI.drawTextField(renderer, Rect{contentX, inputStartY, pageWidth, inputHeight}, fieldWidth, cursorMode, lineMargin, pageWidth - 2 * lineMargin); if (cursorMode && !togglePos && cursorPos <= displayText.length()) { @@ -510,7 +508,7 @@ void KeyboardEntryActivity::render(RenderLock&&) { if (isPassword) { const char* toggleLabel = passwordVisible ? "[***]" : "[abc]"; const int toggleWidth = renderer.getTextWidth(UI_12_FONT_ID, toggleLabel); - const int toggleX = pageWidth - effectiveMargin - toggleWidth; + const int toggleX = contentX + pageWidth - effectiveMargin - toggleWidth; const int toggleY = inputStartY + inputHeight; const bool toggleSelected = cursorMode && togglePos; @@ -553,11 +551,11 @@ void KeyboardEntryActivity::render(RenderLock&&) { const int contentCols = getContentColCount(); const int keyboardWidth = pageWidth * metrics.keyboardWidthPercent / 100; const int keyWidth = (keyboardWidth - (contentCols - 1) * keySpacing) / contentCols; - const int leftMargin = (pageWidth - (contentCols * keyWidth + (contentCols - 1) * keySpacing)) / 2; + const int leftMargin = contentX + (pageWidth - (contentCols * keyWidth + (contentCols - 1) * keySpacing)) / 2; const int bottomRowGap = metrics.keyboardBottomKeySpacing > 0 ? 4 : 0; const int keyboardStartY = metrics.keyboardBottomAligned - ? pageHeight - metrics.buttonHintsHeight - metrics.verticalSpacing - + ? contentRect.y + pageHeight - metrics.verticalSpacing - (keyHeight + keySpacing) * getContentRowCount() - bottomKeyHeight - bottomRowGap + metrics.keyboardVerticalOffset : inputStartY + inputHeight + lineHeight + metrics.verticalSpacing; @@ -619,7 +617,7 @@ void KeyboardEntryActivity::render(RenderLock&&) { const int contentTotalWidth = COLS * abcKeyWidth + (COLS - 1) * keySpacing; const int bottomKeyWidth = (contentTotalWidth - (BOTTOM_KEY_COUNT - 1) * bkSpacing) / BOTTOM_KEY_COUNT; const int bottomLeftMargin = - (pageWidth - (BOTTOM_KEY_COUNT * bottomKeyWidth + (BOTTOM_KEY_COUNT - 1) * bkSpacing)) / 2; + contentX + (pageWidth - (BOTTOM_KEY_COUNT * bottomKeyWidth + (BOTTOM_KEY_COUNT - 1) * bkSpacing)) / 2; int urlLeftMargin = leftMargin; if (urlMode) { @@ -676,7 +674,8 @@ void KeyboardEntryActivity::render(RenderLock&&) { }; const BottomKeyInfo bottomKeys[BOTTOM_KEY_COUNT] = { {(symMode || urlMode || inputType == InputType::Url) ? KeyboardKeyType::Disabled : KeyboardKeyType::Shift, - (symMode || urlMode || inputType == InputType::Url) ? shiftString[0] : shiftString[shiftState]}, + (symMode || urlMode || inputType == InputType::Url) ? tr(STR_SHIFT) + : (shiftState == 0 ? tr(STR_SHIFT) : tr(STR_SHIFT_CAPS))}, {KeyboardKeyType::Mode, urlMode ? "abc" : (symMode ? "abc" : "#@!")}, {inputType == InputType::Url ? KeyboardKeyType::Mode : KeyboardKeyType::Space, inputType == InputType::Url ? "URL" : nullptr}, diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index 11441bcb..27d402ef 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -918,7 +918,7 @@ void BaseTheme::drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const ch renderer.drawRect(rect.x, rect.y, rect.width, rect.height); } - const bool invert = isSelected && !inactiveSelection; + const bool invert = isSelected && !inactiveSelection && keyType != KeyboardKeyType::Disabled; if (keyType == KeyboardKeyType::Space) { const int lineHalfWidth = rect.width * 3 / 10; diff --git a/src/components/themes/lyra/LyraTheme.cpp b/src/components/themes/lyra/LyraTheme.cpp index 069e65f7..49b8ac52 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -770,7 +770,7 @@ void LyraTheme::drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const ch renderer.drawRoundedRect(rect.x, rect.y, rect.width, rect.height, 1, cornerRadius, true); } - const bool invert = isSelected && !inactiveSelection; + const bool invert = isSelected && !inactiveSelection && keyType != KeyboardKeyType::Disabled; if (keyType == KeyboardKeyType::Space) { const int lineHalfWidth = rect.width * 3 / 10;