A fork of https://github.com/crosspoint-reader/crosspoint-reader
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: redesign on-screen keyboard (#1644)

# Refactor: Redesign On-Screen Keyboard

## Summary

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.

## Screenshots

### Base Theme
|**master** | **PR #1644** |
|----------|-------------|
| <img width="480" height="800" alt="image"
src="https://github.com/user-attachments/assets/49125857-12d0-4020-b872-05d0ddbf1d94"
/> | <img width="480" height="800" alt="image"
src="https://github.com/user-attachments/assets/ad16656b-d66e-43dd-8697-2b85f709d7f8"
/> |

### Lyra Theme
| **master** | **PR #1644** |
|----------|-------------|
| <img width="480" height="800" alt="image"
src="https://github.com/user-attachments/assets/d9901251-9154-48d2-83b4-376d3223d132"
/> | <img width="480" height="800" alt="image"
src="https://github.com/user-attachments/assets/84b45949-ed61-4924-af1b-d570c4c61e13"
/> |

### Keyboard States

| ABC Mode | Symbol Mode | URL Mode |
|----------|-------------|----------|
| <img width="480" height="280" alt="image"
src="https://github.com/user-attachments/assets/6397a82e-50b8-4d03-92e0-f707ed7c9054"
/> | <img width="480" height="280" alt="image"
src="https://github.com/user-attachments/assets/205d34fe-0413-49e9-9db2-30569c297ba0"
/> | <img width="480" height="280" alt="image"
src="https://github.com/user-attachments/assets/801ddeab-e082-4a22-a9df-c66adb1afc16"
/> |

| Cursor Mode | Password Toggle |
|-------------|-----------------|
| <img width="480" height="800" alt="image"
src="https://github.com/user-attachments/assets/841773e1-7a3c-45e5-aa89-e5787255608c"
/> | <img width="480" height="800" alt="image"
src="https://github.com/user-attachments/assets/9487a0b3-3f47-41ed-9cd3-b24e56e342a8"
/> |

## Changes

### Layout (10-column uniform grid)
- 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 Symbol Mode (#@!)
- 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

### URL 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

### Cursor Mode
- **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

### Password Mode
- `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

### InputType Enum
- Replaced `bool isPassword` constructor parameter with `enum class
InputType { Text, Password, Url }`
- Callers updated: `WifiSelectionActivity`, `KOReaderSettingsActivity`,
`CalibreSettingsActivity`

### Contextual Tips
- `"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"`

### Hint Phases (cursor mode, Password only)
- **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"`

### Long-Press Alternative Character
- 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.

### Shift (2 sticky states)
- 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)

### SpecialKeyType Enum
- `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<int>()` 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

### Visual Improvements (both Base and Lyra themes)
- **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`

### New Theme Metrics
- `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)

### Defensive Improvements
- **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)
- **`<algorithm>` header**: included for `std::max`

## Files Modified

