From 6f8703842ce343fb4fd4708c6f272df5be53f146 Mon Sep 17 00:00:00 2001 From: 3urobeat Date: Tue, 8 Sep 2026 22:25:39 +0200 Subject: [PATCH] feat: Add PIN input keyboard and use it on lockscreen --- examples/companion_radio/NodePrefs.h | 3 +- .../companion_radio/ui-new/KeyboardWidget.h | 122 +++++++++++++++++- .../companion_radio/ui-new/SettingsScreen.h | 6 +- examples/companion_radio/ui-new/UITask.cpp | 8 +- examples/companion_radio/ui-new/UITask.h | 1 + examples/companion_radio/ui-new/icons.h | 5 + 6 files changed, 139 insertions(+), 6 deletions(-) diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index d426b8ef..c4ea4061 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -214,7 +214,8 @@ struct NodePrefs { // persisted to file static const uint8_t PAGE_ORDER_MAGIC = 0xA5; // On-screen keyboard layout, shared across every text-entry screen (Settings > // Keyboard). 0=ABC grid, alphabetical order (default), 1=T9 multi-tap - // (phone-keypad groups, cycled with repeated Enter presses — see KeyboardWidget.h). + // (phone-keypad groups, cycled with repeated Enter presses — see KeyboardWidget.h), + // 3=PIN keyboard. uint8_t keyboard_type; // Additional (non-Latin) keyboard alphabet, orthogonal to keyboard_type above // — either layout style (ABC grid or T9) can show any alphabet's characters. diff --git a/examples/companion_radio/ui-new/KeyboardWidget.h b/examples/companion_radio/ui-new/KeyboardWidget.h index 1b907ba3..34f70e4e 100644 --- a/examples/companion_radio/ui-new/KeyboardWidget.h +++ b/examples/companion_radio/ui-new/KeyboardWidget.h @@ -43,6 +43,17 @@ static const char* const KB_T9_GROUPS[KB_PAGES][9] = { { "@#&", "*()", "-_+", "=/\\", ":;'\"", "<>[]", "{}|~", "^$%`", ",." }, // page 1 — symbols }; +// PIN input layout: Classic number pad +static const int KB_PIN_ROWS = 4; +static const int KB_PIN_COLS = 3; +static const int KB_PIN_SPECIAL = 3; // backspace, abc kb switch & submit +static const char KB_PIN_DIGITS[KB_PIN_ROWS][KB_PIN_COLS] = { + {'1','2','3'}, + {'4','5','6'}, + {'7','8','9'}, + {' ','0',' '}, +}; + // Non-Latin keyboard scripts. NodePrefs::keyboard_main_alphabet/ // keyboard_alt_alphabet (Settings > Keyboard's Main/Additional rows) pick // which script occupies page 0 (the keyboard's default/opening page) and @@ -325,6 +336,7 @@ struct KeyboardWidget { // to ABC and mainScript()/altScript() default to Latin-only. NodePrefs* prefs = nullptr; bool isT9() const { return prefs && prefs->keyboard_type == 1; } + bool isPin() const { return prefs && prefs->keyboard_type == 2; } // Which script occupies page 0 (the keyboard's default/opening page) and // which occupies page 1 (reached by the #@/abc cycle key) -- Settings > // Keyboard's Main/Additional rows. Additional equal to Main collapses to no @@ -346,8 +358,8 @@ struct KeyboardWidget { // lowercase regardless of Shift. bool t9_caps = false; - int gridRows() const { return isT9() ? KB_T9_ROWS : KB_ROWS_CHAR; } - int gridCols() const { return isT9() ? KB_T9_COLS : KB_COLS_CHAR; } + int gridRows() const { return isPin() ? KB_PIN_ROWS : (isT9() ? KB_T9_ROWS : KB_ROWS_CHAR); } + int gridCols() const { return isPin() ? KB_PIN_COLS : (isT9() ? KB_T9_COLS : KB_COLS_CHAR); } // ── Page model ──────────────────────────────────────────────────────────── // Logical page order: 0 = mainScript(), [1 = altScript(), if it differs], @@ -438,6 +450,12 @@ struct KeyboardWidget { addPlaceholder("{time}"); } + // Open keyboard in PIN mode + void beginPin(const char* initial = "", int max = KB_MAX_LEN) { + if (prefs) prefs->keyboard_type = 2; // Force number input + begin(initial, max); + } + // Insert one UTF-8 codepoint (a grid cell's own glyph, or a picked accent // variant) at cursor_pos, applying Shift/caps-lock the same way every cell // commit does. Shared by the plain-Latin-cell commit below and the accent @@ -628,6 +646,41 @@ struct KeyboardWidget { return 50; } + // PIN mode + if (isPin()) { + const int s = miniIconScale(display); + for (int r = 0; r < rows; r++) { + int y = chars_y + r * cell_h; + for (int c = 0; c < cols; c++) { + bool sel = (row == r && col == c); + int cx = c * cell_w; + display.drawSelectionRow(cx, y - 1, cell_w - 1, cell_h, sel); + char ch = KB_PIN_DIGITS[r][c]; + if (ch == ' ') continue; // blank cells beside 0 + char ch_buf[2]; + ch_buf[0] = ch; + ch_buf[1] = '\0'; + int tw = display.getTextWidth(ch_buf); + display.setCursor(cx + (cell_w - tw) / 2, y); + display.print(ch_buf); + } + } + // special row with backspace, abc kb switch & submit + const int psw = display.width() / KB_PIN_SPECIAL; + const int icy = spec_y + (cell_h - lh) / 2; + for (int i = 0; i < KB_PIN_SPECIAL; i++) { + bool sel = (row == rows && col == i); + int sx = i * psw; + display.drawSelectionRow(sx, spec_y - 1, psw - 1, cell_h, sel); + const MiniIcon& ic = (i == 0) ? ICON_BACKSPACE + : (i == 1) ? ICON_KEYBOARD + : ICON_CHECK; + int ix = sx + (psw - ic.w * s) / 2; + miniIconDraw(display, ix, icy, ic); + } + return 50; + } + // Compact mode (Settings > Keyboard's "Ext. KB" row): an external-keyboard // typist never looks at the letter grid or special-row icons, so skip // drawing them entirely -- no status line either, since nothing it could @@ -791,7 +844,7 @@ struct KeyboardWidget { // just fine -- so this only checks that no other exclusive input mode // (popup/cursor-move) is already in progress, same as inPlainGridState(). bool openAccentFor(char base) { - if (!isVisible() || _ph_menu.active || cursor_mode || accent_active) return false; + if (!isVisible() || isPin() || _ph_menu.active || cursor_mode || accent_active) return false; int gi = findAccentGroup(base); if (gi < 0) return false; accent_active = true; @@ -908,6 +961,69 @@ struct KeyboardWidget { return NONE; } + // PIN mode: a dedicated numeric keypad + if (isPin()) { + const int rows = gridRows(); + const int cols = gridCols(); + if (c == KEY_CONTEXT_MENU) { + if (row == rows && col == 0) { // Backspace + len = 0; buf[0] = '\0'; + cursor_pos = 0; + t9_cell = -1; + } + return NONE; + } + if (c == KEY_UP) { // Navigation + row = (row > 0) ? row - 1 : rows; + return NONE; + } + if (c == KEY_DOWN) { + row = (row < rows) ? row + 1 : 0; + return NONE; + } + if (c == KEY_LEFT) { + int max_col = (row == rows) ? KB_PIN_SPECIAL - 1 : cols - 1; + col = (col > 0) ? col - 1 : max_col; + return NONE; + } + if (c == KEY_RIGHT) { + int max_col = (row == rows) ? KB_PIN_SPECIAL - 1 : cols - 1; + col = (col < max_col) ? col + 1 : 0; + return NONE; + } + if (c == KEY_ENTER) { + if (row < rows) { + char d = KB_PIN_DIGITS[row][col]; + if (d == ' ') return NONE; // ignore blanks + if (len < max_len) { // Digit + memmove(buf + cursor_pos + 1, buf + cursor_pos, len - cursor_pos); + buf[cursor_pos] = d; + len++; cursor_pos++; + buf[len] = '\0'; + } + return NONE; + } + // special row + if (col == 0) { + t9_cell = -1; + if (cursor_pos > 0) { + int n = kbUtf8LastCharBytes(buf, cursor_pos); + memmove(buf + cursor_pos - n, buf + cursor_pos, len - cursor_pos); + len -= n; cursor_pos -= n; + buf[len] = '\0'; + } + } else if (col == 1) { + if (prefs) prefs->keyboard_type = 0; // ABC switch + page = 0; + t9_cell = -1; + } else { + return DONE; // Submit + } + return NONE; + } + return NONE; + } + const int rows = gridRows(); const int cols = gridCols(); diff --git a/examples/companion_radio/ui-new/SettingsScreen.h b/examples/companion_radio/ui-new/SettingsScreen.h index 3b74b680..b855d480 100644 --- a/examples/companion_radio/ui-new/SettingsScreen.h +++ b/examples/companion_radio/ui-new/SettingsScreen.h @@ -721,6 +721,7 @@ class SettingsScreen : public UIScreen { int _edit_slot = -1; // -1 = not editing, 0..9 = slot being edited bool _edit_name = false; // editing DEVICE_NAME via the keyboard bool _edit_lock_pass = false; // editing the lock-screen password via the keyboard + uint8_t _lock_pass_saved_type = 0; // remember keyboard set to restore setting after pin entry KeyboardWidget* _kb; // Scope list management (SCOPE_NAME row -> a full-screen add/rename/ @@ -877,8 +878,10 @@ public: _dirty = true; // savePrefsIfDirty persists new password } _edit_lock_pass = false; + if (p) p->keyboard_type = _lock_pass_saved_type; // restore user setting } else if (res == KeyboardWidget::CANCELLED) { _edit_lock_pass = false; + if (p) p->keyboard_type = _lock_pass_saved_type; // restore user setting } return true; } @@ -1140,7 +1143,8 @@ public: _dirty = true; } else { _edit_lock_pass = true; - _kb->begin("", (int)sizeof(p->lock_screen_password) - 1); + _lock_pass_saved_type = p ? p->keyboard_type : 0; // remember setting to restore after + _kb->beginPin("", (int)sizeof(p->lock_screen_password) - 1); _kb->clearPlaceholders(); // a password is literal, not a template message } return true; diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 6dc9a293..3ab78ac4 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -2260,7 +2260,8 @@ bool UITask::checkNodeLockPassword(const char* entered) const { void UITask::beginUnlockPrompt() { _unlock_kb = true; // Track that keyboard is visible and is waiting for input int max_len = _node_prefs ? (int)sizeof(_node_prefs->lock_screen_password) - 1 : 32; - _kb.begin("", max_len); + _lock_pin_restore_kb_type = _node_prefs ? _node_prefs->keyboard_type : 0; + _kb.beginPin("", max_len); _kb.clearPlaceholders(); _lock_wake_until = millis() + 5000; // keep the display on while typing _next_refresh = 0; @@ -2268,6 +2269,10 @@ void UITask::beginUnlockPrompt() { void UITask::cancelUnlockPrompt() { _unlock_kb = false; + if (_node_prefs) _node_prefs->keyboard_type = _lock_pin_restore_kb_type; // restore user setting + _kb.buf[0] = '\0'; // clear input + _kb.len = 0; + _kb.cursor_pos = 0; _next_refresh = 100; } @@ -2278,6 +2283,7 @@ void UITask::handleUnlockKey(char c) { // Match: Unlock _unlock_kb = false; _locked = false; + if (_node_prefs) _node_prefs->keyboard_type = _lock_pin_restore_kb_type; // restore user setting if (_display && !_display->isOn()) _display->turnOn(); uint32_t aoff = autoOffMillis(); if (aoff > 0) _auto_off = millis() + aoff; diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 964842fe..b59ea80e 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -60,6 +60,7 @@ class UITask : public AbstractUITask { bool _lock_seq_used; // true = suppress next back_btn CLICK (post-sequence release) // True while the lock screen shows the on-screen keyboard and waits for submission bool _unlock_kb = false; + uint8_t _lock_pin_restore_kb_type = 0; // remember keyboard set to restore setting after pin entry char _alert[80]; char _notif_mel_buf[220]; // persistent RTTTL buffer for custom notification melodies // Persistent RTTTL buffer for the bot !buzz command (see botBuzz()) -- sized diff --git a/examples/companion_radio/ui-new/icons.h b/examples/companion_radio/ui-new/icons.h index 8a05e853..89813de4 100644 --- a/examples/companion_radio/ui-new/icons.h +++ b/examples/companion_radio/ui-new/icons.h @@ -451,6 +451,11 @@ MINI_ICON(ICON_MAP_TARGET, 5, // ⚑ flag on a pole — the active Locator/N packRow("#....")); // Keyboard special-key glyphs. +MINI_ICON(ICON_KEYBOARD, 7, // PIN keyboard to ABC keyboard switch icon + packRow("#.#.#.#"), + packRow("#######"), + packRow("#.#.#.#"), + packRow("#######")); MINI_ICON(ICON_SHIFT, 7, // ⇧ caps packRow("...#..."), packRow("..###.."),