feat(keyboard): hold-Enter accent popup; relocate cursor-mode trigger

Replaces the 8 separate Latin-diacritic alt-alphabet pages (Polish, Czech,
Slovak, German, French, Spanish, Portuguese, Nordic) with one popup: Hold
Enter on a Latin letter that has accented variants (a c d e i l n o r s
t u y z) opens a floating horizontal strip of that letter's accents,
anchored over its own row so the grid stays visible underneath (LEFT/
RIGHT picks, Enter inserts via a new shared insertGlyph() helper, Cancel
dismisses). Holding a letter with no variants is a no-op. Cyrillic/Greek
remain full alt-alphabet pages; NodePrefs::keyboard_alt_alphabet shrinks
from 11 to 3 values accordingly -- an old saved Polish..Nordic value just
clamps to Latin via DataStore.cpp's existing range check, no migration
code needed.

Freeing Hold-Enter on letter cells required moving cursor-mode's own
trigger: UP from the top letter row now enters it instead of wrapping to
the special row. To keep that wrap reachable, cursor mode's UP/DOWN
(Home/End) continue the wrap once already at that boundary -- UP again
lands on the special row, DOWN again back on the letter grid -- reusing
the same proportional column mapping the old direct wrap used.

Diagnostics' font-coverage sample swaps its 8 per-language lines for one
line sampling the new accent table. Docs (message_screen, settings_screen,
solo_ui_framework) and release-notes updated to match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-07-17 20:28:11 +02:00
parent ce1fde4fdb
commit 694bbcd68b
7 changed files with 193 additions and 192 deletions

View File

@@ -176,15 +176,26 @@ the board's sensors actually provide. Expand them with `expandMsg()` at send tim
Two layouts share every grid: **ABC** (one key per letter) and **T9**
(phone-keypad multi-tap — repeated Enter within `KB_T9_TIMEOUT_MS` cycles a
cell's letter group, then its digit). `NodePrefs::keyboard_alt_alphabet` adds a
non-Latin alphabet's own page to the cycle (Latin → alphabet → Symbols →
Latin); each alphabet defines both its ABC grid (`KB_*_CHARS`) and T9 group
table (`KB_T9_GROUPS_*`) so the two layouts always offer the same letters.
non-Latin script's own page to the cycle (Latin → alphabet → Symbols → Latin);
Cyrillic/Greek each define both an ABC grid (`KB_CYRILLIC_CHARS`/
`KB_GREEK_CHARS`) and a T9 group table (`KB_T9_GROUPS_CYRILLIC`/`_GREEK`) so
the two layouts always offer the same letters. Latin-diacritic letters (Polish,
Czech, German, etc.) aren't alt-alphabet pages — they're reached by Hold-Enter
on a plain Latin cell instead (see `KB_ACCENT_VARIANTS` below).
Shift is one-shot by default (capitalises the next letter, including whichever
candidate a T9 multi-tap cycle settles on) or Hold-Enter to toggle caps-lock;
Hold-Clear erases the whole field. Hold-Enter elsewhere in the field enters
**cursor mode** (LEFT/RIGHT move the insertion point, UP/DOWN jump to
start/end) so edits/inserts can target any point in the typed text, not just
the end.
Hold-Clear erases the whole field. **UP from the top letter row** enters
**cursor mode** (LEFT/RIGHT move the insertion point; UP/DOWN jump to
start/end, then — pressed again once already at that boundary — continue on
to the special row / letter grid, the same destinations the plain grid wrap
used to reach directly) so edits/inserts can target any point in the typed
text, not just the end; Enter/Cancel exit immediately from anywhere.
Hold-Enter on a Latin-page letter cell with accented variants instead opens
the **accent popup**: one horizontal row of `KB_ACCENT_VARIANTS[group]`
(a UTF-8 string per base letter, same shape as a T9 group string), LEFT/RIGHT
to pick, Enter to insert via the shared `insertGlyph()` helper, Cancel to
dismiss. Holding a letter with no variants, or any T9/alt-alphabet/symbols
cell, is a no-op.
`FullscreenMsgView::wrapLines()` is a standalone pixel-accurate word-wrapper
(O(n), variable-width-font aware) reusable by any multi-line layout; it writes

View File

