mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-08-03 18:56:15 +00:00
feat(ui): keyboard overhaul — shared instance, icon keys, symbols page
- Unify on the shared keyboard: WaypointsView drives UITask::keyboard()
instead of its own instance; openKb() clears {loc}/{time} for literal
fields (labels, coordinates). Reclaims the duplicate widget from the heap.
- Icon glyphs on the special row: caps ⇧, space ⎵ (two halves), delete ⌫,
OK ✓ (reuses ICON_CHECK); {} and the page toggle stay text.
- Second character page (symbols), toggled by a "#@"/"abc" key. Tidy the
letters page: drop the duplicate grid space, add the comma, group
punctuation as . , ! ?
- Harden the preview buffers (KB_PREVIEW_CAP) against very wide displays.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
4145185031
commit
8c0ccaed73
@@ -3,22 +3,39 @@
|
|||||||
#include <helpers/ui/DisplayDriver.h>
|
#include <helpers/ui/DisplayDriver.h>
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include "PopupMenu.h"
|
#include "PopupMenu.h"
|
||||||
|
#include "icons.h" // mini-icons for the special-key row (⇧ ⌫ ⎵ ✓)
|
||||||
|
|
||||||
// Layout constants shared by all keyboard users
|
// Layout constants shared by all keyboard users.
|
||||||
static const char KB_CHARS[4][10] = {
|
// Two pages: letters (page 0) and symbols (page 1), toggled by the "#@"/"abc"
|
||||||
{'a','b','c','d','e','f','g','h','i','j'},
|
// special key. Space lives only on the ⎵ special key now, so the freed grid
|
||||||
{'k','l','m','n','o','p','q','r','s','t'},
|
// slot on page 0 holds the comma; punctuation is grouped as . , ! ?
|
||||||
{'u','v','w','x','y','z','.',' ','!','?'},
|
static const int KB_PAGES = 2;
|
||||||
{'1','2','3','4','5','6','7','8','9','0'},
|
static const char KB_CHARS[KB_PAGES][4][10] = {
|
||||||
|
{ // page 0 — letters + digits
|
||||||
|
{'a','b','c','d','e','f','g','h','i','j'},
|
||||||
|
{'k','l','m','n','o','p','q','r','s','t'},
|
||||||
|
{'u','v','w','x','y','z','.',',','!','?'},
|
||||||
|
{'1','2','3','4','5','6','7','8','9','0'},
|
||||||
|
},
|
||||||
|
{ // page 1 — symbols + digits (ASCII only — one byte per key)
|
||||||
|
{'@','#','&','*','(',')','-','_','+','='},
|
||||||
|
{'/','\\',':',';','\'','"','<','>','[',']'},
|
||||||
|
{'{','}','|','~','^','$','%','`',',','.'},
|
||||||
|
{'1','2','3','4','5','6','7','8','9','0'},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
static const int KB_ROWS_CHAR = 4;
|
static const int KB_ROWS_CHAR = 4;
|
||||||
static const int KB_COLS_CHAR = 10;
|
static const int KB_COLS_CHAR = 10;
|
||||||
static const int KB_SPECIAL = 5; // [^] [Sp] [De] [{}] [OK]
|
static const int KB_SPECIAL = 6; // ⇧ ⎵ ⌫ {} #@/abc ✓
|
||||||
// Buffer cap for typed text, in bytes. Matches MeshCore's MAX_TEXT_LEN
|
// Buffer cap for typed text, in bytes. Matches MeshCore's MAX_TEXT_LEN
|
||||||
// (10*CIPHER_BLOCK_SIZE = 160) so a full-length message can be composed; each
|
// (10*CIPHER_BLOCK_SIZE = 160) so a full-length message can be composed; each
|
||||||
// field passes its own smaller max to begin() where its store is smaller.
|
// field passes its own smaller max to begin() where its store is smaller.
|
||||||
static const int KB_MAX_LEN = 160;
|
static const int KB_MAX_LEN = 160;
|
||||||
|
|
||||||
|
// Longest preview line we render per row. Caps the per-line stack buffers so a
|
||||||
|
// very wide display (small font → many chars per line) can't overrun them.
|
||||||
|
static const int KB_PREVIEW_CAP = 46;
|
||||||
|
|
||||||
static const int KB_PH_MAX = 12; // max placeholders in list
|
static const int KB_PH_MAX = 12; // max placeholders in list
|
||||||
static const int KB_PH_LEN = 9; // max placeholder string length incl. null
|
static const int KB_PH_LEN = 9; // max placeholder string length incl. null
|
||||||
static const int KB_PH_VISIBLE = 3; // items shown at once in overlay
|
static const int KB_PH_VISIBLE = 3; // items shown at once in overlay
|
||||||
@@ -28,6 +45,7 @@ struct KeyboardWidget {
|
|||||||
int len;
|
int len;
|
||||||
int max_len;
|
int max_len;
|
||||||
int row, col;
|
int row, col;
|
||||||
|
int page; // 0 = letters, 1 = symbols
|
||||||
bool caps;
|
bool caps;
|
||||||
char _ph_buf[KB_PH_MAX][KB_PH_LEN];
|
char _ph_buf[KB_PH_MAX][KB_PH_LEN];
|
||||||
int _ph_count;
|
int _ph_count;
|
||||||
@@ -41,6 +59,7 @@ struct KeyboardWidget {
|
|||||||
buf[max_len] = '\0';
|
buf[max_len] = '\0';
|
||||||
len = strlen(buf);
|
len = strlen(buf);
|
||||||
row = col = 0;
|
row = col = 0;
|
||||||
|
page = 0;
|
||||||
caps = false;
|
caps = false;
|
||||||
_ph_menu.active = false;
|
_ph_menu.active = false;
|
||||||
// default placeholders — always available
|
// default placeholders — always available
|
||||||
@@ -79,6 +98,7 @@ struct KeyboardWidget {
|
|||||||
// multi-line text preview: cursor always on last preview line
|
// multi-line text preview: cursor always on last preview line
|
||||||
int cpl = display.width() / cw; // chars per preview line
|
int cpl = display.width() / cw; // chars per preview line
|
||||||
if (cpl < 1) cpl = 1;
|
if (cpl < 1) cpl = 1;
|
||||||
|
if (cpl > KB_PREVIEW_CAP) cpl = KB_PREVIEW_CAP; // never overrun linebuf below
|
||||||
int cursor_line = len / cpl;
|
int cursor_line = len / cpl;
|
||||||
int first_line = (cursor_line >= prev_lines) ? (cursor_line - prev_lines + 1) : 0;
|
int first_line = (cursor_line >= prev_lines) ? (cursor_line - prev_lines + 1) : 0;
|
||||||
int start = first_line * cpl;
|
int start = first_line * cpl;
|
||||||
@@ -86,7 +106,7 @@ struct KeyboardWidget {
|
|||||||
int ps = start + pl * cpl;
|
int ps = start + pl * cpl;
|
||||||
int pe = ps + cpl;
|
int pe = ps + cpl;
|
||||||
bool cursor_here = (ps <= len && (len < pe || pl == prev_lines - 1));
|
bool cursor_here = (ps <= len && (len < pe || pl == prev_lines - 1));
|
||||||
char linebuf[32];
|
char linebuf[KB_PREVIEW_CAP + 2]; // cpl chars + cursor '_' + NUL
|
||||||
if (cursor_here) {
|
if (cursor_here) {
|
||||||
int nc = len - ps; if (nc < 0) nc = 0; if (nc > cpl - 1) nc = cpl - 1;
|
int nc = len - ps; if (nc < 0) nc = 0; if (nc > cpl - 1) nc = cpl - 1;
|
||||||
snprintf(linebuf, sizeof(linebuf), "%.*s_", nc, buf + ps);
|
snprintf(linebuf, sizeof(linebuf), "%.*s_", nc, buf + ps);
|
||||||
@@ -96,7 +116,7 @@ struct KeyboardWidget {
|
|||||||
} else {
|
} else {
|
||||||
linebuf[0] = '\0';
|
linebuf[0] = '\0';
|
||||||
}
|
}
|
||||||
char linebuf_t[32];
|
char linebuf_t[KB_PREVIEW_CAP + 2];
|
||||||
display.translateUTF8ToBlocks(linebuf_t, linebuf, sizeof(linebuf_t));
|
display.translateUTF8ToBlocks(linebuf_t, linebuf, sizeof(linebuf_t));
|
||||||
display.setCursor(0, pl * lh);
|
display.setCursor(0, pl * lh);
|
||||||
display.print(linebuf_t);
|
display.print(linebuf_t);
|
||||||
@@ -108,7 +128,7 @@ struct KeyboardWidget {
|
|||||||
int y = chars_y + r * cell_h;
|
int y = chars_y + r * cell_h;
|
||||||
for (int c = 0; c < KB_COLS_CHAR; c++) {
|
for (int c = 0; c < KB_COLS_CHAR; c++) {
|
||||||
bool sel = (row == r && col == c);
|
bool sel = (row == r && col == c);
|
||||||
char ch = KB_CHARS[r][c];
|
char ch = KB_CHARS[page][r][c];
|
||||||
if (caps && ch >= 'a' && ch <= 'z') ch = ch - 'a' + 'A';
|
if (caps && ch >= 'a' && ch <= 'z') ch = ch - 'a' + 'A';
|
||||||
char ch_buf[2] = { ch == ' ' ? '_' : ch, '\0' };
|
char ch_buf[2] = { ch == ' ' ? '_' : ch, '\0' };
|
||||||
int cx = c * cell_w;
|
int cx = c * cell_w;
|
||||||
@@ -118,16 +138,31 @@ struct KeyboardWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// special row: [^] [Sp] [Dl] [{}] [OK]
|
// special row: caps ⇧ · space ⎵ · delete ⌫ · placeholders {} (text) · OK ✓
|
||||||
const char* spec[] = { "[^]", "[Sp]", "[Dl]", "[{}]", "[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++) {
|
for (int i = 0; i < KB_SPECIAL; i++) {
|
||||||
bool sel = (row == KB_ROWS_CHAR && col == i);
|
bool sel = (row == KB_ROWS_CHAR && col == i);
|
||||||
bool active = (i == 0 && caps);
|
bool active = (i == 0 && caps);
|
||||||
int sx = i * spec_w;
|
int sx = i * spec_w;
|
||||||
display.drawSelectionRow(sx, spec_y - 1, spec_w - 1, cell_h, sel || active);
|
display.drawSelectionRow(sx, spec_y - 1, spec_w - 1, cell_h, sel || active);
|
||||||
int tw = display.getTextWidth(spec[i]);
|
if (i == 3 || i == 4) { // text keys: {} picker, page toggle
|
||||||
display.setCursor(sx + (spec_w - tw) / 2, spec_y);
|
const char* lbl = (i == 3) ? "{}" : (page == 0 ? "#@" : "abc");
|
||||||
display.print(spec[i]);
|
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);
|
display.setColor(DisplayDriver::LIGHT);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,7 +219,7 @@ struct KeyboardWidget {
|
|||||||
if (c == KEY_ENTER) {
|
if (c == KEY_ENTER) {
|
||||||
if (row < KB_ROWS_CHAR) {
|
if (row < KB_ROWS_CHAR) {
|
||||||
if (len < max_len) {
|
if (len < max_len) {
|
||||||
char ch = KB_CHARS[row][col];
|
char ch = KB_CHARS[page][row][col];
|
||||||
if (caps && ch >= 'a' && ch <= 'z') ch = ch - 'a' + 'A';
|
if (caps && ch >= 'a' && ch <= 'z') ch = ch - 'a' + 'A';
|
||||||
buf[len++] = ch;
|
buf[len++] = ch;
|
||||||
buf[len] = '\0';
|
buf[len] = '\0';
|
||||||
@@ -203,6 +238,9 @@ struct KeyboardWidget {
|
|||||||
for (int i = 0; i < _ph_count; i++) _ph_menu.addItem(_ph_buf[i]);
|
for (int i = 0; i < _ph_count; i++) _ph_menu.addItem(_ph_buf[i]);
|
||||||
break;
|
break;
|
||||||
case 4:
|
case 4:
|
||||||
|
page ^= 1; // toggle letters ⇄ symbols
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
return DONE;
|
return DONE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,6 +154,8 @@ public:
|
|||||||
void gotoCompassScreen();
|
void gotoCompassScreen();
|
||||||
TrailStore& trail() { return _trail; }
|
TrailStore& trail() { return _trail; }
|
||||||
WaypointStore& waypoints() { return _waypoints; }
|
WaypointStore& waypoints() { return _waypoints; }
|
||||||
|
// Shared on-screen keyboard — only one screen drives it at a time.
|
||||||
|
KeyboardWidget& keyboard() { return _kb; }
|
||||||
void saveWaypoints();
|
void saveWaypoints();
|
||||||
// Add a waypoint, persist, and show the standard "Waypoint saved" / "Waypoints
|
// Add a waypoint, persist, and show the standard "Waypoint saved" / "Waypoints
|
||||||
// full" alert. Returns true on success. The ts-less overload uses current RTC time.
|
// full" alert. Returns true on success. The ts-less overload uses current RTC time.
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ class WaypointsView {
|
|||||||
int _scroll = 0;
|
int _scroll = 0;
|
||||||
|
|
||||||
PopupMenu _ctx; // Rename / Delete / Send on a selected waypoint
|
PopupMenu _ctx; // Rename / Delete / Send on a selected waypoint
|
||||||
KeyboardWidget _kb; // label entry (mark new / rename)
|
|
||||||
bool _kb_active = false;
|
bool _kb_active = false;
|
||||||
int _kb_rename_idx = -1; // -1 = marking new, ≥0 = renaming that index
|
int _kb_rename_idx = -1; // -1 = marking new, ≥0 = renaming that index
|
||||||
int32_t _mark_lat = 0, _mark_lon = 0; // pending new-mark position
|
int32_t _mark_lat = 0, _mark_lon = 0; // pending new-mark position
|
||||||
@@ -100,6 +99,16 @@ class WaypointsView {
|
|||||||
if (_task->addWaypoint(lat, lon, label)) { _mode = OFF; }
|
if (_task->addWaypoint(lat, lon, label)) { _mode = OFF; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Open the shared keyboard for a literal field. Waypoint labels and the
|
||||||
|
// lat/lon magnitudes are never placeholder-expanded, so {loc}/{time} are
|
||||||
|
// cleared (same intent as the bot's literal trigger field).
|
||||||
|
void openKb(const char* initial, int max) {
|
||||||
|
KeyboardWidget& kb = _task->keyboard();
|
||||||
|
kb.begin(initial, max);
|
||||||
|
kb.clearPlaceholders();
|
||||||
|
_kb_active = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Commit a mark-here / rename label from the keyboard.
|
// Commit a mark-here / rename label from the keyboard.
|
||||||
void commitLabel(const char* buf) {
|
void commitLabel(const char* buf) {
|
||||||
if (_kb_rename_idx >= 0) {
|
if (_kb_rename_idx >= 0) {
|
||||||
@@ -229,15 +238,14 @@ public:
|
|||||||
if (_task->waypoints().full()) { _task->showAlert("Waypoints full", 1000); return; }
|
if (_task->waypoints().full()) { _task->showAlert("Waypoints full", 1000); return; }
|
||||||
_mark_lat = lat; _mark_lon = lon; _mark_ts = (uint32_t)rtc_clock.getCurrentTime();
|
_mark_lat = lat; _mark_lon = lon; _mark_ts = (uint32_t)rtc_clock.getCurrentTime();
|
||||||
_kb_rename_idx = -1;
|
_kb_rename_idx = -1;
|
||||||
_kb.begin("", WAYPOINT_LABEL_LEN - 1);
|
openKb("", WAYPOINT_LABEL_LEN - 1);
|
||||||
_kb_active = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only called while active().
|
// Only called while active().
|
||||||
int render(DisplayDriver& display) {
|
int render(DisplayDriver& display) {
|
||||||
display.setTextSize(1);
|
display.setTextSize(1);
|
||||||
display.setColor(DisplayDriver::LIGHT);
|
display.setColor(DisplayDriver::LIGHT);
|
||||||
if (_kb_active) return _kb.render(display); // keyboard owns the screen
|
if (_kb_active) return _task->keyboard().render(display); // keyboard owns the screen
|
||||||
if (_mode == ADD) { renderAddForm(display); return 1000; }
|
if (_mode == ADD) { renderAddForm(display); return 1000; }
|
||||||
if (_mode == NAV) { renderWpNav(display); return 1000; }
|
if (_mode == NAV) { renderWpNav(display); return 1000; }
|
||||||
renderWpList(display); // LIST
|
renderWpList(display); // LIST
|
||||||
@@ -249,10 +257,11 @@ public:
|
|||||||
bool handleInput(char c) {
|
bool handleInput(char c) {
|
||||||
// Label keyboard (mark new / rename / ADD field) takes all input first.
|
// Label keyboard (mark new / rename / ADD field) takes all input first.
|
||||||
if (_kb_active) {
|
if (_kb_active) {
|
||||||
auto r = _kb.handleInput(c);
|
KeyboardWidget& kb = _task->keyboard();
|
||||||
|
auto r = kb.handleInput(c);
|
||||||
if (r == KeyboardWidget::DONE) {
|
if (r == KeyboardWidget::DONE) {
|
||||||
if (_kb_field >= 0) applyAddField(_kb.buf); // ADD field edit
|
if (_kb_field >= 0) applyAddField(kb.buf); // ADD field edit
|
||||||
else commitLabel(_kb.buf); // mark / rename label
|
else commitLabel(kb.buf); // mark / rename label
|
||||||
_kb_active = false; _kb_field = -1;
|
_kb_active = false; _kb_field = -1;
|
||||||
} else if (r == KeyboardWidget::CANCELLED) {
|
} else if (r == KeyboardWidget::CANCELLED) {
|
||||||
_kb_active = false; _kb_field = -1;
|
_kb_active = false; _kb_field = -1;
|
||||||
@@ -270,8 +279,7 @@ public:
|
|||||||
if (sel == 0) { // Rename
|
if (sel == 0) { // Rename
|
||||||
if (wi >= 0 && wi < _task->waypoints().count()) {
|
if (wi >= 0 && wi < _task->waypoints().count()) {
|
||||||
_kb_rename_idx = wi;
|
_kb_rename_idx = wi;
|
||||||
_kb.begin(_task->waypoints().at(wi).label, WAYPOINT_LABEL_LEN - 1);
|
openKb(_task->waypoints().at(wi).label, WAYPOINT_LABEL_LEN - 1);
|
||||||
_kb_active = true;
|
|
||||||
}
|
}
|
||||||
} else if (sel == 1) { // Delete
|
} else if (sel == 1) { // Delete
|
||||||
if (wi >= 0 && wi < _task->waypoints().count()) {
|
if (wi >= 0 && wi < _task->waypoints().count()) {
|
||||||
@@ -305,9 +313,9 @@ public:
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (c == KEY_ENTER) {
|
if (c == KEY_ENTER) {
|
||||||
if (_add_sel == 0) { _kb_field = 0; _kb.begin(_add_lat, 11); _kb_active = true; }
|
if (_add_sel == 0) { _kb_field = 0; openKb(_add_lat, 11); }
|
||||||
else if (_add_sel == 1) { _kb_field = 1; _kb.begin(_add_lon, 11); _kb_active = true; }
|
else if (_add_sel == 1) { _kb_field = 1; openKb(_add_lon, 11); }
|
||||||
else if (_add_sel == 2) { _kb_field = 2; _kb.begin(_add_label, WAYPOINT_LABEL_LEN - 1); _kb_active = true; }
|
else if (_add_sel == 2) { _kb_field = 2; openKb(_add_label, WAYPOINT_LABEL_LEN - 1); }
|
||||||
else { commitAddForm(); }
|
else { commitAddForm(); }
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,6 +87,36 @@ MINI_ICON(ICON_CROSS, 4, // ✗
|
|||||||
packRow(".##."),
|
packRow(".##."),
|
||||||
packRow("#..#"));
|
packRow("#..#"));
|
||||||
|
|
||||||
|
// Keyboard special-key glyphs.
|
||||||
|
MINI_ICON(ICON_SHIFT, 7, // ⇧ caps
|
||||||
|
packRow("...#..."),
|
||||||
|
packRow("..###.."),
|
||||||
|
packRow(".#####."),
|
||||||
|
packRow("#######"),
|
||||||
|
packRow("..###.."),
|
||||||
|
packRow("..###.."),
|
||||||
|
packRow("..###.."));
|
||||||
|
MINI_ICON(ICON_BACKSPACE, 8, // ⌫ delete-left (× knocked out of the arrow body)
|
||||||
|
packRow("...#####"),
|
||||||
|
packRow("..######"),
|
||||||
|
packRow(".###.#.#"),
|
||||||
|
packRow("#####.##"),
|
||||||
|
packRow(".###.#.#"),
|
||||||
|
packRow("..######"),
|
||||||
|
packRow("...#####"));
|
||||||
|
// Space ⎵ is wider than the 8-px mini-icon limit, so it is two halves drawn
|
||||||
|
// side by side (offset by ICON_SPACE_L.w * scale): ticks at both far ends + bar.
|
||||||
|
MINI_ICON(ICON_SPACE_L, 8,
|
||||||
|
packRow("#......."),
|
||||||
|
packRow("#......."),
|
||||||
|
packRow("#......."),
|
||||||
|
packRow("########"));
|
||||||
|
MINI_ICON(ICON_SPACE_R, 8,
|
||||||
|
packRow(".......#"),
|
||||||
|
packRow(".......#"),
|
||||||
|
packRow(".......#"),
|
||||||
|
packRow("########"));
|
||||||
|
|
||||||
// ── Big ASCII-art icons (skeleton, not yet used) ─────────────────────────────
|
// ── Big ASCII-art icons (skeleton, not yet used) ─────────────────────────────
|
||||||
// Same authoring idea as the mini-icons but for full page glyphs up to 32 px
|
// Same authoring idea as the mini-icons but for full page glyphs up to 32 px
|
||||||
// wide: one uint32_t per row. The existing XBM bitmaps below (logo/bluetooth/…)
|
// wide: one uint32_t per row. The existing XBM bitmaps below (logo/bluetooth/…)
|
||||||
|
|||||||
Reference in New Issue
Block a user