diff --git a/examples/companion_radio/ui-new/KeyboardWidget.h b/examples/companion_radio/ui-new/KeyboardWidget.h index 6a532d5d..ad09fe2b 100644 --- a/examples/companion_radio/ui-new/KeyboardWidget.h +++ b/examples/companion_radio/ui-new/KeyboardWidget.h @@ -3,22 +3,39 @@ #include #include #include "PopupMenu.h" +#include "icons.h" // mini-icons for the special-key row (⇧ ⌫ ⎵ ✓) -// Layout constants shared by all keyboard users -static const char KB_CHARS[4][10] = { - {'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'}, +// Layout constants shared by all keyboard users. +// Two pages: letters (page 0) and symbols (page 1), toggled by the "#@"/"abc" +// special key. Space lives only on the ⎵ special key now, so the freed grid +// slot on page 0 holds the comma; punctuation is grouped as . , ! ? +static const int KB_PAGES = 2; +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_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 // (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. 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_LEN = 9; // max placeholder string length incl. null static const int KB_PH_VISIBLE = 3; // items shown at once in overlay @@ -28,6 +45,7 @@ struct KeyboardWidget { int len; int max_len; int row, col; + int page; // 0 = letters, 1 = symbols bool caps; char _ph_buf[KB_PH_MAX][KB_PH_LEN]; int _ph_count; @@ -41,6 +59,7 @@ struct KeyboardWidget { buf[max_len] = '\0'; len = strlen(buf); row = col = 0; + page = 0; caps = false; _ph_menu.active = false; // default placeholders — always available @@ -79,6 +98,7 @@ struct KeyboardWidget { // multi-line text preview: cursor always on last preview line int cpl = display.width() / cw; // chars per preview line if (cpl < 1) cpl = 1; + if (cpl > KB_PREVIEW_CAP) cpl = KB_PREVIEW_CAP; // never overrun linebuf below int cursor_line = len / cpl; int first_line = (cursor_line >= prev_lines) ? (cursor_line - prev_lines + 1) : 0; int start = first_line * cpl; @@ -86,7 +106,7 @@ struct KeyboardWidget { int ps = start + pl * cpl; int pe = ps + cpl; 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) { int nc = len - ps; if (nc < 0) nc = 0; if (nc > cpl - 1) nc = cpl - 1; snprintf(linebuf, sizeof(linebuf), "%.*s_", nc, buf + ps); @@ -96,7 +116,7 @@ struct KeyboardWidget { } else { linebuf[0] = '\0'; } - char linebuf_t[32]; + char linebuf_t[KB_PREVIEW_CAP + 2]; display.translateUTF8ToBlocks(linebuf_t, linebuf, sizeof(linebuf_t)); display.setCursor(0, pl * lh); display.print(linebuf_t); @@ -108,7 +128,7 @@ struct KeyboardWidget { int y = chars_y + r * cell_h; for (int c = 0; c < KB_COLS_CHAR; 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'; char ch_buf[2] = { ch == ' ' ? '_' : ch, '\0' }; int cx = c * cell_w; @@ -118,16 +138,31 @@ struct KeyboardWidget { } } - // special row: [^] [Sp] [Dl] [{}] [OK] - const char* spec[] = { "[^]", "[Sp]", "[Dl]", "[{}]", "[OK]" }; + // 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 == KB_ROWS_CHAR && 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); - int tw = display.getTextWidth(spec[i]); - display.setCursor(sx + (spec_w - tw) / 2, spec_y); - display.print(spec[i]); + if (i == 3 || i == 4) { // text keys: {} picker, page toggle + const char* lbl = (i == 3) ? "{}" : (page == 0 ? "#@" : "abc"); + 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); } @@ -184,7 +219,7 @@ struct KeyboardWidget { if (c == KEY_ENTER) { if (row < KB_ROWS_CHAR) { 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'; buf[len++] = ch; buf[len] = '\0'; @@ -203,6 +238,9 @@ struct KeyboardWidget { for (int i = 0; i < _ph_count; i++) _ph_menu.addItem(_ph_buf[i]); break; case 4: + page ^= 1; // toggle letters ⇄ symbols + break; + case 5: return DONE; } } diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 585e7231..3f92190c 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -154,6 +154,8 @@ public: void gotoCompassScreen(); TrailStore& trail() { return _trail; } WaypointStore& waypoints() { return _waypoints; } + // Shared on-screen keyboard — only one screen drives it at a time. + KeyboardWidget& keyboard() { return _kb; } void saveWaypoints(); // 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. diff --git a/examples/companion_radio/ui-new/WaypointsView.h b/examples/companion_radio/ui-new/WaypointsView.h index e31a4fa0..f710029b 100644 --- a/examples/companion_radio/ui-new/WaypointsView.h +++ b/examples/companion_radio/ui-new/WaypointsView.h @@ -25,7 +25,6 @@ class WaypointsView { int _scroll = 0; PopupMenu _ctx; // Rename / Delete / Send on a selected waypoint - KeyboardWidget _kb; // label entry (mark new / rename) bool _kb_active = false; int _kb_rename_idx = -1; // -1 = marking new, ≥0 = renaming that index 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; } } + // 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. void commitLabel(const char* buf) { if (_kb_rename_idx >= 0) { @@ -229,15 +238,14 @@ public: if (_task->waypoints().full()) { _task->showAlert("Waypoints full", 1000); return; } _mark_lat = lat; _mark_lon = lon; _mark_ts = (uint32_t)rtc_clock.getCurrentTime(); _kb_rename_idx = -1; - _kb.begin("", WAYPOINT_LABEL_LEN - 1); - _kb_active = true; + openKb("", WAYPOINT_LABEL_LEN - 1); } // Only called while active(). int render(DisplayDriver& display) { display.setTextSize(1); 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 == NAV) { renderWpNav(display); return 1000; } renderWpList(display); // LIST @@ -249,10 +257,11 @@ public: bool handleInput(char c) { // Label keyboard (mark new / rename / ADD field) takes all input first. if (_kb_active) { - auto r = _kb.handleInput(c); + KeyboardWidget& kb = _task->keyboard(); + auto r = kb.handleInput(c); if (r == KeyboardWidget::DONE) { - if (_kb_field >= 0) applyAddField(_kb.buf); // ADD field edit - else commitLabel(_kb.buf); // mark / rename label + if (_kb_field >= 0) applyAddField(kb.buf); // ADD field edit + else commitLabel(kb.buf); // mark / rename label _kb_active = false; _kb_field = -1; } else if (r == KeyboardWidget::CANCELLED) { _kb_active = false; _kb_field = -1; @@ -270,8 +279,7 @@ public: if (sel == 0) { // Rename if (wi >= 0 && wi < _task->waypoints().count()) { _kb_rename_idx = wi; - _kb.begin(_task->waypoints().at(wi).label, WAYPOINT_LABEL_LEN - 1); - _kb_active = true; + openKb(_task->waypoints().at(wi).label, WAYPOINT_LABEL_LEN - 1); } } else if (sel == 1) { // Delete if (wi >= 0 && wi < _task->waypoints().count()) { @@ -305,9 +313,9 @@ public: return true; } if (c == KEY_ENTER) { - if (_add_sel == 0) { _kb_field = 0; _kb.begin(_add_lat, 11); _kb_active = true; } - else if (_add_sel == 1) { _kb_field = 1; _kb.begin(_add_lon, 11); _kb_active = true; } - else if (_add_sel == 2) { _kb_field = 2; _kb.begin(_add_label, WAYPOINT_LABEL_LEN - 1); _kb_active = true; } + if (_add_sel == 0) { _kb_field = 0; openKb(_add_lat, 11); } + else if (_add_sel == 1) { _kb_field = 1; openKb(_add_lon, 11); } + else if (_add_sel == 2) { _kb_field = 2; openKb(_add_label, WAYPOINT_LABEL_LEN - 1); } else { commitAddForm(); } return true; } diff --git a/examples/companion_radio/ui-new/icons.h b/examples/companion_radio/ui-new/icons.h index e38aa546..660c463b 100644 --- a/examples/companion_radio/ui-new/icons.h +++ b/examples/companion_radio/ui-new/icons.h @@ -87,6 +87,36 @@ MINI_ICON(ICON_CROSS, 4, // ✗ 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) ───────────────────────────── // 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/…)