Files
MeshCore-Solo/examples/companion_radio/ui-lvgl/Keyboard.h
T
JakubandClaude Opus 5.5 331a4e64c4 feat(l2): Arduino-ESP32 3.3.12 / IDF 5.5 env, NimBLE and new I2S driver ports, large UI buffers in PSRAM
- New env Wio_Tracker_L2_companion_solo_lvgl_v3 on pioarduino 55.03.312-1;
  the 2.0.17 env stays until the new one passes the hardware checklist
- SerialBLEInterface: NimBLE variant (PIN pairing through encrypted,
  authenticated characteristic properties; setAccessPermissions is a no-op)
- buzzer: i2s_std driver on IDF 5 (legacy driver kept for 2.0.17)
- ESPNOWRadio: IDF 5 callback signatures
- ui-lvgl: psramBuf() for trail drawing, tile cache, map marks, list rows,
  keyboard maps and message metadata; internal heap back to 2.0.17 level

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 09:14:58 +02:00

272 lines
11 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#pragma once
// Phone-style on-screen keyboard on top of lv_keyboard (USER_1 mode):
// - one letter layout per script (Latin QWERTY, Cyrillic ЙЦУКЕН, Greek);
// which scripts are offered comes from the same prefs ui-new uses
// (NodePrefs::keyboard_main_alphabet / keyboard_alt_alphabet). The globe
// key switches between the two when they differ.
// - hold a key for its variants (ą ć ę … / ё є ї … / ά έ … / punctuation
// behind '.'), from the shared table in ui-core/KeyboardData.h. The popup
// opens above the key; tap a variant to type it, tap elsewhere to close.
// - one-shot shift; digits / symbols page behind "1#"; the keyboard key
// closes it (LV_EVENT_CANCEL: the compose bar keeps its text, a dialog
// is left without saving).
// Letters type on release (popover keys), so a long press never types the
// base letter first.
//
// Single-TU fragment: included by ui-lvgl/UITask.cpp only.
namespace kb {
#define KB_BS LV_SYMBOL_BACKSPACE
#define KB_OK LV_SYMBOL_OK
#define KB_SH LV_SYMBOL_UP
#define KB_HIDE LV_SYMBOL_KEYBOARD // closes it: lv_keyboard_def_event_cb sends LV_EVENT_CANCEL
static const char* const K_SYM = "1#";
static const char* const K_BACK = "abc";
static const char* const K_GLOBE = "\xEF\x82\xAC"; // FontAwesome globe (U+F0AC) -- see fonts/generate.sh
static const char* const LAT[] = {
"q","w","e","r","t","y","u","i","o","p","\n",
"a","s","d","f","g","h","j","k","l","\n",
KB_SH,"z","x","c","v","b","n","m",KB_BS,"\n",
nullptr };
static const char* const LAT_UP[] = {
"Q","W","E","R","T","Y","U","I","O","P","\n",
"A","S","D","F","G","H","J","K","L","\n",
KB_SH,"Z","X","C","V","B","N","M",KB_BS,"\n",
nullptr };
static const char* const CYR[] = {
"й","ц","у","к","е","н","г","ш","щ","з","х","\n",
"ф","ы","в","а","п","р","о","л","д","ж","э","\n",
KB_SH,"я","ч","с","м","и","т","ь","б","ю",KB_BS,"\n",
nullptr };
static const char* const CYR_UP[] = {
"Й","Ц","У","К","Е","Н","Г","Ш","Щ","З","Х","\n",
"Ф","Ы","В","А","П","Р","О","Л","Д","Ж","Э","\n",
KB_SH,"Я","Ч","С","М","И","Т","Ь","Б","Ю",KB_BS,"\n",
nullptr };
static const char* const GRK[] = {
"ς","ε","ρ","τ","υ","θ","ι","ο","π","\n",
"α","σ","δ","φ","γ","η","ξ","κ","λ","\n",
KB_SH,"ζ","χ","ψ","ω","β","ν","μ",KB_BS,"\n",
nullptr };
static const char* const GRK_UP[] = {
"Σ","Ε","Ρ","Τ","Υ","Θ","Ι","Ο","Π","\n",
"Α","Σ","Δ","Φ","Γ","Η","Ξ","Κ","Λ","\n",
KB_SH,"Ζ","Χ","Ψ","Ω","Β","Ν","Μ",KB_BS,"\n",
nullptr };
static const char* const SYM[] = {
"1","2","3","4","5","6","7","8","9","0","\n",
"-","/",":",";","(",")","€","&","@","\"","\n",
".",",","?","!","'","#","%","+","=","_",KB_BS,"\n",
nullptr };
// Layout ids; letter layouts in (lower, upper) pairs, in NodePrefs
// KB_ALPHABET_* order (Latin, Cyrillic, Greek) so script * 2 = its lower layout.
enum : uint8_t { L_LAT, L_LAT_UP, L_CYR, L_CYR_UP, L_GRK, L_GRK_UP, L_PAIRED_END = L_GRK_UP,
L_SYM, L_COUNT };
static const char* const* const ROWS[L_COUNT] = { LAT, LAT_UP, CYR, CYR_UP, GRK, GRK_UP, SYM };
// Full lv_keyboard maps: the rows above + a bottom row assembled in
// buildMaps() (globe only when a second script is enabled).
static const int MAX_KEYS = 40;
static const char* (*s_maps)[MAX_KEYS + 8] = psramBuf<const char*[MAX_KEYS + 8]>(L_COUNT); // in PSRAM
static lv_buttonmatrix_ctrl_t (*s_ctrl)[MAX_KEYS] = psramBuf<lv_buttonmatrix_ctrl_t[MAX_KEYS]>(L_COUNT);
static uint8_t s_layout = L_LAT;
static uint8_t s_script = 0; // current script (NodePrefs KB_ALPHABET_*)
static NodePrefs* s_prefs = nullptr;
static bool s_suppress_release = false; // a long press opened the popup: don't type the base
static lv_obj_t* s_popup = nullptr; // overlay (on lv_layer_top) holding the variants row
static lv_obj_t* s_popup_kbd = nullptr;
static uint8_t mainScript() {
uint8_t s = s_prefs ? s_prefs->keyboard_main_alphabet : 0;
return s < NodePrefs::KB_ALPHABET_COUNT ? s : 0;
}
static uint8_t altScript() {
uint8_t s = s_prefs ? s_prefs->keyboard_alt_alphabet : 0;
return s < NodePrefs::KB_ALPHABET_COUNT ? s : 0;
}
static bool hasAlt() { return altScript() != mainScript(); }
static bool isSpecial(const char* t) {
static const char* const SP[] = { KB_SH, KB_BS, KB_OK, KB_HIDE, K_SYM, K_BACK, K_GLOBE };
for (const char* s : SP) if (strcmp(t, s) == 0) return true;
return false;
}
// Assemble the maps and derive key widths / styles from the labels: letters
// narrow, typed on release with a magnified popover and no auto-repeat;
// special keys wider and highlighted; space widest.
static void buildMaps() {
bool alt = hasAlt();
for (int l = 0; l < L_COUNT; l++) {
int m = 0;
for (const char* const* p = ROWS[l]; *p; p++) s_maps[l][m++] = *p;
if (l == L_SYM) {
s_maps[l][m++] = KB_HIDE; s_maps[l][m++] = K_BACK; s_maps[l][m++] = " ";
s_maps[l][m++] = "*"; s_maps[l][m++] = KB_OK;
} else {
s_maps[l][m++] = KB_HIDE; s_maps[l][m++] = K_SYM;
if (alt) s_maps[l][m++] = K_GLOBE;
s_maps[l][m++] = " "; s_maps[l][m++] = "."; s_maps[l][m++] = KB_OK;
}
s_maps[l][m] = "";
int k = 0;
for (const char* const* p = s_maps[l]; **p; p++) {
if (strcmp(*p, "\n") == 0) continue;
uint32_t c;
if (strcmp(*p, " ") == 0) c = 6;
else if (strcmp(*p, KB_BS) == 0) c = 3 | LV_BUTTONMATRIX_CTRL_CHECKED; // keeps auto-repeat
else if (isSpecial(*p)) c = 3 | LV_BUTTONMATRIX_CTRL_CHECKED | LV_BUTTONMATRIX_CTRL_NO_REPEAT;
else c = 2 | LV_BUTTONMATRIX_CTRL_POPOVER | LV_BUTTONMATRIX_CTRL_NO_REPEAT;
if (k < MAX_KEYS) s_ctrl[l][k++] = (lv_buttonmatrix_ctrl_t)c;
}
}
}
// Label of button `id` (buttons numbered without the "\n" row breaks).
static const char* keyText(uint8_t layout, uint32_t id) {
uint32_t k = 0;
for (const char* const* p = s_maps[layout]; **p; p++) {
if (strcmp(*p, "\n") == 0) continue;
if (k++ == id) return *p;
}
return "";
}
static void apply(lv_obj_t* kbd, uint8_t layout) {
s_layout = layout;
lv_keyboard_set_map(kbd, LV_KEYBOARD_MODE_USER_1, s_maps[layout], s_ctrl[layout]);
lv_keyboard_set_mode(kbd, LV_KEYBOARD_MODE_USER_1);
}
static void releaseShift(lv_obj_t* kbd) {
if (s_layout <= L_PAIRED_END && (s_layout & 1)) apply(kbd, s_layout ^ 1);
}
// ── Variants popup ───────────────────────────────────────────────────────────
static char s_var_txt[12][8];
static const char* s_var_map[13];
static void closePopup() {
if (s_popup) { lv_obj_delete_async(s_popup); s_popup = nullptr; }
}
static void onPopupPick(lv_event_t* e) {
lv_obj_t* m = (lv_obj_t*)lv_event_get_target(e);
uint32_t id = lv_buttonmatrix_get_selected_button(m);
if (id == LV_BUTTONMATRIX_BUTTON_NONE || !s_popup_kbd) return;
lv_obj_t* ta = lv_keyboard_get_textarea(s_popup_kbd);
if (ta) lv_textarea_add_text(ta, lv_buttonmatrix_get_button_text(m, id));
releaseShift(s_popup_kbd);
closePopup();
}
static void onPopupOutside(lv_event_t* e) { (void)e; closePopup(); }
static void openPopup(lv_obj_t* kbd, const char* variants, bool upper) {
closePopup();
int n = ::kbd::utf8Len(variants);
if (n > 12) n = 12;
for (int i = 0; i < n; i++) {
char one[8];
::kbd::utf8At(variants, i, one);
if (upper) ::kbd::toUpperUtf8(one, s_var_txt[i], sizeof(s_var_txt[i]));
else memcpy(s_var_txt[i], one, sizeof(one));
s_var_map[i] = s_var_txt[i];
}
s_var_map[n] = "";
// Full-screen transparent overlay: a tap outside the row closes the popup.
s_popup = lv_obj_create(lv_layer_top());
lv_obj_remove_style_all(s_popup);
lv_obj_set_size(s_popup, LV_PCT(100), LV_PCT(100));
lv_obj_add_flag(s_popup, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_event_cb(s_popup, onPopupOutside, LV_EVENT_CLICKED, NULL);
s_popup_kbd = kbd;
lv_obj_t* m = lv_buttonmatrix_create(s_popup);
lv_buttonmatrix_set_map(m, s_var_map);
const int key_w = 30;
int w = n * key_w + 8;
lv_obj_set_size(m, w, 44);
lv_obj_set_style_pad_all(m, 3, 0);
lv_obj_set_style_pad_gap(m, 2, 0);
lv_obj_set_style_bg_color(m, lv_color_hex(theme::SURFACE_2), 0);
lv_obj_set_style_border_color(m, lv_color_hex(theme::ACCENT), 0);
lv_obj_set_style_border_width(m, 1, 0);
lv_obj_set_style_radius(m, theme::RADIUS, 0);
lv_obj_set_style_text_font(m, THEME_FONT_LARGE, LV_PART_ITEMS);
// Above the pressed key, but never above the keyboard's own top edge: the
// compose field sits right there and must stay readable while picking.
lv_point_t pt = { 0, 0 };
lv_indev_t* indev = lv_indev_active();
if (indev) lv_indev_get_point(indev, &pt);
lv_area_t kb_area;
lv_obj_get_coords(kbd, &kb_area);
int scr_w = lv_display_get_horizontal_resolution(NULL);
int x = pt.x - w / 2;
if (x < 2) x = 2;
if (x + w > scr_w - 2) x = scr_w - 2 - w;
int y = pt.y - 60;
if (y < kb_area.y1) y = kb_area.y1;
lv_obj_set_pos(m, x, y);
lv_obj_add_event_cb(m, onPopupPick, LV_EVENT_VALUE_CHANGED, NULL);
}
// ── Events ───────────────────────────────────────────────────────────────────
static void onLongPress(lv_event_t* e) {
lv_obj_t* kbd = (lv_obj_t*)lv_event_get_target(e);
uint32_t id = lv_keyboard_get_selected_button(kbd);
if (id == LV_BUTTONMATRIX_BUTTON_NONE) return;
bool upper = s_layout <= L_PAIRED_END && (s_layout & 1);
uint8_t lower = upper ? (uint8_t)(s_layout ^ 1) : s_layout;
const char* variants = ::kbd::variantsFor(keyText(lower, id));
if (!variants) return; // plain key: the release types it as usual
s_suppress_release = true;
openPopup(kbd, variants, upper);
}
static void onEvent(lv_event_t* e) {
lv_obj_t* kbd = (lv_obj_t*)lv_event_get_target(e);
if (s_suppress_release) { s_suppress_release = false; return; }
uint32_t id = lv_keyboard_get_selected_button(kbd);
if (id == LV_BUTTONMATRIX_BUTTON_NONE) return;
const char* t = lv_keyboard_get_button_text(kbd, id);
if (!t) return;
if (strcmp(t, KB_SH) == 0) { if (s_layout <= L_PAIRED_END) apply(kbd, s_layout ^ 1); return; }
if (strcmp(t, K_SYM) == 0) { apply(kbd, L_SYM); return; }
if (strcmp(t, K_BACK) == 0) { apply(kbd, s_script * 2); return; }
if (strcmp(t, K_GLOBE) == 0) {
s_script = (s_script == mainScript()) ? altScript() : mainScript();
apply(kbd, s_script * 2);
return;
}
lv_keyboard_def_event_cb(e); // type the character / backspace / OK
if (strcmp(t, KB_BS) != 0) releaseShift(kbd);
}
static void onDelete(lv_event_t* e) { (void)e; closePopup(); s_popup_kbd = nullptr; }
// Create a keyboard on `parent`, opening on the main script.
static lv_obj_t* create(lv_obj_t* parent, NodePrefs* prefs) {
s_prefs = prefs;
buildMaps(); // cheap; picks up a changed alphabet setting
s_script = mainScript();
lv_obj_t* kbd = lv_keyboard_create(parent);
lv_obj_remove_event_cb(kbd, lv_keyboard_def_event_cb);
lv_obj_add_event_cb(kbd, onEvent, LV_EVENT_VALUE_CHANGED, NULL);
lv_obj_add_event_cb(kbd, onLongPress, LV_EVENT_LONG_PRESSED, NULL);
lv_obj_add_event_cb(kbd, onDelete, LV_EVENT_DELETE, NULL);
lv_keyboard_set_popovers(kbd, true);
apply(kbd, s_script * 2);
return kbd;
}
} // namespace kb