@@ -23,7 +23,7 @@ Press **Enter** on a contact or channel to open its history, then press **Enter*
- **Custom message** — opens the on-screen keyboard
- **Q1Q10** — quick reply templates editable in Settings Messages
While typing, **Hold Enter** enters cursor mode (LEFT/RIGHT move the insertion point, UP/DOWN jump to start/end, Enter/Cancel exit) so you can edit or insert in the middle of what you've typed instead of only at the end — see the on-screen keyboard section of the [UI framework guide](../../design/solo_ui_framework.md) for the full key set (Shift, T9 multi-tap, alternate alphabets).
While typing, **UP** from the top letter row enters cursor mode (LEFT/RIGHT move the insertion point; UP/DOWN jump to start/end, then continue on to the special row / letter grid if pressed again once already there; Enter/Cancel exit immediately from anywhere) so you can edit or insert in the middle of what you've typed instead of only at the end. **Hold Enter** on a Latin letter with accented variants (e.g. a, e, c, n, o, s, z…) instead opens a one-row popup of that letter's accents — LEFT/RIGHT to pick, Enter to insert, Cancel to dismiss. See the on-screen keyboard section of the [UI framework guide](../../design/solo_ui_framework.md) for the full key set (Shift, T9 multi-tap, Cyrillic/Greek).
The keyboard supports placeholders that insert live data at send time:

View File

@@ -100,10 +100,12 @@ The **repeater** mode and its flood filters live on their own screen — see **T
| Setting | Options | Notes |
| -------- | ---------- | -------------------------------------------------------------------------------------------------- |
| Layout | ABC / T9 | On-screen keyboard style. **ABC**: an a-b-c…z grid, one key per letter (the original layout). **T9**: phone-keypad multi-tap — each key is labelled with its **digit** and a letter group (e.g. `2abc`); repeated **Enter** presses cycle through the letters and then the digit itself. Applies to whichever alphabet page is active (see Alphabet below), not just Latin. |
| Alphabet | Latin / Cyrillic / Greek / Polish / Czech / Slovak / German / French / Spanish / Portuguese / Nordic | Which extra (non-Latin) alphabet, if any, joins the keyboard's page cycle. **Latin** (default): only the Latin letters and Symbols pages, as before. Any other choice adds that alphabet's own page to the same **#@/abc** key's cycle (Latin → alphabet → Symbols → Latin) — no separate key to switch scripts. Each Latin-diacritic language (Polish, Czech, Slovak, German, French, Spanish, Portuguese, Nordic) is its own separate page with that language's full, correct set of non-ASCII letters — e.g. Czech gets `á č ď é ě í ň ó ř š ť ú ů ý ž`, German just `ä ö ü ß`. Nordic covers `å ä æ ö ø`, shared across Danish/Norwegian/Swedish since their alphabets only differ in which of those five each one uses. **Greek** covers the 24-letter alphabet plus final sigma (`ς`) but not the tonos stress accents used in proper Modern Greek spelling. Every alphabet's letters render natively — the display font (a single unified Unicode font used everywhere on-screen) covers all of them, no separate toggle needed. |
| Alphabet | Latin / Cyrillic / Greek | Which extra (non-Latin) script, if any, joins the keyboard's page cycle. **Latin** (default): only the Latin letters and Symbols pages, as before. **Cyrillic**/**Greek** add that script's own page to the same **#@/abc** key's cycle (Latin → alphabet → Symbols → Latin) — no separate key to switch scripts. **Greek** covers the 24-letter alphabet plus final sigma (`ς`) but not the tonos stress accents used in proper Modern Greek spelling. Every alphabet's letters render natively — the display font (a single unified Unicode font used everywhere on-screen) covers all of them, no separate toggle needed. |
Applies to every on-screen text field (messages, waypoint labels, room passwords, preset names). Earlier releases labelled the grid *QWERTY*; the layout has always been alphabetical, so it is now named **ABC**.
European Latin-diacritic letters (Polish, Czech, Slovak, German, French, Spanish, Portuguese, Nordic, etc.) aren't separate alphabet pages — instead, **Hold Enter** on a plain Latin letter that has accented variants (`a c d e i l n o r s t u y z`) opens a one-row popup of its accents (e.g. holding `a` offers `á à â ã ä å ą`); **LEFT/RIGHT** picks, **Enter** inserts it, **Cancel** dismisses with no change. Holding a letter with no accented variants (e.g. `b`) does nothing.
---
### Contacts

