2026-05-13 18:22:41 +02:00
|
|
|
|
#pragma once
|
|
|
|
|
|
|
|
|
|
|
|
#include <helpers/ui/DisplayDriver.h>
|
|
|
|
|
|
#include <Arduino.h>
|
2026-05-14 08:06:26 +02:00
|
|
|
|
#include "PopupMenu.h"
|
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>
2026-06-15 10:47:50 +02:00
|
|
|
|
#include "icons.h" // mini-icons for the special-key row (⇧ ⌫ ⎵ ✓)
|
2026-07-02 12:21:52 +02:00
|
|
|
|
#include "../NodePrefs.h"
|
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>
2026-06-15 10:47:50 +02:00
|
|
|
|
|
|
|
|
|
|
// 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'},
|
|
|
|
|
|
},
|
2026-05-13 18:22:41 +02:00
|
|
|
|
};
|
|
|
|
|
|
static const int KB_ROWS_CHAR = 4;
|
|
|
|
|
|
static const int KB_COLS_CHAR = 10;
|
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>
2026-06-15 10:47:50 +02:00
|
|
|
|
static const int KB_SPECIAL = 6; // ⇧ ⎵ ⌫ {} #@/abc ✓
|
2026-07-02 12:21:52 +02:00
|
|
|
|
|
|
|
|
|
|
// T9 multi-tap layout (Settings › Keyboard). A classic phone keypad: 9 cells (keys
|
|
|
|
|
|
// 1-9) laid out 3x3, each holding a handful of letters/symbols. Repeated Enter
|
|
|
|
|
|
// presses on the same cell within KB_T9_TIMEOUT_MS cycle through the group, ending
|
|
|
|
|
|
// on the cell's own digit (computed as '1'+cell, not stored here) before wrapping.
|
|
|
|
|
|
// Keys 0/*/# aren't part of the grid — space/backspace/etc. already live on the
|
2026-07-02 18:33:40 +02:00
|
|
|
|
// special row below, shared with the ABC layout.
|
2026-07-02 12:21:52 +02:00
|
|
|
|
static const int KB_T9_ROWS = 3;
|
|
|
|
|
|
static const int KB_T9_COLS = 3;
|
|
|
|
|
|
static const uint32_t KB_T9_TIMEOUT_MS = 800;
|
|
|
|
|
|
static const char* const KB_T9_GROUPS[KB_PAGES][9] = {
|
|
|
|
|
|
{ ".,!?'-", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz" }, // page 0 — letters
|
|
|
|
|
|
{ "@#&", "*()", "-_+", "=/\\", ":;'\"", "<>[]", "{}|~", "^$%`", ",." }, // page 1 — symbols
|
|
|
|
|
|
};
|
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets
Alarm repeat (Clock Tools):
- NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday
convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends.
computeAlarmNextFire() scans the next 7 days for a matching weekday when
set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is
empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot
behaviour, so existing users see no change.
On-screen keyboard alphabets (Settings > Keyboard > Alphabet):
- KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints
(insertion, backspace and T9 in-place cycling all codepoint-aware now, so a
multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/
kbUtf8CharAt/kbUtf8LastCharBytes.
- Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/
French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page
cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed.
NodePrefs::keyboard_alt_alphabet picks which one, if any, is active.
- Lemon font is forced on transiently inside KeyboardWidget::render() (saved
and restored every call) whenever the alt-alphabet page is showing or
already-typed text has non-ASCII bytes, so composing is visible even if
Font is set to Default.
- Each script's caps-shift rule is distinct and documented in
kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς
exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for
Latin Extended-A (verified against every character actually used, not a
blanket rule for that whole Unicode block).
NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask,
keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D,
sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile).
Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the
Rooms keyboard note (no longer ASCII-only once an alphabet is enabled).
Not build-verified — no PlatformIO toolchain available this session. Caps
mappings cross-checked against Python's Unicode case tables; UTF-8 literal
bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
|
|
|
|
|
|
|
|
|
|
// Additional (non-Latin) keyboard alphabets — NodePrefs::keyboard_alt_alphabet
|
|
|
|
|
|
// picks which one (if any) joins the Latin/symbols page cycle. Every alphabet
|
|
|
|
|
|
// here must fall inside Lemon's U+0020-04FF range (src/helpers/ui/LemonFont.h)
|
|
|
|
|
|
// since that's what actually draws these glyphs on-screen.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Unlike KB_CHARS (single ASCII byte per cell), these hold UTF-8 strings —
|
|
|
|
|
|
// Cyrillic is 2 bytes/codepoint — so cells are `const char*`, not `char`.
|
|
|
|
|
|
// KeyboardWidget's insertion/backspace/T9-cycle logic works in codepoints via
|
|
|
|
|
|
// the kbUtf8*() helpers below, not raw bytes, to stay correct for either.
|
|
|
|
|
|
|
|
|
|
|
|
// Cyrillic ABC grid: rows 0-2 hold the 30 letters that fit alphabetically
|
|
|
|
|
|
// (а-э); row 3 holds the remaining 3 (ё, ю, я) plus basic punctuation. No
|
|
|
|
|
|
// digit row on this page (digits are already reachable via the Symbols page,
|
|
|
|
|
|
// same as the Latin page's own row 3 duplicates them there).
|
|
|
|
|
|
static const char* const KB_CYRILLIC_CHARS[4][10] = {
|
|
|
|
|
|
{ "а","б","в","г","д","е","ж","з","и","й" },
|
|
|
|
|
|
{ "к","л","м","н","о","п","р","с","т","у" },
|
|
|
|
|
|
{ "ф","х","ц","ч","ш","щ","ъ","ы","ь","э" },
|
|
|
|
|
|
{ "ю","я","ё",".",",","!","?","-","'","\"" },
|
|
|
|
|
|
};
|
|
|
|
|
|
// Cyrillic T9 groups: the classic Russian phone-keypad distribution. Cell 0
|
|
|
|
|
|
// (digit '1') is punctuation, matching KB_T9_GROUPS' page-0 convention;
|
|
|
|
|
|
// cells 1-8 (digits '2'-'9') hold the 33 letters, 3-5 per key.
|
|
|
|
|
|
static const char* const KB_T9_GROUPS_CYRILLIC[9] = {
|
|
|
|
|
|
".,!?'-", "абвг", "деёжз", "ийкл", "мноп", "рсту", "фхцч", "шщъыь", "эюя"
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Greek ABC grid: the 24-letter modern alphabet plus final sigma (ς, used
|
|
|
|
|
|
// only at the end of a word — σ is the regular form) = 25 letters, fitting
|
|
|
|
|
|
// rows 0-2 with 5 basic punctuation marks to spare; row 3 keeps the digit row
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
|
// (unlike Cyrillic or most of the Latin-diacritic alphabets below, Greek has
|
|
|
|
|
|
// room left over in its own letter rows).
|
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets
Alarm repeat (Clock Tools):
- NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday
convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends.
computeAlarmNextFire() scans the next 7 days for a matching weekday when
set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is
empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot
behaviour, so existing users see no change.
On-screen keyboard alphabets (Settings > Keyboard > Alphabet):
- KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints
(insertion, backspace and T9 in-place cycling all codepoint-aware now, so a
multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/
kbUtf8CharAt/kbUtf8LastCharBytes.
- Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/
French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page
cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed.
NodePrefs::keyboard_alt_alphabet picks which one, if any, is active.
- Lemon font is forced on transiently inside KeyboardWidget::render() (saved
and restored every call) whenever the alt-alphabet page is showing or
already-typed text has non-ASCII bytes, so composing is visible even if
Font is set to Default.
- Each script's caps-shift rule is distinct and documented in
kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς
exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for
Latin Extended-A (verified against every character actually used, not a
blanket rule for that whole Unicode block).
NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask,
keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D,
sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile).
Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the
Rooms keyboard note (no longer ASCII-only once an alphabet is enabled).
Not build-verified — no PlatformIO toolchain available this session. Caps
mappings cross-checked against Python's Unicode case tables; UTF-8 literal
bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
|
|
|
|
// NOTE: monotonic Modern Greek normally marks stress with a tonos accent
|
|
|
|
|
|
// (ά έ ή ί ό ύ ώ) — omitted here to keep this a single, simple page. Fine for
|
|
|
|
|
|
// informal/transliteration-style typing; flag if proper accented Greek
|
|
|
|
|
|
// composition turns out to matter and we can add a second Greek page for them.
|
|
|
|
|
|
static const char* const KB_GREEK_CHARS[4][10] = {
|
|
|
|
|
|
{ "α","β","γ","δ","ε","ζ","η","θ","ι","κ" },
|
|
|
|
|
|
{ "λ","μ","ν","ξ","ο","π","ρ","σ","ς","τ" },
|
|
|
|
|
|
{ "υ","φ","χ","ψ","ω",".",",","!","?","'" },
|
|
|
|
|
|
{ "1","2","3","4","5","6","7","8","9","0" },
|
|
|
|
|
|
};
|
|
|
|
|
|
// Greek T9 groups: cell 0 (digit '1') is punctuation; cells 1-8 (digits
|
|
|
|
|
|
// '2'-'9') split the 25 letters roughly evenly, final sigma grouped with the
|
|
|
|
|
|
// regular sigma it's a variant of.
|
|
|
|
|
|
static const char* const KB_T9_GROUPS_GREEK[9] = {
|
|
|
|
|
|
".,!?'-", "αβγ", "δεζ", "ηθι", "κλμ", "νξο", "πρσς", "τυφ", "χψω"
|
|
|
|
|
|
};
|
|
|
|
|
|
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
|
// Per-language Latin-diacritic ABC grids. Each is that language's own full
|
|
|
|
|
|
// set of non-ASCII letters (not a shared curated subset — see NodePrefs.h's
|
|
|
|
|
|
// KB_ALPHABET_POLISH..KB_ALPHABET_NORDIC doc comment for why these replaced a
|
|
|
|
|
|
// single combined "Ext.Latin" page). Every grid is filled to all 40 cells the
|
|
|
|
|
|
// same way: letters first, then punctuation to round out the last letter
|
|
|
|
|
|
// row, then a full digit row, then any further rows filled with more
|
|
|
|
|
|
// punctuation/symbols — consistent shape regardless of how few letters a
|
|
|
|
|
|
// given language actually needs (German's 4, at the small end, still gets a
|
|
|
|
|
|
// full page rather than mostly-blank cells).
|
|
|
|
|
|
static const char* const KB_POLISH_CHARS[4][10] = {
|
|
|
|
|
|
{ "ą", "ć", "ę", "ł", "ń", "ó", "ś", "ź", "ż", "." },
|
|
|
|
|
|
{ "1", "2", "3", "4", "5", "6", "7", "8", "9", "0" },
|
|
|
|
|
|
{ ",", "!", "?", "'", "-", "\"", ":", ";", "(", ")" },
|
|
|
|
|
|
{ "@", "#", "&", "*", "_", "+", "=", "/", "\\", "~" },
|
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets
Alarm repeat (Clock Tools):
- NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday
convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends.
computeAlarmNextFire() scans the next 7 days for a matching weekday when
set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is
empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot
behaviour, so existing users see no change.
On-screen keyboard alphabets (Settings > Keyboard > Alphabet):
- KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints
(insertion, backspace and T9 in-place cycling all codepoint-aware now, so a
multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/
kbUtf8CharAt/kbUtf8LastCharBytes.
- Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/
French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page
cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed.
NodePrefs::keyboard_alt_alphabet picks which one, if any, is active.
- Lemon font is forced on transiently inside KeyboardWidget::render() (saved
and restored every call) whenever the alt-alphabet page is showing or
already-typed text has non-ASCII bytes, so composing is visible even if
Font is set to Default.
- Each script's caps-shift rule is distinct and documented in
kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς
exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for
Latin Extended-A (verified against every character actually used, not a
blanket rule for that whole Unicode block).
NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask,
keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D,
sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile).
Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the
Rooms keyboard note (no longer ASCII-only once an alphabet is enabled).
Not build-verified — no PlatformIO toolchain available this session. Caps
mappings cross-checked against Python's Unicode case tables; UTF-8 literal
bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
|
|
|
|
};
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
|
static const char* const KB_T9_GROUPS_POLISH[9] = {
|
|
|
|
|
|
".,!?'-", "ąć", "ęł", "ńó", "śź", "ż", ";()", "@#&*", "_+=/"
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
static const char* const KB_CZECH_CHARS[4][10] = {
|
|
|
|
|
|
{ "á", "č", "ď", "é", "ě", "í", "ň", "ó", "ř", "š" },
|
|
|
|
|
|
{ "ť", "ú", "ů", "ý", "ž", ".", ",", "!", "?", "'" },
|
|
|
|
|
|
{ "1", "2", "3", "4", "5", "6", "7", "8", "9", "0" },
|
|
|
|
|
|
{ "-", "\"", ":", ";", "(", ")", "@", "#", "&", "*" },
|
|
|
|
|
|
};
|
|
|
|
|
|
static const char* const KB_T9_GROUPS_CZECH[9] = {
|
|
|
|
|
|
".,!?'-", "áč", "ďé", "ěí", "ňó", "řš", "ťú", "ůý", "ž"
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Slovak overlaps Czech heavily (both descend from a shared diacritic
|
|
|
|
|
|
// tradition) but adds ä/ĺ/ľ/ô/ŕ and drops none — kept as its own page rather
|
|
|
|
|
|
// than merged, per the "each language separate" request.
|
|
|
|
|
|
static const char* const KB_SLOVAK_CHARS[4][10] = {
|
|
|
|
|
|
{ "á", "ä", "č", "ď", "é", "í", "ĺ", "ľ", "ň", "ó" },
|
|
|
|
|
|
{ "ô", "ŕ", "š", "ť", "ú", "ý", "ž", ".", ",", "!" },
|
|
|
|
|
|
{ "1", "2", "3", "4", "5", "6", "7", "8", "9", "0" },
|
|
|
|
|
|
{ "?", "'", "-", "\"", ":", ";", "(", ")", "@", "#" },
|
|
|
|
|
|
};
|
|
|
|
|
|
static const char* const KB_T9_GROUPS_SLOVAK[9] = {
|
|
|
|
|
|
".,!?'-", "áäč", "ďé", "íĺ", "ľň", "óô", "ŕš", "ťú", "ýž"
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// German: only 4 non-ASCII letters (ä ö ü ß) — the smallest of these
|
|
|
|
|
|
// keyboards, still filled to a full page rather than left sparse.
|
|
|
|
|
|
static const char* const KB_GERMAN_CHARS[4][10] = {
|
|
|
|
|
|
{ "ä", "ö", "ü", "ß", ".", ",", "!", "?", "'", "-" },
|
|
|
|
|
|
{ "1", "2", "3", "4", "5", "6", "7", "8", "9", "0" },
|
|
|
|
|
|
{ "\"", ":", ";", "(", ")", "@", "#", "&", "*", "_" },
|
|
|
|
|
|
{ "+", "=", "/", "\\", "~", "<", ">", "[", "]", "{" },
|
|
|
|
|
|
};
|
|
|
|
|
|
static const char* const KB_T9_GROUPS_GERMAN[9] = {
|
|
|
|
|
|
".,!?'-", "äö", "üß", ";()", "@#&*", "_+=/", "\\~<>", "[]{}", "|^$%"
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// French: à â ç é è ê ë î ï ô ù û ü ÿ œ. ÿ's uppercase (Ÿ, U+0178) is the one
|
|
|
|
|
|
// case-shift exception outside the Latin-1 flat -0x20 rule — see
|
|
|
|
|
|
// kbApplyCapsUtf8. œ is Latin Extended-A (adjacent-pair rule, verified).
|
|
|
|
|
|
static const char* const KB_FRENCH_CHARS[4][10] = {
|
|
|
|
|
|
{ "à", "â", "ç", "é", "è", "ê", "ë", "î", "ï", "ô" },
|
|
|
|
|
|
{ "ù", "û", "ü", "ÿ", "œ", ".", ",", "!", "?", "'" },
|
|
|
|
|
|
{ "1", "2", "3", "4", "5", "6", "7", "8", "9", "0" },
|
|
|
|
|
|
{ "-", "\"", ":", ";", "(", ")", "@", "#", "&", "*" },
|
|
|
|
|
|
};
|
|
|
|
|
|
static const char* const KB_T9_GROUPS_FRENCH[9] = {
|
|
|
|
|
|
".,!?'-", "àâ", "çé", "èê", "ëî", "ïô", "ùû", "üÿ", "œ"
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Spanish: the 5 accented vowels, ñ, and ü (only appears in güe/güi).
|
|
|
|
|
|
static const char* const KB_SPANISH_CHARS[4][10] = {
|
|
|
|
|
|
{ "á", "é", "í", "ñ", "ó", "ú", "ü", ".", ",", "!" },
|
|
|
|
|
|
{ "1", "2", "3", "4", "5", "6", "7", "8", "9", "0" },
|
|
|
|
|
|
{ "?", "'", "-", "\"", ":", ";", "(", ")", "@", "#" },
|
|
|
|
|
|
{ "&", "*", "_", "+", "=", "/", "\\", "~", "<", ">" },
|
|
|
|
|
|
};
|
|
|
|
|
|
static const char* const KB_T9_GROUPS_SPANISH[9] = {
|
|
|
|
|
|
".,!?'-", "áé", "íñ", "óú", "ü", ";()", "@#&*", "_+=/", "\\~<>"
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Portuguese: á à â ã ç é ê í ó ô õ ú (ü dropped from modern orthography).
|
|
|
|
|
|
static const char* const KB_PORTUGUESE_CHARS[4][10] = {
|
|
|
|
|
|
{ "á", "à", "â", "ã", "ç", "é", "ê", "í", "ó", "ô" },
|
|
|
|
|
|
{ "õ", "ú", ".", ",", "!", "?", "'", "-", "\"", ":" },
|
|
|
|
|
|
{ "1", "2", "3", "4", "5", "6", "7", "8", "9", "0" },
|
|
|
|
|
|
{ ";", "(", ")", "@", "#", "&", "*", "_", "+", "=" },
|
|
|
|
|
|
};
|
|
|
|
|
|
static const char* const KB_T9_GROUPS_PORTUGUESE[9] = {
|
|
|
|
|
|
".,!?'-", "áà", "âã", "çé", "êí", "óô", "õú", ";()", "@#&*"
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Nordic: å ä æ ö ø — shared across Danish/Norwegian/Swedish (their
|
|
|
|
|
|
// alphabets differ only in which of these 5 each uses), so kept as one page
|
|
|
|
|
|
// rather than three near-identical ones.
|
|
|
|
|
|
static const char* const KB_NORDIC_CHARS[4][10] = {
|
|
|
|
|
|
{ "å", "ä", "æ", "ö", "ø", ".", ",", "!", "?", "'" },
|
|
|
|
|
|
{ "1", "2", "3", "4", "5", "6", "7", "8", "9", "0" },
|
|
|
|
|
|
{ "-", "\"", ":", ";", "(", ")", "@", "#", "&", "*" },
|
|
|
|
|
|
{ "_", "+", "=", "/", "\\", "~", "<", ">", "[", "]" },
|
|
|
|
|
|
};
|
|
|
|
|
|
static const char* const KB_T9_GROUPS_NORDIC[9] = {
|
|
|
|
|
|
".,!?'-", "åä", "æö", "ø", ";()", "@#&*", "_+=/", "\\~<>", "[]{}"
|
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets
Alarm repeat (Clock Tools):
- NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday
convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends.
computeAlarmNextFire() scans the next 7 days for a matching weekday when
set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is
empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot
behaviour, so existing users see no change.
On-screen keyboard alphabets (Settings > Keyboard > Alphabet):
- KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints
(insertion, backspace and T9 in-place cycling all codepoint-aware now, so a
multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/
kbUtf8CharAt/kbUtf8LastCharBytes.
- Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/
French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page
cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed.
NodePrefs::keyboard_alt_alphabet picks which one, if any, is active.
- Lemon font is forced on transiently inside KeyboardWidget::render() (saved
and restored every call) whenever the alt-alphabet page is showing or
already-typed text has non-ASCII bytes, so composing is visible even if
Font is set to Default.
- Each script's caps-shift rule is distinct and documented in
kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς
exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for
Latin Extended-A (verified against every character actually used, not a
blanket rule for that whole Unicode block).
NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask,
keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D,
sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile).
Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the
Rooms keyboard note (no longer ASCII-only once an alphabet is enabled).
Not build-verified — no PlatformIO toolchain available this session. Caps
mappings cross-checked against Python's Unicode case tables; UTF-8 literal
bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
|
|
|
|
};
|
2026-06-04 12:15:30 +02:00
|
|
|
|
// 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;
|
2026-05-13 18:22:41 +02: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>
2026-06-15 10:47:50 +02:00
|
|
|
|
// 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;
|
|
|
|
|
|
|
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets
Alarm repeat (Clock Tools):
- NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday
convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends.
computeAlarmNextFire() scans the next 7 days for a matching weekday when
set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is
empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot
behaviour, so existing users see no change.
On-screen keyboard alphabets (Settings > Keyboard > Alphabet):
- KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints
(insertion, backspace and T9 in-place cycling all codepoint-aware now, so a
multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/
kbUtf8CharAt/kbUtf8LastCharBytes.
- Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/
French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page
cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed.
NodePrefs::keyboard_alt_alphabet picks which one, if any, is active.
- Lemon font is forced on transiently inside KeyboardWidget::render() (saved
and restored every call) whenever the alt-alphabet page is showing or
already-typed text has non-ASCII bytes, so composing is visible even if
Font is set to Default.
- Each script's caps-shift rule is distinct and documented in
kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς
exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for
Latin Extended-A (verified against every character actually used, not a
blanket rule for that whole Unicode block).
NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask,
keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D,
sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile).
Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the
Rooms keyboard note (no longer ASCII-only once an alphabet is enabled).
Not build-verified — no PlatformIO toolchain available this session. Caps
mappings cross-checked against Python's Unicode case tables; UTF-8 literal
bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
|
|
|
|
// ── UTF-8 helpers for the alt-alphabet pages ─────────────────────────────────
|
|
|
|
|
|
// KB_CHARS/KB_T9_GROUPS content is ASCII (1 byte/char); KB_CYRILLIC_CHARS and
|
|
|
|
|
|
// KB_T9_GROUPS_CYRILLIC are UTF-8 (2 bytes/char in this range). These helpers
|
|
|
|
|
|
// let insertion, backspace and T9 cycling work in codepoints for either,
|
|
|
|
|
|
// instead of assuming 1 byte == 1 character.
|
|
|
|
|
|
|
|
|
|
|
|
// Apply Shift/caps to every codepoint in a UTF-8 string, writing the result
|
|
|
|
|
|
// (same codepoint count, each independently shifted) into `out`. Every script
|
|
|
|
|
|
// here pairs lower/uppercase differently, so each gets its own rule:
|
|
|
|
|
|
// - ASCII a-z and Cyrillic а-я: flat -0x20 codepoint offset. ё/Ё (U+0451/
|
|
|
|
|
|
// U+0401) break the Cyrillic pattern by 0x50 and are special-cased.
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
|
// - Latin-1 Supplement à-þ (U+00E0-00FE, used by every Latin-diacritic
|
|
|
|
|
|
// alphabet below — ö/ü/é/á/etc.): also a flat -0x20 offset, same as ASCII —
|
|
|
|
|
|
// the block was designed as parallel case pairs. U+00F7 (÷, division sign)
|
|
|
|
|
|
// sits in that numeric range but isn't a letter; excluded. ß (U+00DF) has
|
|
|
|
|
|
// no simple uppercase in this range (its uppercase ẞ is U+1E9E, outside
|
|
|
|
|
|
// Lemon's U+0020-04FF) — left as-is. ÿ (U+00FF, French) is the one letter
|
|
|
|
|
|
// in this block whose uppercase Ÿ (U+0178) falls outside it entirely —
|
|
|
|
|
|
// special-cased before the range rule.
|
|
|
|
|
|
// - Latin Extended-A (ą/č/ĺ/œ/etc., U+0100-017F): NOT a flat offset like
|
|
|
|
|
|
// Latin-1 — this block alternates even=uppercase/odd=lowercase in adjacent
|
|
|
|
|
|
// pairs, so lowercase - 1 = uppercase. Verified true (against Python's own
|
|
|
|
|
|
// str.upper()) for every character actually used across the Polish/Czech/
|
|
|
|
|
|
// Slovak/French keyboards below; NOT a universal rule for the whole block
|
|
|
|
|
|
// (it has a handful of unpaired/irregular codepoints elsewhere) — recheck
|
|
|
|
|
|
// before adding more from it.
|
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets
Alarm repeat (Clock Tools):
- NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday
convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends.
computeAlarmNextFire() scans the next 7 days for a matching weekday when
set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is
empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot
behaviour, so existing users see no change.
On-screen keyboard alphabets (Settings > Keyboard > Alphabet):
- KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints
(insertion, backspace and T9 in-place cycling all codepoint-aware now, so a
multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/
kbUtf8CharAt/kbUtf8LastCharBytes.
- Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/
French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page
cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed.
NodePrefs::keyboard_alt_alphabet picks which one, if any, is active.
- Lemon font is forced on transiently inside KeyboardWidget::render() (saved
and restored every call) whenever the alt-alphabet page is showing or
already-typed text has non-ASCII bytes, so composing is visible even if
Font is set to Default.
- Each script's caps-shift rule is distinct and documented in
kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς
exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for
Latin Extended-A (verified against every character actually used, not a
blanket rule for that whole Unicode block).
NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask,
keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D,
sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile).
Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the
Rooms keyboard note (no longer ASCII-only once an alphabet is enabled).
Not build-verified — no PlatformIO toolchain available this session. Caps
mappings cross-checked against Python's Unicode case tables; UTF-8 literal
bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
|
|
|
|
// - Greek α-ω (U+03B1-03C9): flat -0x20 offset, same shape as Cyrillic/ASCII.
|
|
|
|
|
|
// Final sigma ς (U+03C2) is the one exception — it has no uppercase of its
|
|
|
|
|
|
// own; -0x20 would land on U+03A2, which is unassigned. It capitalizes to
|
|
|
|
|
|
// regular Σ (U+03A3) instead, same as σ, and is special-cased before the
|
|
|
|
|
|
// general range rule (03C2 falls inside 03B1-03C9, so order matters here).
|
|
|
|
|
|
// Used both for a single cell (one codepoint) and a whole T9 group label/string.
|
|
|
|
|
|
static void kbApplyCapsUtf8(const char* in, bool caps, char* out, size_t out_size) {
|
|
|
|
|
|
size_t o = 0;
|
|
|
|
|
|
const uint8_t* p = (const uint8_t*)in;
|
|
|
|
|
|
while (*p && o + 2 < out_size) {
|
|
|
|
|
|
uint32_t cp = DisplayDriver::decodeCodepoint(p);
|
|
|
|
|
|
if (caps) {
|
|
|
|
|
|
if (cp == 0x0451) cp = 0x0401; // ё -> Ё
|
|
|
|
|
|
else if (cp == 0x03C2) cp = 0x03A3; // ς -> Σ
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
|
else if (cp == 0x00FF) cp = 0x0178; // ÿ -> Ÿ (French; breaks the à-þ flat -0x20 rule below — Ÿ sits outside Latin-1 Supplement entirely)
|
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets
Alarm repeat (Clock Tools):
- NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday
convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends.
computeAlarmNextFire() scans the next 7 days for a matching weekday when
set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is
empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot
behaviour, so existing users see no change.
On-screen keyboard alphabets (Settings > Keyboard > Alphabet):
- KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints
(insertion, backspace and T9 in-place cycling all codepoint-aware now, so a
multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/
kbUtf8CharAt/kbUtf8LastCharBytes.
- Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/
French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page
cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed.
NodePrefs::keyboard_alt_alphabet picks which one, if any, is active.
- Lemon font is forced on transiently inside KeyboardWidget::render() (saved
and restored every call) whenever the alt-alphabet page is showing or
already-typed text has non-ASCII bytes, so composing is visible even if
Font is set to Default.
- Each script's caps-shift rule is distinct and documented in
kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς
exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for
Latin Extended-A (verified against every character actually used, not a
blanket rule for that whole Unicode block).
NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask,
keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D,
sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile).
Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the
Rooms keyboard note (no longer ASCII-only once an alphabet is enabled).
Not build-verified — no PlatformIO toolchain available this session. Caps
mappings cross-checked against Python's Unicode case tables; UTF-8 literal
bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
|
|
|
|
else if (cp >= 0x0430 && cp <= 0x044F) cp -= 0x20; // а-я -> А-Я
|
|
|
|
|
|
else if (cp >= 0x03B1 && cp <= 0x03C9) cp -= 0x20; // α-ω -> Α-Ω
|
|
|
|
|
|
else if (cp >= 0x00E0 && cp <= 0x00FE && cp != 0x00F7) cp -= 0x20; // à-þ -> À-Þ
|
|
|
|
|
|
else if (cp >= 0x0100 && cp < 0x0180 && (cp & 1) == 1) cp -= 1; // ą-ż (odd=lower) -> Ą-Ż
|
|
|
|
|
|
else if (cp >= 'a' && cp <= 'z') cp -= 0x20; // a-z -> A-Z
|
|
|
|
|
|
}
|
|
|
|
|
|
if (cp < 0x80) {
|
|
|
|
|
|
out[o++] = (char)cp;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
out[o++] = (char)(0xC0 | (cp >> 6));
|
|
|
|
|
|
out[o++] = (char)(0x80 | (cp & 0x3F));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
out[o] = '\0';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Codepoint count of a UTF-8 string (byte length overcounts once a 2-byte
|
|
|
|
|
|
// alphabet is involved — this is what T9 cycling needs instead of strlen()).
|
|
|
|
|
|
static int kbUtf8Len(const char* s) {
|
|
|
|
|
|
int n = 0;
|
|
|
|
|
|
const uint8_t* p = (const uint8_t*)s;
|
|
|
|
|
|
while (*p) { DisplayDriver::decodeCodepoint(p); n++; }
|
|
|
|
|
|
return n;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Extract the idx-th codepoint of a UTF-8 string as its own NUL-terminated
|
|
|
|
|
|
// UTF-8 bytes in `out` (>= 5 bytes). Empty string if idx is out of range.
|
|
|
|
|
|
static void kbUtf8CharAt(const char* s, int idx, char* out) {
|
|
|
|
|
|
const uint8_t* p = (const uint8_t*)s;
|
|
|
|
|
|
for (int i = 0; *p; i++) {
|
|
|
|
|
|
const uint8_t* start = p;
|
|
|
|
|
|
DisplayDriver::decodeCodepoint(p);
|
|
|
|
|
|
if (i == idx) {
|
|
|
|
|
|
int n = (int)(p - start);
|
|
|
|
|
|
memcpy(out, start, n);
|
|
|
|
|
|
out[n] = '\0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
out[0] = '\0';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Byte width of the LAST codepoint in buf[0..len) — for backspace and the T9
|
|
|
|
|
|
// in-place replace, which must remove/overwrite a whole codepoint, not one
|
|
|
|
|
|
// byte (a lone trailing continuation byte would otherwise corrupt a 2-byte
|
|
|
|
|
|
// character). Capped at 4 (UTF-8's max), though this codebase's alphabets are
|
|
|
|
|
|
// all <= 2 bytes today.
|
|
|
|
|
|
static int kbUtf8LastCharBytes(const char* buf, int len) {
|
|
|
|
|
|
if (len <= 0) return 0;
|
|
|
|
|
|
int n = 1;
|
|
|
|
|
|
while (n < len && n < 4 && ((uint8_t)buf[len - n] & 0xC0) == 0x80) n++;
|
|
|
|
|
|
return n;
|
2026-07-06 20:18:36 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-12 20:32:04 +02:00
|
|
|
|
static const int KB_PH_MAX = 20; // max placeholders in list (PopupMenu::PM_MAX_ITEMS=24 is the hard ceiling)
|
|
|
|
|
|
static const int KB_PH_LEN = 30; // max placeholder string length incl. null -- sized for the longest
|
|
|
|
|
|
// CLI-command candidate (AdminScreen), not just the short {x} tokens
|
2026-05-14 00:12:07 +02:00
|
|
|
|
static const int KB_PH_VISIBLE = 3; // items shown at once in overlay
|
|
|
|
|
|
|
2026-07-12 20:32:04 +02:00
|
|
|
|
struct KeyboardWidget;
|
|
|
|
|
|
// Optional hook: if set, called right before the placeholder picker opens so
|
|
|
|
|
|
// the caller can repopulate the list contextually (e.g. AdminScreen's CLI
|
|
|
|
|
|
// command autocomplete, filtered by what's already typed). When set, picking
|
|
|
|
|
|
// an entry also replaces the in-progress word (the text since the last
|
|
|
|
|
|
// space) instead of appending it -- true completion, not insertion. Fields
|
|
|
|
|
|
// that don't set this (the common case -- {loc}/{time} etc.) keep the
|
|
|
|
|
|
// original static-list, append-only behaviour untouched.
|
|
|
|
|
|
typedef void (*PlaceholderRefreshFn)(KeyboardWidget& kb, void* ctx);
|
|
|
|
|
|
|
2026-05-13 18:22:41 +02:00
|
|
|
|
struct KeyboardWidget {
|
|
|
|
|
|
char buf[KB_MAX_LEN + 1];
|
|
|
|
|
|
int len;
|
|
|
|
|
|
int max_len;
|
|
|
|
|
|
int row, col;
|
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets
Alarm repeat (Clock Tools):
- NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday
convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends.
computeAlarmNextFire() scans the next 7 days for a matching weekday when
set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is
empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot
behaviour, so existing users see no change.
On-screen keyboard alphabets (Settings > Keyboard > Alphabet):
- KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints
(insertion, backspace and T9 in-place cycling all codepoint-aware now, so a
multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/
kbUtf8CharAt/kbUtf8LastCharBytes.
- Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/
French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page
cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed.
NodePrefs::keyboard_alt_alphabet picks which one, if any, is active.
- Lemon font is forced on transiently inside KeyboardWidget::render() (saved
and restored every call) whenever the alt-alphabet page is showing or
already-typed text has non-ASCII bytes, so composing is visible even if
Font is set to Default.
- Each script's caps-shift rule is distinct and documented in
kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς
exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for
Latin Extended-A (verified against every character actually used, not a
blanket rule for that whole Unicode block).
NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask,
keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D,
sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile).
Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the
Rooms keyboard note (no longer ASCII-only once an alphabet is enabled).
Not build-verified — no PlatformIO toolchain available this session. Caps
mappings cross-checked against Python's Unicode case tables; UTF-8 literal
bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
|
|
|
|
int page; // see totalPages()/pageIsAltAlphabet()/pageIsSymbols() below
|
2026-05-13 18:22:41 +02:00
|
|
|
|
bool caps;
|
2026-07-17 10:02:00 +02:00
|
|
|
|
// Shift is one-shot by default (like a phone keyboard: capitalises just the
|
|
|
|
|
|
// next letter, then reverts) — Hold-Enter on Shift toggles caps_lock, which
|
|
|
|
|
|
// keeps it on for a whole run of capitals instead.
|
|
|
|
|
|
bool caps_lock = false;
|
2026-05-14 00:12:07 +02:00
|
|
|
|
char _ph_buf[KB_PH_MAX][KB_PH_LEN];
|
|
|
|
|
|
int _ph_count;
|
2026-05-14 08:06:26 +02:00
|
|
|
|
PopupMenu _ph_menu;
|
2026-07-12 20:32:04 +02:00
|
|
|
|
PlaceholderRefreshFn _ph_refresh = nullptr;
|
|
|
|
|
|
void* _ph_refresh_ctx = nullptr;
|
|
|
|
|
|
const char* _ph_title = "Placeholder:"; // popup title -- overridable so e.g. AdminScreen can say "Commands:"
|
|
|
|
|
|
void setPlaceholderRefresh(PlaceholderRefreshFn fn, void* ctx, const char* title = "Placeholder:") {
|
|
|
|
|
|
_ph_refresh = fn; _ph_refresh_ctx = ctx; _ph_title = title;
|
|
|
|
|
|
}
|
2026-05-13 18:22:41 +02:00
|
|
|
|
|
2026-07-02 12:21:52 +02:00
|
|
|
|
// Live setting lookup — set once by UITask::begin(). NULL only in tests/tools
|
|
|
|
|
|
// that construct a KeyboardWidget standalone, in which case isT9() defaults
|
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets
Alarm repeat (Clock Tools):
- NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday
convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends.
computeAlarmNextFire() scans the next 7 days for a matching weekday when
set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is
empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot
behaviour, so existing users see no change.
On-screen keyboard alphabets (Settings > Keyboard > Alphabet):
- KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints
(insertion, backspace and T9 in-place cycling all codepoint-aware now, so a
multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/
kbUtf8CharAt/kbUtf8LastCharBytes.
- Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/
French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page
cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed.
NodePrefs::keyboard_alt_alphabet picks which one, if any, is active.
- Lemon font is forced on transiently inside KeyboardWidget::render() (saved
and restored every call) whenever the alt-alphabet page is showing or
already-typed text has non-ASCII bytes, so composing is visible even if
Font is set to Default.
- Each script's caps-shift rule is distinct and documented in
kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς
exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for
Latin Extended-A (verified against every character actually used, not a
blanket rule for that whole Unicode block).
NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask,
keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D,
sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile).
Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the
Rooms keyboard note (no longer ASCII-only once an alphabet is enabled).
Not build-verified — no PlatformIO toolchain available this session. Caps
mappings cross-checked against Python's Unicode case tables; UTF-8 literal
bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
|
|
|
|
// to ABC and hasAltAlphabet() defaults to Latin-only.
|
2026-07-02 12:21:52 +02:00
|
|
|
|
NodePrefs* prefs = nullptr;
|
|
|
|
|
|
bool isT9() const { return prefs && prefs->keyboard_type == 1; }
|
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets
Alarm repeat (Clock Tools):
- NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday
convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends.
computeAlarmNextFire() scans the next 7 days for a matching weekday when
set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is
empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot
behaviour, so existing users see no change.
On-screen keyboard alphabets (Settings > Keyboard > Alphabet):
- KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints
(insertion, backspace and T9 in-place cycling all codepoint-aware now, so a
multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/
kbUtf8CharAt/kbUtf8LastCharBytes.
- Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/
French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page
cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed.
NodePrefs::keyboard_alt_alphabet picks which one, if any, is active.
- Lemon font is forced on transiently inside KeyboardWidget::render() (saved
and restored every call) whenever the alt-alphabet page is showing or
already-typed text has non-ASCII bytes, so composing is visible even if
Font is set to Default.
- Each script's caps-shift rule is distinct and documented in
kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς
exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for
Latin Extended-A (verified against every character actually used, not a
blanket rule for that whole Unicode block).
NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask,
keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D,
sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile).
Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the
Rooms keyboard note (no longer ASCII-only once an alphabet is enabled).
Not build-verified — no PlatformIO toolchain available this session. Caps
mappings cross-checked against Python's Unicode case tables; UTF-8 literal
bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
|
|
|
|
bool hasAltAlphabet() const {
|
|
|
|
|
|
return prefs && prefs->keyboard_alt_alphabet != NodePrefs::KB_ALPHABET_LATIN_ONLY;
|
|
|
|
|
|
}
|
2026-07-02 12:21:52 +02:00
|
|
|
|
|
|
|
|
|
|
// T9 multi-tap state: which grid cell is mid-cycle (-1 = none), its cycle
|
|
|
|
|
|
// position, and when the last Enter landed on it (for the timeout).
|
|
|
|
|
|
int t9_cell = -1;
|
|
|
|
|
|
int t9_cycle = 0;
|
|
|
|
|
|
uint32_t t9_last_ms = 0;
|
|
|
|
|
|
|
|
|
|
|
|
int gridRows() const { return isT9() ? KB_T9_ROWS : KB_ROWS_CHAR; }
|
|
|
|
|
|
int gridCols() const { return isT9() ? KB_T9_COLS : KB_COLS_CHAR; }
|
|
|
|
|
|
|
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets
Alarm repeat (Clock Tools):
- NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday
convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends.
computeAlarmNextFire() scans the next 7 days for a matching weekday when
set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is
empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot
behaviour, so existing users see no change.
On-screen keyboard alphabets (Settings > Keyboard > Alphabet):
- KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints
(insertion, backspace and T9 in-place cycling all codepoint-aware now, so a
multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/
kbUtf8CharAt/kbUtf8LastCharBytes.
- Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/
French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page
cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed.
NodePrefs::keyboard_alt_alphabet picks which one, if any, is active.
- Lemon font is forced on transiently inside KeyboardWidget::render() (saved
and restored every call) whenever the alt-alphabet page is showing or
already-typed text has non-ASCII bytes, so composing is visible even if
Font is set to Default.
- Each script's caps-shift rule is distinct and documented in
kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς
exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for
Latin Extended-A (verified against every character actually used, not a
blanket rule for that whole Unicode block).
NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask,
keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D,
sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile).
Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the
Rooms keyboard note (no longer ASCII-only once an alphabet is enabled).
Not build-verified — no PlatformIO toolchain available this session. Caps
mappings cross-checked against Python's Unicode case tables; UTF-8 literal
bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
|
|
|
|
// ── Page model ────────────────────────────────────────────────────────────
|
|
|
|
|
|
// Logical page order: 0 = Latin letters, [1 = the enabled alt alphabet],
|
|
|
|
|
|
// last = symbols. Without an alt alphabet this is exactly the original
|
|
|
|
|
|
// 2-page Latin/symbols cycle; enabling one inserts its page in the middle,
|
|
|
|
|
|
// so the #@/abc key's existing cycle (case 4 below) reaches it for free.
|
|
|
|
|
|
int totalPages() const { return hasAltAlphabet() ? 3 : 2; }
|
|
|
|
|
|
bool pageIsAltAlphabet(int pg) const { return hasAltAlphabet() && pg == 1; }
|
|
|
|
|
|
bool pageIsSymbols(int pg) const { return pg == totalPages() - 1; }
|
|
|
|
|
|
|
|
|
|
|
|
// ABC-grid cell content as a NUL-terminated UTF-8 string (1 codepoint).
|
|
|
|
|
|
// Latin/symbols cells come back through a small scratch buffer since
|
|
|
|
|
|
// KB_CHARS stores single ASCII bytes, not strings; alt-alphabet cells are
|
|
|
|
|
|
// literal string-table entries, returned directly.
|
|
|
|
|
|
const char* cellStr(int r, int c) const {
|
|
|
|
|
|
if (pageIsAltAlphabet(page)) {
|
|
|
|
|
|
switch (prefs->keyboard_alt_alphabet) {
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
|
case NodePrefs::KB_ALPHABET_CYRILLIC: return KB_CYRILLIC_CHARS[r][c];
|
|
|
|
|
|
case NodePrefs::KB_ALPHABET_GREEK: return KB_GREEK_CHARS[r][c];
|
|
|
|
|
|
case NodePrefs::KB_ALPHABET_POLISH: return KB_POLISH_CHARS[r][c];
|
|
|
|
|
|
case NodePrefs::KB_ALPHABET_CZECH: return KB_CZECH_CHARS[r][c];
|
|
|
|
|
|
case NodePrefs::KB_ALPHABET_SLOVAK: return KB_SLOVAK_CHARS[r][c];
|
|
|
|
|
|
case NodePrefs::KB_ALPHABET_GERMAN: return KB_GERMAN_CHARS[r][c];
|
|
|
|
|
|
case NodePrefs::KB_ALPHABET_FRENCH: return KB_FRENCH_CHARS[r][c];
|
|
|
|
|
|
case NodePrefs::KB_ALPHABET_SPANISH: return KB_SPANISH_CHARS[r][c];
|
|
|
|
|
|
case NodePrefs::KB_ALPHABET_PORTUGUESE: return KB_PORTUGUESE_CHARS[r][c];
|
|
|
|
|
|
case NodePrefs::KB_ALPHABET_NORDIC: return KB_NORDIC_CHARS[r][c];
|
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets
Alarm repeat (Clock Tools):
- NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday
convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends.
computeAlarmNextFire() scans the next 7 days for a matching weekday when
set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is
empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot
behaviour, so existing users see no change.
On-screen keyboard alphabets (Settings > Keyboard > Alphabet):
- KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints
(insertion, backspace and T9 in-place cycling all codepoint-aware now, so a
multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/
kbUtf8CharAt/kbUtf8LastCharBytes.
- Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/
French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page
cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed.
NodePrefs::keyboard_alt_alphabet picks which one, if any, is active.
- Lemon font is forced on transiently inside KeyboardWidget::render() (saved
and restored every call) whenever the alt-alphabet page is showing or
already-typed text has non-ASCII bytes, so composing is visible even if
Font is set to Default.
- Each script's caps-shift rule is distinct and documented in
kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς
exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for
Latin Extended-A (verified against every character actually used, not a
blanket rule for that whole Unicode block).
NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask,
keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D,
sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile).
Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the
Rooms keyboard note (no longer ASCII-only once an alphabet is enabled).
Not build-verified — no PlatformIO toolchain available this session. Caps
mappings cross-checked against Python's Unicode case tables; UTF-8 literal
bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
|
|
|
|
default: return "?";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
static char single[2];
|
|
|
|
|
|
single[0] = KB_CHARS[pageIsSymbols(page) ? 1 : 0][r][c];
|
|
|
|
|
|
single[1] = '\0';
|
|
|
|
|
|
return single;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// T9 group string (UTF-8) for the given cell (0-8), page-aware like cellStr.
|
|
|
|
|
|
const char* t9GroupStr(int cell) const {
|
|
|
|
|
|
if (pageIsAltAlphabet(page)) {
|
|
|
|
|
|
switch (prefs->keyboard_alt_alphabet) {
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
|
case NodePrefs::KB_ALPHABET_CYRILLIC: return KB_T9_GROUPS_CYRILLIC[cell];
|
|
|
|
|
|
case NodePrefs::KB_ALPHABET_GREEK: return KB_T9_GROUPS_GREEK[cell];
|
|
|
|
|
|
case NodePrefs::KB_ALPHABET_POLISH: return KB_T9_GROUPS_POLISH[cell];
|
|
|
|
|
|
case NodePrefs::KB_ALPHABET_CZECH: return KB_T9_GROUPS_CZECH[cell];
|
|
|
|
|
|
case NodePrefs::KB_ALPHABET_SLOVAK: return KB_T9_GROUPS_SLOVAK[cell];
|
|
|
|
|
|
case NodePrefs::KB_ALPHABET_GERMAN: return KB_T9_GROUPS_GERMAN[cell];
|
|
|
|
|
|
case NodePrefs::KB_ALPHABET_FRENCH: return KB_T9_GROUPS_FRENCH[cell];
|
|
|
|
|
|
case NodePrefs::KB_ALPHABET_SPANISH: return KB_T9_GROUPS_SPANISH[cell];
|
|
|
|
|
|
case NodePrefs::KB_ALPHABET_PORTUGUESE: return KB_T9_GROUPS_PORTUGUESE[cell];
|
|
|
|
|
|
case NodePrefs::KB_ALPHABET_NORDIC: return KB_T9_GROUPS_NORDIC[cell];
|
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets
Alarm repeat (Clock Tools):
- NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday
convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends.
computeAlarmNextFire() scans the next 7 days for a matching weekday when
set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is
empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot
behaviour, so existing users see no change.
On-screen keyboard alphabets (Settings > Keyboard > Alphabet):
- KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints
(insertion, backspace and T9 in-place cycling all codepoint-aware now, so a
multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/
kbUtf8CharAt/kbUtf8LastCharBytes.
- Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/
French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page
cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed.
NodePrefs::keyboard_alt_alphabet picks which one, if any, is active.
- Lemon font is forced on transiently inside KeyboardWidget::render() (saved
and restored every call) whenever the alt-alphabet page is showing or
already-typed text has non-ASCII bytes, so composing is visible even if
Font is set to Default.
- Each script's caps-shift rule is distinct and documented in
kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς
exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for
Latin Extended-A (verified against every character actually used, not a
blanket rule for that whole Unicode block).
NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask,
keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D,
sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile).
Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the
Rooms keyboard note (no longer ASCII-only once an alphabet is enabled).
Not build-verified — no PlatformIO toolchain available this session. Caps
mappings cross-checked against Python's Unicode case tables; UTF-8 literal
bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
|
|
|
|
default: return "?";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return KB_T9_GROUPS[pageIsSymbols(page) ? 1 : 0][cell];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Compact ASCII hint for the #@/abc key when it's about to switch to the
|
|
|
|
|
|
// alt-alphabet page. Deliberately ASCII (not the alphabet's own script) so
|
|
|
|
|
|
// it renders correctly even when Lemon isn't the user's current font choice
|
|
|
|
|
|
// — unlike the alphabet's own grid, this hint is visible from the Latin/
|
|
|
|
|
|
// symbols pages too, where render() doesn't force Lemon on (see render()).
|
|
|
|
|
|
static const char* altAlphabetHint(uint8_t alt) {
|
|
|
|
|
|
switch (alt) {
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
|
case NodePrefs::KB_ALPHABET_CYRILLIC: return "CY";
|
|
|
|
|
|
case NodePrefs::KB_ALPHABET_GREEK: return "GR";
|
|
|
|
|
|
case NodePrefs::KB_ALPHABET_POLISH: return "PL";
|
|
|
|
|
|
case NodePrefs::KB_ALPHABET_CZECH: return "CZ";
|
|
|
|
|
|
case NodePrefs::KB_ALPHABET_SLOVAK: return "SK";
|
|
|
|
|
|
case NodePrefs::KB_ALPHABET_GERMAN: return "DE";
|
|
|
|
|
|
case NodePrefs::KB_ALPHABET_FRENCH: return "FR";
|
|
|
|
|
|
case NodePrefs::KB_ALPHABET_SPANISH: return "ES";
|
|
|
|
|
|
case NodePrefs::KB_ALPHABET_PORTUGUESE: return "PT";
|
|
|
|
|
|
case NodePrefs::KB_ALPHABET_NORDIC: return "ND";
|
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets
Alarm repeat (Clock Tools):
- NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday
convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends.
computeAlarmNextFire() scans the next 7 days for a matching weekday when
set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is
empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot
behaviour, so existing users see no change.
On-screen keyboard alphabets (Settings > Keyboard > Alphabet):
- KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints
(insertion, backspace and T9 in-place cycling all codepoint-aware now, so a
multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/
kbUtf8CharAt/kbUtf8LastCharBytes.
- Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/
French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page
cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed.
NodePrefs::keyboard_alt_alphabet picks which one, if any, is active.
- Lemon font is forced on transiently inside KeyboardWidget::render() (saved
and restored every call) whenever the alt-alphabet page is showing or
already-typed text has non-ASCII bytes, so composing is visible even if
Font is set to Default.
- Each script's caps-shift rule is distinct and documented in
kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς
exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for
Latin Extended-A (verified against every character actually used, not a
blanket rule for that whole Unicode block).
NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask,
keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D,
sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile).
Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the
Rooms keyboard note (no longer ASCII-only once an alphabet is enabled).
Not build-verified — no PlatformIO toolchain available this session. Caps
mappings cross-checked against Python's Unicode case tables; UTF-8 literal
bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
|
|
|
|
default: return "?";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 18:22:41 +02:00
|
|
|
|
enum Result { NONE, DONE, CANCELLED };
|
|
|
|
|
|
|
2026-05-13 18:34:33 +02:00
|
|
|
|
void begin(const char* initial = "", int max = KB_MAX_LEN) {
|
2026-05-13 18:22:41 +02:00
|
|
|
|
max_len = (max > KB_MAX_LEN) ? KB_MAX_LEN : max;
|
|
|
|
|
|
strncpy(buf, initial, max_len);
|
|
|
|
|
|
buf[max_len] = '\0';
|
|
|
|
|
|
len = strlen(buf);
|
|
|
|
|
|
row = col = 0;
|
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>
2026-06-15 10:47:50 +02:00
|
|
|
|
page = 0;
|
2026-05-14 08:06:26 +02:00
|
|
|
|
caps = false;
|
2026-07-17 10:02:00 +02:00
|
|
|
|
caps_lock = false;
|
2026-07-02 12:21:52 +02:00
|
|
|
|
t9_cell = -1;
|
|
|
|
|
|
t9_cycle = 0;
|
2026-05-14 08:06:26 +02:00
|
|
|
|
_ph_menu.active = false;
|
2026-07-12 20:32:04 +02:00
|
|
|
|
_ph_refresh = nullptr; // opt-in per session -- the owning screen re-sets it if it wants
|
|
|
|
|
|
_ph_refresh_ctx = nullptr; // contextual autocomplete right after this begin()
|
|
|
|
|
|
_ph_title = "Placeholder:";
|
2026-05-14 00:12:07 +02:00
|
|
|
|
// default placeholders — always available
|
|
|
|
|
|
_ph_count = 0;
|
|
|
|
|
|
addPlaceholder("{loc}");
|
|
|
|
|
|
addPlaceholder("{time}");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
refactor(ui): apply code review fixes — bugs, risk, optimisations, cleanup
Fixes 7 bugs (timing UB on unsigned long, abs() no-op, XBM CRC PROGMEM
scan, CayenneLPP heap thrash ×2, UTF-8 reply prefix, lock-seq reset on
wake), 3 risk items (notif melody stop guard, bot trigger pre-lowercase,
lock-seq cleared on display wake), plus optimisations (fmtMsgAge/
getTextWidth caching, RTTTL builder consolidated to NodePrefs, HP_*
constants and homePageLabel table moved to NodePrefs, DataStore flat
lambda schema, translateUTF8Static factored out) and minor cleanup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 20:03:45 +02:00
|
|
|
|
void clearPlaceholders() { _ph_count = 0; }
|
|
|
|
|
|
|
2026-05-14 00:12:07 +02:00
|
|
|
|
void addPlaceholder(const char* ph) {
|
|
|
|
|
|
if (_ph_count < KB_PH_MAX) {
|
|
|
|
|
|
strncpy(_ph_buf[_ph_count], ph, KB_PH_LEN - 1);
|
|
|
|
|
|
_ph_buf[_ph_count][KB_PH_LEN - 1] = '\0';
|
|
|
|
|
|
_ph_count++;
|
|
|
|
|
|
}
|
2026-05-13 18:22:41 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int render(DisplayDriver& display) {
|
2026-07-02 12:21:52 +02:00
|
|
|
|
// A stale mid-cycle T9 press (no further input since) finalizes on its own —
|
|
|
|
|
|
// the character is already committed to buf, this just stops a later Enter
|
|
|
|
|
|
// on the same cell from being treated as a continued cycle.
|
|
|
|
|
|
if (t9_cell >= 0 && millis() - t9_last_ms > KB_T9_TIMEOUT_MS) t9_cell = -1;
|
|
|
|
|
|
|
2026-07-15 11:16:25 +02:00
|
|
|
|
// Single UI font (misc-fixed 5x7) covers Latin/Greek/Cyrillic — the keyboard
|
|
|
|
|
|
// renders in it directly, no per-render font switching or headroom padding.
|
2026-05-13 18:22:41 +02:00
|
|
|
|
display.setTextSize(1);
|
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
|
|
|
|
|
2026-07-02 12:21:52 +02:00
|
|
|
|
const int rows = gridRows();
|
|
|
|
|
|
const int cols = gridCols();
|
2026-05-22 18:34:20 +02:00
|
|
|
|
const int lh = display.getLineHeight();
|
|
|
|
|
|
const int cw = display.getCharWidth();
|
2026-07-02 12:21:52 +02:00
|
|
|
|
const int cell_w = display.width() / cols;
|
2026-05-22 23:13:27 +02:00
|
|
|
|
// compact: don't stretch cells beyond lh; freed vertical space goes to preview lines
|
2026-07-02 12:21:52 +02:00
|
|
|
|
const int kb_h = (rows + 1) * lh;
|
2026-06-18 09:16:57 +02:00
|
|
|
|
const int preview_h = display.height() - kb_h - display.sepH();
|
2026-05-22 23:13:27 +02:00
|
|
|
|
const int prev_lines = (preview_h / lh) > 1 ? (preview_h / lh) : 1;
|
|
|
|
|
|
const int sep_y = prev_lines * lh;
|
2026-07-15 11:16:25 +02:00
|
|
|
|
const int chars_y = sep_y + display.sepH();
|
2026-07-02 12:21:52 +02:00
|
|
|
|
const int cell_h = (display.height() - chars_y) / (rows + 1);
|
|
|
|
|
|
const int spec_y = chars_y + rows * cell_h;
|
2026-05-22 18:34:20 +02:00
|
|
|
|
const int spec_w = display.width() / KB_SPECIAL;
|
|
|
|
|
|
|
2026-05-22 23:13:27 +02:00
|
|
|
|
// multi-line text preview: cursor always on last preview line
|
|
|
|
|
|
int cpl = display.width() / cw; // chars per preview line
|
|
|
|
|
|
if (cpl < 1) cpl = 1;
|
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>
2026-06-15 10:47:50 +02:00
|
|
|
|
if (cpl > KB_PREVIEW_CAP) cpl = KB_PREVIEW_CAP; // never overrun linebuf below
|
2026-05-22 23:13:27 +02:00
|
|
|
|
int cursor_line = len / cpl;
|
|
|
|
|
|
int first_line = (cursor_line >= prev_lines) ? (cursor_line - prev_lines + 1) : 0;
|
|
|
|
|
|
int start = first_line * cpl;
|
|
|
|
|
|
for (int pl = 0; pl < prev_lines; pl++) {
|
|
|
|
|
|
int ps = start + pl * cpl;
|
|
|
|
|
|
int pe = ps + cpl;
|
|
|
|
|
|
bool cursor_here = (ps <= len && (len < pe || pl == prev_lines - 1));
|
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>
2026-06-15 10:47:50 +02:00
|
|
|
|
char linebuf[KB_PREVIEW_CAP + 2]; // cpl chars + cursor '_' + NUL
|
2026-05-22 23:13:27 +02:00
|
|
|
|
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);
|
|
|
|
|
|
} else if (len > ps) {
|
|
|
|
|
|
int nc = (len < pe) ? (len - ps) : cpl;
|
|
|
|
|
|
snprintf(linebuf, sizeof(linebuf), "%.*s", nc, buf + ps);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
linebuf[0] = '\0';
|
|
|
|
|
|
}
|
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>
2026-06-15 10:47:50 +02:00
|
|
|
|
char linebuf_t[KB_PREVIEW_CAP + 2];
|
2026-05-24 15:28:10 +02:00
|
|
|
|
display.translateUTF8ToBlocks(linebuf_t, linebuf, sizeof(linebuf_t));
|
2026-05-22 23:13:27 +02:00
|
|
|
|
display.setCursor(0, pl * lh);
|
2026-05-24 15:28:10 +02:00
|
|
|
|
display.print(linebuf_t);
|
2026-05-22 23:13:27 +02:00
|
|
|
|
}
|
2026-06-18 09:16:57 +02:00
|
|
|
|
display.fillRect(0, sep_y, display.width(), display.sepH());
|
2026-05-13 18:22:41 +02:00
|
|
|
|
|
|
|
|
|
|
// character grid
|
2026-07-02 12:21:52 +02:00
|
|
|
|
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;
|
feat(keyboard): show digit on each T9 key; complete v1.22 docs
T9 keys now read like a phone keypad — each cell is labelled <digit><group>
(e.g. 2abc), with the digit matching what the multi-tap cycle lands on after
the letters. No separator space: the widest group (".,!?'-") plus the digit
already fills a narrow 128px OLED cell at 3 columns.
Also fills in the v1.22 release notes (T9 keyboard, trail auto-save, Clock
Tools in Tools, Map/Shutdown reorderable, battery-read fix, the UI-audit fix
batch, discover-dedup, favourites self-heal, preset-name placeholder, OLED
frame-skip, e-ink full-refresh) and syncs the solo-feature docs: new Keyboard
settings section, Trail Auto-save row, and Clock Tools reachable from Tools.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 20:08:29 +02:00
|
|
|
|
// 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
|
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets
Alarm repeat (Clock Tools):
- NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday
convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends.
computeAlarmNextFire() scans the next 7 days for a matching weekday when
set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is
empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot
behaviour, so existing users see no change.
On-screen keyboard alphabets (Settings > Keyboard > Alphabet):
- KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints
(insertion, backspace and T9 in-place cycling all codepoint-aware now, so a
multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/
kbUtf8CharAt/kbUtf8LastCharBytes.
- Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/
French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page
cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed.
NodePrefs::keyboard_alt_alphabet picks which one, if any, is active.
- Lemon font is forced on transiently inside KeyboardWidget::render() (saved
and restored every call) whenever the alt-alphabet page is showing or
already-typed text has non-ASCII bytes, so composing is visible even if
Font is set to Default.
- Each script's caps-shift rule is distinct and documented in
kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς
exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for
Latin Extended-A (verified against every character actually used, not a
blanket rule for that whole Unicode block).
NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask,
keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D,
sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile).
Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the
Rooms keyboard note (no longer ASCII-only once an alphabet is enabled).
Not build-verified — no PlatformIO toolchain available this session. Caps
mappings cross-checked against Python's Unicode case tables; UTF-8 literal
bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
|
|
|
|
// (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);
|
2026-07-02 12:21:52 +02:00
|
|
|
|
int cx = c * cell_w;
|
|
|
|
|
|
display.drawSelectionRow(cx, y - 1, cell_w - 1, cell_h, sel);
|
feat(keyboard): show digit on each T9 key; complete v1.22 docs
T9 keys now read like a phone keypad — each cell is labelled <digit><group>
(e.g. 2abc), with the digit matching what the multi-tap cycle lands on after
the letters. No separator space: the widest group (".,!?'-") plus the digit
already fills a narrow 128px OLED cell at 3 columns.
Also fills in the v1.22 release notes (T9 keyboard, trail auto-save, Clock
Tools in Tools, Map/Shutdown reorderable, battery-read fix, the UI-audit fix
batch, discover-dedup, favourites self-heal, preset-name placeholder, OLED
frame-skip, e-ink full-refresh) and syncs the solo-feature docs: new Keyboard
settings section, Trail Auto-save row, and Clock Tools reachable from Tools.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 20:08:29 +02:00
|
|
|
|
int tw = display.getTextWidth(label);
|
2026-07-02 12:21:52 +02:00
|
|
|
|
display.setCursor(cx + (cell_w - tw) / 2, y);
|
feat(keyboard): show digit on each T9 key; complete v1.22 docs
T9 keys now read like a phone keypad — each cell is labelled <digit><group>
(e.g. 2abc), with the digit matching what the multi-tap cycle lands on after
the letters. No separator space: the widest group (".,!?'-") plus the digit
already fills a narrow 128px OLED cell at 3 columns.
Also fills in the v1.22 release notes (T9 keyboard, trail auto-save, Clock
Tools in Tools, Map/Shutdown reorderable, battery-read fix, the UI-audit fix
batch, discover-dedup, favourites self-heal, preset-name placeholder, OLED
frame-skip, e-ink full-refresh) and syncs the solo-feature docs: new Keyboard
settings section, Trail Auto-save row, and Clock Tools reachable from Tools.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 20:08:29 +02:00
|
|
|
|
display.print(label);
|
2026-07-02 12:21:52 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} 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);
|
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets
Alarm repeat (Clock Tools):
- NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday
convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends.
computeAlarmNextFire() scans the next 7 days for a matching weekday when
set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is
empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot
behaviour, so existing users see no change.
On-screen keyboard alphabets (Settings > Keyboard > Alphabet):
- KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints
(insertion, backspace and T9 in-place cycling all codepoint-aware now, so a
multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/
kbUtf8CharAt/kbUtf8LastCharBytes.
- Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/
French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page
cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed.
NodePrefs::keyboard_alt_alphabet picks which one, if any, is active.
- Lemon font is forced on transiently inside KeyboardWidget::render() (saved
and restored every call) whenever the alt-alphabet page is showing or
already-typed text has non-ASCII bytes, so composing is visible even if
Font is set to Default.
- Each script's caps-shift rule is distinct and documented in
kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς
exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for
Latin Extended-A (verified against every character actually used, not a
blanket rule for that whole Unicode block).
NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask,
keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D,
sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile).
Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the
Rooms keyboard note (no longer ASCII-only once an alphabet is enabled).
Not build-verified — no PlatformIO toolchain available this session. Caps
mappings cross-checked against Python's Unicode case tables; UTF-8 literal
bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
|
|
|
|
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] = '_';
|
2026-07-02 12:21:52 +02:00
|
|
|
|
int cx = c * cell_w;
|
|
|
|
|
|
display.drawSelectionRow(cx, y - 1, cell_w - 1, cell_h, sel);
|
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets
Alarm repeat (Clock Tools):
- NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday
convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends.
computeAlarmNextFire() scans the next 7 days for a matching weekday when
set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is
empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot
behaviour, so existing users see no change.
On-screen keyboard alphabets (Settings > Keyboard > Alphabet):
- KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints
(insertion, backspace and T9 in-place cycling all codepoint-aware now, so a
multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/
kbUtf8CharAt/kbUtf8LastCharBytes.
- Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/
French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page
cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed.
NodePrefs::keyboard_alt_alphabet picks which one, if any, is active.
- Lemon font is forced on transiently inside KeyboardWidget::render() (saved
and restored every call) whenever the alt-alphabet page is showing or
already-typed text has non-ASCII bytes, so composing is visible even if
Font is set to Default.
- Each script's caps-shift rule is distinct and documented in
kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς
exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for
Latin Extended-A (verified against every character actually used, not a
blanket rule for that whole Unicode block).
NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask,
keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D,
sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile).
Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the
Rooms keyboard note (no longer ASCII-only once an alphabet is enabled).
Not build-verified — no PlatformIO toolchain available this session. Caps
mappings cross-checked against Python's Unicode case tables; UTF-8 literal
bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
|
|
|
|
int tw = display.getTextWidth(ch_buf);
|
|
|
|
|
|
display.setCursor(cx + (cell_w - tw) / 2, y);
|
2026-07-02 12:21:52 +02:00
|
|
|
|
display.print(ch_buf);
|
|
|
|
|
|
}
|
2026-05-13 18:22:41 +02: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>
2026-06-15 10:47:50 +02:00
|
|
|
|
// 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
|
2026-05-13 18:22:41 +02:00
|
|
|
|
for (int i = 0; i < KB_SPECIAL; i++) {
|
2026-07-02 12:21:52 +02:00
|
|
|
|
bool sel = (row == rows && col == i);
|
2026-05-13 18:22:41 +02:00
|
|
|
|
bool active = (i == 0 && caps);
|
2026-05-22 18:34:20 +02:00
|
|
|
|
int sx = i * spec_w;
|
refactor(ui): drawSelectionRow helper for the 5-line invert/restore pattern
Eight screens repeated the same if/else block to invert the row
background for selected items. Add DisplayDriver::drawSelectionRow that
sets LIGHT, optionally fills the rect, and leaves the colour as DARK
when sel (so the caller's text renders inverted). 61 lines removed
across SettingsScreen, KeyboardWidget (cells + special row), ToolsScreen,
DashboardConfigScreen, BotScreen, RingtoneEditorScreen (note slots +
menu), NearbyScreen, QuickMsgScreen (4 list views).
Card-style rows (QuickMsg hist, NearbyScreen discover, RingtoneEditor
notes display) keep their original drawRect/partial-fill outline for
unselected — they don't match the simple-invert pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 20:33:02 +02:00
|
|
|
|
display.drawSelectionRow(sx, spec_y - 1, spec_w - 1, cell_h, sel || active);
|
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>
2026-06-15 10:47:50 +02:00
|
|
|
|
if (i == 3 || i == 4) { // text keys: {} picker, page toggle
|
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets
Alarm repeat (Clock Tools):
- NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday
convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends.
computeAlarmNextFire() scans the next 7 days for a matching weekday when
set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is
empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot
behaviour, so existing users see no change.
On-screen keyboard alphabets (Settings > Keyboard > Alphabet):
- KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints
(insertion, backspace and T9 in-place cycling all codepoint-aware now, so a
multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/
kbUtf8CharAt/kbUtf8LastCharBytes.
- Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/
French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page
cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed.
NodePrefs::keyboard_alt_alphabet picks which one, if any, is active.
- Lemon font is forced on transiently inside KeyboardWidget::render() (saved
and restored every call) whenever the alt-alphabet page is showing or
already-typed text has non-ASCII bytes, so composing is visible even if
Font is set to Default.
- Each script's caps-shift rule is distinct and documented in
kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς
exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for
Latin Extended-A (verified against every character actually used, not a
blanket rule for that whole Unicode block).
NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask,
keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D,
sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile).
Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the
Rooms keyboard note (no longer ASCII-only once an alphabet is enabled).
Not build-verified — no PlatformIO toolchain available this session. Caps
mappings cross-checked against Python's Unicode case tables; UTF-8 literal
bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
|
|
|
|
// 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) ? "#@"
|
|
|
|
|
|
: pageIsAltAlphabet(next) ? altAlphabetHint(prefs->keyboard_alt_alphabet)
|
|
|
|
|
|
: "abc";
|
|
|
|
|
|
}
|
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>
2026-06-15 10:47:50 +02:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
2026-05-13 18:22:41 +02:00
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-14 08:06:26 +02:00
|
|
|
|
// placeholder picker overlay (drawn on top of keyboard)
|
|
|
|
|
|
if (_ph_menu.active) _ph_menu.render(display);
|
2026-05-13 18:22:41 +02:00
|
|
|
|
return 50;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Result handleInput(char c) {
|
|
|
|
|
|
// placeholder overlay consumes all input
|
2026-05-14 08:06:26 +02:00
|
|
|
|
if (_ph_menu.active) {
|
|
|
|
|
|
auto res = _ph_menu.handleInput(c);
|
|
|
|
|
|
if (res == PopupMenu::SELECTED) {
|
|
|
|
|
|
int idx = _ph_menu.selectedIndex();
|
|
|
|
|
|
const char* ph = _ph_buf[idx];
|
2026-05-13 18:22:41 +02:00
|
|
|
|
int ph_len = strlen(ph);
|
2026-07-12 20:32:04 +02:00
|
|
|
|
// Contextual (refresh-hook) fields complete the in-progress word --
|
|
|
|
|
|
// the text since the last space -- instead of appending after it, so
|
|
|
|
|
|
// picking a match doesn't duplicate what's already been typed.
|
|
|
|
|
|
int base_len = len;
|
|
|
|
|
|
if (_ph_refresh) {
|
|
|
|
|
|
while (base_len > 0 && buf[base_len - 1] != ' ') base_len--;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (base_len + ph_len <= max_len) {
|
|
|
|
|
|
memcpy(buf + base_len, ph, ph_len);
|
|
|
|
|
|
len = base_len + ph_len;
|
2026-05-13 18:22:41 +02:00
|
|
|
|
buf[len] = '\0';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return NONE;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-17 10:02:00 +02:00
|
|
|
|
if (c == KEY_CANCEL) return CANCELLED;
|
2026-05-13 18:22:41 +02:00
|
|
|
|
|
2026-07-02 12:21:52 +02:00
|
|
|
|
const int rows = gridRows();
|
|
|
|
|
|
const int cols = gridCols();
|
|
|
|
|
|
|
2026-07-17 10:02:00 +02:00
|
|
|
|
// Hold-Enter is normally "cancel", but over the two keys where a long-press
|
|
|
|
|
|
// has its own well-known meaning on a phone keyboard, it does that instead:
|
|
|
|
|
|
// Shift -> toggle a persistent caps-lock (a plain tap is one-shot -- see the
|
|
|
|
|
|
// commit sites below); Backspace -> clear the whole field in one action
|
|
|
|
|
|
// instead of holding it down. Every other cell keeps hold-to-cancel.
|
|
|
|
|
|
if (c == KEY_CONTEXT_MENU) {
|
|
|
|
|
|
if (row == rows && col == 0) { // Shift
|
|
|
|
|
|
caps_lock = !caps_lock;
|
|
|
|
|
|
caps = caps_lock;
|
|
|
|
|
|
return NONE;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (row == rows && col == 2) { // Backspace
|
|
|
|
|
|
len = 0; buf[0] = '\0';
|
|
|
|
|
|
t9_cell = -1;
|
|
|
|
|
|
return NONE;
|
|
|
|
|
|
}
|
|
|
|
|
|
return CANCELLED;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 18:22:41 +02:00
|
|
|
|
if (c == KEY_UP) {
|
|
|
|
|
|
if (row > 0) {
|
|
|
|
|
|
row--;
|
2026-07-02 12:21:52 +02:00
|
|
|
|
if (row == rows - 1) // leaving special row upward
|
|
|
|
|
|
col = col * cols / KB_SPECIAL;
|
2026-06-18 09:16:57 +02:00
|
|
|
|
} else {
|
2026-07-02 12:21:52 +02:00
|
|
|
|
row = rows; // wrap up onto the special row
|
|
|
|
|
|
col = col * KB_SPECIAL / cols;
|
2026-05-13 18:22:41 +02:00
|
|
|
|
}
|
2026-07-02 12:21:52 +02:00
|
|
|
|
t9_cell = -1; // navigating away finalizes any pending multi-tap cycle
|
2026-05-13 18:22:41 +02:00
|
|
|
|
return NONE;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (c == KEY_DOWN) {
|
2026-07-02 12:21:52 +02:00
|
|
|
|
if (row < rows) {
|
2026-05-13 18:22:41 +02:00
|
|
|
|
row++;
|
2026-07-02 12:21:52 +02:00
|
|
|
|
if (row == rows) // entering special row
|
|
|
|
|
|
col = col * KB_SPECIAL / cols;
|
2026-06-18 09:16:57 +02:00
|
|
|
|
} else {
|
|
|
|
|
|
row = 0; // wrap down onto the first char row
|
2026-07-02 12:21:52 +02:00
|
|
|
|
col = col * cols / KB_SPECIAL;
|
2026-05-13 18:22:41 +02:00
|
|
|
|
}
|
2026-07-02 12:21:52 +02:00
|
|
|
|
t9_cell = -1;
|
2026-05-13 18:22:41 +02:00
|
|
|
|
return NONE;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (c == KEY_LEFT) {
|
2026-07-02 12:21:52 +02:00
|
|
|
|
int max_col = (row == rows) ? KB_SPECIAL - 1 : cols - 1;
|
2026-06-18 09:16:57 +02:00
|
|
|
|
col = (col > 0) ? col - 1 : max_col;
|
2026-07-02 12:21:52 +02:00
|
|
|
|
t9_cell = -1;
|
2026-05-13 18:22:41 +02:00
|
|
|
|
return NONE;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (c == KEY_RIGHT) {
|
2026-07-02 12:21:52 +02:00
|
|
|
|
int max_col = (row == rows) ? KB_SPECIAL - 1 : cols - 1;
|
2026-06-18 09:16:57 +02:00
|
|
|
|
col = (col < max_col) ? col + 1 : 0;
|
2026-07-02 12:21:52 +02:00
|
|
|
|
t9_cell = -1;
|
2026-05-13 18:22:41 +02:00
|
|
|
|
return NONE;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (c == KEY_ENTER) {
|
2026-07-02 12:21:52 +02:00
|
|
|
|
if (row < rows && isT9()) {
|
|
|
|
|
|
int cell = row * cols + col;
|
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets
Alarm repeat (Clock Tools):
- NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday
convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends.
computeAlarmNextFire() scans the next 7 days for a matching weekday when
set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is
empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot
behaviour, so existing users see no change.
On-screen keyboard alphabets (Settings > Keyboard > Alphabet):
- KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints
(insertion, backspace and T9 in-place cycling all codepoint-aware now, so a
multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/
kbUtf8CharAt/kbUtf8LastCharBytes.
- Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/
French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page
cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed.
NodePrefs::keyboard_alt_alphabet picks which one, if any, is active.
- Lemon font is forced on transiently inside KeyboardWidget::render() (saved
and restored every call) whenever the alt-alphabet page is showing or
already-typed text has non-ASCII bytes, so composing is visible even if
Font is set to Default.
- Each script's caps-shift rule is distinct and documented in
kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς
exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for
Latin Extended-A (verified against every character actually used, not a
blanket rule for that whole Unicode block).
NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask,
keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D,
sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile).
Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the
Rooms keyboard note (no longer ASCII-only once an alphabet is enabled).
Not build-verified — no PlatformIO toolchain available this session. Caps
mappings cross-checked against Python's Unicode case tables; UTF-8 literal
bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
|
|
|
|
const char* group = t9GroupStr(cell);
|
|
|
|
|
|
int glen = kbUtf8Len(group); // codepoint count, not byte length
|
2026-07-02 12:21:52 +02:00
|
|
|
|
int total = glen + 1; // + the cell's own digit, at the end of the cycle
|
|
|
|
|
|
bool cycling = (t9_cell == cell) && (millis() - t9_last_ms < KB_T9_TIMEOUT_MS);
|
|
|
|
|
|
if (cycling) {
|
|
|
|
|
|
t9_cycle = (t9_cycle + 1) % total;
|
|
|
|
|
|
if (len > 0) {
|
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets
Alarm repeat (Clock Tools):
- NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday
convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends.
computeAlarmNextFire() scans the next 7 days for a matching weekday when
set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is
empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot
behaviour, so existing users see no change.
On-screen keyboard alphabets (Settings > Keyboard > Alphabet):
- KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints
(insertion, backspace and T9 in-place cycling all codepoint-aware now, so a
multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/
kbUtf8CharAt/kbUtf8LastCharBytes.
- Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/
French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page
cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed.
NodePrefs::keyboard_alt_alphabet picks which one, if any, is active.
- Lemon font is forced on transiently inside KeyboardWidget::render() (saved
and restored every call) whenever the alt-alphabet page is showing or
already-typed text has non-ASCII bytes, so composing is visible even if
Font is set to Default.
- Each script's caps-shift rule is distinct and documented in
kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς
exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for
Latin Extended-A (verified against every character actually used, not a
blanket rule for that whole Unicode block).
NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask,
keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D,
sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile).
Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the
Rooms keyboard note (no longer ASCII-only once an alphabet is enabled).
Not build-verified — no PlatformIO toolchain available this session. Caps
mappings cross-checked against Python's Unicode case tables; UTF-8 literal
bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
|
|
|
|
char one[5];
|
|
|
|
|
|
if (t9_cycle < glen) kbUtf8CharAt(group, t9_cycle, one);
|
|
|
|
|
|
else { one[0] = (char)('1' + cell); one[1] = '\0'; }
|
|
|
|
|
|
char shown[5];
|
|
|
|
|
|
kbApplyCapsUtf8(one, caps, shown, sizeof(shown));
|
|
|
|
|
|
int old_n = kbUtf8LastCharBytes(buf, len);
|
|
|
|
|
|
int new_len = len - old_n;
|
|
|
|
|
|
int n = (int)strlen(shown);
|
|
|
|
|
|
if (new_len + n <= max_len) {
|
|
|
|
|
|
memcpy(buf + new_len, shown, n);
|
|
|
|
|
|
len = new_len + n;
|
|
|
|
|
|
buf[len] = '\0';
|
|
|
|
|
|
}
|
2026-07-02 12:21:52 +02:00
|
|
|
|
}
|
|
|
|
|
|
} else if (len < max_len) {
|
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets
Alarm repeat (Clock Tools):
- NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday
convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends.
computeAlarmNextFire() scans the next 7 days for a matching weekday when
set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is
empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot
behaviour, so existing users see no change.
On-screen keyboard alphabets (Settings > Keyboard > Alphabet):
- KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints
(insertion, backspace and T9 in-place cycling all codepoint-aware now, so a
multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/
kbUtf8CharAt/kbUtf8LastCharBytes.
- Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/
French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page
cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed.
NodePrefs::keyboard_alt_alphabet picks which one, if any, is active.
- Lemon font is forced on transiently inside KeyboardWidget::render() (saved
and restored every call) whenever the alt-alphabet page is showing or
already-typed text has non-ASCII bytes, so composing is visible even if
Font is set to Default.
- Each script's caps-shift rule is distinct and documented in
kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς
exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for
Latin Extended-A (verified against every character actually used, not a
blanket rule for that whole Unicode block).
NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask,
keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D,
sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile).
Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the
Rooms keyboard note (no longer ASCII-only once an alphabet is enabled).
Not build-verified — no PlatformIO toolchain available this session. Caps
mappings cross-checked against Python's Unicode case tables; UTF-8 literal
bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
|
|
|
|
char one[5]; kbUtf8CharAt(group, 0, one);
|
|
|
|
|
|
char shown[5]; kbApplyCapsUtf8(one, caps, shown, sizeof(shown));
|
|
|
|
|
|
int n = (int)strlen(shown);
|
|
|
|
|
|
if (len + n <= max_len) {
|
|
|
|
|
|
memcpy(buf + len, shown, n);
|
|
|
|
|
|
len += n;
|
|
|
|
|
|
buf[len] = '\0';
|
|
|
|
|
|
t9_cell = cell;
|
|
|
|
|
|
t9_cycle = 0;
|
2026-07-17 10:02:00 +02:00
|
|
|
|
if (caps && !caps_lock) caps = false; // one-shot: only this first tap gets capitalised
|
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets
Alarm repeat (Clock Tools):
- NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday
convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends.
computeAlarmNextFire() scans the next 7 days for a matching weekday when
set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is
empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot
behaviour, so existing users see no change.
On-screen keyboard alphabets (Settings > Keyboard > Alphabet):
- KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints
(insertion, backspace and T9 in-place cycling all codepoint-aware now, so a
multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/
kbUtf8CharAt/kbUtf8LastCharBytes.
- Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/
French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page
cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed.
NodePrefs::keyboard_alt_alphabet picks which one, if any, is active.
- Lemon font is forced on transiently inside KeyboardWidget::render() (saved
and restored every call) whenever the alt-alphabet page is showing or
already-typed text has non-ASCII bytes, so composing is visible even if
Font is set to Default.
- Each script's caps-shift rule is distinct and documented in
kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς
exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for
Latin Extended-A (verified against every character actually used, not a
blanket rule for that whole Unicode block).
NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask,
keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D,
sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile).
Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the
Rooms keyboard note (no longer ASCII-only once an alphabet is enabled).
Not build-verified — no PlatformIO toolchain available this session. Caps
mappings cross-checked against Python's Unicode case tables; UTF-8 literal
bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
|
|
|
|
}
|
2026-07-02 12:21:52 +02:00
|
|
|
|
}
|
|
|
|
|
|
t9_last_ms = millis();
|
|
|
|
|
|
} else if (row < rows) {
|
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets
Alarm repeat (Clock Tools):
- NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday
convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends.
computeAlarmNextFire() scans the next 7 days for a matching weekday when
set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is
empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot
behaviour, so existing users see no change.
On-screen keyboard alphabets (Settings > Keyboard > Alphabet):
- KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints
(insertion, backspace and T9 in-place cycling all codepoint-aware now, so a
multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/
kbUtf8CharAt/kbUtf8LastCharBytes.
- Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/
French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page
cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed.
NodePrefs::keyboard_alt_alphabet picks which one, if any, is active.
- Lemon font is forced on transiently inside KeyboardWidget::render() (saved
and restored every call) whenever the alt-alphabet page is showing or
already-typed text has non-ASCII bytes, so composing is visible even if
Font is set to Default.
- Each script's caps-shift rule is distinct and documented in
kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς
exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for
Latin Extended-A (verified against every character actually used, not a
blanket rule for that whole Unicode block).
NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask,
keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D,
sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile).
Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the
Rooms keyboard note (no longer ASCII-only once an alphabet is enabled).
Not build-verified — no PlatformIO toolchain available this session. Caps
mappings cross-checked against Python's Unicode case tables; UTF-8 literal
bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
|
|
|
|
char shown[3];
|
|
|
|
|
|
kbApplyCapsUtf8(cellStr(row, col), caps, shown, sizeof(shown));
|
|
|
|
|
|
int n = (int)strlen(shown);
|
|
|
|
|
|
if (len + n <= max_len) {
|
|
|
|
|
|
memcpy(buf + len, shown, n);
|
|
|
|
|
|
len += n;
|
2026-05-13 18:22:41 +02:00
|
|
|
|
buf[len] = '\0';
|
2026-07-17 10:02:00 +02:00
|
|
|
|
if (caps && !caps_lock) caps = false; // one-shot: revert after the letter it capitalised
|
2026-05-13 18:22:41 +02:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
2026-07-02 12:21:52 +02:00
|
|
|
|
t9_cell = -1; // any special-row action finalizes a pending multi-tap cycle
|
2026-05-13 18:22:41 +02:00
|
|
|
|
switch (col) {
|
2026-07-17 10:02:00 +02:00
|
|
|
|
// Tap toggles one-shot caps on/off; while caps_lock is held (Hold-Enter
|
|
|
|
|
|
// on this key, see handleInput's top), a tap cancels the lock instead.
|
|
|
|
|
|
case 0: if (caps_lock) { caps = false; caps_lock = false; } else { caps = !caps; } break;
|
2026-05-13 18:22:41 +02:00
|
|
|
|
case 1:
|
|
|
|
|
|
if (len < max_len) { buf[len++] = ' '; buf[len] = '\0'; }
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 2:
|
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets
Alarm repeat (Clock Tools):
- NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday
convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends.
computeAlarmNextFire() scans the next 7 days for a matching weekday when
set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is
empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot
behaviour, so existing users see no change.
On-screen keyboard alphabets (Settings > Keyboard > Alphabet):
- KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints
(insertion, backspace and T9 in-place cycling all codepoint-aware now, so a
multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/
kbUtf8CharAt/kbUtf8LastCharBytes.
- Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/
French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page
cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed.
NodePrefs::keyboard_alt_alphabet picks which one, if any, is active.
- Lemon font is forced on transiently inside KeyboardWidget::render() (saved
and restored every call) whenever the alt-alphabet page is showing or
already-typed text has non-ASCII bytes, so composing is visible even if
Font is set to Default.
- Each script's caps-shift rule is distinct and documented in
kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς
exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for
Latin Extended-A (verified against every character actually used, not a
blanket rule for that whole Unicode block).
NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask,
keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D,
sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile).
Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the
Rooms keyboard note (no longer ASCII-only once an alphabet is enabled).
Not build-verified — no PlatformIO toolchain available this session. Caps
mappings cross-checked against Python's Unicode case tables; UTF-8 literal
bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
|
|
|
|
if (len > 0) { len -= kbUtf8LastCharBytes(buf, len); buf[len] = '\0'; }
|
2026-05-13 18:22:41 +02:00
|
|
|
|
break;
|
|
|
|
|
|
case 3:
|
2026-07-12 20:32:04 +02:00
|
|
|
|
if (_ph_refresh) _ph_refresh(*this, _ph_refresh_ctx); // contextual repopulate, if wired up
|
|
|
|
|
|
_ph_menu.begin(_ph_title, KB_PH_VISIBLE);
|
2026-05-14 08:06:26 +02:00
|
|
|
|
for (int i = 0; i < _ph_count; i++) _ph_menu.addItem(_ph_buf[i]);
|
2026-05-13 18:22:41 +02:00
|
|
|
|
break;
|
|
|
|
|
|
case 4:
|
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets
Alarm repeat (Clock Tools):
- NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday
convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends.
computeAlarmNextFire() scans the next 7 days for a matching weekday when
set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is
empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot
behaviour, so existing users see no change.
On-screen keyboard alphabets (Settings > Keyboard > Alphabet):
- KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints
(insertion, backspace and T9 in-place cycling all codepoint-aware now, so a
multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/
kbUtf8CharAt/kbUtf8LastCharBytes.
- Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/
French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page
cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed.
NodePrefs::keyboard_alt_alphabet picks which one, if any, is active.
- Lemon font is forced on transiently inside KeyboardWidget::render() (saved
and restored every call) whenever the alt-alphabet page is showing or
already-typed text has non-ASCII bytes, so composing is visible even if
Font is set to Default.
- Each script's caps-shift rule is distinct and documented in
kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς
exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for
Latin Extended-A (verified against every character actually used, not a
blanket rule for that whole Unicode block).
NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask,
keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D,
sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile).
Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the
Rooms keyboard note (no longer ASCII-only once an alphabet is enabled).
Not build-verified — no PlatformIO toolchain available this session. Caps
mappings cross-checked against Python's Unicode case tables; UTF-8 literal
bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
|
|
|
|
page = (page + 1) % totalPages(); // cycle letters -> [alt alphabet] -> symbols
|
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>
2026-06-15 10:47:50 +02:00
|
|
|
|
break;
|
|
|
|
|
|
case 5:
|
2026-05-13 18:22:41 +02:00
|
|
|
|
return DONE;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return NONE;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|