mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-26 14:58:12 +00:00
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:
@@ -551,6 +551,12 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
|
||||
rd(&_prefs.gpio4_mode, sizeof(_prefs.gpio4_mode));
|
||||
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
|
||||
// (or a different schema); rd() already zero-inits any fields not present,
|
||||
// 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.gpio3_mode, sizeof(_prefs.gpio3_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
|
||||
// the one we check: once the flash fills, writes return 0, so a good
|
||||
|
||||
@@ -422,6 +422,17 @@ struct NodePrefs { // persisted to file
|
||||
uint8_t gpio3_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
|
||||
// UI labels and the auto-send engine in UITask).
|
||||
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
|
||||
// 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.
|
||||
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
|
||||
// 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)
|
||||
// 4. bump SCHEMA_SENTINEL's low byte
|
||||
// (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
|
||||
// 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
|
||||
|
||||
@@ -526,81 +526,106 @@ struct KeyboardWidget {
|
||||
return 50;
|
||||
}
|
||||
|
||||
// character grid
|
||||
if (isT9()) {
|
||||
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 cell = r * cols + c;
|
||||
// Label the cell "<digit><group>" so it reads like a phone keypad. The
|
||||
// digit is what the multi-tap cycle lands on after the letters (see
|
||||
// handleInput: '1'+cell). No separator space — the widest group
|
||||
// (Cyrillic "деёжз"/"шщъыь", 5 letters x up to 2 UTF-8 bytes) + digit
|
||||
// still fits with room to spare.
|
||||
char group_shown[12];
|
||||
kbApplyCapsUtf8(t9GroupStr(cell), caps, group_shown, sizeof(group_shown));
|
||||
char label[14];
|
||||
snprintf(label, sizeof(label), "%c%s", (char)('1' + cell), group_shown);
|
||||
int cx = c * cell_w;
|
||||
display.drawSelectionRow(cx, y - 1, cell_w - 1, cell_h, sel);
|
||||
int tw = display.getTextWidth(label);
|
||||
display.setCursor(cx + (cell_w - tw) / 2, y);
|
||||
display.print(label);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
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);
|
||||
char ch_buf[3];
|
||||
kbApplyCapsUtf8(cellStr(r, c), caps, ch_buf, sizeof(ch_buf));
|
||||
if (ch_buf[0] == ' ' && ch_buf[1] == '\0') ch_buf[0] = '_';
|
||||
int cx = c * cell_w;
|
||||
display.drawSelectionRow(cx, y - 1, cell_w - 1, cell_h, sel);
|
||||
int tw = display.getTextWidth(ch_buf);
|
||||
display.setCursor(cx + (cell_w - tw) / 2, y);
|
||||
display.print(ch_buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// special row: caps ⇧ · space ⎵ · delete ⌫ · placeholders {} (text) · OK ✓
|
||||
const int s = miniIconScale(display);
|
||||
const int icy = spec_y + (cell_h - lh) / 2; // centre icons within the cell
|
||||
for (int i = 0; i < KB_SPECIAL; i++) {
|
||||
bool sel = (row == rows && col == i);
|
||||
bool active = (i == 0 && caps);
|
||||
int sx = i * spec_w;
|
||||
display.drawSelectionRow(sx, spec_y - 1, spec_w - 1, cell_h, sel || active);
|
||||
if (i == 3 || i == 4) { // text keys: {} picker, page toggle
|
||||
// Shows what pressing it lands on next, same "reads as the
|
||||
// destination" convention as the original 2-page abc<->#@ toggle,
|
||||
// generalized to however many pages are in the cycle right now.
|
||||
const char* lbl;
|
||||
if (i == 3) {
|
||||
lbl = "{}";
|
||||
} else {
|
||||
int next = (page + 1) % totalPages();
|
||||
lbl = pageIsSymbols(next) ? "#@" : scriptHint(scriptAt(next));
|
||||
}
|
||||
int tw = display.getTextWidth(lbl);
|
||||
display.setCursor(sx + (spec_w - tw) / 2, spec_y);
|
||||
display.print(lbl);
|
||||
} else if (i == 1) { // space ⎵ — two halves side by side
|
||||
int icw = (ICON_SPACE_L.w + ICON_SPACE_R.w) * s;
|
||||
int ix = sx + (spec_w - icw) / 2;
|
||||
miniIconDraw(display, ix, icy, ICON_SPACE_L);
|
||||
miniIconDraw(display, ix + ICON_SPACE_L.w * s, icy, ICON_SPACE_R);
|
||||
} else {
|
||||
const MiniIcon& ic = (i == 0) ? ICON_SHIFT
|
||||
: (i == 2) ? ICON_BACKSPACE
|
||||
: ICON_CHECK; // i == 5 → OK
|
||||
int ix = sx + (spec_w - ic.w * s) / 2;
|
||||
miniIconDraw(display, ix, icy, ic);
|
||||
}
|
||||
// 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
|
||||
if (isT9()) {
|
||||
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 cell = r * cols + c;
|
||||
// Label the cell "<digit><group>" so it reads like a phone keypad. The
|
||||
// digit is what the multi-tap cycle lands on after the letters (see
|
||||
// handleInput: '1'+cell). No separator space — the widest group
|
||||
// (Cyrillic "деёжз"/"шщъыь", 5 letters x up to 2 UTF-8 bytes) + digit
|
||||
// still fits with room to spare.
|
||||
char group_shown[12];
|
||||
kbApplyCapsUtf8(t9GroupStr(cell), caps, group_shown, sizeof(group_shown));
|
||||
char label[14];
|
||||
snprintf(label, sizeof(label), "%c%s", (char)('1' + cell), group_shown);
|
||||
int cx = c * cell_w;
|
||||
display.drawSelectionRow(cx, y - 1, cell_w - 1, cell_h, sel);
|
||||
int tw = display.getTextWidth(label);
|
||||
display.setCursor(cx + (cell_w - tw) / 2, y);
|
||||
display.print(label);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
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);
|
||||
char ch_buf[3];
|
||||
kbApplyCapsUtf8(cellStr(r, c), caps, ch_buf, sizeof(ch_buf));
|
||||
if (ch_buf[0] == ' ' && ch_buf[1] == '\0') ch_buf[0] = '_';
|
||||
int cx = c * cell_w;
|
||||
display.drawSelectionRow(cx, y - 1, cell_w - 1, cell_h, sel);
|
||||
int tw = display.getTextWidth(ch_buf);
|
||||
display.setCursor(cx + (cell_w - tw) / 2, y);
|
||||
display.print(ch_buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// special row: caps ⇧ · space ⎵ · delete ⌫ · placeholders {} (text) · OK ✓
|
||||
const int s = miniIconScale(display);
|
||||
const int icy = spec_y + (cell_h - lh) / 2; // centre icons within the cell
|
||||
for (int i = 0; i < KB_SPECIAL; i++) {
|
||||
bool sel = (row == rows && col == i);
|
||||
bool active = (i == 0 && caps);
|
||||
int sx = i * spec_w;
|
||||
display.drawSelectionRow(sx, spec_y - 1, spec_w - 1, cell_h, sel || active);
|
||||
if (i == 3 || i == 4) { // text keys: {} picker, page toggle
|
||||
// Shows what pressing it lands on next, same "reads as the
|
||||
// destination" convention as the original 2-page abc<->#@ toggle,
|
||||
// generalized to however many pages are in the cycle right now.
|
||||
const char* lbl;
|
||||
if (i == 3) {
|
||||
lbl = "{}";
|
||||
} else {
|
||||
int next = (page + 1) % totalPages();
|
||||
lbl = pageIsSymbols(next) ? "#@" : scriptHint(scriptAt(next));
|
||||
}
|
||||
int tw = display.getTextWidth(lbl);
|
||||
display.setCursor(sx + (spec_w - tw) / 2, spec_y);
|
||||
display.print(lbl);
|
||||
} else if (i == 1) { // space ⎵ — two halves side by side
|
||||
int icw = (ICON_SPACE_L.w + ICON_SPACE_R.w) * s;
|
||||
int ix = sx + (spec_w - icw) / 2;
|
||||
miniIconDraw(display, ix, icy, ICON_SPACE_L);
|
||||
miniIconDraw(display, ix + ICON_SPACE_L.w * s, icy, ICON_SPACE_R);
|
||||
} else {
|
||||
const MiniIcon& ic = (i == 0) ? ICON_SHIFT
|
||||
: (i == 2) ? ICON_BACKSPACE
|
||||
: ICON_CHECK; // i == 5 → OK
|
||||
int ix = sx + (spec_w - ic.w * s) / 2;
|
||||
miniIconDraw(display, ix, icy, ic);
|
||||
}
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
}
|
||||
}
|
||||
|
||||
// Accent popup: floats over the still-visible grid (same idea as the
|
||||
|
||||
@@ -73,6 +73,9 @@ class SettingsScreen : public UIScreen {
|
||||
KEYBOARD_TYPE,
|
||||
KEYBOARD_MAIN_ALPHABET,
|
||||
KEYBOARD_ALPHABET,
|
||||
#if defined(ENV_PIN_SDA) && defined(ENV_PIN_SCL)
|
||||
KEYBOARD_CARDKB_COMPACT,
|
||||
#endif
|
||||
// Contacts section
|
||||
SECTION_CONTACTS, DM_FILTER, CH_FILTER, ROOM_FILTER,
|
||||
// Messages section
|
||||
@@ -574,6 +577,12 @@ class SettingsScreen : public UIScreen {
|
||||
display.print("Additional");
|
||||
display.setCursor(valCol(display), y);
|
||||
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) {
|
||||
display.print("BattDisp");
|
||||
display.setCursor(valCol(display), y);
|
||||
@@ -945,6 +954,13 @@ public:
|
||||
_dirty = 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) {
|
||||
int n = p->dm_resend_count;
|
||||
if (right || enter) n = (n + 1) % 6; // 0..5, wraps
|
||||
|
||||
Reference in New Issue
Block a user