diff --git a/docs/design/solo_ui_framework.md b/docs/design/solo_ui_framework.md index c739fe6c..aca809ae 100644 --- a/docs/design/solo_ui_framework.md +++ b/docs/design/solo_ui_framework.md @@ -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 diff --git a/docs/solo_features/message_screen/message_screen.md b/docs/solo_features/message_screen/message_screen.md index f5eff567..77088cab 100644 --- a/docs/solo_features/message_screen/message_screen.md +++ b/docs/solo_features/message_screen/message_screen.md @@ -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 - **Q1–Q10** — 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: diff --git a/docs/solo_features/settings_screen/settings_screen.md b/docs/solo_features/settings_screen/settings_screen.md index 7dea20f1..eddf8b54 100644 --- a/docs/solo_features/settings_screen/settings_screen.md +++ b/docs/solo_features/settings_screen/settings_screen.md @@ -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 diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 066232a3..15412f65 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -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]; } diff --git a/examples/companion_radio/ui-new/DiagnosticsScreen.h b/examples/companion_radio/ui-new/DiagnosticsScreen.h index afb9b887..57e47916 100644 --- a/examples/companion_radio/ui-new/DiagnosticsScreen.h +++ b/examples/companion_radio/ui-new/DiagnosticsScreen.h @@ -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"); diff --git a/examples/companion_radio/ui-new/KeyboardWidget.h b/examples/companion_radio/ui-new/KeyboardWidget.h index 50128541..56f5631d 100644 --- a/examples/companion_radio/ui-new/KeyboardWidget.h +++ b/examples/companion_radio/ui-new/KeyboardWidget.h @@ -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'; - if (caps && !caps_lock) caps = false; // one-shot: revert after the letter it capitalised - } + 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) { diff --git a/release-notes.md b/release-notes.md index cdaa184e..1f7f0871 100644 --- a/release-notes.md +++ b/release-notes.md @@ -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.