| 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` |

## Backward Compatibility

- **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

## Testing

### Input & Text Handling
- [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

### Mode Switching
- [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

### Shift Behavior
- [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

### Long-Press
- [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

### Cursor Mode
- [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)

### Password Mode
- [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

### URL Mode
- [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

### Re-entry State Reset
- [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

### Navigation
- [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

### Visual (both themes)
- [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

### Device & Theme Coverage
- [ ] 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

### Toggle Position
- [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

### Caret Visual in Toggle
- [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

### Contextual Tips
- [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

### Hint Phases
- [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

### Long-Press `InputType::Url` Behavior
- [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

### Number Row Reorder
- [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)

### Block Cursor Alignment
- [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

### Integration
- [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)

authored by

pablohc and committed by
GitHub
77b2c316 fedcb2f5

+948 -300
+12 -14
src/activities/network/WifiSelectionActivity.cpp
··· 190 190 // Show password entry 191 191 state = WifiSelectionState::PASSWORD_ENTRY; 192 192 // Don't allow screen updates while changing activity 193 - startActivityForResult( 194 - std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_ENTER_WIFI_PASSWORD), 195 - "", // No initial text 196 - 64, // Max password length 197 - false // Show password by default (hard keyboard to use) 198 - ), 199 - [this](const ActivityResult& result) { 200 - if (result.isCancelled) { 201 - state = WifiSelectionState::NETWORK_LIST; 202 - } else { 203 - enteredPassword = std::get<KeyboardResult>(result.data).text; 204 - // state will be updated in next loop iteration 205 - } 206 - }); 193 + startActivityForResult(std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_ENTER_WIFI_PASSWORD), 194 + "", // No initial text 195 + 64, // Max password length 196 + InputType::Password), 197 + [this](const ActivityResult& result) { 198 + if (result.isCancelled) { 199 + state = WifiSelectionState::NETWORK_LIST; 200 + } else { 201 + enteredPassword = std::get<KeyboardResult>(result.data).text; 202 + // state will be updated in next loop iteration 203 + } 204 + }); 207 205 } else { 208 206 // Connect directly for open networks 209 207 attemptConnection();
+9 -5
src/activities/settings/CalibreSettingsActivity.cpp
··· 50 50 51 51 void CalibreSettingsActivity::handleSelection() { 52 52 if (selectedIndex == 0) { 53 - // OPDS Server URL 53 + // OPDS Server URL - prefill with https:// if empty to save typing 54 + const std::string currentUrl = SETTINGS.opdsServerUrl; 55 + const std::string prefillUrl = currentUrl.empty() ? "https://" : currentUrl; 54 56 startActivityForResult(std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_CALIBRE_WEB_URL), 55 - SETTINGS.opdsServerUrl, 127, false), 57 + prefillUrl, 127, InputType::Url), 56 58 [this](const ActivityResult& result) { 57 59 if (!result.isCancelled) { 58 60 const auto& kb = std::get<KeyboardResult>(result.data); 59 - strncpy(SETTINGS.opdsServerUrl, kb.text.c_str(), sizeof(SETTINGS.opdsServerUrl) - 1); 61 + const std::string urlToSave = 62 + (kb.text == "https://" || kb.text == "http://") ? "" : kb.text; 63 + strncpy(SETTINGS.opdsServerUrl, urlToSave.c_str(), sizeof(SETTINGS.opdsServerUrl) - 1); 60 64 SETTINGS.opdsServerUrl[sizeof(SETTINGS.opdsServerUrl) - 1] = '\0'; 61 65 SETTINGS.saveToFile(); 62 66 } ··· 64 68 } else if (selectedIndex == 1) { 65 69 // Username 66 70 startActivityForResult(std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_USERNAME), 67 - SETTINGS.opdsUsername, 63, false), 71 + SETTINGS.opdsUsername, 63, InputType::Text), 68 72 [this](const ActivityResult& result) { 69 73 if (!result.isCancelled) { 70 74 const auto& kb = std::get<KeyboardResult>(result.data); ··· 76 80 } else if (selectedIndex == 2) { 77 81 // Password 78 82 startActivityForResult(std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_PASSWORD), 79 - SETTINGS.opdsPassword, 63, false), 83 + SETTINGS.opdsPassword, 63, InputType::Password), 80 84 [this](const ActivityResult& result) { 81 85 if (!result.isCancelled) { 82 86 const auto& kb = std::get<KeyboardResult>(result.data);
+19 -23
src/activities/settings/KOReaderSettingsActivity.cpp
··· 54 54 if (selectedIndex == 0) { 55 55 // Username 56 56 startActivityForResult(std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_KOREADER_USERNAME), 57 - KOREADER_STORE.getUsername(), 58 - 64, // maxLength 59 - false), // not password 57 + KOREADER_STORE.getUsername(), 64, InputType::Text), 60 58 [this](const ActivityResult& result) { 61 59 if (!result.isCancelled) { 62 60 const auto& kb = std::get<KeyboardResult>(result.data); ··· 66 64 }); 67 65 } else if (selectedIndex == 1) { 68 66 // Password 69 - startActivityForResult(std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_KOREADER_PASSWORD), 70 - KOREADER_STORE.getPassword(), 71 - 64, // maxLength 72 - false), // show characters 73 - [this](const ActivityResult& result) { 74 - if (!result.isCancelled) { 75 - const auto& kb = std::get<KeyboardResult>(result.data); 76 - KOREADER_STORE.setCredentials(KOREADER_STORE.getUsername(), kb.text); 77 - KOREADER_STORE.saveToFile(); 78 - } 79 - }); 80 - } else if (selectedIndex == 2) { 81 - // Sync Server URL - prefill with https:// if empty to save typing 82 - const std::string currentUrl = KOREADER_STORE.getServerUrl(); 83 - const std::string prefillUrl = currentUrl.empty() ? "https://" : currentUrl; 84 67 startActivityForResult( 85 - std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_SYNC_SERVER_URL), prefillUrl, 86 - 128, // maxLength - URLs can be long 87 - false), // not password 68 + std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_KOREADER_PASSWORD), 69 + KOREADER_STORE.getPassword(), 64, InputType::Password), 88 70 [this](const ActivityResult& result) { 89 71 if (!result.isCancelled) { 90 72 const auto& kb = std::get<KeyboardResult>(result.data); 91 - const std::string urlToSave = (kb.text == "https://" || kb.text == "http://") ? "" : kb.text; 92 - KOREADER_STORE.setServerUrl(urlToSave); 73 + KOREADER_STORE.setCredentials(KOREADER_STORE.getUsername(), kb.text); 93 74 KOREADER_STORE.saveToFile(); 94 75 } 95 76 }); 77 + } else if (selectedIndex == 2) { 78 + // Sync Server URL - prefill with https:// if empty to save typing 79 + const std::string currentUrl = KOREADER_STORE.getServerUrl(); 80 + const std::string prefillUrl = currentUrl.empty() ? "https://" : currentUrl; 81 + startActivityForResult(std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_SYNC_SERVER_URL), 82 + prefillUrl, 128, InputType::Url), 83 + [this](const ActivityResult& result) { 84 + if (!result.isCancelled) { 85 + const auto& kb = std::get<KeyboardResult>(result.data); 86 + const std::string urlToSave = 87 + (kb.text == "https://" || kb.text == "http://") ? "" : kb.text; 88 + KOREADER_STORE.setServerUrl(urlToSave); 89 + KOREADER_STORE.saveToFile(); 90 + } 91 + }); 96 92 } else if (selectedIndex == 3) { 97 93 // Document Matching - toggle between Filename and Binary 98 94 const auto current = KOREADER_STORE.getMatchMethod();
+610 -195
src/activities/util/KeyboardEntryActivity.cpp
··· 1 1 #include "KeyboardEntryActivity.h" 2 2 3 + #include <HalGPIO.h> 3 4 #include <I18n.h> 5 + 6 + #include <algorithm> 4 7 5 8 #include "MappedInputManager.h" 6 9 #include "components/UITheme.h" 7 10 #include "fontIds.h" 8 11 9 - // Keyboard layouts - lowercase 10 - const char* const KeyboardEntryActivity::keyboard[NUM_ROWS] = { 11 - "`1234567890-=", "qwertyuiop[]\\", "asdfghjkl;'", "zxcvbnm,./", 12 - "^ _____<OK" // ^ = shift, _ = space, < = backspace, OK = done 13 - }; 14 - 15 - // Keyboard layouts - uppercase/symbols 16 - const char* const KeyboardEntryActivity::keyboardShift[NUM_ROWS] = {"~!@#$%^&*()_+", "QWERTYUIOP{}|", "ASDFGHJKL:\"", 17 - "ZXCVBNM<>?", "SPECIAL ROW"}; 18 - 19 - // Shift state strings 20 - const char* const KeyboardEntryActivity::shiftString[3] = {"shift", "SHIFT", "LOCK"}; 12 + const char* const KeyboardEntryActivity::shiftString[2] = {"shift", "SHIFT"}; 21 13 22 14 void KeyboardEntryActivity::onEnter() { 23 15 Activity::onEnter(); 24 - 25 - // Trigger first update 16 + cursorPos = text.length(); 17 + symMode = false; 18 + urlMode = false; 19 + cursorMode = false; 20 + togglePos = false; 21 + passwordVisible = false; 22 + shiftState = 0; 23 + selectedRow = 0; 24 + selectedCol = 0; 25 + delPressCount = 0; 26 + hintVisible = false; 27 + hintShowTime = 0; 28 + rightHeld = false; 29 + rightLongHandled = false; 30 + savedCursorPos = 0; 31 + rightStartCursorPos = 0; 26 32 requestUpdate(); 27 33 } 28 34 29 35 void KeyboardEntryActivity::onExit() { Activity::onExit(); } 30 36 31 - int KeyboardEntryActivity::getRowLength(const int row) const { 32 - if (row < 0 || row >= NUM_ROWS) return 0; 37 + int KeyboardEntryActivity::getContentRowCount() const { 38 + if (urlMode) return 3; 39 + return ABC_ROWS; 40 + } 33 41 34 - // Return actual length of each row based on keyboard layout 35 - switch (row) { 36 - case 0: 37 - return 13; // `1234567890-= 38 - case 1: 39 - return 13; // qwertyuiop[]backslash 40 - case 2: 41 - return 11; // asdfghjkl;' 42 - case 3: 43 - return 10; // zxcvbnm,./ 44 - case 4: 45 - return 11; // shift (2 wide), space (5 wide), backspace (2 wide), OK (2 wide) 46 - default: 47 - return 0; 48 - } 42 + int KeyboardEntryActivity::getContentColCount() const { 43 + if (urlMode) return 3; 44 + return COLS; 49 45 } 50 46 47 + int KeyboardEntryActivity::getTotalRowCount() const { return getContentRowCount() + 1; } 48 + 49 + bool KeyboardEntryActivity::isBottomRow(const int row) const { return row == getContentRowCount(); } 50 + 51 51 char KeyboardEntryActivity::getSelectedChar() const { 52 - const char* const* layout = shiftState ? keyboardShift : keyboard; 52 + const KeyDef(*layout)[COLS] = symMode ? symLayout : abcLayout; 53 53 54 - if (selectedRow < 0 || selectedRow >= NUM_ROWS) return '\0'; 55 - if (selectedCol < 0 || selectedCol >= getRowLength(selectedRow)) return '\0'; 54 + if (selectedRow < 0 || selectedRow >= getContentRowCount()) return '\0'; 55 + if (selectedCol < 0 || selectedCol >= COLS) return '\0'; 56 56 57 - return layout[selectedRow][selectedCol]; 57 + const KeyDef& key = layout[selectedRow][selectedCol]; 58 + return (shiftState > 0 && key.secondary != '\0') ? key.secondary : key.primary; 58 59 } 59 60 60 - bool KeyboardEntryActivity::handleKeyPress() { 61 - // Handle special row (bottom row with shift, space, backspace, done) 62 - if (selectedRow == SPECIAL_ROW) { 63 - if (selectedCol >= SHIFT_COL && selectedCol < SPACE_COL) { 64 - // Shift toggle (0 = lower case, 1 = upper case, 2 = shift lock) 65 - shiftState = (shiftState + 1) % 3; 66 - return true; 67 - } 61 + char KeyboardEntryActivity::getAlternativeChar() const { 62 + if (symMode || urlMode) return '\0'; 63 + if (inputType == InputType::Url && selectedRow > 0) return '\0'; 64 + 65 + const KeyDef(*layout)[COLS] = abcLayout; 68 66 69 - if (selectedCol >= SPACE_COL && selectedCol < BACKSPACE_COL) { 70 - // Space bar 71 - if (maxLength == 0 || text.length() < maxLength) { 72 - text += ' '; 73 - } 74 - return true; 75 - } 67 + if (selectedRow < 0 || selectedRow >= getContentRowCount()) return '\0'; 68 + if (selectedCol < 0 || selectedCol >= COLS) return '\0'; 76 69 77 - if (selectedCol >= BACKSPACE_COL && selectedCol < DONE_COL) { 78 - // Backspace 79 - if (!text.empty()) { 80 - text.pop_back(); 70 + const KeyDef& key = layout[selectedRow][selectedCol]; 71 + const char current = getSelectedChar(); 72 + if (current == key.primary && key.secondary != '\0') return key.secondary; 73 + if (current == key.secondary) return key.primary; 74 + return '\0'; 75 + } 76 + 77 + bool KeyboardEntryActivity::insertChar(char c) { 78 + if (c == '\0') return true; 79 + if (maxLength != 0 && text.length() >= maxLength) return true; 80 + if (cursorPos > text.length()) cursorPos = text.length(); 81 + 82 + text.insert(cursorPos, 1, c); 83 + cursorPos++; 84 + return true; 85 + } 86 + 87 + void KeyboardEntryActivity::insertString(const std::string& str) { 88 + if (str.empty()) return; 89 + if (maxLength != 0 && text.length() + str.length() > maxLength) return; 90 + if (cursorPos > text.length()) cursorPos = text.length(); 91 + 92 + text.insert(cursorPos, str); 93 + cursorPos += str.length(); 94 + } 95 + 96 + bool KeyboardEntryActivity::handleKeyPress() { 97 + if (isBottomRow(selectedRow)) { 98 + switch (static_cast<SpecialKeyType>(selectedCol)) { 99 + case SpecialKeyType::Shift: 100 + delPressCount = 0; 101 + hintVisible = false; 102 + if (urlMode || inputType == InputType::Url) return true; 103 + if (symMode) return true; 104 + shiftState = (shiftState + 1) % 2; 105 + return true; 106 + case SpecialKeyType::Mode: { 107 + delPressCount = 0; 108 + hintVisible = false; 109 + if (urlMode) { 110 + urlMode = false; 111 + symMode = false; 112 + selectedRow = getTotalRowCount() - 1; 113 + selectedCol = static_cast<int>(SpecialKeyType::Mode); 114 + requestUpdate(); 115 + return true; 116 + } 117 + symMode = !symMode; 118 + int maxRow = getTotalRowCount() - 1; 119 + if (selectedRow > maxRow) selectedRow = maxRow; 120 + if (isBottomRow(selectedRow)) { 121 + if (selectedCol >= BOTTOM_KEY_COUNT) selectedCol = BOTTOM_KEY_COUNT - 1; 122 + } else { 123 + if (selectedCol >= getContentColCount()) selectedCol = getContentColCount() - 1; 124 + } 125 + return true; 81 126 } 82 - return true; 127 + case SpecialKeyType::Space: 128 + delPressCount = 0; 129 + hintVisible = false; 130 + if (inputType == InputType::Url) { 131 + urlMode = !urlMode; 132 + if (urlMode) { 133 + symMode = false; 134 + } 135 + selectedRow = getTotalRowCount() - 1; 136 + selectedCol = static_cast<int>(SpecialKeyType::Space); 137 + requestUpdate(); 138 + } else { 139 + return insertChar(' '); 140 + } 141 + return true; 142 + case SpecialKeyType::Del: 143 + delPressCount++; 144 + if (delPressCount >= 2) { 145 + hintVisible = true; 146 + hintShowTime = millis(); 147 + } 148 + if (cursorPos > 0 && !text.empty()) { 149 + text.erase(cursorPos - 1, 1); 150 + cursorPos--; 151 + } 152 + return true; 153 + case SpecialKeyType::Ok: 154 + delPressCount = 0; 155 + hintVisible = false; 156 + onComplete(text); 157 + return false; 158 + default: 159 + return true; 83 160 } 161 + } 84 162 85 - if (selectedCol >= DONE_COL) { 86 - // Done button 87 - onComplete(text); 88 - return false; 163 + if (urlMode) { 164 + delPressCount = 0; 165 + hintVisible = false; 166 + const int idx = selectedCol + selectedRow * 3; 167 + if (idx < URL_SNIPPET_COUNT) { 168 + insertString(urlSnippets[idx]); 89 169 } 170 + return true; 90 171 } 91 172 92 - // Regular character 93 - const char c = getSelectedChar(); 94 - if (c == '\0') { 95 - return true; 96 - } 173 + delPressCount = 0; 174 + hintVisible = false; 175 + 176 + return insertChar(getSelectedChar()); 177 + } 97 178 98 - if (maxLength == 0 || text.length() < maxLength) { 99 - text += c; 100 - // Auto-disable shift after typing a character in non-lock mode 101 - if (shiftState == 1) { 102 - shiftState = 0; 103 - } 179 + void KeyboardEntryActivity::mapColContentBottom(int& col, bool goingUp) const { 180 + if (urlMode) { 181 + col = goingUp ? col - 1 : col + 1; 182 + if (col < 0) col = 0; 183 + if (col >= 3) col = 2; 184 + } else { 185 + col = goingUp ? col * 2 : col / 2; 104 186 } 105 - 106 - return true; 107 187 } 108 188 109 189 void KeyboardEntryActivity::loop() { 110 - // Handle navigation 111 - buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Up}, [this] { 112 - selectedRow = ButtonNavigator::previousIndex(selectedRow, NUM_ROWS); 190 + const int totalRows = getTotalRowCount(); 191 + 192 + if (!cursorMode && mappedInput.wasPressed(MappedInputManager::Button::Up)) { 193 + upHeld = true; 194 + upLongHandled = false; 195 + } 113 196 114 - const int maxCol = getRowLength(selectedRow) - 1; 115 - if (selectedCol > maxCol) selectedCol = maxCol; 197 + if (upHeld && !upLongHandled && mappedInput.isPressed(MappedInputManager::Button::Up) && 198 + mappedInput.getHeldTime() > LONG_PRESS_MS) { 199 + cursorMode = true; 200 + upLongHandled = true; 201 + hintVisible = true; 202 + hintShowTime = millis(); 116 203 requestUpdate(); 117 - }); 204 + } 205 + 206 + if (mappedInput.wasReleased(MappedInputManager::Button::Up)) { 207 + if (upHeld && !upLongHandled && !cursorMode) { 208 + bool wasBottom = isBottomRow(selectedRow); 209 + const int contentCols = getContentColCount(); 210 + selectedRow = ButtonNavigator::previousIndex(selectedRow, totalRows); 211 + if (wasBottom && !isBottomRow(selectedRow)) { 212 + mapColContentBottom(selectedCol, true); 213 + } else if (!wasBottom && isBottomRow(selectedRow)) { 214 + mapColContentBottom(selectedCol, false); 215 + } 216 + int maxCol = isBottomRow(selectedRow) ? BOTTOM_KEY_COUNT - 1 : contentCols - 1; 217 + if (selectedCol > maxCol) selectedCol = maxCol; 218 + requestUpdate(); 219 + } 220 + upHeld = false; 221 + upLongHandled = false; 222 + } 118 223 119 - buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Down}, [this] { 120 - selectedRow = ButtonNavigator::nextIndex(selectedRow, NUM_ROWS); 224 + if (mappedInput.wasPressed(MappedInputManager::Button::Down)) { 225 + downHeld = true; 226 + if (cursorMode) { 227 + togglePos = false; 228 + passwordVisible = false; 229 + cursorMode = false; 230 + hintVisible = false; 231 + downLongHandled = true; 232 + requestUpdate(); 233 + } else { 234 + downLongHandled = false; 235 + } 236 + } 121 237 122 - const int maxCol = getRowLength(selectedRow) - 1; 123 - if (selectedCol > maxCol) selectedCol = maxCol; 238 + if (mappedInput.wasReleased(MappedInputManager::Button::Down)) { 239 + if (downHeld && !downLongHandled && !cursorMode) { 240 + bool wasBottom = isBottomRow(selectedRow); 241 + const int contentCols = getContentColCount(); 242 + selectedRow = ButtonNavigator::nextIndex(selectedRow, totalRows); 243 + if (wasBottom && !isBottomRow(selectedRow)) { 244 + mapColContentBottom(selectedCol, true); 245 + } else if (!wasBottom && isBottomRow(selectedRow)) { 246 + mapColContentBottom(selectedCol, false); 247 + } 248 + int maxCol = isBottomRow(selectedRow) ? BOTTOM_KEY_COUNT - 1 : contentCols - 1; 249 + if (selectedCol > maxCol) selectedCol = maxCol; 250 + requestUpdate(); 251 + } 252 + downHeld = false; 253 + downLongHandled = false; 254 + } 255 + 256 + buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Left}, [this] { 257 + if (cursorMode) return; 258 + int maxCol = isBottomRow(selectedRow) ? BOTTOM_KEY_COUNT - 1 : getContentColCount() - 1; 259 + selectedCol = ButtonNavigator::previousIndex(selectedCol, maxCol + 1); 124 260 requestUpdate(); 125 261 }); 126 262 127 - buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Left}, [this] { 128 - const int maxCol = getRowLength(selectedRow) - 1; 129 - 130 - // Special bottom row case 131 - if (selectedRow == SPECIAL_ROW) { 132 - // Bottom row has special key widths 133 - if (selectedCol >= SHIFT_COL && selectedCol < SPACE_COL) { 134 - // In shift key, wrap to end of row 135 - selectedCol = maxCol; 136 - } else if (selectedCol >= SPACE_COL && selectedCol < BACKSPACE_COL) { 137 - // In space bar, move to shift 138 - selectedCol = SHIFT_COL; 139 - } else if (selectedCol >= BACKSPACE_COL && selectedCol < DONE_COL) { 140 - // In backspace, move to space 141 - selectedCol = SPACE_COL; 142 - } else if (selectedCol >= DONE_COL) { 143 - // At done button, move to backspace 144 - selectedCol = BACKSPACE_COL; 263 + if (mappedInput.wasReleased(MappedInputManager::Button::Left)) { 264 + if (cursorMode) { 265 + if (togglePos) { 266 + cursorPos = savedCursorPos; 267 + togglePos = false; 268 + requestUpdate(); 269 + } else if (cursorPos > 0) { 270 + cursorPos--; 271 + requestUpdate(); 145 272 } 146 - } else { 147 - selectedCol = ButtonNavigator::previousIndex(selectedCol, maxCol + 1); 273 + } 274 + } 275 + 276 + if (mappedInput.wasPressed(MappedInputManager::Button::Right)) { 277 + if (cursorMode && inputType == InputType::Password && !togglePos) { 278 + rightHeld = true; 279 + rightLongHandled = false; 280 + rightStartCursorPos = cursorPos; 148 281 } 282 + } 149 283 284 + buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Right}, [this] { 285 + if (cursorMode) return; 286 + int maxCol = isBottomRow(selectedRow) ? BOTTOM_KEY_COUNT - 1 : getContentColCount() - 1; 287 + selectedCol = ButtonNavigator::nextIndex(selectedCol, maxCol + 1); 150 288 requestUpdate(); 151 289 }); 152 290 153 - buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Right}, [this] { 154 - const int maxCol = getRowLength(selectedRow) - 1; 291 + if (rightHeld && !rightLongHandled && mappedInput.isPressed(MappedInputManager::Button::Right) && 292 + mappedInput.getHeldTime() > LONG_PRESS_MS) { 293 + if (cursorMode && inputType == InputType::Password && !togglePos) { 294 + savedCursorPos = rightStartCursorPos; 295 + togglePos = true; 296 + rightLongHandled = true; 297 + requestUpdate(); 298 + } 299 + } 155 300 156 - // Special bottom row case 157 - if (selectedRow == SPECIAL_ROW) { 158 - // Bottom row has special key widths 159 - if (selectedCol >= SHIFT_COL && selectedCol < SPACE_COL) { 160 - // In shift key, move to space 161 - selectedCol = SPACE_COL; 162 - } else if (selectedCol >= SPACE_COL && selectedCol < BACKSPACE_COL) { 163 - // In space bar, move to backspace 164 - selectedCol = BACKSPACE_COL; 165 - } else if (selectedCol >= BACKSPACE_COL && selectedCol < DONE_COL) { 166 - // In backspace, move to done 167 - selectedCol = DONE_COL; 168 - } else if (selectedCol >= DONE_COL) { 169 - // At done button, wrap to beginning of row 170 - selectedCol = SHIFT_COL; 171 - } 172 - } else { 173 - selectedCol = ButtonNavigator::nextIndex(selectedCol, maxCol + 1); 301 + if (mappedInput.wasReleased(MappedInputManager::Button::Right)) { 302 + if (cursorMode && inputType == InputType::Password) { 303 + rightHeld = false; 304 + rightLongHandled = false; 174 305 } 306 + if (cursorMode && !togglePos && cursorPos < text.length()) { 307 + cursorPos++; 308 + requestUpdate(); 309 + } 310 + if (cursorMode) return; 311 + rightHeld = false; 312 + rightLongHandled = false; 313 + } 314 + 315 + if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { 316 + confirmHeld = true; 317 + confirmLongHandled = false; 318 + } 319 + 320 + if (confirmHeld && !confirmLongHandled && mappedInput.isPressed(MappedInputManager::Button::Confirm) && 321 + mappedInput.getHeldTime() > DEL_LONG_PRESS_MS && isBottomRow(selectedRow) && 322 + selectedCol == static_cast<int>(SpecialKeyType::Del)) { 323 + text.clear(); 324 + cursorPos = 0; 325 + confirmLongHandled = true; 175 326 requestUpdate(); 176 - }); 327 + } 328 + 329 + if (confirmHeld && !confirmLongHandled && mappedInput.isPressed(MappedInputManager::Button::Confirm) && 330 + mappedInput.getHeldTime() > LONG_PRESS_MS) { 331 + char alt = getAlternativeChar(); 332 + if (alt != '\0') { 333 + insertChar(alt); 334 + requestUpdate(); 335 + confirmLongHandled = true; 336 + } 337 + } 177 338 178 - // Selection 179 - if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { 180 - if (handleKeyPress()) { 339 + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { 340 + if (confirmHeld && !confirmLongHandled && !cursorMode) { 341 + if (handleKeyPress()) { 342 + requestUpdate(); 343 + } 344 + } else if (confirmHeld && !confirmLongHandled && cursorMode && inputType == InputType::Password && togglePos) { 345 + passwordVisible = !passwordVisible; 181 346 requestUpdate(); 182 347 } 183 - // If handleKeyPress returns false, it means onComplete was triggered, no update needed 348 + confirmHeld = false; 349 + confirmLongHandled = false; 184 350 } 185 351 186 - // Cancel 187 352 if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { 188 353 onCancel(); 189 354 } 355 + 356 + if (hintVisible && !cursorMode && millis() - hintShowTime > 4000) { 357 + hintVisible = false; 358 + requestUpdate(); 359 + } 190 360 } 191 361 192 362 void KeyboardEntryActivity::render(RenderLock&&) { ··· 198 368 199 369 GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, title.c_str()); 200 370 201 - // Draw input field 202 371 const int lineHeight = renderer.getLineHeight(UI_12_FONT_ID); 203 - const int inputStartY = 204 - metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing + metrics.verticalSpacing * 4; 372 + const int inputStartY = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing + 373 + metrics.verticalSpacing * 4 + metrics.keyboardVerticalOffset; 205 374 int inputHeight = 0; 206 375 207 376 std::string displayText; 208 - if (isPassword) { 209 - displayText = std::string(text.length(), '*'); 377 + if (inputType == InputType::Password && !passwordVisible) { 378 + size_t revealPos; 379 + if (cursorMode) { 380 + revealPos = text.length(); // no reveal in displayText; block draws actual char directly 381 + } else { 382 + revealPos = (text.length() > 0 && cursorPos > 0) ? cursorPos - 1 : std::string::npos; 383 + } 384 + displayText = text; 385 + for (size_t i = 0; i < displayText.length(); i++) { 386 + if (i != revealPos) { 387 + displayText[i] = '*'; 388 + } 389 + } 210 390 } else { 211 391 displayText = text; 212 392 } 213 393 214 - // Show cursor at end 215 - displayText += "_"; 394 + const bool isPassword = (inputType == InputType::Password); 395 + int availableWidth = pageWidth; 396 + if (gpio.deviceIsX3()) { 397 + availableWidth -= 2 * metrics.sideButtonHintsWidth; 398 + } 399 + const int effectiveMargin = (pageWidth - availableWidth * metrics.keyboardTextFieldWidthPercent / 100) / 2; 400 + const int toggleGap = isPassword ? 4 : 0; 401 + const int toggleReserve = isPassword ? std::max(renderer.getTextWidth(UI_12_FONT_ID, "[abc]"), 402 + renderer.getTextWidth(UI_12_FONT_ID, "[***]")) + 403 + toggleGap 404 + : 0; 405 + const int textAreaWidth = pageWidth - 2 * effectiveMargin - toggleReserve; 406 + const int maxLineWidth = textAreaWidth; 407 + const bool centerText = metrics.keyboardCenteredText; 408 + 409 + int cursorCharWidth = 6; 410 + if (cursorPos < text.length()) { 411 + int w = renderer.getTextWidth(UI_12_FONT_ID, text.substr(cursorPos, 1).c_str()); 412 + if (w > cursorCharWidth) cursorCharWidth = w; 413 + } 216 414 217 - // Render input text across multiple lines 218 415 int lineStartIdx = 0; 219 416 int lineEndIdx = displayText.length(); 220 417 int textWidth = 0; 418 + int cursorPixelX = effectiveMargin; 419 + int cursorLineY = inputStartY; 420 + bool cursorDrawn = false; 421 + 221 422 while (true) { 222 423 std::string lineText = displayText.substr(lineStartIdx, lineEndIdx - lineStartIdx); 223 424 textWidth = renderer.getTextWidth(UI_12_FONT_ID, lineText.c_str()); 224 - if (textWidth <= pageWidth - 2 * metrics.contentSidePadding) { 225 - if (metrics.keyboardCenteredText) { 226 - renderer.drawCenteredText(UI_12_FONT_ID, inputStartY + inputHeight, lineText.c_str()); 425 + if (textWidth <= maxLineWidth) { 426 + const bool isLastLine = (lineEndIdx == static_cast<int>(displayText.length())); 427 + bool isCursorLine = false; 428 + if (!cursorDrawn && cursorPos >= lineStartIdx && 429 + (isLastLine ? cursorPos <= lineEndIdx : cursorPos < lineEndIdx)) { 430 + std::string beforeCursor; 431 + if (isPassword && !passwordVisible && cursorMode) { 432 + beforeCursor = std::string(cursorPos - lineStartIdx, '*'); 433 + } else { 434 + beforeCursor = displayText.substr(lineStartIdx, cursorPos - lineStartIdx); 435 + } 436 + int beforeWidth = renderer.getTextWidth(UI_12_FONT_ID, beforeCursor.c_str()); 437 + int kernOffset = 0; 438 + if (cursorPos < displayText.length()) { 439 + std::string beforeAndCursor = beforeCursor + displayText.substr(cursorPos, 1); 440 + int beforeAndCursorWidth = renderer.getTextWidth(UI_12_FONT_ID, beforeAndCursor.c_str()); 441 + int charAdvance = renderer.getTextWidth(UI_12_FONT_ID, displayText.substr(cursorPos, 1).c_str()); 442 + kernOffset = beforeAndCursorWidth - beforeWidth - charAdvance; 443 + } 444 + if (centerText) { 445 + cursorPixelX = effectiveMargin + (maxLineWidth - textWidth) / 2 + beforeWidth + kernOffset; 446 + } else { 447 + cursorPixelX = effectiveMargin + beforeWidth + kernOffset; 448 + } 449 + cursorLineY = inputStartY + inputHeight; 450 + cursorDrawn = true; 451 + isCursorLine = true; 452 + } 453 + 454 + const int lineStartX = centerText ? effectiveMargin + (maxLineWidth - textWidth) / 2 : effectiveMargin; 455 + if (isCursorLine && cursorMode && isPassword && !passwordVisible && !togglePos) { 456 + // Draw text in 3 parts to avoid block cursor overflowing onto next char. 457 + // displayText uses '*' for all chars; actual char may be wider than '*'. 458 + // Part 1: chars before cursor position 459 + const std::string part1 = displayText.substr(lineStartIdx, cursorPos - lineStartIdx); 460 + renderer.drawText(UI_12_FONT_ID, lineStartX, inputStartY + inputHeight, part1.c_str()); 461 + // Part 2: skip cursor slot (block + actual char drawn later) 462 + // Part 3: chars after cursor position (skip char under cursor), starting at cursorPixelX + cursorCharWidth 463 + const int afterStart = static_cast<int>(cursorPos) + (cursorPos < text.length() ? 1 : 0); 464 + const int afterEnd = lineEndIdx; 465 + if (afterStart < afterEnd) { 466 + const std::string part3 = displayText.substr(afterStart, afterEnd - afterStart); 467 + renderer.drawText(UI_12_FONT_ID, cursorPixelX + cursorCharWidth, inputStartY + inputHeight, part3.c_str()); 468 + } 227 469 } else { 228 - renderer.drawText(UI_12_FONT_ID, metrics.contentSidePadding, inputStartY + inputHeight, lineText.c_str()); 470 + renderer.drawText(UI_12_FONT_ID, lineStartX, inputStartY + inputHeight, lineText.c_str()); 229 471 } 230 472 if (lineEndIdx == displayText.length()) { 231 473 break; ··· 239 481 } 240 482 } 241 483 242 - GUI.drawTextField(renderer, Rect{0, inputStartY, pageWidth, inputHeight}, textWidth); 484 + const int fieldWidth = (inputHeight > 0) ? maxLineWidth : textWidth; 485 + const int lineMargin = effectiveMargin; 486 + GUI.drawTextField(renderer, Rect{0, inputStartY, pageWidth, inputHeight}, fieldWidth, cursorMode, lineMargin, 487 + pageWidth - 2 * lineMargin); 243 488 244 - // Draw keyboard - use compact spacing to fit 5 rows on screen 245 - const int keyboardStartY = metrics.keyboardBottomAligned 246 - ? pageHeight - metrics.buttonHintsHeight - metrics.verticalSpacing - 247 - (metrics.keyboardKeyHeight + metrics.keyboardKeySpacing) * NUM_ROWS 248 - : inputStartY + inputHeight + metrics.verticalSpacing * 4; 249 - const int keyWidth = metrics.keyboardKeyWidth; 489 + if (cursorMode && !togglePos && cursorPos <= displayText.length()) { 490 + static constexpr int blockPadding = 1; 491 + renderer.fillRect(cursorPixelX - blockPadding, cursorLineY, cursorCharWidth + blockPadding * 2, lineHeight, true); 492 + if (cursorPos < text.length()) { 493 + const char buf[2] = {text[cursorPos], '\0'}; 494 + renderer.drawText(UI_12_FONT_ID, cursorPixelX, cursorLineY, buf, false); 495 + } 496 + } else if (cursorPos <= displayText.length()) { 497 + static constexpr int serifW = 3; 498 + const int cX = cursorPixelX; 499 + const int cY = cursorLineY; 500 + const int cBottom = cursorLineY + lineHeight - 1; 501 + renderer.fillRect(cX, cY, 2, lineHeight, true); 502 + renderer.drawLine(cX - serifW, cY, cX - 1, cY, 2, true); 503 + renderer.drawLine(cX + 1, cY, cX + serifW, cY, 2, true); 504 + renderer.drawLine(cX - serifW, cBottom, cX - 1, cBottom, 2, true); 505 + renderer.drawLine(cX + 1, cBottom, cX + serifW, cBottom, 2, true); 506 + } 507 + 508 + if (isPassword) { 509 + const char* toggleLabel = passwordVisible ? "[***]" : "[abc]"; 510 + const int toggleWidth = renderer.getTextWidth(UI_12_FONT_ID, toggleLabel); 511 + const int toggleX = pageWidth - effectiveMargin - toggleWidth; 512 + const int toggleY = inputStartY + inputHeight; 513 + const bool toggleSelected = cursorMode && togglePos; 514 + 515 + if (toggleSelected) { 516 + renderer.fillRect(toggleX - 2, toggleY, toggleWidth + 5, lineHeight + 3, true); 517 + renderer.drawText(UI_12_FONT_ID, toggleX, toggleY, toggleLabel, false); 518 + } else { 519 + renderer.drawText(UI_12_FONT_ID, toggleX, toggleY, toggleLabel, true); 520 + } 521 + } 522 + 523 + if (hintVisible && !text.empty()) { 524 + const int hintLh = renderer.getLineHeight(SMALL_FONT_ID); 525 + const int underlineY = inputStartY + inputHeight + lineHeight + metrics.verticalSpacing; 526 + const int hintY = underlineY + 4; 527 + if (cursorMode) { 528 + int hintLineY = hintY; 529 + renderer.drawCenteredText(SMALL_FONT_ID, hintLineY, "Press < or > to move cursor", true); 530 + hintLineY += hintLh; 531 + if (inputType == InputType::Password) { 532 + const char* passTip; 533 + if (togglePos) { 534 + passTip = "Press < to return to cursor position"; 535 + } else { 536 + passTip = 537 + passwordVisible ? "Hold > then press [***] to hide password" : "Hold > then press [abc] to show password"; 538 + } 539 + renderer.drawCenteredText(SMALL_FONT_ID, hintLineY, passTip, true); 540 + } 541 + } else { 542 + renderer.drawCenteredText(SMALL_FONT_ID, hintY, "Hold UP to edit entry", true); 543 + } 544 + } 545 + 250 546 const int keyHeight = metrics.keyboardKeyHeight; 547 + const int bottomKeyHeight = metrics.keyboardBottomKeyHeight; 251 548 const int keySpacing = metrics.keyboardKeySpacing; 549 + const int contentCols = getContentColCount(); 550 + const int keyboardWidth = pageWidth * metrics.keyboardWidthPercent / 100; 551 + const int keyWidth = (keyboardWidth - (contentCols - 1) * keySpacing) / contentCols; 552 + const int leftMargin = (pageWidth - (contentCols * keyWidth + (contentCols - 1) * keySpacing)) / 2; 252 553 253 - const char* const* layout = shiftState ? keyboardShift : keyboard; 554 + const int bottomRowGap = metrics.keyboardBottomKeySpacing > 0 ? 4 : 0; 555 + const int keyboardStartY = metrics.keyboardBottomAligned 556 + ? pageHeight - metrics.buttonHintsHeight - metrics.verticalSpacing - 557 + (keyHeight + keySpacing) * getContentRowCount() - bottomKeyHeight - 558 + bottomRowGap + metrics.keyboardVerticalOffset 559 + : inputStartY + inputHeight + lineHeight + metrics.verticalSpacing; 254 560 255 - // Calculate left margin to center the longest row (13 keys) 256 - const int maxRowWidth = KEYS_PER_ROW * (keyWidth + keySpacing); 257 - const int leftMargin = (pageWidth - maxRowWidth) / 2; 561 + const int tipsLh = renderer.getLineHeight(SMALL_FONT_ID); 562 + const int underlineBottom = inputStartY + inputHeight + lineHeight + metrics.verticalSpacing + 4; 563 + auto drawTip = [&](const char* tip, int y) { renderer.drawCenteredText(SMALL_FONT_ID, y, tip, true); }; 564 + 565 + int tipCount = 0; 566 + if (cursorMode) { 567 + tipCount = 1; 568 + } else if (urlMode) { 569 + tipCount = 1 + (!text.empty() ? 1 : 0); 570 + } else if (symMode) { 571 + tipCount = !text.empty() ? 1 : 0; 572 + } else { 573 + tipCount = 1 + (inputType == InputType::Url ? 1 : 0) + (!text.empty() ? 1 : 0); 574 + } 575 + 576 + if (tipCount > 0) { 577 + int y = (underlineBottom + keyboardStartY) / 2 - (tipCount + 1) * tipsLh / 2; 578 + drawTip("Tips:", y); 579 + y += tipsLh; 580 + if (cursorMode) { 581 + drawTip("Press DOWN to return to keyboard", y); 582 + } else if (urlMode) { 583 + drawTip("Press ABC to exit URL mode", y); 584 + y += tipsLh; 585 + if (!text.empty()) { 586 + drawTip("Hold DEL to clear all text", y); 587 + } 588 + } else if (symMode) { 589 + if (!text.empty()) { 590 + drawTip("Hold DEL to clear all text", y); 591 + } 592 + } else { 593 + const char* altCharTip; 594 + if (inputType == InputType::Url) { 595 + altCharTip = "Hold SELECT for secondary char"; 596 + } else if (shiftState > 0) { 597 + altCharTip = "Hold SELECT for lowercase or secondary char"; 598 + } else { 599 + altCharTip = "Hold SELECT for UPPERCASE or secondary char"; 600 + } 601 + drawTip(altCharTip, y); 602 + y += tipsLh; 603 + if (inputType == InputType::Url) { 604 + drawTip("Press URL for snippets", y); 605 + y += tipsLh; 606 + } 607 + if (!text.empty()) { 608 + drawTip("Hold DEL to clear all text", y); 609 + } 610 + } 611 + } 612 + 613 + const int bkSpacing = metrics.keyboardBottomKeySpacing; 614 + const int abcKeyWidth = (keyboardWidth - (COLS - 1) * keySpacing) / COLS; 615 + const int contentTotalWidth = COLS * abcKeyWidth + (COLS - 1) * keySpacing; 616 + const int bottomKeyWidth = (contentTotalWidth - (BOTTOM_KEY_COUNT - 1) * bkSpacing) / BOTTOM_KEY_COUNT; 617 + const int bottomLeftMargin = 618 + (pageWidth - (BOTTOM_KEY_COUNT * bottomKeyWidth + (BOTTOM_KEY_COUNT - 1) * bkSpacing)) / 2; 619 + 620 + int urlLeftMargin = leftMargin; 621 + if (urlMode) { 622 + const int urlTotalWidth = 3 * keyWidth + 2 * keySpacing; 623 + const int urlCenterX = 624 + bottomLeftMargin + static_cast<int>(SpecialKeyType::Space) * (bottomKeyWidth + bkSpacing) + bottomKeyWidth / 2; 625 + urlLeftMargin = urlCenterX - urlTotalWidth / 2; 626 + } 258 627 259 - for (int row = 0; row < NUM_ROWS; row++) { 628 + const KeyDef(*layout)[COLS] = symMode ? symLayout : abcLayout; 629 + const int contentRows = getContentRowCount(); 630 + 631 + for (int row = 0; row < contentRows; row++) { 260 632 const int rowY = keyboardStartY + row * (keyHeight + keySpacing); 633 + const int rowLeftMargin = urlMode ? urlLeftMargin : leftMargin; 261 634 262 - // Left-align all rows for consistent navigation 263 - const int startX = leftMargin; 635 + for (int col = 0; col < contentCols; col++) { 636 + const int keyX = rowLeftMargin + col * (keyWidth + keySpacing); 637 + const bool isSelected = row == selectedRow && col == selectedCol; 638 + const bool activeKeySelected = isSelected && !cursorMode; 264 639 265 - // Handle bottom row (row 4) specially with proper multi-column keys 266 - if (row == SPECIAL_ROW) { 267 - // Bottom row layout: SHIFT (2 cols) | SPACE (5 cols) | <- (2 cols) | OK (2 cols) 268 - // Total: 11 visual columns, but we use logical positions for selection 640 + if (urlMode) { 641 + const int snippetIdx = col + row * 3; 642 + if (snippetIdx < URL_SNIPPET_COUNT) { 643 + GUI.drawKeyboardKey(renderer, Rect{keyX, rowY, keyWidth, keyHeight}, urlSnippets[snippetIdx], 644 + activeKeySelected, nullptr); 645 + } 646 + } else { 647 + const KeyDef& key = layout[row][col]; 648 + 649 + char primaryChar = key.primary; 650 + char secondaryChar = key.secondary; 651 + 652 + if (!symMode && shiftState > 0 && key.secondary != '\0') { 653 + primaryChar = key.secondary; 654 + secondaryChar = key.primary; 655 + } 656 + 657 + const char primaryBuf[2] = {primaryChar, '\0'}; 658 + const char secondaryBuf[2] = {secondaryChar, '\0'}; 659 + const bool showSecondary = !symMode && row == 0 && secondaryChar != '\0'; 660 + GUI.drawKeyboardKey(renderer, Rect{keyX, rowY, keyWidth, keyHeight}, primaryBuf, activeKeySelected, 661 + showSecondary ? secondaryBuf : nullptr); 662 + } 663 + } 664 + } 269 665 270 - int currentX = startX; 666 + const int bottomRowY = keyboardStartY + contentRows * (keyHeight + keySpacing) + bottomRowGap; 667 + const bool bottomSelected = isBottomRow(selectedRow); 271 668 272 - // SHIFT key (logical col 0, spans 2 key widths) 273 - const bool shiftSelected = (selectedRow == SPECIAL_ROW && selectedCol >= SHIFT_COL && selectedCol < SPACE_COL); 274 - const int shiftWidth = SPACE_COL - SHIFT_COL; 275 - const int shiftXWidth = shiftWidth * (keyWidth + keySpacing); 276 - GUI.drawKeyboardKey(renderer, Rect{currentX, rowY, shiftXWidth, keyHeight}, shiftString[shiftState], 277 - shiftSelected); 278 - currentX += shiftXWidth; 669 + struct BottomKeyInfo { 670 + KeyboardKeyType themeType; 671 + const char* label; 672 + }; 673 + const BottomKeyInfo bottomKeys[BOTTOM_KEY_COUNT] = { 674 + {(symMode || urlMode || inputType == InputType::Url) ? KeyboardKeyType::Disabled : KeyboardKeyType::Shift, 675 + (symMode || urlMode || inputType == InputType::Url) ? shiftString[0] : shiftString[shiftState]}, 676 + {KeyboardKeyType::Mode, urlMode ? "abc" : (symMode ? "abc" : "#@!")}, 677 + {inputType == InputType::Url ? KeyboardKeyType::Mode : KeyboardKeyType::Space, 678 + inputType == InputType::Url ? "URL" : nullptr}, 679 + {KeyboardKeyType::Del, nullptr}, 680 + {KeyboardKeyType::Ok, tr(STR_OK_BUTTON)}, 681 + }; 279 682 280 - // Space bar (logical cols 2-6, spans 5 key widths) 281 - const bool spaceSelected = 282 - (selectedRow == SPECIAL_ROW && selectedCol >= SPACE_COL && selectedCol < BACKSPACE_COL); 283 - const int spaceWidth = BACKSPACE_COL - SPACE_COL; 284 - const int spaceXWidth = spaceWidth * (keyWidth + keySpacing); 285 - GUI.drawKeyboardKey(renderer, Rect{currentX, rowY, spaceXWidth, keyHeight}, "_____", spaceSelected); 286 - currentX += spaceXWidth; 683 + for (int i = 0; i < BOTTOM_KEY_COUNT; i++) { 684 + const int keyX = bottomLeftMargin + i * (bottomKeyWidth + bkSpacing); 685 + const bool isSelected = bottomSelected && i == selectedCol; 287 686 288 - // Backspace key (logical col 7, spans 2 key widths) 289 - const bool bsSelected = (selectedRow == SPECIAL_ROW && selectedCol >= BACKSPACE_COL && selectedCol < DONE_COL); 290 - const int backspaceWidth = DONE_COL - BACKSPACE_COL; 291 - const int backspaceXWidth = backspaceWidth * (keyWidth + keySpacing); 292 - GUI.drawKeyboardKey(renderer, Rect{currentX, rowY, backspaceXWidth, keyHeight}, "<-", bsSelected); 293 - currentX += backspaceXWidth; 687 + const bool activeKeySelected = isSelected && !cursorMode; 688 + GUI.drawKeyboardKey(renderer, Rect{keyX, bottomRowY, bottomKeyWidth, bottomKeyHeight}, bottomKeys[i].label, 689 + activeKeySelected, nullptr, bottomKeys[i].themeType); 690 + } 294 691 295 - // OK button (logical col 9, spans 2 key widths) 296 - const bool okSelected = (selectedRow == SPECIAL_ROW && selectedCol >= DONE_COL); 297 - const int okWidth = getRowLength(row) - DONE_COL; 298 - const int okXWidth = okWidth * (keyWidth + keySpacing); 299 - GUI.drawKeyboardKey(renderer, Rect{currentX, rowY, okXWidth, keyHeight}, tr(STR_OK_BUTTON), okSelected); 692 + if (cursorMode) { 693 + int selKeyX, selKeyY, selKeyW, selKeyH; 694 + if (isBottomRow(selectedRow)) { 695 + selKeyX = bottomLeftMargin + selectedCol * (bottomKeyWidth + bkSpacing); 696 + selKeyY = bottomRowY; 697 + selKeyW = bottomKeyWidth; 698 + selKeyH = bottomKeyHeight; 300 699 } else { 301 - // Regular rows: render each key individually 302 - for (int col = 0; col < getRowLength(row); col++) { 303 - // Get the character to display 304 - const char c = layout[row][col]; 305 - std::string keyLabel(1, c); 306 - 307 - const int keyX = startX + col * (keyWidth + keySpacing); 308 - const bool isSelected = row == selectedRow && col == selectedCol; 309 - GUI.drawKeyboardKey(renderer, Rect{keyX, rowY, keyWidth, keyHeight}, keyLabel.c_str(), isSelected); 700 + const int rowLM = urlMode ? urlLeftMargin : leftMargin; 701 + selKeyX = rowLM + selectedCol * (keyWidth + keySpacing); 702 + selKeyY = keyboardStartY + selectedRow * (keyHeight + keySpacing); 703 + selKeyW = keyWidth; 704 + selKeyH = keyHeight; 705 + } 706 + if (isBottomRow(selectedRow)) { 707 + GUI.drawKeyboardKey(renderer, Rect{selKeyX, selKeyY, selKeyW, selKeyH}, bottomKeys[selectedCol].label, true, 708 + nullptr, bottomKeys[selectedCol].themeType, true); 709 + } else if (urlMode) { 710 + const int idx = selectedCol + selectedRow * 3; 711 + if (idx < URL_SNIPPET_COUNT) { 712 + GUI.drawKeyboardKey(renderer, Rect{selKeyX, selKeyY, selKeyW, selKeyH}, urlSnippets[idx], true, nullptr, 713 + KeyboardKeyType::Normal, true); 310 714 } 715 + } else { 716 + const KeyDef& selKey = layout[selectedRow][selectedCol]; 717 + char selPrimary = selKey.primary; 718 + char selSecondary = selKey.secondary; 719 + if (!symMode && shiftState > 0 && selKey.secondary != '\0') { 720 + selPrimary = selKey.secondary; 721 + selSecondary = selKey.primary; 722 + } 723 + const char selPrimaryBuf[2] = {selPrimary, '\0'}; 724 + const char selSecondaryBuf[2] = {selSecondary, '\0'}; 725 + const bool selShowSecondary = !symMode && selectedRow == 0 && selSecondary != '\0'; 726 + GUI.drawKeyboardKey(renderer, Rect{selKeyX, selKeyY, selKeyW, selKeyH}, selPrimaryBuf, true, 727 + selShowSecondary ? selSecondaryBuf : nullptr, KeyboardKeyType::Normal, true); 311 728 } 312 729 } 313 730 314 - // Draw help text 315 731 const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_LEFT), tr(STR_DIR_RIGHT)); 316 732 GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); 317 733 318 - // Draw side button hints for Up/Down navigation 319 734 GUI.drawSideButtonHints(renderer, ">", "<"); 320 735 321 736 renderer.displayBuffer();
+142 -34
src/activities/util/KeyboardEntryActivity.h
··· 1 1 #pragma once 2 2 #include <GfxRenderer.h> 3 3 4 + #include <cstdint> 4 5 #include <functional> 5 6 #include <string> 6 7 #include <utility> ··· 8 9 #include "../Activity.h" 9 10 #include "util/ButtonNavigator.h" 10 11 11 - /** 12 - * Reusable keyboard entry activity for text input. 13 - * Can be started from any activity that needs text entry via startActivityForResult() 14 - */ 12 + struct KeyDef { 13 + char primary; 14 + char secondary; 15 + }; 16 + 17 + enum class SpecialKeyType { Shift, Mode, Space, Del, Ok }; 18 + 19 + enum class InputType { Text, Password, Url }; 20 + 15 21 class KeyboardEntryActivity : public Activity { 16 22 public: 17 - /** 18 - * Constructor 19 - * @param renderer Reference to the GfxRenderer for drawing 20 - * @param mappedInput Reference to MappedInputManager for handling input 21 - * @param title Title to display above the keyboard 22 - * @param initialText Initial text to show in the input field 23 - * @param maxLength Maximum length of input text (0 for unlimited) 24 - * @param isPassword If true, display asterisks instead of actual characters 25 - */ 26 23 explicit KeyboardEntryActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, 27 24 std::string title = "Enter Text", std::string initialText = "", 28 - const size_t maxLength = 0, const bool isPassword = false) 25 + const size_t maxLength = 0, InputType inputType = InputType::Text) 29 26 : Activity("KeyboardEntry", renderer, mappedInput), 30 27 title(std::move(title)), 31 28 text(std::move(initialText)), 32 29 maxLength(maxLength), 33 - isPassword(isPassword) {} 30 + inputType(inputType) {} 34 31 35 - // Activity overrides 36 32 void onEnter() override; 37 33 void onExit() override; 38 34 void loop() override; ··· 42 38 std::string title; 43 39 std::string text; 44 40 size_t maxLength; 45 - bool isPassword; 41 + InputType inputType; 42 + bool passwordVisible = false; 46 43 47 44 ButtonNavigator buttonNavigator; 48 45 49 - // Keyboard state 50 46 int selectedRow = 0; 51 47 int selectedCol = 0; 52 - int shiftState = 0; // 0 = lower case, 1 = upper case, 2 = shift lock) 48 + int shiftState = 0; 49 + bool symMode = false; 50 + bool confirmHeld = false; 51 + bool confirmLongHandled = false; 53 52 54 - // Handlers 53 + bool cursorMode = false; 54 + bool togglePos = false; 55 + size_t cursorPos = 0; 56 + bool upHeld = false; 57 + bool upLongHandled = false; 58 + bool downHeld = false; 59 + bool downLongHandled = false; 60 + bool rightHeld = false; 61 + bool rightLongHandled = false; 62 + size_t savedCursorPos = 0; 63 + size_t rightStartCursorPos = 0; 64 + 65 + bool urlMode = false; 66 + static constexpr int URL_SNIPPET_COUNT = 9; 67 + static constexpr const char* const urlSnippets[URL_SNIPPET_COUNT] = { 68 + "https://", "www.", ".com", "http://", "192.168.", ".org", "/opds", ":8080", ".net"}; 69 + 70 + int delPressCount = 0; 71 + bool hintVisible = false; 72 + unsigned long hintShowTime = 0; 73 + 55 74 void onComplete(std::string text); 56 75 void onCancel(); 57 76 58 - // Keyboard layout 59 - static constexpr int NUM_ROWS = 5; 60 - static constexpr int KEYS_PER_ROW = 13; // Max keys per row (rows 0 and 1 have 13 keys) 61 - static const char* const keyboard[NUM_ROWS]; 62 - static const char* const keyboardShift[NUM_ROWS]; 63 - static const char* const shiftString[3]; 77 + static constexpr uint16_t LONG_PRESS_MS = 500; 78 + static constexpr uint16_t DEL_LONG_PRESS_MS = 1500; 64 79 65 - // Special key positions (bottom row) 66 - static constexpr int SPECIAL_ROW = 4; 67 - static constexpr int SHIFT_COL = 0; 68 - static constexpr int SPACE_COL = 2; 69 - static constexpr int BACKSPACE_COL = 7; 70 - static constexpr int DONE_COL = 9; 80 + static constexpr int COLS = 10; 81 + static constexpr int ABC_ROWS = 4; 82 + static constexpr int SYM_ROWS = 4; 83 + static constexpr int BOTTOM_KEY_COUNT = 5; 71 84 85 + static constexpr KeyDef abcLayout[ABC_ROWS][COLS] = { 86 + {{'1', '!'}, 87 + {'2', '@'}, 88 + {'3', '#'}, 89 + {'4', '$'}, 90 + {'5', '%'}, 91 + {'6', '^'}, 92 + {'7', '&'}, 93 + {'8', '*'}, 94 + {'9', '('}, 95 + {'0', ')'}}, 96 + {{'q', 'Q'}, 97 + {'w', 'W'}, 98 + {'e', 'E'}, 99 + {'r', 'R'}, 100 + {'t', 'T'}, 101 + {'y', 'Y'}, 102 + {'u', 'U'}, 103 + {'i', 'I'}, 104 + {'o', 'O'}, 105 + {'p', 'P'}}, 106 + {{'a', 'A'}, 107 + {'s', 'S'}, 108 + {'d', 'D'}, 109 + {'f', 'F'}, 110 + {'g', 'G'}, 111 + {'h', 'H'}, 112 + {'j', 'J'}, 113 + {'k', 'K'}, 114 + {'l', 'L'}, 115 + {'-', '_'}}, 116 + {{'z', 'Z'}, 117 + {'x', 'X'}, 118 + {'c', 'C'}, 119 + {'v', 'V'}, 120 + {'b', 'B'}, 121 + {'n', 'N'}, 122 + {'m', 'M'}, 123 + {'=', '+'}, 124 + {'.', '>'}, 125 + {',', '<'}}, 126 + }; 127 + 128 + static constexpr KeyDef symLayout[SYM_ROWS][COLS] = { 129 + {{'1', '\0'}, 130 + {'2', '\0'}, 131 + {'3', '\0'}, 132 + {'4', '\0'}, 133 + {'5', '\0'}, 134 + {'6', '\0'}, 135 + {'7', '\0'}, 136 + {'8', '\0'}, 137 + {'9', '\0'}, 138 + {'0', '\0'}}, 139 + {{'!', '\0'}, 140 + {'@', '\0'}, 141 + {'#', '\0'}, 142 + {'$', '\0'}, 143 + {'%', '\0'}, 144 + {'^', '\0'}, 145 + {'&', '\0'}, 146 + {'*', '\0'}, 147 + {'(', '\0'}, 148 + {')', '\0'}}, 149 + {{'-', '\0'}, 150 + {'_', '\0'}, 151 + {'=', '\0'}, 152 + {'+', '\0'}, 153 + {'[', '\0'}, 154 + {']', '\0'}, 155 + {'{', '\0'}, 156 + {'}', '\0'}, 157 + {';', '\0'}, 158 + {':', '\0'}}, 159 + {{'\'', '\0'}, 160 + {'"', '\0'}, 161 + {'/', '\0'}, 162 + {'\\', '\0'}, 163 + {'|', '\0'}, 164 + {'?', '\0'}, 165 + {'.', '\0'}, 166 + {',', '\0'}, 167 + {'~', '\0'}, 168 + {'`', '\0'}}, 169 + }; 170 + 171 + static const char* const shiftString[2]; 172 + 173 + int getContentRowCount() const; 174 + int getContentColCount() const; 175 + int getTotalRowCount() const; 176 + bool isBottomRow(int row) const; 72 177 char getSelectedChar() const; 73 - bool handleKeyPress(); // false if onComplete was triggered 74 - int getRowLength(int row) const; 178 + char getAlternativeChar() const; 179 + bool handleKeyPress(); 180 + bool insertChar(char c); 181 + void insertString(const std::string& str); 182 + void mapColContentBottom(int& col, bool goingUp) const; 75 183 };
+62 -10
src/components/themes/BaseTheme.cpp
··· 781 781 renderer.drawCenteredText(SMALL_FONT_ID, rect.y, truncatedLabel.c_str()); 782 782 } 783 783 784 - void BaseTheme::drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth) const { 785 - renderer.drawText(UI_12_FONT_ID, rect.x + 10, rect.y, "["); 786 - renderer.drawText(UI_12_FONT_ID, rect.x + rect.width - 15, rect.y + rect.height, "]"); 784 + void BaseTheme::drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth, bool cursorMode, 785 + int contentStartX, int contentWidth) const { 786 + const int lineHeight = renderer.getLineHeight(UI_12_FONT_ID); 787 + const int lineY = rect.y + rect.height + lineHeight + BaseMetrics::values.verticalSpacing; 788 + const int thickness = cursorMode ? 3 : 1; 789 + if (contentWidth > 0) { 790 + renderer.drawLine(rect.x + contentStartX, lineY, rect.x + contentStartX + contentWidth, lineY, thickness, true); 791 + } else { 792 + const int hPadding = 4; 793 + const int lineW = textWidth + hPadding * 2; 794 + renderer.drawLine(rect.x + (rect.width - lineW) / 2, lineY, rect.x + (rect.width + lineW) / 2, lineY, thickness, 795 + true); 796 + } 787 797 } 788 798 789 - void BaseTheme::drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, 790 - const bool isSelected) const { 791 - const int itemWidth = renderer.getTextWidth(UI_10_FONT_ID, label); 792 - const int textX = rect.x + (rect.width - itemWidth) / 2; 799 + void BaseTheme::drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected, 800 + const char* secondaryLabel, const KeyboardKeyType keyType, 801 + const bool inactiveSelection) const { 793 802 if (isSelected) { 794 - renderer.drawText(UI_10_FONT_ID, textX - 6, rect.y, "["); 795 - renderer.drawText(UI_10_FONT_ID, textX + itemWidth, rect.y, "]"); 803 + if (inactiveSelection) { 804 + renderer.drawRect(rect.x, rect.y, rect.width, rect.height, 2, true); 805 + } else if (keyType == KeyboardKeyType::Disabled) { 806 + renderer.fillRectDither(rect.x, rect.y, rect.width, rect.height, Color::LightGray); 807 + } else { 808 + renderer.fillRect(rect.x, rect.y, rect.width, rect.height, true); 809 + } 810 + } else if (keyType == KeyboardKeyType::Shift || keyType == KeyboardKeyType::Mode || keyType == KeyboardKeyType::Del || 811 + keyType == KeyboardKeyType::Space || keyType == KeyboardKeyType::Ok || 812 + keyType == KeyboardKeyType::Disabled) { 813 + renderer.drawRect(rect.x, rect.y, rect.width, rect.height); 796 814 } 797 - renderer.drawText(UI_10_FONT_ID, textX, rect.y, label); 815 + 816 + const bool invert = isSelected && !inactiveSelection; 817 + 818 + if (keyType == KeyboardKeyType::Space) { 819 + const int lineHalfWidth = rect.width * 3 / 10; 820 + const int centerX = rect.x + rect.width / 2; 821 + const int lineY = rect.y + rect.height / 2 + 3; 822 + renderer.drawLine(centerX - lineHalfWidth, lineY, centerX + lineHalfWidth, lineY, 3, !invert); 823 + return; 824 + } 825 + 826 + if (keyType == KeyboardKeyType::Del) { 827 + const int centerX = rect.x + rect.width / 2; 828 + const int centerY = rect.y + rect.height / 2; 829 + const int arrowLen = rect.width / 4; 830 + const int arrowHead = arrowLen / 2; 831 + renderer.drawLine(centerX - arrowLen / 2, centerY, centerX + arrowLen / 2, centerY, 3, !invert); 832 + renderer.drawLine(centerX - arrowLen / 2, centerY, centerX - arrowLen / 2 + arrowHead, centerY - arrowHead, 3, 833 + !invert); 834 + renderer.drawLine(centerX - arrowLen / 2, centerY, centerX - arrowLen / 2 + arrowHead, centerY + arrowHead, 3, 835 + !invert); 836 + return; 837 + } 838 + 839 + const bool hasSecondary = secondaryLabel != nullptr && secondaryLabel[0] != '\0'; 840 + const int itemWidth = renderer.getTextWidth(UI_12_FONT_ID, label); 841 + const int textX = rect.x + (rect.width - itemWidth) / 2; 842 + const int textY = rect.y + (rect.height - renderer.getLineHeight(UI_12_FONT_ID)) / 2; 843 + 844 + if (hasSecondary) { 845 + const int secWidth = renderer.getTextWidth(SMALL_FONT_ID, secondaryLabel); 846 + renderer.drawText(SMALL_FONT_ID, rect.x + rect.width - secWidth - 1, rect.y, secondaryLabel, !invert); 847 + } 848 + 849 + renderer.drawText(UI_12_FONT_ID, textX, textY, label, !invert); 798 850 }
+21 -6
src/components/themes/BaseTheme.h
··· 60 60 int keyboardKeyWidth; 61 61 int keyboardKeyHeight; 62 62 int keyboardKeySpacing; 63 + int keyboardBottomKeyHeight; 64 + int keyboardBottomKeySpacing; 63 65 bool keyboardBottomAligned; 64 66 bool keyboardCenteredText; 67 + int keyboardVerticalOffset; 68 + int keyboardTextFieldWidthPercent; 69 + int keyboardWidthPercent; 65 70 }; 66 71 67 72 enum UIIcon { Folder, Text, Image, Book, File, Recent, Settings, Transfer, Library, Wifi, Hotspot }; 73 + 74 + enum class KeyboardKeyType { Normal, Shift, Mode, Space, Del, Ok, Disabled }; 68 75 69 76 // Default theme implementation (Classic Theme) 70 77 // Additional themes can inherit from this and override methods as needed ··· 96 103 .statusBarHorizontalMargin = 5, 97 104 .statusBarVerticalMargin = 19, 98 105 .keyboardKeyWidth = 22, 99 - .keyboardKeyHeight = 30, 100 - .keyboardKeySpacing = 10, 101 - .keyboardBottomAligned = false, 102 - .keyboardCenteredText = false}; 106 + .keyboardKeyHeight = 40, 107 + .keyboardKeySpacing = 0, 108 + .keyboardBottomKeyHeight = 35, 109 + .keyboardBottomKeySpacing = 5, 110 + .keyboardBottomAligned = true, 111 + .keyboardCenteredText = false, 112 + .keyboardVerticalOffset = -13, 113 + .keyboardTextFieldWidthPercent = 85, 114 + .keyboardWidthPercent = 90}; 103 115 } 104 116 105 117 class BaseTheme { ··· 139 151 const int pageCount, std::string title, const int paddingBottom = 0, 140 152 const int textYOffset = 0) const; 141 153 virtual void drawHelpText(const GfxRenderer& renderer, Rect rect, const char* label) const; 142 - virtual void drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth) const; 143 - virtual void drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected) const; 154 + virtual void drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth, bool cursorMode = false, 155 + int contentStartX = 0, int contentWidth = 0) const; 156 + virtual void drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected, 157 + const char* secondaryLabel = nullptr, KeyboardKeyType keyType = KeyboardKeyType::Normal, 158 + bool inactiveSelection = false) const; 144 159 virtual bool showsFileIcons() const { return false; } 145 160 146 161 // Shared constants and helpers for battery drawing (used by all themes)
+7 -2
src/components/themes/lyra/Lyra3CoversTheme.h
··· 34 34 .statusBarHorizontalMargin = 5, 35 35 .statusBarVerticalMargin = 19, 36 36 .keyboardKeyWidth = 31, 37 - .keyboardKeyHeight = 50, 37 + .keyboardKeyHeight = 40, 38 38 .keyboardKeySpacing = 0, 39 + .keyboardBottomKeyHeight = 35, 40 + .keyboardBottomKeySpacing = 5, 39 41 .keyboardBottomAligned = true, 40 - .keyboardCenteredText = true}; 42 + .keyboardCenteredText = false, 43 + .keyboardVerticalOffset = -7, 44 + .keyboardTextFieldWidthPercent = 85, 45 + .keyboardWidthPercent = 90}; 41 46 } 42 47 43 48 class Lyra3CoversTheme : public LyraTheme {
+54 -7
src/components/themes/lyra/LyraTheme.cpp
··· 578 578 renderer.displayBuffer(HalDisplay::FAST_REFRESH); 579 579 } 580 580 581 - void LyraTheme::drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth) const { 581 + void LyraTheme::drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth, bool cursorMode, 582 + int contentStartX, int contentWidth) const { 582 583 int lineY = rect.y + rect.height + renderer.getLineHeight(UI_12_FONT_ID) + LyraMetrics::values.verticalSpacing; 583 - int lineW = textWidth + hPaddingInSelection * 2; 584 - renderer.drawLine(rect.x + (rect.width - lineW) / 2, lineY, rect.x + (rect.width + lineW) / 2, lineY, 3); 584 + const int thickness = cursorMode ? 3 : 1; 585 + if (contentWidth > 0) { 586 + renderer.drawLine(rect.x + contentStartX, lineY, rect.x + contentStartX + contentWidth, lineY, thickness, true); 587 + } else { 588 + int lineW = textWidth + hPaddingInSelection * 2; 589 + renderer.drawLine(rect.x + (rect.width - lineW) / 2, lineY, rect.x + (rect.width + lineW) / 2, lineY, thickness, 590 + true); 591 + } 585 592 } 586 593 587 - void LyraTheme::drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, 588 - const bool isSelected) const { 594 + void LyraTheme::drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected, 595 + const char* secondaryLabel, const KeyboardKeyType keyType, 596 + const bool inactiveSelection) const { 589 597 if (isSelected) { 590 - renderer.fillRoundedRect(rect.x, rect.y, rect.width, rect.height, cornerRadius, Color::Black); 598 + if (inactiveSelection) { 599 + renderer.fillRoundedRect(rect.x, rect.y, rect.width, rect.height, cornerRadius, Color::LightGray); 600 + } else if (keyType == KeyboardKeyType::Disabled) { 601 + renderer.fillRoundedRect(rect.x, rect.y, rect.width, rect.height, cornerRadius, Color::LightGray); 602 + } else { 603 + renderer.fillRoundedRect(rect.x, rect.y, rect.width, rect.height, cornerRadius, Color::Black); 604 + } 605 + } else if (keyType == KeyboardKeyType::Shift || keyType == KeyboardKeyType::Mode || keyType == KeyboardKeyType::Del || 606 + keyType == KeyboardKeyType::Space || keyType == KeyboardKeyType::Ok || 607 + keyType == KeyboardKeyType::Disabled) { 608 + renderer.drawRoundedRect(rect.x, rect.y, rect.width, rect.height, 1, cornerRadius, true); 609 + } 610 + 611 + const bool invert = isSelected && !inactiveSelection; 612 + 613 + if (keyType == KeyboardKeyType::Space) { 614 + const int lineHalfWidth = rect.width * 3 / 10; 615 + const int centerX = rect.x + rect.width / 2; 616 + const int lineY = rect.y + rect.height / 2 + 3; 617 + renderer.drawLine(centerX - lineHalfWidth, lineY, centerX + lineHalfWidth, lineY, 3, !invert); 618 + return; 591 619 } 592 620 621 + if (keyType == KeyboardKeyType::Del) { 622 + const int centerX = rect.x + rect.width / 2; 623 + const int centerY = rect.y + rect.height / 2; 624 + const int arrowLen = rect.width / 4; 625 + const int arrowHead = arrowLen / 2; 626 + renderer.drawLine(centerX - arrowLen / 2, centerY, centerX + arrowLen / 2, centerY, 3, !invert); 627 + renderer.drawLine(centerX - arrowLen / 2, centerY, centerX - arrowLen / 2 + arrowHead, centerY - arrowHead, 3, 628 + !invert); 629 + renderer.drawLine(centerX - arrowLen / 2, centerY, centerX - arrowLen / 2 + arrowHead, centerY + arrowHead, 3, 630 + !invert); 631 + return; 632 + } 633 + 634 + const bool hasSecondary = secondaryLabel != nullptr && secondaryLabel[0] != '\0'; 593 635 const int textWidth = renderer.getTextWidth(UI_12_FONT_ID, label); 594 636 const int textX = rect.x + (rect.width - textWidth) / 2; 595 637 const int textY = rect.y + (rect.height - renderer.getLineHeight(UI_12_FONT_ID)) / 2; 596 - renderer.drawText(UI_12_FONT_ID, textX, textY, label, !isSelected); 638 + renderer.drawText(UI_12_FONT_ID, textX, textY, label, !invert); 639 + 640 + if (hasSecondary) { 641 + const int secWidth = renderer.getTextWidth(SMALL_FONT_ID, secondaryLabel); 642 + renderer.drawText(SMALL_FONT_ID, rect.x + rect.width - secWidth - 1, rect.y, secondaryLabel, !invert); 643 + } 597 644 }
+12 -4
src/components/themes/lyra/LyraTheme.h
··· 32 32 .statusBarHorizontalMargin = 5, 33 33 .statusBarVerticalMargin = 19, 34 34 .keyboardKeyWidth = 31, 35 - .keyboardKeyHeight = 50, 35 + .keyboardKeyHeight = 40, 36 36 .keyboardKeySpacing = 0, 37 + .keyboardBottomKeyHeight = 35, 38 + .keyboardBottomKeySpacing = 5, 37 39 .keyboardBottomAligned = true, 38 - .keyboardCenteredText = true}; 40 + .keyboardCenteredText = false, 41 + .keyboardVerticalOffset = -7, 42 + .keyboardTextFieldWidthPercent = 85, 43 + .keyboardWidthPercent = 90}; 39 44 } 40 45 41 46 class LyraTheme : public BaseTheme { ··· 66 71 void drawEmptyRecents(const GfxRenderer& renderer, const Rect rect) const; 67 72 Rect drawPopup(const GfxRenderer& renderer, const char* message) const override; 68 73 void fillPopupProgress(const GfxRenderer& renderer, const Rect& layout, const int progress) const override; 69 - void drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth) const override; 70 - void drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected) const override; 74 + void drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth, bool cursorMode = false, 75 + int contentStartX = 0, int contentWidth = 0) const override; 76 + void drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected, 77 + const char* secondaryLabel = nullptr, KeyboardKeyType keyType = KeyboardKeyType::Normal, 78 + bool inactiveSelection = false) const override; 71 79 bool showsFileIcons() const override { return true; } 72 80 };