View File

@@ -332,28 +332,19 @@ struct NodePrefs { // persisted to file
// and the misc-fixed font (src/helpers/ui/MiscFixedFont.h) for on-screen
// rendering — every alphabet here must be in its U+0020-04FF range.
//
// The Latin-diacritic entries (Polish..Nordic) replace what used to be a
// single combined "Ext.Latin" curated subset — each is now its own full,
// linguistically-correct set of that language's non-ASCII letters instead
// of a shared partial one. Not a schema change: keyboard_alt_alphabet is
// still just an index into keyboardAlphabetLabel()/KeyboardWidget's tables,
// so widening KB_ALPHABET_COUNT needs no persisted-data migration — only
// note that an index of 3 now means Polish, not the old combined Ext.Latin.
// Latin-diacritic letters (Polish/Czech/Slovak/German/French/Spanish/
// Portuguese/Nordic) used to each be their own full alt-alphabet page here;
// they're now reached instead by holding Enter on a Latin letter that has
// accented variants (see KeyboardWidget.h's KB_ACCENT_VARIANTS), so this
// enum only covers actual non-Latin scripts. Not a schema change: an old
// saved value of 3+ (one of the removed languages) just clamps to 0 (Latin)
// via DataStore.cpp's existing `>= KB_ALPHABET_COUNT` range check.
static const uint8_t KB_ALPHABET_LATIN_ONLY = 0;
static const uint8_t KB_ALPHABET_CYRILLIC = 1;
static const uint8_t KB_ALPHABET_GREEK = 2;
static const uint8_t KB_ALPHABET_POLISH = 3; // ą ć ę ł ń ó ś ź ż
static const uint8_t KB_ALPHABET_CZECH = 4; // á č ď é ě í ň ó ř š ť ú ů ý ž
static const uint8_t KB_ALPHABET_SLOVAK = 5; // á ä č ď é í ĺ ľ ň ó ô ŕ š ť ú ý ž
static const uint8_t KB_ALPHABET_GERMAN = 6; // ä ö ü ß
static const uint8_t KB_ALPHABET_FRENCH = 7; // à â ç é è ê ë î ï ô ù û ü ÿ œ
static const uint8_t KB_ALPHABET_SPANISH = 8; // á é í ñ ó ú ü
static const uint8_t KB_ALPHABET_PORTUGUESE = 9; // á à â ã ç é ê í ó ô õ ú
static const uint8_t KB_ALPHABET_NORDIC = 10; // å ä æ ö ø (Danish/Norwegian/Swedish share this set)
static const uint8_t KB_ALPHABET_COUNT = 11;
static const uint8_t KB_ALPHABET_COUNT = 3;
static const char* keyboardAlphabetLabel(uint8_t idx) {
static const char* L[KB_ALPHABET_COUNT] = { "Latin", "Cyrillic", "Greek", "Polish", "Czech",
"Slovak", "German", "French", "Spanish", "Portuguese", "Nordic" };
static const char* L[KB_ALPHABET_COUNT] = { "Latin", "Cyrillic", "Greek" };
return L[idx < KB_ALPHABET_COUNT ? idx : 0];
}

View File

@@ -175,20 +175,14 @@ class DiagnosticsScreen : public UIScreen {
// One sample line per script/keyboard alphabet the UI font claims to cover
// (all inside the U+0020-04FF glyph range the OLED misc-fixed / e-ink Lemon
// fonts carry), so the font's coverage can be checked by eye on real
// hardware. Per-language lines use the same 2-letter codes as
// KeyboardWidget::altAlphabetHint and each shows that language's own
// non-ASCII letters (see KeyboardWidget.h's KB_*_CHARS tables).
// hardware. "Acc" samples one variant from each KeyboardWidget.h
// KB_ACCENT_VARIANTS group (the Hold-Enter accent popup that replaced the
// old per-language alt-alphabet pages) so every accented letter actually
// reachable in the UI still gets an eyeball check.
void buildFontLines() {
_line_count = 0;
addLine("Latin ABCabc xyz");
addLine("PL ąćęłńóśźż");
addLine("CZ áčďéěíňóřš");
addLine("SK áäčďíĺľňôŕ");
addLine("DE äöüß");
addLine("FR àâçéèêëîïœ");
addLine("ES áéíñóúü");
addLine("PT áàâãçéêíóôõú");
addLine("ND åäæöø");
addLine("Acc áçďéíłñóřśťúýź");
addLine("Grk ΑΒΓ αβγξω"); // ΑΒΓ αβγξω
addLine("Cyr АБВ абвжя"); // АБВ абвжя
addLine("Num 0123456789");

View File

@@ -93,107 +93,6 @@ static const char* const KB_T9_GROUPS_GREEK[9] = {
".,!?'-", "αβγ", "δεζ", "ηθι", "κλμ", "νξο", "πρσς", "τυφ", "χψω"
};
// 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" },
{ ",", "!", "?", "'", "-", "\"", ":", ";", "(", ")" },
{ "@", "#", "&", "*", "_", "+", "=", "/", "\\", "~" },
};
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] = {
".,!?'-", "åä", "æö", "ø", ";()", "@#&*", "_+=/", "\\~<>", "[]{}"
};
// 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.
@@ -312,6 +211,41 @@ static int kbUtf8CharBytesAt(const char* buf, int pos, int len) {
return n;
}
// Accented variants of a Latin base letter, reachable by Hold-Enter on that
// letter's cell on the plain Latin ABC page (see handleInput's KEY_CONTEXT_MENU
// block and the accent_active state machine). Replaces the old per-language
// full alt-alphabet pages (Polish/Czech/Slovak/German/French/Spanish/
// Portuguese/Nordic) with one popup covering the union of all their accented
// letters -- closer to a phone keyboard's long-press accent picker than a
// second full page per language. Each entry is one UTF-8 string of
// concatenated variants (like KB_T9_GROUPS_* group strings), read with the
// same kbUtf8Len()/kbUtf8CharAt() helpers used there.
// Ligature/non-diacritic letters are filed under their conventional key, same
// as a phone keyboard's long-press: ß (German) -> s, œ (French) -> o. ĺ/ŕ
// (Slovak) are l/r with an acute, not i/e variants, so they're filed there.
static const char KB_ACCENT_BASES[] = "acdeilnorstuyz";
static const char* const KB_ACCENT_VARIANTS[] = {
"áàâãäåą", // a
"çćč", // c
"ď", // d
"éèêëěę", // e
"íîï", // i
"łĺľ", // l
"ñńň", // n
"óòôõöøœ", // o
"řŕ", // r
"śšß", // s
"ť", // t
"úùûüů", // u
"ýÿ", // y
"źżž", // z
};
static const int KB_ACCENT_COUNT = sizeof(KB_ACCENT_BASES) - 1; // exclude the trailing NUL
static int findAccentGroup(char base) {
for (int i = 0; i < KB_ACCENT_COUNT; i++) if (KB_ACCENT_BASES[i] == base) return i;
return -1;
}
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
@@ -332,7 +266,10 @@ struct KeyboardWidget {
int len;
int max_len;
int cursor_pos; // insertion point into buf, in bytes; defaults to len (append)
bool cursor_mode; // true while Hold-Enter has parked the grid to reposition cursor_pos
bool cursor_mode; // true while UP-from-row-0 has parked the grid to reposition cursor_pos
bool accent_active = false; // true while the Hold-Enter accent popup is open
int accent_group = -1; // index into KB_ACCENT_VARIANTS for the held cell's base letter
int accent_sel = 0; // selected variant within that group
int row, col;
int page; // see totalPages()/pageIsAltAlphabet()/pageIsSymbols() below
bool caps;
@@ -392,14 +329,6 @@ struct KeyboardWidget {
switch (prefs->keyboard_alt_alphabet) {
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];
default: return "?";
}
}
@@ -415,14 +344,6 @@ struct KeyboardWidget {
switch (prefs->keyboard_alt_alphabet) {
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];
default: return "?";
}
}
@@ -438,14 +359,6 @@ struct KeyboardWidget {
switch (alt) {
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";
default: return "?";
}
}
@@ -459,6 +372,9 @@ struct KeyboardWidget {
len = strlen(buf);
cursor_pos = len;
cursor_mode = false;
accent_active = false;
accent_group = -1;
accent_sel = 0;
row = col = 0;
page = 0;
caps = false;
@@ -475,6 +391,24 @@ struct KeyboardWidget {
addPlaceholder("{time}");
}
// Insert one UTF-8 codepoint (a grid cell's own glyph, or a picked accent
// variant) at cursor_pos, applying Shift/caps-lock the same way every cell
// commit does. Shared by the plain-Latin-cell commit below and the accent
// popup's Enter commit, so the two paths can't drift apart.
void insertGlyph(const char* one, bool use_caps) {
char shown[5];
kbApplyCapsUtf8(one, use_caps, shown, sizeof(shown));
int n = (int)strlen(shown);
int tail_len = len - cursor_pos;
if (len + n <= max_len) {
memmove(buf + cursor_pos + n, buf + cursor_pos, tail_len);
memcpy(buf + cursor_pos, shown, n);
len += n;
cursor_pos += n;
buf[len] = '\0';
}
}
void clearPlaceholders() { _ph_count = 0; }
void addPlaceholder(const char* ph) {
@@ -643,6 +577,43 @@ struct KeyboardWidget {
display.setColor(DisplayDriver::LIGHT);
}
// Accent popup: floats over the still-visible grid (same idea as the
// placeholder overlay just below), anchored on the held letter's own row
// so it reads as "popping out of" that key instead of taking over the
// whole keyboard. LEFT/RIGHT picks, Enter commits, Cancel dismisses.
if (accent_active) {
const char* group = KB_ACCENT_VARIANTS[accent_group];
int n = kbUtf8Len(group);
const int pad = 3;
int seg_w = cw * 2 + pad;
int bw = n * seg_w;
int max_bw = display.width() - 4;
if (bw > max_bw) { bw = max_bw; seg_w = bw / n; }
int bx = (display.width() - bw) / 2;
int by = chars_y + row * cell_h - 1;
int bh = cell_h + 1;
display.setColor(DisplayDriver::DARK);
display.fillRect(bx, by, bw, bh);
display.setColor(DisplayDriver::LIGHT);
display.drawRect(bx, by, bw, bh);
for (int i = 0; i < n; i++) {
char one[5]; kbUtf8CharAt(group, i, one);
char shown[5]; kbApplyCapsUtf8(one, caps, shown, sizeof(shown));
int x = bx + i * seg_w;
if (i == accent_sel) {
display.setColor(DisplayDriver::LIGHT);
display.fillRect(x + 1, by + 1, seg_w - 1, bh - 2);
display.setColor(DisplayDriver::DARK);
} else {
display.setColor(DisplayDriver::LIGHT);
}
int tw = display.getTextWidth(shown);
display.setCursor(x + (seg_w - tw) / 2, by + 1);
display.print(shown);
}
display.setColor(DisplayDriver::LIGHT);
}
// placeholder picker overlay (drawn on top of keyboard)
if (_ph_menu.active) _ph_menu.render(display);
return 50;
@@ -677,20 +648,51 @@ struct KeyboardWidget {
return NONE;
}
// Cursor-positioning sub-mode (see the Hold-Enter block below): the grid
// Cursor-positioning sub-mode (see the KEY_UP block below): the grid
// selection is parked while LEFT/RIGHT walk cursor_pos one codepoint at a
// time and UP/DOWN jump to the very start/end -- Home/End, in effect.
// Enter/Cancel just leave the mode; the actual edit (insert/backspace)
// happens back in normal typing, now targeting the repositioned cursor.
// time. UP/DOWN jump to the very start/end -- Home/End, in effect -- and,
// once already at that boundary, continue the wrap the entry trigger
// interrupted: UP again lands on the special row, DOWN again back on the
// letter grid's row 0, same destinations the plain grid wrap used to reach
// directly (see the entry/exit comment below). Enter/Cancel just leave the
// mode from anywhere; the actual edit (insert/backspace) happens back in
// normal typing, now targeting the repositioned cursor.
if (cursor_mode) {
if (c == KEY_LEFT) { if (cursor_pos > 0) cursor_pos -= kbUtf8LastCharBytes(buf, cursor_pos); return NONE; }
if (c == KEY_RIGHT) { if (cursor_pos < len) cursor_pos += kbUtf8CharBytesAt(buf, cursor_pos, len); return NONE; }
if (c == KEY_UP) { cursor_pos = 0; return NONE; }
if (c == KEY_DOWN) { cursor_pos = len; return NONE; }
if (c == KEY_UP) {
if (cursor_pos > 0) { cursor_pos = 0; return NONE; }
cursor_mode = false;
row = gridRows();
col = col * KB_SPECIAL / gridCols();
return NONE;
}
if (c == KEY_DOWN) {
if (cursor_pos < len) { cursor_pos = len; return NONE; }
cursor_mode = false;
row = 0;
return NONE;
}
if (c == KEY_ENTER || c == KEY_CANCEL) { cursor_mode = false; return NONE; }
return NONE;
}
if (accent_active) {
const char* group = KB_ACCENT_VARIANTS[accent_group];
int n = kbUtf8Len(group);
if (keyIsPrev(c)) { accent_sel = (accent_sel > 0) ? accent_sel - 1 : n - 1; return NONE; }
if (keyIsNext(c)) { accent_sel = (accent_sel < n - 1) ? accent_sel + 1 : 0; return NONE; }
if (c == KEY_ENTER) {
char one[5]; kbUtf8CharAt(group, accent_sel, one);
insertGlyph(one, caps);
if (caps && !caps_lock) caps = false;
accent_active = false;
return NONE;
}
if (c == KEY_CANCEL) { accent_active = false; return NONE; }
return NONE;
}
if (c == KEY_CANCEL) return CANCELLED;
const int rows = gridRows();
@@ -699,9 +701,14 @@ struct KeyboardWidget {
// Hold-Enter is normally "cancel", but three places give it a more useful
// meaning 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; anywhere on the letter/
// symbol grid -> enter cursor-positioning mode (see above). Every other
// special-row cell keeps hold-to-cancel.
// field in one action instead of holding it down; a Latin-page letter cell
// with accented variants -> open the accent popup (see accent_active
// above). Every other special-row cell keeps hold-to-cancel; any other
// letter/symbol cell (a plain letter with no accents, or any T9/alt-
// alphabet/symbols cell) is a silent no-op instead, so it can't
// accidentally close the keyboard. Cursor mode itself moved off Hold-Enter
// entirely -- see the KEY_UP block below, where UP from row 0 now enters
// it instead.
if (c == KEY_CONTEXT_MENU) {
if (row == rows && col == 0) { // Shift
caps_lock = !caps_lock;
@@ -714,10 +721,12 @@ struct KeyboardWidget {
t9_cell = -1;
return NONE;
}
if (row < rows) { // any letter/symbol cell
cursor_mode = true;
t9_cell = -1;
return NONE;
if (row < rows) {
if (!isT9() && page == 0) {
int gi = findAccentGroup(cellStr(row, col)[0]);
if (gi >= 0) { accent_active = true; accent_group = gi; accent_sel = 0; t9_cell = -1; return NONE; }
}
return NONE; // no variants for this cell, or T9/alt-alphabet/symbols page
}
return CANCELLED;
}
@@ -728,8 +737,12 @@ struct KeyboardWidget {
if (row == rows - 1) // leaving special row upward
col = col * cols / KB_SPECIAL;
} else {
row = rows; // wrap up onto the special row
col = col * KB_SPECIAL / cols;
// row 0: enter cursor mode instead of wrapping to the special row --
// row/col are deliberately left as-is so the cursor-mode UP/DOWN
// continuation above can still reach the special row proportionally.
cursor_mode = true;
t9_cell = -1;
return NONE;
}
t9_cell = -1; // navigating away finalizes any pending multi-tap cycle
return NONE;
@@ -806,18 +819,8 @@ struct KeyboardWidget {
}
t9_last_ms = millis();
} else if (row < rows) {
char shown[3];
kbApplyCapsUtf8(cellStr(row, col), caps, shown, sizeof(shown));
int n = (int)strlen(shown);
int tail_len = len - cursor_pos;
if (len + n <= max_len) {
memmove(buf + cursor_pos + n, buf + cursor_pos, tail_len);
memcpy(buf + cursor_pos, shown, n);
len += n;
cursor_pos += n;
buf[len] = '\0';
insertGlyph(cellStr(row, col), caps);
if (caps && !caps_lock) caps = false; // one-shot: revert after the letter it capitalised
}
} else {
t9_cell = -1; // any special-row action finalizes a pending multi-tap cycle
switch (col) {

View File

@@ -6,7 +6,7 @@
- **On-device channel management.** Messages Channels gets a **"+ Add channel"** row and per-channel **Edit**/**Delete** — create or join a channel by typing either a passphrase (hashed to the channel secret, same idea as a room password) or the exact 32-hex-character key from a channel QR code. No phone app required.
- **Tools Admin — administer a remote repeater/room from the device.** Log into a node you have admin rights on (same self-healing saved-password handshake as room logins) and browse its settings in category tabs — **System** (name, owner info), **Radio** (Frequency/Bandwidth/Spreading factor/Coding rate/TX power), **Routing** (Repeat/Advert interval/Flood advert interval/Max hops), **Actions** (reboot, send advert, sync clock, …) — plus a free-text **Custom command…** escape hatch (with **{}**-key command-name completion) for anything not covered. Radio and Routing fields use a type-appropriate editor instead of raw text — a digit-cursor for Frequency (the same widget Settings' own Radio screen uses locally), discrete-set stepping for Bandwidth/Spreading factor/Coding rate, plain number steppers for the rest, and an ON/OFF toggle for Repeat — adjusting a value costs no mesh traffic until you actually confirm it with **Enter** (**Cancel** sends nothing). Reachable either from **Tools Admin** (opens the same Nodes list to pick a target) or directly via a repeater/room's own **Hold Enter Admin** action.
- **Auto-Reply Bot gains a third target: room servers**, alongside DM and Channel, with its own trigger/reply/commands — the screen is redesigned as a circular tab carousel (**Direct/Channel/Room/Other**, **LEFT/RIGHT** to switch, **UP/DOWN** within a tab) instead of one long list. Each target's **Enable** and **Commands** toggle is now fully independent (they used to secretly share the DM tab's setting for Channel/Room too). New `{name}`/`{hops}` reply placeholders, a DM allow-list (all chat contacts or favourites only), and a dedicated stepper for Quiet Hours.
- **Keyboard: more languages, better editing.** Eight more per-language keyboards — Polish, Czech, Slovak, German, French, Spanish, Portuguese, Nordic — join Cyrillic and Greek under **Settings Keyboard Alphabet**, each with a linguistically complete letter set in both the ABC grid and T9 multi-tap groups. Shift is now **one-shot** by default (capitalises just the next letter; **Hold Enter** on Shift toggles the old sticky caps-lock back on), **Hold Enter** on Backspace clears the whole field, and a new **Hold Enter** cursor-positioning mode (**LEFT/RIGHT** move, **UP/DOWN** jump to start/end) lets you edit or insert anywhere in what you've typed, not just at the end.
- **Keyboard: accent popup, better editing.** Holding **Enter** on a Latin letter with accented variants (`a c d e i l n o r s t u y z`) now opens a one-row popup of that letter's accents — e.g. holding `a` offers `á à â ã ä å ą` — covering every European Latin-diacritic letter (Polish, Czech, Slovak, German, French, Spanish, Portuguese, Nordic, …) from one popup instead of a separate page per language; **Cyrillic**/**Greek** remain full alt-alphabet pages under **Settings Keyboard Alphabet**. Shift is now **one-shot** by default (capitalises just the next letter; **Hold Enter** on Shift toggles the old sticky caps-lock back on), **Hold Enter** on Backspace clears the whole field, and **UP** from the top letter row enters a cursor-positioning mode (**LEFT/RIGHT** move; **UP/DOWN** jump to start/end, then continue on to the special row / letter grid if pressed again once already there) so you can edit or insert anywhere in what you've typed, not just at the end.
- **Messages get a messenger-style history.** History bubbles are now sized to their content and anchored **right** (outgoing) / **left** (incoming); the list runs **newest at the bottom**, growing upward as you scroll into the past; the **[+ send]** compose button moved to the right edge to match.
- **Clock Tools: alarm repeat.** The Alarm screen gains a **Repeat** row (Off/Daily/Weekdays/Weekends) — Hour/Minute merge into one **Time** row edited with an HH:MM digit cursor, and **Repeat**/**Armed** now respond to **LEFT/RIGHT** like every other Settings field instead of Enter-only.
- **Diagnostics becomes a tab carousel** (Live/System/Font): **System** adds firmware version + build date, device model and the active radio parameters; **Font** is a rendering test card with one sample line per script the on-device font covers, so coverage can be eyeballed directly.