feat(ui): compact on-screen keyboard toggle for external keyboards

Add Settings > Keyboard "Ext. KB" row (boards with a CardKB-capable I2C
bus only): switching it to Compact hides the letter grid and special-row
icons in favour of a one-line status (script/page, caps) plus a Fn-shortcut
reminder, since an external-keyboard typist never looks at the on-screen
grid. Accent/placeholder popups still render as before. Off by default.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-07-24 15:51:51 +02:00
co-authored by Claude Sonnet 5
parent ad4668242b
commit a0b1c80c85
5 changed files with 138 additions and 76 deletions
+7
View File
@@ -551,6 +551,12 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
rd(&_prefs.gpio4_mode, sizeof(_prefs.gpio4_mode)); rd(&_prefs.gpio4_mode, sizeof(_prefs.gpio4_mode));
if (_prefs.gpio4_mode > 3) _prefs.gpio4_mode = 0; if (_prefs.gpio4_mode > 3) _prefs.gpio4_mode = 0;
// → 0xC0DE0023: append the external-keyboard compact-display toggle at the
// tail. A pre-0x23 file has no byte here; clamp to 0 (full grid, unchanged
// behaviour for upgraders).
rd(&_prefs.keyboard_cardkb_compact, sizeof(_prefs.keyboard_cardkb_compact));
if (_prefs.keyboard_cardkb_compact > 1) _prefs.keyboard_cardkb_compact = 0;
// Schema sentinel: bumped on layout changes. Mismatch means an older file // Schema sentinel: bumped on layout changes. Mismatch means an older file
// (or a different schema); rd() already zero-inits any fields not present, // (or a different schema); rd() already zero-inits any fields not present,
// so we just log it — next savePrefs writes the current sentinel. // so we just log it — next savePrefs writes the current sentinel.
@@ -762,6 +768,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
file.write((uint8_t *)&_prefs.gpio2_mode, sizeof(_prefs.gpio2_mode)); file.write((uint8_t *)&_prefs.gpio2_mode, sizeof(_prefs.gpio2_mode));
file.write((uint8_t *)&_prefs.gpio3_mode, sizeof(_prefs.gpio3_mode)); file.write((uint8_t *)&_prefs.gpio3_mode, sizeof(_prefs.gpio3_mode));
file.write((uint8_t *)&_prefs.gpio4_mode, sizeof(_prefs.gpio4_mode)); file.write((uint8_t *)&_prefs.gpio4_mode, sizeof(_prefs.gpio4_mode));
file.write((uint8_t *)&_prefs.keyboard_cardkb_compact, sizeof(_prefs.keyboard_cardkb_compact));
// Tail sentinel — must be last. See NodePrefs::SCHEMA_SENTINEL. Its write is // Tail sentinel — must be last. See NodePrefs::SCHEMA_SENTINEL. Its write is
// the one we check: once the flash fills, writes return 0, so a good // the one we check: once the flash fills, writes return 0, so a good
+15 -2
View File
@@ -422,6 +422,17 @@ struct NodePrefs { // persisted to file
uint8_t gpio3_mode; uint8_t gpio3_mode;
uint8_t gpio4_mode; uint8_t gpio4_mode;
// Settings > Keyboard's "Ext. KB" row (boards with a second I2C bus for an
// optional CardKB, see ENV_PIN_SDA/ENV_PIN_SCL, only). When on, the
// on-screen keyboard skips drawing its full letter grid + special-row icons
// -- an external-keyboard typist never looks at them -- and shows a compact
// one-line status (current script/page, caps) instead; the accent and
// placeholder popups still render on top exactly as before (see
// KeyboardWidget::render()). Manual toggle rather than auto-detected, so it
// stays put even if the module is briefly unplugged. Default 0 (full grid,
// unchanged behaviour) on upgrade.
uint8_t keyboard_cardkb_compact;
// Single source of truth for the live-share option tables (shared by the Map // Single source of truth for the live-share option tables (shared by the Map
// UI labels and the auto-send engine in UITask). // UI labels and the auto-send engine in UITask).
static const uint8_t LOC_SHARE_MOVE_COUNT = 4; static const uint8_t LOC_SHARE_MOVE_COUNT = 4;
@@ -484,7 +495,7 @@ struct NodePrefs { // persisted to file
// adding/removing/reordering fields in DataStore::savePrefs/loadPrefsInt so // adding/removing/reordering fields in DataStore::savePrefs/loadPrefsInt so
// older saves are detected on load and skipped (zero-init defaults kept). // older saves are detected on load and skipped (zero-init defaults kept).
// High 24 bits identify the file format; low byte is the schema revision. // High 24 bits identify the file format; low byte is the schema revision.
static const uint32_t SCHEMA_SENTINEL = 0xC0DE0022; static const uint32_t SCHEMA_SENTINEL = 0xC0DE0023;
// Bit-index for each home page. Used by page_order (entries store bit+1) and // Bit-index for each home page. Used by page_order (entries store bit+1) and
// by home_pages_mask. Single source of truth — both HomeScreen::pageBit/bitToPage // by home_pages_mask. Single source of truth — both HomeScreen::pageBit/bitToPage
@@ -581,7 +592,9 @@ struct NodePrefs { // persisted to file
// 3. clamp it on load (an upgrader's file lacks it → stray bytes) // 3. clamp it on load (an upgrader's file lacks it → stray bytes)
// 4. bump SCHEMA_SENTINEL's low byte // 4. bump SCHEMA_SENTINEL's low byte
// (Padding can also shift sizeof; a "false" trip just means re-check + rebump.) // (Padding can also shift sizeof; a "false" trip just means re-check + rebump.)
// keyboard_main_alphabet (added in the prior bump) landed in existing tail // keyboard_cardkb_compact (0xC0DE0023) also landed in existing tail padding --
// confirmed via a real build -- leaving sizeof unchanged at 2720.
// keyboard_main_alphabet (added in an earlier bump) landed in existing tail
// padding -- confirmed via a real build's sizeof() -- so that bump left the // padding -- confirmed via a real build's sizeof() -- so that bump left the
// size unchanged. bot_actions_dm/ch/room and gpio1..4_mode (the last two // size unchanged. bot_actions_dm/ch/room and gpio1..4_mode (the last two
// bumps, 7 more uint8_t total) added 8 bytes, not 7 -- one byte of tail // bumps, 7 more uint8_t total) added 8 bytes, not 7 -- one byte of tail
@@ -526,6 +526,30 @@ struct KeyboardWidget {
return 50; 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 and show a one-line status (current script/page, caps)
// instead. row/col/page keep updating exactly as before even while this
// is on (arrows and Fn+letter both still work; the accent popup below
// still anchors on `row`), so nothing breaks if physical buttons get used
// meanwhile -- it just won't be visible which cell is selected.
if (prefs && prefs->keyboard_cardkb_compact) {
const int hh = lh + 2;
display.setColor(DisplayDriver::LIGHT);
display.fillRect(0, chars_y, display.width(), hh);
display.setColor(DisplayDriver::DARK);
const char* script_name = pageIsSymbols(page) ? "Symbols"
: (scriptAt(page) == NodePrefs::KB_ALPHABET_CYRILLIC) ? "Cyrillic"
: (scriptAt(page) == NodePrefs::KB_ALPHABET_GREEK) ? "Greek"
: "Latin";
char status[32];
if (pageIsSymbols(page)) snprintf(status, sizeof(status), "%s%s", script_name, caps ? " CAPS" : "");
else snprintf(status, sizeof(status), "%s %s%s", script_name, isT9() ? "T9" : "ABC", caps ? " CAPS" : "");
display.drawTextCentered(display.width() / 2, chars_y + 1, status);
display.setColor(DisplayDriver::LIGHT);
display.drawTextCentered(display.width() / 2, chars_y + hh + 2, "Fn+Tab menu");
display.drawTextCentered(display.width() / 2, chars_y + hh + 2 + lh, "Fn+letter accent");
} else {
// character grid // character grid
if (isT9()) { if (isT9()) {
for (int r = 0; r < rows; r++) { for (int r = 0; r < rows; r++) {
@@ -602,6 +626,7 @@ struct KeyboardWidget {
} }
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
} }
}
// Accent popup: floats over the still-visible grid (same idea as the // Accent popup: floats over the still-visible grid (same idea as the
// placeholder overlay just below), anchored on the held letter's own row // placeholder overlay just below), anchored on the held letter's own row
@@ -73,6 +73,9 @@ class SettingsScreen : public UIScreen {
KEYBOARD_TYPE, KEYBOARD_TYPE,
KEYBOARD_MAIN_ALPHABET, KEYBOARD_MAIN_ALPHABET,
KEYBOARD_ALPHABET, KEYBOARD_ALPHABET,
#if defined(ENV_PIN_SDA) && defined(ENV_PIN_SCL)
KEYBOARD_CARDKB_COMPACT,
#endif
// Contacts section // Contacts section
SECTION_CONTACTS, DM_FILTER, CH_FILTER, ROOM_FILTER, SECTION_CONTACTS, DM_FILTER, CH_FILTER, ROOM_FILTER,
// Messages section // Messages section
@@ -574,6 +577,12 @@ class SettingsScreen : public UIScreen {
display.print("Additional"); display.print("Additional");
display.setCursor(valCol(display), y); display.setCursor(valCol(display), y);
display.print(NodePrefs::keyboardAlphabetLabel(p ? p->keyboard_alt_alphabet : 0)); display.print(NodePrefs::keyboardAlphabetLabel(p ? p->keyboard_alt_alphabet : 0));
#if defined(ENV_PIN_SDA) && defined(ENV_PIN_SCL)
} else if (item == KEYBOARD_CARDKB_COMPACT) {
display.print("Ext. KB");
display.setCursor(valCol(display), y);
display.print((p && p->keyboard_cardkb_compact) ? "Compact" : "Full");
#endif
} else if (item == BATT_DISPLAY) { } else if (item == BATT_DISPLAY) {
display.print("BattDisp"); display.print("BattDisp");
display.setCursor(valCol(display), y); display.setCursor(valCol(display), y);
@@ -945,6 +954,13 @@ public:
_dirty = true; _dirty = true;
return true; return true;
} }
#if defined(ENV_PIN_SDA) && defined(ENV_PIN_SCL)
if (_selected == KEYBOARD_CARDKB_COMPACT && p && (left || right || enter)) {
p->keyboard_cardkb_compact ^= 1;
_dirty = true;
return true;
}
#endif
if (_selected == DM_RESEND && p) { if (_selected == DM_RESEND && p) {
int n = p->dm_resend_count; int n = p->dm_resend_count;
if (right || enter) n = (n + 1) % 6; // 0..5, wraps if (right || enter) n = (n + 1) % 6; // 0..5, wraps
+1
View File
@@ -7,6 +7,7 @@
- **Bot Trigger fields accept multiple phrases.** Pack several trigger words into one Trigger field, comma-separated (`hi,hello there,yo`) — matching any one of them fires the reply, same as before for a single phrase. - **Bot Trigger fields accept multiple phrases.** Pack several trigger words into one Trigger field, comma-separated (`hi,hello there,yo`) — matching any one of them fires the reply, same as before for a single phrase.
- **Remote Bot Actions gain `!gpio1`..`!gpio4`** (Wio Tracker L1 only — 4 otherwise-unused pins). Each pin is independently set to Off/Input/Output (GPIO1/GPIO2 also offer Analog) from a new **Tools GPIO** screen; `!gpio1 on`/`!gpio1 off` drives an Output pin remotely, a bare `!gpio1` reports the current mode and reading (including a millivolt value in Analog mode). Gated by the same per-target Actions toggle as `!buzz`/`!gps`/`!advert`. - **Remote Bot Actions gain `!gpio1`..`!gpio4`** (Wio Tracker L1 only — 4 otherwise-unused pins). Each pin is independently set to Off/Input/Output (GPIO1/GPIO2 also offer Analog) from a new **Tools GPIO** screen; `!gpio1 on`/`!gpio1 off` drives an Output pin remotely, a bare `!gpio1` reports the current mode and reading (including a millivolt value in Analog mode). Gated by the same per-target Actions toggle as `!buzz`/`!gps`/`!advert`.
- **Optional CardKB support (Wio Tracker L1, Grove I2C) — full keyboard-only navigation.** Plug an M5Stack CardKB into the Grove connector and it's auto-detected at boot — no setting to flip. Typing goes straight into the message/name field instead of navigating the on-screen keyboard grid; arrows and Esc work as expected everywhere else too (including inside the placeholder and accent popups). Enter acts like the physical centre button (advances the on-screen grid selection) rather than submitting, so **Fn+Enter** sends the message/confirms the field from anywhere, **Fn+`<letter>`** opens that letter's accent popup directly (no arrow-hunting needed — handy for Polish/Czech/etc. diacritics), and **Fn+Tab** opens the Hold-Enter equivalent (Shift-lock, clear field) for whatever's selected; plain **Tab** still opens the Hold-Enter context menu everywhere else (message reply/navigate, Bot/Admin/Repeater menus, …). - **Optional CardKB support (Wio Tracker L1, Grove I2C) — full keyboard-only navigation.** Plug an M5Stack CardKB into the Grove connector and it's auto-detected at boot — no setting to flip. Typing goes straight into the message/name field instead of navigating the on-screen keyboard grid; arrows and Esc work as expected everywhere else too (including inside the placeholder and accent popups). Enter acts like the physical centre button (advances the on-screen grid selection) rather than submitting, so **Fn+Enter** sends the message/confirms the field from anywhere, **Fn+`<letter>`** opens that letter's accent popup directly (no arrow-hunting needed — handy for Polish/Czech/etc. diacritics), and **Fn+Tab** opens the Hold-Enter equivalent (Shift-lock, clear field) for whatever's selected; plain **Tab** still opens the Hold-Enter context menu everywhere else (message reply/navigate, Bot/Admin/Repeater menus, …).
- **Settings Keyboard gets an "Ext. KB" row** (boards that support CardKB, above). Switching it to **Compact** hides the on-screen letter grid and special-row icons — a CardKB typist never looks at them — replacing them with a one-line status (current script/page, caps) plus a reminder of the Fn shortcuts; the accent and placeholder popups still show normally. Off (**Full**) by default.
### Fixes ### Fixes