feat(pager): keyboard navigation and remappable tab keys, as on the T-Deck (#591)

The wheel is the Pager's only way to move the highlight, which is slow
and imprecise. Bring the T-Deck's keyboard navigation to it:

- With no text field focused, programmable letters move the highlight
  (default I up, K down, J left, L right) and scroll (U / O). Select
  and Back default to no letter, since Enter and Backspace-hold (the
  wheel's click / long-press) already do them. On the Map the letters
  move the highlight; W/A/X/D stay the map pan.
- The tab keys are programmable like the T-Deck's, defaulting to the
  Pager's M/C/H/A/S, and the bar's labels show whatever is bound.
- Settings > Keyboard gets the same "Keyboard navigation" switch (on by
  default here) and the same tap-a-row-then-press-a-key remap rows; the
  T-Deck's rows move into settingsNavKeyRows() so both boards share them.
  The Pager's switch only turns the letters on or off; it never empties
  the focus group, which the wheel always needs.
- A remap refuses letters the Pager uses for fixed actions (Q/E slider
  nudge, W/A/X/D map pan; A stays allowed as a tab key).
- In wheel edit mode on a slider, J/L preview like a turn and the
  select key closes the edit, matching the wheel.

Settings: the Pager's kbd_nav and key bindings were never shown on that
board, so they held the T-Deck defaults. Schema v67 (no new field) puts
the Pager defaults in once on that board only. Two new strings,
translated in every deploy/apps/lang file; i18n_builtin.h regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019M5VJ43X3nUULPqVcgrAH2
This commit is contained in:
Michael A. Cojocari
2026-10-04 15:06:40 -04:00
co-authored by Claude Opus 5.5
parent 8adea3558e
commit 6d655b8301
18 changed files with 283 additions and 69 deletions
+2
View File
@@ -39,7 +39,9 @@ Condense Nav saved — restart to apply Компактната навигаци
Day Ден
High contrast Висок контраст
High contrast - restarting to apply it… Висок контраст - рестартиране за прилагане…
Keyboard nav: on Клавиатурна навиг.: вкл.
Landscape only Само в хоризонтална ориентация
Letters move the highlight when no text field is focused. Enter selects; hold Backspace to go back. Буквите местят маркера, когато няма активно текстово поле. Enter избира; задръжте Backspace за назад.
Night Нощ
Theme save failed Неуспешно запазване на темата
Ack Ack
+2
View File
@@ -37,7 +37,9 @@ Appearance Vzhled
Condense Nav (restart to apply) Kompaktní navigace (pro použití restartujte)
Condense Nav saved — restart to apply Kompaktní navigace uložena — pro použití restartujte
Day Den
Keyboard nav: on Navigace klávesnicí: zapnuta
Landscape only Pouze na šířku
Letters move the highlight when no text field is focused. Enter selects; hold Backspace to go back. Písmena posouvají zvýraznění, když není aktivní textové pole. Enter vybírá; podržením Backspace se vrátíte zpět.
Night Noc
Theme save failed Uložení motivu selhalo
Ack Potvrzení
+2
View File
@@ -39,7 +39,9 @@ Condense Nav saved — restart to apply Kompakte Navigation gespeichert — Neus
Day Tag
High contrast Hoher Kontrast
High contrast - restarting to apply it… Hoher Kontrast - Neustart zum Anwenden…
Keyboard nav: on Tastaturnav.: an
Landscape only Nur im Querformat
Letters move the highlight when no text field is focused. Enter selects; hold Backspace to go back. Buchstaben bewegen die Markierung, wenn kein Textfeld aktiv ist. Enter wählt aus; Rücktaste halten geht zurück.
Night Nacht
Theme save failed Design konnte nicht gespeichert werden
Ack Ack
+2
View File
@@ -39,7 +39,9 @@ Condense Nav saved — restart to apply Η συμπαγής πλοήγηση α
Day Ημέρα
High contrast Υψηλή αντίθεση
High contrast - restarting to apply it… Υψηλή αντίθεση - επανεκκίνηση για εφαρμογή…
Keyboard nav: on Πλοήγηση πληκτρ.: ανοιχτό
Landscape only Μόνο σε οριζόντιο προσανατολισμό
Letters move the highlight when no text field is focused. Enter selects; hold Backspace to go back. Τα γράμματα μετακινούν την επισήμανση όταν δεν είναι ενεργό πεδίο κειμένου. Το Enter επιλέγει· κρατήστε το Backspace για επιστροφή.
Night Νύχτα
Theme save failed Αποτυχία αποθήκευσης θέματος
Ack Ack
+2
View File
@@ -39,7 +39,9 @@ Condense Nav saved — restart to apply Navegación compacta guardada — reinic
Day Día
High contrast Alto contraste
High contrast - restarting to apply it… Alto contraste - reiniciando para aplicarlo…
Keyboard nav: on Nav. teclado: activ.
Landscape only Solo en horizontal
Letters move the highlight when no text field is focused. Enter selects; hold Backspace to go back. Las letras mueven el resaltado cuando no hay un campo de texto activo. Intro selecciona; mantén Retroceso para volver.
Night Noche
Theme save failed No se pudo guardar el tema
Ack Ack
+2
View File
@@ -39,7 +39,9 @@ Condense Nav saved — restart to apply Navigation compacte enregistrée — red
Day Jour
High contrast Contraste élevé
High contrast - restarting to apply it… Contraste élevé - redémarrage pour l'appliquer…
Keyboard nav: on Nav. clavier : activé
Landscape only Paysage uniquement
Letters move the highlight when no text field is focused. Enter selects; hold Backspace to go back. Les lettres déplacent la sélection quand aucun champ de texte n'est actif. Entrée sélectionne ; maintenez Retour arrière pour revenir.
Night Nuit
Theme save failed Échec de l’enregistrement du thème
Ack Accusé
+2
View File
@@ -39,7 +39,9 @@ Condense Nav saved — restart to apply Tömör navigáció mentve – indítsa
Day Nappali
High contrast Nagy kontraszt
High contrast - restarting to apply it… Nagy kontraszt - újraindítás az alkalmazáshoz…
Keyboard nav: on Billentyűzetnavigáció: bekapcsolva
Landscape only Csak fekvő tájolásban
Letters move the highlight when no text field is focused. Enter selects; hold Backspace to go back. A betűk mozgatják a kijelölést, ha nincs aktív szövegmező. Az Enter kiválaszt; a Backspace nyomva tartása visszalép.
Night Éjszakai
Theme save failed Téma mentése sikertelen
Ack Ack
+2
View File
@@ -39,7 +39,9 @@ Condense Nav saved — restart to apply Navigazione compatta salvata — riavvia
Day Giorno
High contrast Contrasto elevato
High contrast - restarting to apply it… Contrasto elevato - riavvio per applicarlo…
Keyboard nav: on Nav. tastiera: on
Landscape only Solo in orizzontale
Letters move the highlight when no text field is focused. Enter selects; hold Backspace to go back. Le lettere spostano l'evidenziazione quando nessun campo di testo è attivo. Invio seleziona; tieni premuto Backspace per tornare indietro.
Night Notte
Theme save failed Salvataggio tema non riuscito
Ack Ack
+2
View File
@@ -39,7 +39,9 @@ Condense Nav saved — restart to apply Compacte navigatie opgeslagen — hersta
Day Dag
High contrast Hoog contrast
High contrast - restarting to apply it… Hoog contrast - opnieuw opstarten om toe te passen…
Keyboard nav: on Toetsenbordnav.: aan
Landscape only Alleen liggend
Letters move the highlight when no text field is focused. Enter selects; hold Backspace to go back. Letters verplaatsen de markering als er geen tekstveld actief is. Enter selecteert; houd Backspace ingedrukt om terug te gaan.
Night Nacht
Theme save failed Thema opslaan mislukt
Ack Ack
+2
View File
@@ -39,7 +39,9 @@ Condense Nav saved — restart to apply Navegação compacta salva — reinicie
Day Dia
High contrast Alto contraste
High contrast - restarting to apply it… Alto contraste - reiniciando para aplicar…
Keyboard nav: on Nav. teclado: ligado
Landscape only Somente em paisagem
Letters move the highlight when no text field is focused. Enter selects; hold Backspace to go back. As letras movem o destaque quando nenhum campo de texto está ativo. Enter seleciona; segure Backspace para voltar.
Night Noite
Theme save failed Falha ao salvar o tema
Ack Ack
+2
View File
@@ -39,7 +39,9 @@ Condense Nav saved — restart to apply Navigare compactă salvată — reporne
Day Zi
High contrast Contrast ridicat
High contrast - restarting to apply it… Contrast ridicat - repornire pentru aplicare…
Keyboard nav: on Navigare tastatură: pornită
Landscape only Doar în modul peisaj
Letters move the highlight when no text field is focused. Enter selects; hold Backspace to go back. Literele mută selecția când niciun câmp de text nu este activ. Enter selectează; ține apăsat Backspace pentru înapoi.
Night Noapte
Theme save failed Salvarea temei a eșuat
Ack Ack
+2
View File
@@ -39,7 +39,9 @@ Condense Nav saved — restart to apply Компактная навигация
Day День
High contrast Высокая контрастность
High contrast - restarting to apply it… Высокая контрастность - перезагрузка для применения…
Keyboard nav: on Навигация клавиатурой: вкл.
Landscape only Только в альбомной ориентации
Letters move the highlight when no text field is focused. Enter selects; hold Backspace to go back. Буквы перемещают выделение, когда не активно текстовое поле. Enter выбирает; удерживайте Backspace, чтобы вернуться.
Night Ночь
Theme save failed Не удалось сохранить тему
Ack Подтв.
+2
View File
@@ -39,7 +39,9 @@ Condense Nav saved — restart to apply Компактна навигација
Day Дан
High contrast Висок контраст
High contrast - restarting to apply it… Висок контраст - рестартовање ради примене…
Keyboard nav: on Навиг. тастатуром: укљ.
Landscape only Само у хоризонталном положају
Letters move the highlight when no text field is focused. Enter selects; hold Backspace to go back. Слова померају истицање када ниједно текстуално поље није активно. Enter бира; држите Backspace за назад.
Night Ноћ
Theme save failed Чување теме није успело
Ack Ack
+2
View File
@@ -39,7 +39,9 @@ Condense Nav saved — restart to apply Компактну навігацію з
Day День
High contrast Висока контрастність
High contrast - restarting to apply it… Висока контрастність - перезавантаження для застосування…
Keyboard nav: on Навігація клавіатурою: увімк.
Landscape only Лише в альбомній орієнтації
Letters move the highlight when no text field is focused. Enter selects; hold Backspace to go back. Літери переміщують виділення, коли не активне текстове поле. Enter вибирає; утримуйте Backspace, щоб повернутися.
Night Ніч
Theme save failed Не вдалося зберегти тему
Ack Підтв.
+1 -1
View File
@@ -9,7 +9,7 @@
namespace TouchPrefsSchema {
static constexpr uint16_t MAGIC = 0x5743; // 'WC' (WadaCfg)
static constexpr uint8_t CURRENT_VERSION = 66; // v49: fem_lna default flip on the V4-R8; v50: map tile z/x/y line off by default (no new fields); v51: console_mode (boot into the text console; new trailing field, default OFF); v52: console_monitor (show incoming messages in the console, default ON); v54: boot_wifi_time + boot_wifi_open (cold-boot saved-Wi-Fi time sync, #383; both OFF); v55: loud_alerts (resonant-pitch chime, #388; OFF); v56: theme_mode (Night/Day, with high-contrast variants, #296/#544; default Night); v57: gps_fuzz_m (displace the advertised position, #399; 0 = off); v58: Attaky notification blink enable + room/DM color indexes (#423); v59: telem_loc_exact (answer position requests from chosen contacts with the REAL fix instead of the advert displacement; 0 = keep it displaced); v60: home_key_keeps_drawer (#491; default OFF); v61: ble_kbd_* (Bluetooth serves a keyboard instead of the phone app, its layout and the paired keyboard; default phone app); v62: ble_kbd_back (a second key that acts as Back; 0 = only Esc); v63: V4-R8 ui_scale reset before exposing font-only presets (#536); v64: report_ping + report_done_n (beta test reports: the opt-in install count and the tag this device already reported on; both default 0); v65: ThinkNode M9 ui_scale reset before exposing font-only presets, the same one-time correction v63 made for the V4-R8 (no new field); v66: condense_nav (landscape nav rail + icon-only Home launchers, #592; default OFF)
static constexpr uint8_t CURRENT_VERSION = 67; // v49: fem_lna default flip on the V4-R8; v50: map tile z/x/y line off by default (no new fields); v51: console_mode (boot into the text console; new trailing field, default OFF); v52: console_monitor (show incoming messages in the console, default ON); v54: boot_wifi_time + boot_wifi_open (cold-boot saved-Wi-Fi time sync, #383; both OFF); v55: loud_alerts (resonant-pitch chime, #388; OFF); v56: theme_mode (Night/Day, with high-contrast variants, #296/#544; default Night); v57: gps_fuzz_m (displace the advertised position, #399; 0 = off); v58: Attaky notification blink enable + room/DM color indexes (#423); v59: telem_loc_exact (answer position requests from chosen contacts with the REAL fix instead of the advert displacement; 0 = keep it displaced); v60: home_key_keeps_drawer (#491; default OFF); v61: ble_kbd_* (Bluetooth serves a keyboard instead of the phone app, its layout and the paired keyboard; default phone app); v62: ble_kbd_back (a second key that acts as Back; 0 = only Esc); v63: V4-R8 ui_scale reset before exposing font-only presets (#536); v64: report_ping + report_done_n (beta test reports: the opt-in install count and the tag this device already reported on; both default 0); v65: ThinkNode M9 ui_scale reset before exposing font-only presets, the same one-time correction v63 made for the V4-R8 (no new field); v66: condense_nav (landscape nav rail + icon-only Home launchers, #592; default OFF); v67: T-Pager keyboard navigation (#591): kbd_nav ON and its own key bindings, a one-time reset on that board only (no new field)
static constexpr uint8_t BROKEN_MID_INSERT_VERSION = 44;
// Persisted byte layout. New fields must be appended at the end: older blobs
+32
View File
@@ -66,6 +66,17 @@ static const uint8_t DEFAULT_UI_ROTATION = 0;
using TouchCfg = TouchPrefsSchema::Config;
static TouchCfg s_cfg;
#if defined(TLORA_PAGER)
// The Pager's keyboard-navigation keys (#591): I up, K down, J left, L right, no
// Select/Back letter (Enter and Backspace-hold do those), U/O scroll. Clear of the
// Pager's fixed letters (Q/E slider nudge, W/A/X/D map pan) and of its tab keys.
static void touchPrefsPagerNavKeyDefaults(TouchCfg& c) {
const char* d = "ikjl";
for (int i = 0; i < 6; i++) c.nav_dir_keys[i] = (uint8_t)(i < 4 ? d[i] : 0);
c.nav_scroll_keys[0] = 'u';
c.nav_scroll_keys[1] = 'o';
}
#endif
static bool s_cfg_loaded = false;
// Legacy per-key names — only referenced by the one-time migration below.
@@ -128,6 +139,8 @@ static void cfgSetDefaults(TouchCfg& c) {
#endif
#if defined(HAS_TANMATSU)
c.kbd_nav = 1; // Tanmatsu: no touchscreen — keyboard nav is the only input, always on
#elif defined(TLORA_PAGER)
c.kbd_nav = 1; // Pager (#591): letter navigation on by default (I/K/J/L, U/O)
#else
c.kbd_nav = 0; // T-Deck / V4: keyboard navigation OFF by default (opt-in; persists once toggled on)
#endif
@@ -174,16 +187,24 @@ static void cfgSetDefaults(TouchCfg& c) {
c.app_hide = (1u << 12); // APPHIDE_MQTT: the MQTT bridge starts hidden (experimental + privacy)
memset(c.lang_file, 0, sizeof c.lang_file); // no file language: built-in ui_lang column
c.sleep_idle = 0; // default: idle light-sleep OFF
#if defined(TLORA_PAGER)
{ const char* d = "mchas"; for (int i = 0; i < 5; i++) c.nav_keys[i] = (uint8_t)d[i]; } // Pager tab mnemonics M/C/H/A/S (#591)
#else
{ const char* d = "ertui"; for (int i = 0; i < 5; i++) c.nav_keys[i] = (uint8_t)d[i]; } // default tab hotkeys E/R/T/U/I
#endif
c.map_zoom_buttons = 0; // default: map zoom = slider
#if defined(HAS_TANMATSU)
{ const char* d = "wxads"; for (int i = 0; i < 6; i++) c.nav_dir_keys[i] = (uint8_t)d[i]; } // Tanmatsu: W up/X down/A left/D right/S select; no Back letter (Esc/F-key), d[5]='\0'
#elif defined(TLORA_PAGER)
touchPrefsPagerNavKeyDefaults(c); // Pager (#591): I/K/J/L, no Select/Back letter, U/O scroll
#else
{ const char* d = "wzadsq"; for (int i = 0; i < 6; i++) c.nav_dir_keys[i] = (uint8_t)d[i]; } // default W/Z/A/D/S/Q
#endif
c.home_is_drawer = 0; // default: Home = Commander screen
#if defined(HAS_TANMATSU)
c.nav_scroll_keys[0] = 'f'; c.nav_scroll_keys[1] = 'v'; // Tanmatsu scroll-up F / scroll-down V
#elif defined(TLORA_PAGER)
// set with the direction keys above (touchPrefsPagerNavKeyDefaults)
#else
c.nav_scroll_keys[0] = 'f'; c.nav_scroll_keys[1] = 'c'; // default scroll-up F / scroll-down C
#endif
@@ -270,6 +291,17 @@ static void cfgLoadOrMigrate() {
// v64 new trailing fields. Forced off rather than inherited: a garbage 1
// would start sending an install count nobody opted into.
if (stored_version < 64) { s_cfg.report_ping = 0; s_cfg.report_done_n = 0; }
#if defined(TLORA_PAGER)
// v67 (#591): the Pager gains the T-Deck's keyboard navigation. Its kbd_nav and key
// bindings were never shown on this board, so the stored values are just the T-Deck
// defaults (W/Z/A/D..., E/R/T/U/I, nav off), which collide with the Pager's own
// letters. Put this board's defaults in once; later choices persist. No new field.
if (stored_version < 67) {
s_cfg.kbd_nav = 1;
{ const char* d = "mchas"; for (int i = 0; i < 5; i++) s_cfg.nav_keys[i] = (uint8_t)d[i]; }
touchPrefsPagerNavKeyDefaults(s_cfg);
}
#endif
if (stored_version < 66) s_cfg.condense_nav = 0; // v66 new trailing field: never stored before
if (stored_version < 29 && s_cfg.ui_scale == 0) s_cfg.ui_scale = 1; // bump old 100% default -> Large (150%)
if (stored_version < 30) s_cfg.boot_advert = 0; // #76 new trailing field: advert-on-boot off by default
+194 -68
View File
@@ -2444,6 +2444,14 @@ static bool s_tb_nav = false; // no trackball — read by the
static lv_indev_drv_t s_nav_keypad_drv;
#endif
#if defined(TLORA_PAGER)
// Keyboard navigation (#591): the T-Deck's programmable letter nav on the Pager's
// keyboard. Unlike the T-Deck this never gates the focus group (the wheel always
// drives it); it only decides whether the navigation letters move the highlight.
// On by default; loaded from the pref at init, toggled in Settings > Keyboard.
static bool s_kbd_nav = true;
#endif
// Touchscreen-only boards whose only focus-group driver is a Bluetooth keyboard
// (the Attaky's D-pad drives its own group): bleKbdUiTick() turns s_kbd_nav on
// while a keyboard is connected. Off, the group stays empty and shows nothing.
@@ -4531,10 +4539,20 @@ static void navHideFocus() {
}
// ---- Keyboard-nav tab hotkeys (programmable; default E/R/T/U/I) ----
static uint8_t s_nav_keys[5] = { 'e','r','t','u','i' }; // per main tab [chat,contacts,home,map,settings]; loaded from prefs at boot
static uint8_t s_nav_keys[5] =
#if defined(TLORA_PAGER)
{ 'm','c','h','a','s' }; // Pager: its long-standing tab mnemonics, printed on the bar, now remappable (#591)
#else
{ 'e','r','t','u','i' }; // per main tab [chat,contacts,home,map,settings]; loaded from prefs at boot
#endif
static uint8_t s_dir_keys[8] =
#if defined(HAS_TANMATSU)
{ 'w','x','a','d','s', 0, 'f','v' }; // Tanmatsu control keys: up,down,left,right,select,(no back — Esc/F-key),scroll-up,scroll-down
#elif defined(TLORA_PAGER)
// Pager (#591): I/K/J/L move, U/O scroll. Select and Back stay on Enter and
// Backspace-hold (or the wheel's click / long-press), so those slots start empty.
// Clear of the Pager's fixed letters: M/C/H/A/S tabs, Q/E slider, W/A/X/D map pan.
{ 'i','k','j','l', 0, 0, 'u','o' };
#else
{ 'w','z','a','d','s','q','f','c' }; // control keys: up,down,left,right,select,back,scroll-up,scroll-down; loaded from prefs at boot
#endif
@@ -5061,6 +5079,34 @@ static void navMenubarKeysSync() {
}
#endif
}
#if defined(TLORA_PAGER)
// The Pager prints each tab's hotkey on the bar: the icon, then the letter beside it
// (or under it on the Condense Nav rail). Built from s_nav_keys so a remap shows up.
static void pagerTabLabel(int tab, char* out, size_t cap) {
static const char* const kIcons[5] = { LV_SYMBOL_ENVELOPE, TOUCH_SYM_PERSON, LV_SYMBOL_HOME,
LV_SYMBOL_GPS, LV_SYMBOL_SETTINGS };
if (tab < 0 || tab >= 5) { if (cap) out[0] = '\0'; return; }
const int lk = navKeyLower(s_nav_keys[tab]);
const char up = (lk >= 'a' && lk <= 'z') ? (char)(lk - 'a' + 'A') : (char)lk;
snprintf(out, cap, "%s%s%c", kIcons[tab], s_nav_condensed ? "\n" : " ", up);
}
static void pagerRefreshTabLabels() {
if (!g_lv.tabview) return;
char b[16];
for (int t = 0; t < 5; t++) { pagerTabLabel(t, b, sizeof b); lv_tabview_rename_tab(g_lv.tabview, t, b); }
}
// Letters the Pager uses for fixed actions, so a remap cannot take them: Q/E nudge a
// slider, and W/A/X/D pan the map. A stays allowed as a tab key (it is the Map key by
// default, and on the Map a pan would only ever follow a jump to the tab you are on).
static bool pagerKeyReserved(int lk, bool tab_key) {
switch (lk) {
case 'q': case 'e': case 'w': case 'x': case 'd': return true;
case 'a': return !tab_key;
default: return false;
}
}
#endif
// Apply a captured key to the tab being remapped (Settings → Keyboard).
static void navKeyCaptureApply(int key) {
const int t = s_navkey_capture; // 0-4 = tab hotkey, 5-12 = up/down/left/right/select/back/scroll-up/scroll-down
@@ -5069,13 +5115,20 @@ static void navKeyCaptureApply(int key) {
if (key == 0x1B || key == 0x08 || key == 0x7F) { if (g_lv.task) g_lv.task->showAlert(TR("Cancelled"), 700); return; }
if (!((key>='a'&&key<='z') || (key>='A'&&key<='Z'))) { if (g_lv.task) g_lv.task->showAlert(TR("Letters only"), 900); return; }
const int lk = navKeyLower(key);
if (navKeyUsedBy(lk, t) >= 0) { if (g_lv.task) g_lv.task->showAlert(TR("Key already in use"), 1100); return; }
if (navKeyUsedBy(lk, t) >= 0
#if defined(TLORA_PAGER)
|| pagerKeyReserved(lk, t < 5)
#endif
) { if (g_lv.task) g_lv.task->showAlert(TR("Key already in use"), 1100); return; }
if (t < 5) {
s_nav_keys[t] = (uint8_t)lk;
#if defined(ESP32)
touchPrefsSetNavKey(t, (uint8_t)lk);
#endif
navMenubarKeysSync(); // tab hotkey changed → refresh the menubar hint
#if defined(TLORA_PAGER)
pagerRefreshTabLabels(); // the Pager prints the letters in the tab labels themselves
#endif
} else {
s_dir_keys[t - 5] = (uint8_t)lk;
#if defined(ESP32)
@@ -14392,6 +14445,18 @@ static void navMbarKeysToggleCb(lv_event_t* e) {
}
#endif
#if defined(TLORA_PAGER)
// Keyboard navigation on the Pager (#591). Unlike the T-Deck's toggle this leaves the
// focus group alone (the wheel always needs it); it only turns the letters on or off.
static void pagerKbdNavToggleCb(lv_event_t* e) {
if (lv_event_get_code(e) != LV_EVENT_VALUE_CHANGED) return;
const bool on = lv_obj_has_state(lv_event_get_target(e), LV_STATE_CHECKED);
touchPrefsSetKbdNav(on);
s_kbd_nav = on;
if (g_lv.task) g_lv.task->showAlert(on ? TR("Keyboard nav: on") : TR("Keyboard nav: off"), 1100);
}
#endif
// Hide the device/profile name in the status bar and park the clock on the left
// where the name used to be. Applies immediately via updateGlobalStatusBar().
static void hideNameToggleCb(lv_event_t* e) {
@@ -15067,6 +15132,62 @@ static void themeContrastToggleCb(lv_event_t* e) {
: (s_theme_day ? TOUCH_THEME_DAY : TOUCH_THEME_NIGHT));
}
#if CAP_TRACKBALL || defined(TLORA_PAGER)
// Settings > Keyboard: the programmable tab hotkeys and navigation keys, one row
// each; tap (or click) a row, then press the new key. Shared by the T-Deck and the
// Pager (#591) so both boards remap keys the same way. Returns the new y.
static int settingsNavKeyRows(lv_obj_t* body, int y) {
// Programmable tab hotkeys — tap a row, then press a key to reassign it.
y += settingsRowLabel(body, y, 0, TR("Tab hotkeys \xe2\x80\x94 tap a row, then press a key"), COLOR_SUB, &g_font_12, 0) + 2;
for (int t = 0; t < 5; t++) {
lv_obj_t* row = lv_btn_create(body);
lv_obj_set_size(row, lv_pct(100), SC(30));
lv_obj_set_pos(row, 2, y);
styleButton(row);
lv_obj_add_event_cb(row, navKeyCaptureStartCb, LV_EVENT_CLICKED, (void*)(intptr_t)t);
lv_obj_t* nm = lv_label_create(row);
lv_label_set_text(nm, TR(kNavTabNames[t]));
lv_obj_set_style_text_font(nm, &g_font_12, LV_PART_MAIN);
lv_obj_set_style_text_color(nm, lv_color_hex(COLOR_TEXT), LV_PART_MAIN);
lv_obj_align(nm, LV_ALIGN_LEFT_MID, 8, 0);
lv_obj_t* kv = lv_label_create(row);
const int lk = navKeyLower(s_nav_keys[t]);
char kb[2] = { (char)(lk >= 'a' && lk <= 'z' ? lk - 'a' + 'A' : (lk ? lk : '-')), 0 };
lv_label_set_text(kv, kb);
lv_obj_set_style_text_font(kv, &g_font_14, LV_PART_MAIN);
lv_obj_set_style_text_color(kv, lv_color_hex(COLOR_ACCENT), LV_PART_MAIN);
lv_obj_align(kv, LV_ALIGN_RIGHT_MID, -10, 0);
s_navkey_row_val[t] = kv;
y += SC(36);
}
// Programmable control keys (move/select/back + scroll up/down) — tap a row, press a key.
y += settingsRowLabel(body, y, 0, TR("Navigation keys \xe2\x80\x94 tap a row, then press a key"), COLOR_SUB, &g_font_12, 0) + 2;
for (int d = 0; d < 8; d++) {
const int bi = d + 5; // binding index 5-12
lv_obj_t* row = lv_btn_create(body);
lv_obj_set_size(row, lv_pct(100), SC(30));
lv_obj_set_pos(row, 2, y);
styleButton(row);
lv_obj_add_event_cb(row, navKeyCaptureStartCb, LV_EVENT_CLICKED, (void*)(intptr_t)bi);
lv_obj_t* nm = lv_label_create(row);
lv_label_set_text(nm, TR(kNavDirNames[d]));
lv_obj_set_style_text_font(nm, &g_font_12, LV_PART_MAIN);
lv_obj_set_style_text_color(nm, lv_color_hex(COLOR_TEXT), LV_PART_MAIN);
lv_obj_align(nm, LV_ALIGN_LEFT_MID, 8, 0);
lv_obj_t* kv = lv_label_create(row);
const int lk = navKeyLower(s_dir_keys[d]);
char kb[2] = { (char)(lk >= 'a' && lk <= 'z' ? lk - 'a' + 'A' : (lk ? lk : '-')), 0 };
lv_label_set_text(kv, kb);
lv_obj_set_style_text_font(kv, &g_font_14, LV_PART_MAIN);
lv_obj_set_style_text_color(kv, lv_color_hex(COLOR_ACCENT), LV_PART_MAIN);
lv_obj_align(kv, LV_ALIGN_RIGHT_MID, -10, 0);
s_navkey_row_val[bi] = kv;
y += SC(36);
}
return y;
}
#endif
static void buildDeviceSettings(int sec) {
// One detail page per section: each block below is gated to its DSEC_* section
// (skipped blocks don't advance y, so every page lays out from the top).
@@ -16177,6 +16298,24 @@ static void buildDeviceSettings(int sec) {
}
#endif
#if defined(TLORA_PAGER)
/* Keyboard navigation (#591), the T-Deck's feature on the Pager: with no text field
focused, letters move the highlight (default I/K/J/L) and scroll (U/O), and the
tab keys are the letters printed on the bar. On by default. Applied live. */
{
int h = settingsRowLabel(body, y, 4, TR("Keyboard navigation"), COLOR_TEXT, &g_font_12, 56);
lv_obj_t* sw = lv_switch_create(body);
lv_obj_align(sw, LV_ALIGN_TOP_RIGHT, 0, y);
if (touchPrefsGetKbdNav()) lv_obj_add_state(sw, LV_STATE_CHECKED);
lv_obj_add_event_cb(sw, pagerKbdNavToggleCb, LV_EVENT_VALUE_CHANGED, nullptr);
y += LV_MAX(34, h + 10);
y += settingsRowLabel(body, y, 0,
TR("Letters move the highlight when no text field is focused. Enter selects; hold Backspace to go back."),
COLOR_SUB, &g_font_12, 0) + 2;
y = settingsNavKeyRows(body, y);
}
#endif
#if CAP_TRACKBALL
#if defined(HAS_TDECK_KEYBOARD) // never the M9: nav is force-set on at boot there (the board's only input) and must not grow an off-switch
/* Keyboard navigation: off (default) vs on. When no text field is focused, the
@@ -16224,53 +16363,7 @@ static void buildDeviceSettings(int sec) {
lv_obj_add_event_cb(sw2, navMbarKeysToggleCb, LV_EVENT_VALUE_CHANGED, nullptr);
y += LV_MAX(34, h2 + 10);
}
// Programmable tab hotkeys — tap a row, then press a key to reassign it.
y += settingsRowLabel(body, y, 0, TR("Tab hotkeys \xe2\x80\x94 tap a row, then press a key"), COLOR_SUB, &g_font_12, 0) + 2;
for (int t = 0; t < 5; t++) {
lv_obj_t* row = lv_btn_create(body);
lv_obj_set_size(row, lv_pct(100), SC(30));
lv_obj_set_pos(row, 2, y);
styleButton(row);
lv_obj_add_event_cb(row, navKeyCaptureStartCb, LV_EVENT_CLICKED, (void*)(intptr_t)t);
lv_obj_t* nm = lv_label_create(row);
lv_label_set_text(nm, TR(kNavTabNames[t]));
lv_obj_set_style_text_font(nm, &g_font_12, LV_PART_MAIN);
lv_obj_set_style_text_color(nm, lv_color_hex(COLOR_TEXT), LV_PART_MAIN);
lv_obj_align(nm, LV_ALIGN_LEFT_MID, 8, 0);
lv_obj_t* kv = lv_label_create(row);
const int lk = navKeyLower(s_nav_keys[t]);
char kb[2] = { (char)(lk >= 'a' && lk <= 'z' ? lk - 'a' + 'A' : lk), 0 };
lv_label_set_text(kv, kb);
lv_obj_set_style_text_font(kv, &g_font_14, LV_PART_MAIN);
lv_obj_set_style_text_color(kv, lv_color_hex(COLOR_ACCENT), LV_PART_MAIN);
lv_obj_align(kv, LV_ALIGN_RIGHT_MID, -10, 0);
s_navkey_row_val[t] = kv;
y += SC(36);
}
// Programmable control keys (move/select/back + scroll up/down) — tap a row, press a key.
y += settingsRowLabel(body, y, 0, TR("Navigation keys \xe2\x80\x94 tap a row, then press a key"), COLOR_SUB, &g_font_12, 0) + 2;
for (int d = 0; d < 8; d++) {
const int bi = d + 5; // binding index 5-12
lv_obj_t* row = lv_btn_create(body);
lv_obj_set_size(row, lv_pct(100), SC(30));
lv_obj_set_pos(row, 2, y);
styleButton(row);
lv_obj_add_event_cb(row, navKeyCaptureStartCb, LV_EVENT_CLICKED, (void*)(intptr_t)bi);
lv_obj_t* nm = lv_label_create(row);
lv_label_set_text(nm, TR(kNavDirNames[d]));
lv_obj_set_style_text_font(nm, &g_font_12, LV_PART_MAIN);
lv_obj_set_style_text_color(nm, lv_color_hex(COLOR_TEXT), LV_PART_MAIN);
lv_obj_align(nm, LV_ALIGN_LEFT_MID, 8, 0);
lv_obj_t* kv = lv_label_create(row);
const int lk = navKeyLower(s_dir_keys[d]);
char kb[2] = { (char)(lk >= 'a' && lk <= 'z' ? lk - 'a' + 'A' : lk), 0 };
lv_label_set_text(kv, kb);
lv_obj_set_style_text_font(kv, &g_font_14, LV_PART_MAIN);
lv_obj_set_style_text_color(kv, lv_color_hex(COLOR_ACCENT), LV_PART_MAIN);
lv_obj_align(kv, LV_ALIGN_RIGHT_MID, -10, 0);
s_navkey_row_val[bi] = kv;
y += SC(36);
}
y = settingsNavKeyRows(body, y); // tab hotkeys + navigation keys (shared with the Pager)
}
#endif
@@ -43866,14 +43959,9 @@ static int tabForKey(int key) {
// an open chat, settings detail, app page, or popup they remain inert so a
// letter cannot unexpectedly abandon the inner screen.
if (!navOnMainPage()) return -1;
switch (key) {
case 'm': case 'M': return CHAT_INBOX_TAB_INDEX;
case 'c': case 'C': return CONTACTS_TAB_INDEX;
case 'h': case 'H': return HOME_TAB_INDEX;
case 'a': case 'A': return MAP_TAB_INDEX;
case 's': case 'S': return SETTINGS_TAB_INDEX;
default: return -1;
}
// Programmable like the T-Deck's tab hotkeys (#591), defaulting to M/C/H/A/S
// (Settings > Keyboard). Always active, as the Pager's mnemonics always were.
return navTabForHotkey(key);
#else
// Old fixed letter tab-jumps (h/m/c/l/s) removed — tab jumps are now the
// programmable keyboard-nav hotkeys (navTabForHotkey, default E/R/T/U/I), active
@@ -46148,7 +46236,7 @@ if (g_lv.task && g_lv.task->isManualLock()) {
{ char _pb[28]; snprintf(_pb, sizeof _pb, "key 0x%02X '%c'", key & 0xFF,
(key >= 32 && key < 127) ? (char)key : '.'); if (g_lv.task) g_lv.task->showAlert(_pb, 1400); }
#endif
#if CAP_TRACKBALL
#if CAP_TRACKBALL || defined(TLORA_PAGER)
// Remapping a tab hotkey (Settings → Keyboard): capture the next key press.
if (s_navkey_capture >= 0) { navKeyCaptureApply(key); return; }
#endif
@@ -46298,6 +46386,40 @@ if (g_lv.task && g_lv.task->isManualLock()) {
}
return;
}
// Keyboard navigation (#591): the T-Deck's programmable letters, defaulting here to
// I/K/J/L to move the highlight and U/O to scroll (Settings > Keyboard). Only with
// no text field focused (this is the !ta branch), so letters in a field still type.
// On the Map the letters move the highlight too; W/A/X/D above stay the map pan.
if (s_kbd_nav) {
const int act = navDirForKey(key);
if (act >= 0) {
lv_obj_t* focused = s_nav_group ? lv_group_get_focused(s_nav_group) : nullptr;
(void)focused;
switch (act) {
case 0: navMoveDir(NAV_UP); break;
case 1: navMoveDir(NAV_DOWN); break;
case 2: case 3:
#if defined(HAS_PAGER_ENCODER)
// Wheel edit mode on a slider: preview only, saved on the closing click.
if (focused && focused == s_pager_slider_edit) { navSliderNudge(focused, act == 3 ? +1 : -1); break; }
#endif
navMoveDir(act == 3 ? NAV_RIGHT : NAV_LEFT);
break;
case 4: // select: the same as Enter / the wheel's click
#if defined(HAS_PAGER_ENCODER)
if (pagerSliderEditClick()) break;
#endif
if (!navEnterBubble()) navPushTap(LV_KEY_ENTER);
break;
case 5: pagerNavGoBack(); break; // back: the same as Backspace-hold / long-press
case 6: navScrollFocused(true); break; // scroll up
default: navScrollFocused(false); break; // scroll down
}
s_nav_show = true;
if (g_lv.task) g_lv.task->noteUserInput();
return;
}
}
#endif
#if CAP_TRACKBALL || defined(HAS_TDECK_PRO) || defined(HAS_THINKNODE_M9)
// A field is focused but we're in navigate mode: select/Enter starts editing it, so the
@@ -55551,10 +55673,11 @@ static void buildUiTree() {
// Condense Nav rail the letter sits under the icon instead of beside it).
// Add order == index order: Chats(0), Contacts(1), Home(2, middle), Map(3), Settings(4).
#if defined(TLORA_PAGER)
const char* const pager_chats_tab_label =
s_nav_condensed ? LV_SYMBOL_ENVELOPE "\nM" : LV_SYMBOL_ENVELOPE " M";
const char* const pager_settings_tab_label =
s_nav_condensed ? LV_SYMBOL_SETTINGS "\nS" : LV_SYMBOL_SETTINGS " S";
// The letters are the (remappable, #591) tab hotkeys; see pagerTabLabel().
char pager_tab_labels[5][16];
for (int t = 0; t < 5; t++) pagerTabLabel(t, pager_tab_labels[t], sizeof pager_tab_labels[t]);
const char* const pager_chats_tab_label = pager_tab_labels[CHAT_INBOX_TAB_INDEX];
const char* const pager_settings_tab_label = pager_tab_labels[4];
auto pagerTabIconBadgeX = [](int tab_index, const char* label, const char* icon) -> lv_coord_t {
const lv_coord_t cell_w = lv_disp_get_hor_res(nullptr) / 5;
const lv_coord_t label_w = lv_txt_get_width(label, strlen(label), &g_font_tab, 0, LV_TEXT_FLAG_NONE);
@@ -55562,12 +55685,9 @@ static void buildUiTree() {
return cell_w * tab_index + cell_w / 2 - (label_w - icon_w) / 2 - 8;
};
lv_obj_t* tab_chats = lv_tabview_add_tab(g_lv.tabview, pager_chats_tab_label);
lv_obj_t* tab_contacts = lv_tabview_add_tab(g_lv.tabview,
s_nav_condensed ? TOUCH_SYM_PERSON "\nC" : TOUCH_SYM_PERSON " C");
lv_obj_t* tab_home = lv_tabview_add_tab(g_lv.tabview,
s_nav_condensed ? LV_SYMBOL_HOME "\nH" : LV_SYMBOL_HOME " H");
lv_obj_t* tab_map = lv_tabview_add_tab(g_lv.tabview,
s_nav_condensed ? LV_SYMBOL_GPS "\nA" : LV_SYMBOL_GPS " A");
lv_obj_t* tab_contacts = lv_tabview_add_tab(g_lv.tabview, pager_tab_labels[CONTACTS_TAB_INDEX]);
lv_obj_t* tab_home = lv_tabview_add_tab(g_lv.tabview, pager_tab_labels[HOME_TAB_INDEX]);
lv_obj_t* tab_map = lv_tabview_add_tab(g_lv.tabview, pager_tab_labels[MAP_TAB_INDEX]);
#else
lv_obj_t* tab_chats = lv_tabview_add_tab(g_lv.tabview, LV_SYMBOL_ENVELOPE);
lv_obj_t* tab_contacts = lv_tabview_add_tab(g_lv.tabview, TOUCH_SYM_PERSON); // person icon (FA user)
@@ -61975,6 +62095,12 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
#if defined(HAS_TANMATSU)
bsp_input_get_queue(&s_nav_queue);
#endif
#if defined(TLORA_PAGER)
// Keyboard navigation (#591): on/off plus the programmable tab and navigation keys.
s_kbd_nav = touchPrefsGetKbdNav();
for (int i = 0; i < 5; i++) { uint8_t k = touchPrefsGetNavKey(i); if (k) s_nav_keys[i] = k; }
for (int i = 0; i < 8; i++) { uint8_t k = touchPrefsGetNavDirKey(i); if (k) s_dir_keys[i] = k; }
#endif
#else
// Physical touchscreen indev — skipped in remote mode (the panel is a placeholder;
// its 320x240 coords would map wrong onto the 480x800 web UI). The web pointer drives it.
+28
View File
@@ -422,6 +422,7 @@ static const I18nPair kBuiltin_hu[] = {
{ "Keyboard light on", "Billentyűzetfény be" },
{ "Keyboard nav: WASDZ on", "Billentyűzet navigáció: WASDZ bekapcsolva" },
{ "Keyboard nav: off", "Billentyűzetnavigáció: kikapcsolva" },
{ "Keyboard nav: on", "Billentyűzetnavigáció: bekapcsolva" },
{ "Keyboard navigation", "Billentyűzetnavigáció" },
{ "Keys", "Kulcsok" },
{ "Known regions", "Ismert régiók" },
@@ -439,6 +440,7 @@ static const I18nPair kBuiltin_hu[] = {
{ "Leaving remote mode - rebooting...", "Kilépés a távoli módból - újraindítás..." },
{ "Left", "Balra" },
{ "Let's set up your device.\n\nYou'll pick a name and choose your LoRa region. Takes about a minute.", "Állítsuk be az eszközt.\n\nVálassz egy nevet, és válaszd ki a LoRa régiódat. Körülbelül egy percet vesz igénybe." },
{ "Letters move the highlight when no text field is focused. Enter selects; hold Backspace to go back.", "A betűk mozgatják a kijelölést, ha nincs aktív szövegmező. Az Enter kiválaszt; a Backspace nyomva tartása visszalép." },
{ "Letters only", "Csak betűk" },
{ "Loading catalog…", "Katalógus betöltése…" },
{ "Loading map tiles…\n\nKeep Wi-Fi connected.\nTiles appear as they arrive.", "Térképcsempék betöltése…\n\nWi-Fi kapcsolaton keresztül.\nA csempék érkezésükkor jelennek meg." },
@@ -1434,6 +1436,7 @@ static const I18nPair kBuiltin_nl[] = {
{ "Keyboard light on", "Toetsenbordverlichting aan" },
{ "Keyboard nav: WASDZ on", "Toetsenbordnav.: WASDZ aan" },
{ "Keyboard nav: off", "Toetsenbordnav.: uit" },
{ "Keyboard nav: on", "Toetsenbordnav.: aan" },
{ "Keyboard navigation", "Toetsenbordnavigatie" },
{ "Keys", "Toetsen" },
{ "LOS → %s", "Zichtlijn → %s" },
@@ -1450,6 +1453,7 @@ static const I18nPair kBuiltin_nl[] = {
{ "Leaving remote mode - rebooting...", "Externe modus verlaten - herstarten..." },
{ "Left", "Links" },
{ "Let's set up your device.\n\nYou'll pick a name and choose your LoRa region. Takes about a minute.", "Laten we je apparaat instellen.\n\nJe kiest een naam en je LoRa-regio. Duurt ongeveer een minuut." },
{ "Letters move the highlight when no text field is focused. Enter selects; hold Backspace to go back.", "Letters verplaatsen de markering als er geen tekstveld actief is. Enter selecteert; houd Backspace ingedrukt om terug te gaan." },
{ "Letters only", "Alleen letters" },
{ "LoRa antenna (P4)", "LoRa-antenne (P4)" },
{ "Loading catalog…", "Catalogus laden…" },
@@ -2369,6 +2373,7 @@ static const I18nPair kBuiltin_de[] = {
{ "Keyboard light on", "Tastaturlicht ein" },
{ "Keyboard nav: WASDZ on", "Tastaturnav.: WASDZ an" },
{ "Keyboard nav: off", "Tastaturnav.: aus" },
{ "Keyboard nav: on", "Tastaturnav.: an" },
{ "Keyboard navigation", "Tastaturnavigation" },
{ "Keys", "Tasten" },
{ "LOS → %s", "Sichtlinie → %s" },
@@ -2385,6 +2390,7 @@ static const I18nPair kBuiltin_de[] = {
{ "Leaving remote mode - rebooting...", "Fernmodus wird beendet - Neustart..." },
{ "Left", "Links" },
{ "Let's set up your device.\n\nYou'll pick a name and choose your LoRa region. Takes about a minute.", "Richten wir dein Gerät ein.\n\nDu wählst einen Namen und deine LoRa-Region. Dauert etwa eine Minute." },
{ "Letters move the highlight when no text field is focused. Enter selects; hold Backspace to go back.", "Buchstaben bewegen die Markierung, wenn kein Textfeld aktiv ist. Enter wählt aus; Rücktaste halten geht zurück." },
{ "Letters only", "Nur Buchstaben" },
{ "LoRa antenna (P4)", "LoRa-Antenne (P4)" },
{ "Loading catalog…", "Katalog wird geladen…" },
@@ -3290,6 +3296,7 @@ static const I18nPair kBuiltin_fr[] = {
{ "Keyboard light on", "Éclairage clavier activé" },
{ "Keyboard nav: WASDZ on", "Nav. clavier : WASDZ activé" },
{ "Keyboard nav: off", "Nav. clavier : désactivé" },
{ "Keyboard nav: on", "Nav. clavier : activé" },
{ "Keyboard navigation", "Navigation clavier" },
{ "LOS → %s", "Vue directe → %s" },
{ "Landscape (desktop)", "Paysage (bureau)" },
@@ -3302,6 +3309,7 @@ static const I18nPair kBuiltin_fr[] = {
{ "Leave empty if not required", "Laisser vide si non requis" },
{ "Leaving remote mode - rebooting...", "Sortie du mode distant - redémarrage..." },
{ "Let's set up your device.\n\nYou'll pick a name and choose your LoRa region. Takes about a minute.", "Configurons votre appareil.\n\nVous choisirez un nom et votre région LoRa. Cela prend environ une minute." },
{ "Letters move the highlight when no text field is focused. Enter selects; hold Backspace to go back.", "Les lettres déplacent la sélection quand aucun champ de texte n'est actif. Entrée sélectionne ; maintenez Retour arrière pour revenir." },
{ "Letters only", "Lettres uniquement" },
{ "LoRa antenna (P4)", "Antenne LoRa (P4)" },
{ "Loading catalog…", "Chargement du catalogue…" },
@@ -4184,6 +4192,7 @@ static const I18nPair kBuiltin_es[] = {
{ "Keyboard light on", "Luz del teclado encendida" },
{ "Keyboard nav: WASDZ on", "Nav. teclado: WASDZ act." },
{ "Keyboard nav: off", "Nav. teclado: desact." },
{ "Keyboard nav: on", "Nav. teclado: activ." },
{ "Keyboard navigation", "Navegación por teclado" },
{ "LOS → %s", "Línea de vista → %s" },
{ "Landscape (desktop)", "Horizontal (escritorio)" },
@@ -4197,6 +4206,7 @@ static const I18nPair kBuiltin_es[] = {
{ "Leave empty if not required", "Dejar vacío si no es necesario" },
{ "Leaving remote mode - rebooting...", "Saliendo del modo remoto - reiniciando..." },
{ "Let's set up your device.\n\nYou'll pick a name and choose your LoRa region. Takes about a minute.", "Configuremos tu dispositivo.\n\nElegirás un nombre y tu región LoRa. Tarda alrededor de un minuto." },
{ "Letters move the highlight when no text field is focused. Enter selects; hold Backspace to go back.", "Las letras mueven el resaltado cuando no hay un campo de texto activo. Intro selecciona; mantén Retroceso para volver." },
{ "Letters only", "Solo letras" },
{ "LoRa antenna (P4)", "Antena LoRa (P4)" },
{ "Loading catalog…", "Cargando catálogo…" },
@@ -5084,6 +5094,7 @@ static const I18nPair kBuiltin_it[] = {
{ "Keyboard light on", "Luce tastiera accesa" },
{ "Keyboard nav: WASDZ on", "Nav. tastiera: WASDZ on" },
{ "Keyboard nav: off", "Nav. tastiera: off" },
{ "Keyboard nav: on", "Nav. tastiera: on" },
{ "Keyboard navigation", "Navigazione da tastiera" },
{ "LOS → %s", "Vista diretta → %s" },
{ "Landscape (desktop)", "Orizzontale (desktop)" },
@@ -5097,6 +5108,7 @@ static const I18nPair kBuiltin_it[] = {
{ "Leave empty if not required", "Lascia vuoto se non richiesto" },
{ "Leaving remote mode - rebooting...", "Uscita dalla modalità remota - riavvio..." },
{ "Let's set up your device.\n\nYou'll pick a name and choose your LoRa region. Takes about a minute.", "Configuriamo il tuo dispositivo.\n\nSceglierai un nome e la tua regione LoRa. Richiede circa un minuto." },
{ "Letters move the highlight when no text field is focused. Enter selects; hold Backspace to go back.", "Le lettere spostano l'evidenziazione quando nessun campo di testo è attivo. Invio seleziona; tieni premuto Backspace per tornare indietro." },
{ "Letters only", "Solo lettere" },
{ "LoRa antenna (P4)", "Antenna LoRa (P4)" },
{ "Loading catalog…", "Caricamento catalogo…" },
@@ -5988,6 +6000,7 @@ static const I18nPair kBuiltin_ru[] = {
{ "Keyboard light on", "Подсветка клавиатуры вкл." },
{ "Keyboard nav: WASDZ on", "Навигация клавиатурой: WASDZ вкл." },
{ "Keyboard nav: off", "Навигация клавиатурой: выкл." },
{ "Keyboard nav: on", "Навигация клавиатурой: вкл." },
{ "Keyboard navigation", "Навигация клавиатурой" },
{ "LOS → %s", "Прямая видимость → %s" },
{ "Landscape (desktop)", "Альбомная (десктоп)" },
@@ -6001,6 +6014,7 @@ static const I18nPair kBuiltin_ru[] = {
{ "Leave empty if not required", "Оставьте пустым, если не нужно" },
{ "Leaving remote mode - rebooting...", "Выход из удалённого режима - перезагрузка..." },
{ "Let's set up your device.\n\nYou'll pick a name and choose your LoRa region. Takes about a minute.", "Настроим ваше устройство.\n\nВы выберете имя и регион LoRa. Займёт около минуты." },
{ "Letters move the highlight when no text field is focused. Enter selects; hold Backspace to go back.", "Буквы перемещают выделение, когда не активно текстовое поле. Enter выбирает; удерживайте Backspace, чтобы вернуться." },
{ "Letters only", "Только буквы" },
{ "LoRa antenna (P4)", "Антенна LoRa (P4)" },
{ "Loading catalog…", "Загрузка каталога…" },
@@ -6899,6 +6913,7 @@ static const I18nPair kBuiltin_uk[] = {
{ "Keyboard light on", "Підсвітка клавіатури увімк." },
{ "Keyboard nav: WASDZ on", "Навігація клавіатурою: WASDZ увімк." },
{ "Keyboard nav: off", "Навігація клавіатурою: вимк." },
{ "Keyboard nav: on", "Навігація клавіатурою: увімк." },
{ "Keyboard navigation", "Навігація клавіатурою" },
{ "LOS → %s", "Пряма видимість → %s" },
{ "Landscape (desktop)", "Альбомна (десктоп)" },
@@ -6912,6 +6927,7 @@ static const I18nPair kBuiltin_uk[] = {
{ "Leave empty if not required", "Залиште порожнім, якщо не потрібно" },
{ "Leaving remote mode - rebooting...", "Вихід із віддаленого режиму - перезавантаження..." },
{ "Let's set up your device.\n\nYou'll pick a name and choose your LoRa region. Takes about a minute.", "Налаштуймо ваш пристрій.\n\nВи виберете ім'я та регіон LoRa. Займе близько хвилини." },
{ "Letters move the highlight when no text field is focused. Enter selects; hold Backspace to go back.", "Літери переміщують виділення, коли не активне текстове поле. Enter вибирає; утримуйте Backspace, щоб повернутися." },
{ "Letters only", "Лише літери" },
{ "LoRa antenna (P4)", "Антена LoRa (P4)" },
{ "Loading catalog…", "Завантаження каталогу…" },
@@ -7808,6 +7824,7 @@ static const I18nPair kBuiltin_bg[] = {
{ "Keyboard light on", "Подсветката на клавиатурата е вкл." },
{ "Keyboard nav: WASDZ on", "Клавиатурна навиг.: WASDZ вкл." },
{ "Keyboard nav: off", "Клавиатурна навиг.: изкл." },
{ "Keyboard nav: on", "Клавиатурна навиг.: вкл." },
{ "Keyboard navigation", "Навигация с клавиатура" },
{ "LOS → %s", "Пряка видимост → %s" },
{ "Landscape (desktop)", "Пейзаж (десктоп)" },
@@ -7821,6 +7838,7 @@ static const I18nPair kBuiltin_bg[] = {
{ "Leave empty if not required", "Оставете празно, ако не е нужно" },
{ "Leaving remote mode - rebooting...", "Излизане от отдалечен режим - рестартиране..." },
{ "Let's set up your device.\n\nYou'll pick a name and choose your LoRa region. Takes about a minute.", "Нека настроим устройството ви.\n\nЩе изберете име и вашия LoRa регион. Отнема около минута." },
{ "Letters move the highlight when no text field is focused. Enter selects; hold Backspace to go back.", "Буквите местят маркера, когато няма активно текстово поле. Enter избира; задръжте Backspace за назад." },
{ "Letters only", "Само букви" },
{ "LoRa antenna (P4)", "LoRa антена (P4)" },
{ "Loading catalog…", "Зареждане на каталога…" },
@@ -8717,6 +8735,7 @@ static const I18nPair kBuiltin_sr[] = {
{ "Keyboard light on", "Осветљење тастатуре укљ." },
{ "Keyboard nav: WASDZ on", "Навиг. тастатуром: WASDZ укљ." },
{ "Keyboard nav: off", "Навиг. тастатуром: искљ." },
{ "Keyboard nav: on", "Навиг. тастатуром: укљ." },
{ "Keyboard navigation", "Навигација тастатуром" },
{ "LOS → %s", "Оптичка видљивост → %s" },
{ "Landscape (desktop)", "Положено (десктоп)" },
@@ -8730,6 +8749,7 @@ static const I18nPair kBuiltin_sr[] = {
{ "Leave empty if not required", "Оставите празно ако није потребно" },
{ "Leaving remote mode - rebooting...", "Излазак из даљинског режима - рестартовање..." },
{ "Let's set up your device.\n\nYou'll pick a name and choose your LoRa region. Takes about a minute.", "Хајде да подесимо ваш уређај.\n\nИзабраћете име и ваш LoRa регион. Траје око минут." },
{ "Letters move the highlight when no text field is focused. Enter selects; hold Backspace to go back.", "Слова померају истицање када ниједно текстуално поље није активно. Enter бира; држите Backspace за назад." },
{ "Letters only", "Само слова" },
{ "LoRa antenna (P4)", "LoRa антена (P4)" },
{ "Loading catalog…", "Учитавање каталога…" },
@@ -9624,6 +9644,7 @@ static const I18nPair kBuiltin_el[] = {
{ "Keyboard light on", "Φωτισμός πληκτρολογίου ενεργός" },
{ "Keyboard nav: WASDZ on", "Πλοήγηση πληκτρ.: WASDZ ενεργό" },
{ "Keyboard nav: off", "Πλοήγηση πληκτρ.: κλειστό" },
{ "Keyboard nav: on", "Πλοήγηση πληκτρ.: ανοιχτό" },
{ "Keyboard navigation", "Πλοήγηση με πληκτρολόγιο" },
{ "LOS → %s", "Οπτική επαφή → %s" },
{ "Landscape (desktop)", "Οριζόντια (desktop)" },
@@ -9637,6 +9658,7 @@ static const I18nPair kBuiltin_el[] = {
{ "Leave empty if not required", "Αφήστε κενό αν δεν απαιτείται" },
{ "Leaving remote mode - rebooting...", "Έξοδος από απομακρυσμένη λειτουργία - επανεκκίνηση..." },
{ "Let's set up your device.\n\nYou'll pick a name and choose your LoRa region. Takes about a minute.", "Ας ρυθμίσουμε τη συσκευή σας.\n\nΘα επιλέξετε όνομα και την περιοχή LoRa. Διαρκεί περίπου ένα λεπτό." },
{ "Letters move the highlight when no text field is focused. Enter selects; hold Backspace to go back.", "Τα γράμματα μετακινούν την επισήμανση όταν δεν είναι ενεργό πεδίο κειμένου. Το Enter επιλέγει· κρατήστε το Backspace για επιστροφή." },
{ "Letters only", "Μόνο γράμματα" },
{ "LoRa antenna (P4)", "Κεραία LoRa (P4)" },
{ "Loading catalog…", "Φόρτωση καταλόγου…" },
@@ -10523,6 +10545,7 @@ static const I18nPair kBuiltin_pt_br[] = {
{ "Keyboard light on", "Luz do teclado ligada" },
{ "Keyboard nav: WASDZ on", "Nav. teclado: WASDZ ligado" },
{ "Keyboard nav: off", "Nav. teclado: desligado" },
{ "Keyboard nav: on", "Nav. teclado: ligado" },
{ "Keyboard navigation", "Navegação por teclado" },
{ "LOS → %s", "Linha de visada → %s" },
{ "Landscape (desktop)", "Paisagem (desktop)" },
@@ -10535,6 +10558,7 @@ static const I18nPair kBuiltin_pt_br[] = {
{ "Leave empty if not required", "Deixe vazio se não for necessário" },
{ "Leaving remote mode - rebooting...", "Saindo do modo remoto - reiniciando..." },
{ "Let's set up your device.\n\nYou'll pick a name and choose your LoRa region. Takes about a minute.", "Vamos configurar seu dispositivo.\n\nVocê escolherá um nome e sua região LoRa. Leva cerca de um minuto." },
{ "Letters move the highlight when no text field is focused. Enter selects; hold Backspace to go back.", "As letras movem o destaque quando nenhum campo de texto está ativo. Enter seleciona; segure Backspace para voltar." },
{ "Letters only", "Somente letras" },
{ "LoRa antenna (P4)", "Antena LoRa (P4)" },
{ "Loading catalog…", "Carregando catálogo…" },
@@ -11413,6 +11437,7 @@ static const I18nPair kBuiltin_ro[] = {
{ "Keyboard light on", "Lumina tastaturii pornită" },
{ "Keyboard nav: WASDZ on", "Navigare tastatură: WASDZ pornit" },
{ "Keyboard nav: off", "Navigare tastatură: oprită" },
{ "Keyboard nav: on", "Navigare tastatură: pornită" },
{ "Keyboard navigation", "Navigare cu tastatura" },
{ "LOS → %s", "Linie vizuală → %s" },
{ "Landscape (desktop)", "Peisaj (desktop)" },
@@ -11426,6 +11451,7 @@ static const I18nPair kBuiltin_ro[] = {
{ "Leave empty if not required", "Lasă gol dacă nu e necesar" },
{ "Leaving remote mode - rebooting...", "Se iese din modul la distanţă - repornire..." },
{ "Let's set up your device.\n\nYou'll pick a name and choose your LoRa region. Takes about a minute.", "Să-ţi configurăm dispozitivul.\n\nÎţi alegi un nume şi regiunea LoRa. Durează cam un minut." },
{ "Letters move the highlight when no text field is focused. Enter selects; hold Backspace to go back.", "Literele mută selecția când niciun câmp de text nu este activ. Enter selectează; ține apăsat Backspace pentru înapoi." },
{ "Letters only", "Doar litere" },
{ "LoRa antenna (P4)", "Antenă LoRa (P4)" },
{ "Loading catalog…", "Se încarcă catalogul…" },
@@ -12427,6 +12453,7 @@ static const I18nPair kBuiltin_cs[] = {
{ "Keyboard light on", "Světlo klávesnice zapnuto" },
{ "Keyboard nav: WASDZ on", "Navigace klávesnicí: WASDZ zapnuta" },
{ "Keyboard nav: off", "Navigace klávesnicí: vypnuta" },
{ "Keyboard nav: on", "Navigace klávesnicí: zapnuta" },
{ "Keyboard navigation", "Navigace klávesnicí" },
{ "Keys", "Klávesy" },
{ "Known regions", "Známé regiony" },
@@ -12446,6 +12473,7 @@ static const I18nPair kBuiltin_cs[] = {
{ "Leaving remote mode - restarting…", "Ukončování vzdáleného režimu - restartování…" },
{ "Left", "Vlevo" },
{ "Let's set up your device.\n\nYou'll pick a name and choose your LoRa region. Takes about a minute.", "Nastavme vaše zařízení.\n\nVyberete jméno a region LoRa. Zabere to asi minutu." },
{ "Letters move the highlight when no text field is focused. Enter selects; hold Backspace to go back.", "Písmena posouvají zvýraznění, když není aktivní textové pole. Enter vybírá; podržením Backspace se vrátíte zpět." },
{ "Letters only", "Pouze písmena" },
{ "Link lost", "Spojení ztraceno" },
{ "LoRa antenna (P4)", "LoRa anténa (P4)" },