|
|
|
@@ -1815,11 +1815,19 @@ static bool s_nav_ta_editing = false;
|
|
|
|
|
|
|
|
|
|
static void navFocusCb(lv_group_t* g) {
|
|
|
|
|
lv_obj_t* f = lv_group_get_focused(g);
|
|
|
|
|
// Edit mode on focus: the chat composer auto-edits (you opened the chat to type, and the
|
|
|
|
|
// cursor shows immediately); every OTHER field starts in navigate mode so the letter-nav
|
|
|
|
|
// keys keep working — press select/Enter to edit it. Esc/Enter on an empty composer drops
|
|
|
|
|
// it back to navigate mode (handled in navPump / handleHwKey).
|
|
|
|
|
s_nav_ta_editing = (f != nullptr && (f == g_lv.ch.composer_ta || f == g_lv.dm.composer_ta));
|
|
|
|
|
// Edit mode — decided only on a GENUINE move to a different field. The chat composer
|
|
|
|
|
// auto-edits (you opened the chat to type), and a field focused by a TOUCH tap edits
|
|
|
|
|
// immediately too (you pointed right at it). Every OTHER field (reached with the letter-nav
|
|
|
|
|
// keys) starts in navigate mode so those keys keep working — press select/Enter to edit it.
|
|
|
|
|
// Crucially, a navMaybeRebuild() that re-focuses the SAME field (e.g. while the accent box is
|
|
|
|
|
// up) must NOT reset edit mode — otherwise it kicks you out of typing mid-word.
|
|
|
|
|
static lv_obj_t* s_nav_focus_prev = nullptr;
|
|
|
|
|
if (f && f != s_nav_focus_prev) { // ignore the transient f==null a rebuild's remove-all emits
|
|
|
|
|
lv_indev_t* act = lv_indev_get_act(); // the indev driving this focus change (null if programmatic)
|
|
|
|
|
const bool by_touch = act && lv_indev_get_type(act) == LV_INDEV_TYPE_POINTER;
|
|
|
|
|
s_nav_ta_editing = (f != nullptr && (by_touch || f == g_lv.ch.composer_ta || f == g_lv.dm.composer_ta));
|
|
|
|
|
s_nav_focus_prev = f;
|
|
|
|
|
}
|
|
|
|
|
if (s_nav_styled && s_nav_styled != f) navUnstyle(s_nav_styled);
|
|
|
|
|
s_nav_styled = nullptr; // old highlight (if any) is now restored; nothing styled yet
|
|
|
|
|
if (!f || !s_nav_show) return; // focus-visible: paint only while actively keyboard-navigating
|
|
|
|
@@ -2050,6 +2058,23 @@ static lv_obj_t* navFocusedTextarea() {
|
|
|
|
|
return (f && lv_obj_check_type(f, &lv_textarea_class)) ? f : nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The text cursor is the reliable "you can type now" cue: show it only while editing, hide it
|
|
|
|
|
// while a focused field is merely the keyboard-nav target. Synced each nav tick — the per-field
|
|
|
|
|
// LV_PART_CURSOR opacity override is a no-op when unchanged, so it's cheap; the previous field's
|
|
|
|
|
// cursor is restored when focus moves off it (so it reads normally outside keyboard-nav).
|
|
|
|
|
static lv_obj_t* s_nav_cursor_ta = nullptr;
|
|
|
|
|
static bool s_nav_cursor_shown = true;
|
|
|
|
|
static void navSyncCursor() {
|
|
|
|
|
lv_obj_t* tf = navFocusedTextarea();
|
|
|
|
|
const bool want = (tf != nullptr) && s_nav_ta_editing;
|
|
|
|
|
if (tf == s_nav_cursor_ta && want == s_nav_cursor_shown) return; // unchanged → nothing to do
|
|
|
|
|
if (s_nav_cursor_ta && s_nav_cursor_ta != tf && lv_obj_is_valid(s_nav_cursor_ta))
|
|
|
|
|
lv_obj_set_style_opa(s_nav_cursor_ta, LV_OPA_COVER, LV_PART_CURSOR); // restore on leave
|
|
|
|
|
if (tf) lv_obj_set_style_opa(tf, want ? LV_OPA_COVER : LV_OPA_TRANSP, LV_PART_CURSOR);
|
|
|
|
|
s_nav_cursor_ta = tf;
|
|
|
|
|
s_nav_cursor_shown = want;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Drain badge-bsp key events (called from UITask::loop). Focus-movement / activation
|
|
|
|
|
// is queued for the LVGL keypad indev; TEXT is applied DIRECTLY to the focused field
|
|
|
|
|
// so the physical keyboard types straight in (T-Deck-style) — no on-screen keyboard,
|
|
|
|
@@ -2264,11 +2289,22 @@ static void navMarkDirty() { s_nav_dirty = true; }
|
|
|
|
|
// wizard steps that swap content without changing the top-level child count (a plain
|
|
|
|
|
// child-count signature missed those, leaving the focus group empty after a step swap).
|
|
|
|
|
static uint32_t navTreeSig(lv_obj_t* obj, int depth) {
|
|
|
|
|
if (!obj || depth > 7 || lv_obj_has_flag(obj, LV_OBJ_FLAG_HIDDEN)) return 0;
|
|
|
|
|
// Skip NAV_SKIP_FLAG overlays (accent box, @-mention picker): they're not nav targets, so
|
|
|
|
|
// their appearing/disappearing must NOT change the signature and trigger a rebuild — that
|
|
|
|
|
// rebuild's remove-all + re-focus churn would reset edit mode and break typing mid-word.
|
|
|
|
|
if (!obj || depth > 7 || lv_obj_has_flag(obj, LV_OBJ_FLAG_HIDDEN) || lv_obj_has_flag(obj, NAV_SKIP_FLAG)) return 0;
|
|
|
|
|
uint32_t h = (uint32_t)(uintptr_t)obj * 2654435761u + (uint32_t)depth + 1u;
|
|
|
|
|
uint32_t n = lv_obj_get_child_cnt(obj);
|
|
|
|
|
for (uint32_t i = 0; i < n; i++)
|
|
|
|
|
h = h * 2654435761u + navTreeSig(lv_obj_get_child(obj, i), depth + 1);
|
|
|
|
|
for (uint32_t i = 0; i < n; i++) {
|
|
|
|
|
lv_obj_t* c = lv_obj_get_child(obj, i);
|
|
|
|
|
// Skip the child ENTIRELY (don't even count it) when it's hidden or a passive NAV_SKIP
|
|
|
|
|
// overlay — returning 0 from the recursion wasn't enough, because merely iterating over an
|
|
|
|
|
// added/removed child still moves `h`. The accent box + @-mention picker are created and
|
|
|
|
|
// deleted on every keystroke, so counting them flips the signature → rebuild → edit-mode
|
|
|
|
|
// reset → typing breaks mid-word (and Esc then reads as "navigate back", exiting the screen).
|
|
|
|
|
if (!c || lv_obj_has_flag(c, LV_OBJ_FLAG_HIDDEN) || lv_obj_has_flag(c, NAV_SKIP_FLAG)) continue;
|
|
|
|
|
h = h * 2654435761u + navTreeSig(c, depth + 1);
|
|
|
|
|
}
|
|
|
|
|
return h;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -2292,7 +2328,13 @@ static LvChatPanel* s_nav_prev_chat = nullptr; // last chat panel we focused (
|
|
|
|
|
|
|
|
|
|
static void navMaybeRebuild() {
|
|
|
|
|
if (!s_nav_group) return;
|
|
|
|
|
navSyncCursor(); // keep the text cursor showing only in edit mode (reliable "ready to type" cue)
|
|
|
|
|
if (navOpenDropdown()) return; // an open dropdown owns the group — rebuilding would close it
|
|
|
|
|
// Issue #45: when an overlay (settings sheet, modal) or a chat closes, the element that had focus
|
|
|
|
|
// lived inside it and is now gone, so focus would fall to the first list item and the underlying
|
|
|
|
|
// list would snap to the top. Stash the page focus on the way out, restore it on the way back.
|
|
|
|
|
static lv_obj_t* s_nav_page_focus = nullptr;
|
|
|
|
|
static bool s_nav_prev_on_page = true;
|
|
|
|
|
lv_obj_t* scr = lv_scr_act();
|
|
|
|
|
lv_obj_t* top = lv_layer_top();
|
|
|
|
|
bool useTop = navTopHasVisibleChild(top);
|
|
|
|
@@ -2301,6 +2343,7 @@ static void navMaybeRebuild() {
|
|
|
|
|
// screens) and the composer can be focused for typing.
|
|
|
|
|
LvChatPanel* chat = useTop ? nullptr : navOpenChatPanel();
|
|
|
|
|
lv_obj_t* root = useTop ? top : (chat ? chat->overlay : scr);
|
|
|
|
|
const bool on_page = !useTop && !chat; // #45: true = main tab content (the list that scrolls)
|
|
|
|
|
lv_obj_t* tabbar = nullptr;
|
|
|
|
|
if (!useTop && !chat && g_lv.tabview) {
|
|
|
|
|
// Main app: collect ONLY the active tab's content (inactive tabs are scrolled off-screen, not
|
|
|
|
@@ -2315,6 +2358,7 @@ static void navMaybeRebuild() {
|
|
|
|
|
if (!s_nav_dirty && scr == s_nav_prev_scr && sig == s_nav_prev_sig) return;
|
|
|
|
|
s_nav_dirty = false; s_nav_prev_scr = scr; s_nav_prev_sig = sig;
|
|
|
|
|
lv_obj_t* keep = lv_group_get_focused(s_nav_group); // preserve focus across a same-page rebuild (toggle/click)
|
|
|
|
|
if (!on_page && s_nav_prev_on_page) s_nav_page_focus = keep; // #45: leaving the page → remember where we were
|
|
|
|
|
lv_group_remove_all_objs(s_nav_group);
|
|
|
|
|
s_nav_first = s_nav_last = nullptr; s_nav_count = 0;
|
|
|
|
|
if (root) navCollect(root); // content items (focus starts here)
|
|
|
|
@@ -2330,6 +2374,11 @@ static void navMaybeRebuild() {
|
|
|
|
|
// not auto-selected). Only on the open transition, so new messages don't steal focus mid-typing.
|
|
|
|
|
if (chat && chat != s_nav_prev_chat && chat->composer_ta && lv_obj_is_valid(chat->composer_ta)) {
|
|
|
|
|
lv_group_focus_obj(chat->composer_ta); // chat just opened → focus the composer
|
|
|
|
|
} else if (on_page && !s_nav_prev_on_page && s_nav_page_focus && lv_obj_is_valid(s_nav_page_focus)) {
|
|
|
|
|
// #45: just returned to the page from an overlay/chat — restore the item we left from so the
|
|
|
|
|
// list stays put instead of snapping to the top.
|
|
|
|
|
const int n = s_nav_count < kNavMax ? s_nav_count : kNavMax;
|
|
|
|
|
for (int i = 0; i < n; i++) if (s_nav_objs[i] == s_nav_page_focus) { lv_group_focus_obj(s_nav_page_focus); break; }
|
|
|
|
|
} else if (keep && lv_obj_is_valid(keep)) {
|
|
|
|
|
// A toggle/click triggers a rebuild; if the previously-focused element survived,
|
|
|
|
|
// keep focus on it instead of jumping back to the top of the page.
|
|
|
|
@@ -2337,6 +2386,7 @@ static void navMaybeRebuild() {
|
|
|
|
|
for (int i = 0; i < n; i++) if (s_nav_objs[i] == keep) { lv_group_focus_obj(keep); break; }
|
|
|
|
|
}
|
|
|
|
|
s_nav_prev_chat = chat;
|
|
|
|
|
s_nav_prev_on_page = on_page; // #45
|
|
|
|
|
if (s_nav_debug) printf("[NAV] rebuilt count=%d %s tab=%d\n", s_nav_count, useTop ? "top" : chat ? "chat" : "tab", getActiveTab());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -2538,6 +2588,7 @@ static void refreshLogModalView();
|
|
|
|
|
static void hideKb();
|
|
|
|
|
static void accentExit(); // long-press accent picker (issue #22, dead on touch)
|
|
|
|
|
static void accentBoxHide(); // tap-to-pick accent box (issue #22)
|
|
|
|
|
static void mentionBoxHide(); // tap-to-pick @-mention contact picker (issue #42)
|
|
|
|
|
static void txtMenuHide(); // cut/copy/paste/select-all edit menu
|
|
|
|
|
static void showKb(LvChatPanel* p);
|
|
|
|
|
static void closeSettingsModal();
|
|
|
|
@@ -3247,6 +3298,7 @@ static void kbMirrorBind(lv_obj_t* real_ta) {
|
|
|
|
|
static void hideKb() {
|
|
|
|
|
accentExit(); // tear down any open accent picker
|
|
|
|
|
accentBoxHide();
|
|
|
|
|
mentionBoxHide(); // tear down any open @-mention picker
|
|
|
|
|
txtMenuHide(); // tear down any open edit menu
|
|
|
|
|
kbMirrorSyncToReal();
|
|
|
|
|
s_kb_bind_ta = nullptr;
|
|
|
|
@@ -3635,6 +3687,127 @@ static void accentBoxMaybeShow() {
|
|
|
|
|
lv_obj_move_foreground(s_accbox);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- @-mention contact picker (issue #42) --------------------------------
|
|
|
|
|
// Typing "@name" in a chat composer pops a touch list of matching contacts —
|
|
|
|
|
// modeled on the accent box: a passive overlay (NAV_SKIP_FLAG) that never steals
|
|
|
|
|
// keyboard-nav focus and, for now, is selected by TOUCH only. Tapping a row
|
|
|
|
|
// replaces the "@partial" with "@FullName ".
|
|
|
|
|
static lv_obj_t* s_mentionbox = nullptr;
|
|
|
|
|
static lv_obj_t* s_mentionbox_ta = nullptr;
|
|
|
|
|
static constexpr int k_mention_max = 6;
|
|
|
|
|
static char s_mention_names[k_mention_max][32]; // kept alive for the cell callbacks
|
|
|
|
|
static void mentionBoxHide() {
|
|
|
|
|
if (s_mentionbox) { lv_obj_del(s_mentionbox); s_mentionbox = nullptr; }
|
|
|
|
|
s_mentionbox_ta = nullptr;
|
|
|
|
|
}
|
|
|
|
|
// Index of the active mention's '@' in `text`, or -1. The active mention is the
|
|
|
|
|
// last '@' that begins a word (start of text or after whitespace) with no
|
|
|
|
|
// whitespace between it and the end (where the caret is).
|
|
|
|
|
static int mentionAtPos(const char* text) {
|
|
|
|
|
if (!text) return -1;
|
|
|
|
|
for (int i = (int)strlen(text) - 1; i >= 0; --i) {
|
|
|
|
|
const char c = text[i];
|
|
|
|
|
if (c == ' ' || c == '\n' || c == '\t') return -1; // a space before any '@' -> not in a mention
|
|
|
|
|
if (c == '@') {
|
|
|
|
|
const bool at_word_start = (i == 0) || text[i-1] == ' ' || text[i-1] == '\n' || text[i-1] == '\t';
|
|
|
|
|
return at_word_start ? i : -1; // '@' mid-word (e.g. an email) isn't a mention
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
static void mentionBoxCellCb(lv_event_t* e) {
|
|
|
|
|
if (lv_event_get_code(e) != LV_EVENT_CLICKED) return;
|
|
|
|
|
const char* name = static_cast<const char*>(lv_event_get_user_data(e));
|
|
|
|
|
lv_obj_t* ta = s_mentionbox_ta;
|
|
|
|
|
if (ta && name && name[0]) {
|
|
|
|
|
const char* text = lv_textarea_get_text(ta);
|
|
|
|
|
const int at = mentionAtPos(text);
|
|
|
|
|
if (at >= 0) {
|
|
|
|
|
char out[300];
|
|
|
|
|
snprintf(out, sizeof out, "%.*s@%s ", at, text, name); // keep text before '@', then "@Name "
|
|
|
|
|
lv_textarea_set_text(ta, out);
|
|
|
|
|
if (ta == s_kb_mirror_ta) kbMirrorSyncToReal();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
mentionBoxHide();
|
|
|
|
|
}
|
|
|
|
|
// Show the contact picker when the composer ends in "@partial". Returns true
|
|
|
|
|
// when a picker is shown (so the caller suppresses the accent box).
|
|
|
|
|
static bool mentionBoxMaybeShow() {
|
|
|
|
|
mentionBoxHide();
|
|
|
|
|
if (!g_lv.keyboard) return false;
|
|
|
|
|
lv_obj_t* ta = lv_keyboard_get_textarea(g_lv.keyboard);
|
|
|
|
|
if (!ta) return false;
|
|
|
|
|
const char* text = lv_textarea_get_text(ta);
|
|
|
|
|
const int at = mentionAtPos(text);
|
|
|
|
|
if (at < 0) return false;
|
|
|
|
|
const char* partial = text + at + 1;
|
|
|
|
|
const size_t plen = strlen(partial);
|
|
|
|
|
int n = 0;
|
|
|
|
|
const int total = the_mesh.getNumContacts();
|
|
|
|
|
for (int i = 0; i < total && n < k_mention_max; ++i) {
|
|
|
|
|
ContactInfo c;
|
|
|
|
|
if (!the_mesh.getContactByIdx(i, c) || !c.name[0]) continue;
|
|
|
|
|
if (plen > 0 && strncasecmp(c.name, partial, plen) != 0) continue; // prefix match (case-insensitive)
|
|
|
|
|
strlcpy(s_mention_names[n], c.name, sizeof s_mention_names[n]);
|
|
|
|
|
++n;
|
|
|
|
|
}
|
|
|
|
|
if (n == 0) return false;
|
|
|
|
|
s_mentionbox_ta = ta;
|
|
|
|
|
s_mentionbox = lv_obj_create(lv_layer_top());
|
|
|
|
|
lv_obj_add_flag(s_mentionbox, NAV_SKIP_FLAG); // passive, touch-only — never a keyboard-nav stop (issue #42)
|
|
|
|
|
lv_obj_remove_style_all(s_mentionbox);
|
|
|
|
|
lv_obj_set_style_bg_color(s_mentionbox, lv_color_hex(COLOR_PANEL), LV_PART_MAIN);
|
|
|
|
|
lv_obj_set_style_bg_opa(s_mentionbox, LV_OPA_COVER, LV_PART_MAIN);
|
|
|
|
|
lv_obj_set_style_radius(s_mentionbox, 8, LV_PART_MAIN);
|
|
|
|
|
lv_obj_set_style_border_color(s_mentionbox, lv_color_hex(0x2A3D52), LV_PART_MAIN);
|
|
|
|
|
lv_obj_set_style_border_width(s_mentionbox, 1, LV_PART_MAIN);
|
|
|
|
|
lv_obj_set_style_pad_all(s_mentionbox, 4, LV_PART_MAIN);
|
|
|
|
|
lv_obj_set_style_pad_row(s_mentionbox, 3, LV_PART_MAIN);
|
|
|
|
|
lv_obj_set_flex_flow(s_mentionbox, LV_FLEX_FLOW_COLUMN);
|
|
|
|
|
lv_obj_clear_flag(s_mentionbox, LV_OBJ_FLAG_SCROLLABLE);
|
|
|
|
|
const int rowh = 30, boxw = 168;
|
|
|
|
|
for (int i = 0; i < n; ++i) {
|
|
|
|
|
lv_obj_t* b = lv_btn_create(s_mentionbox);
|
|
|
|
|
lv_obj_add_flag(b, NAV_SKIP_FLAG);
|
|
|
|
|
lv_obj_set_size(b, boxw, rowh);
|
|
|
|
|
lv_obj_set_style_radius(b, 5, LV_PART_MAIN);
|
|
|
|
|
lv_obj_set_style_bg_color(b, lv_color_hex(0x1B2B3A), LV_PART_MAIN);
|
|
|
|
|
lv_obj_set_style_bg_color(b, lv_color_hex(COLOR_ACCENT), LV_PART_MAIN | LV_STATE_PRESSED);
|
|
|
|
|
lv_obj_add_event_cb(b, mentionBoxCellCb, LV_EVENT_CLICKED, (void*)s_mention_names[i]);
|
|
|
|
|
lv_obj_t* l = lv_label_create(b);
|
|
|
|
|
lv_label_set_text_fmt(l, "@%s", s_mention_names[i]);
|
|
|
|
|
lv_obj_set_style_text_font(l, &g_font_14, LV_PART_MAIN);
|
|
|
|
|
lv_obj_set_style_text_color(l, lv_color_hex(COLOR_TEXT), LV_PART_MAIN);
|
|
|
|
|
lv_label_set_long_mode(l, LV_LABEL_LONG_DOT);
|
|
|
|
|
lv_obj_set_width(l, boxw - 18);
|
|
|
|
|
lv_obj_center(l);
|
|
|
|
|
}
|
|
|
|
|
lv_obj_set_size(s_mentionbox, boxw + 8, n * rowh + (n - 1) * 3 + 8);
|
|
|
|
|
// Place it above the composer, clamped above the keyboard (mirrors the accent box).
|
|
|
|
|
lv_obj_update_layout(s_mentionbox);
|
|
|
|
|
lv_area_t a; lv_obj_get_coords(ta, &a);
|
|
|
|
|
const lv_coord_t bw = lv_obj_get_width(s_mentionbox), bh = lv_obj_get_height(s_mentionbox);
|
|
|
|
|
lv_coord_t bx = (lv_disp_get_hor_res(nullptr) - bw) / 2;
|
|
|
|
|
lv_coord_t by = a.y1 - bh - 4;
|
|
|
|
|
if (g_lv.keyboard && !lv_obj_has_flag(g_lv.keyboard, LV_OBJ_FLAG_HIDDEN)) {
|
|
|
|
|
lv_coord_t limit = lv_disp_get_ver_res(nullptr) - chatKbH() - 2;
|
|
|
|
|
if (s_kb_panel) limit -= s_comp_h;
|
|
|
|
|
if (by + bh > limit) by = limit - bh;
|
|
|
|
|
}
|
|
|
|
|
if (by < STATUSBAR_H + 2) by = STATUSBAR_H + 2;
|
|
|
|
|
lv_obj_set_pos(s_mentionbox, bx, by);
|
|
|
|
|
lv_obj_move_foreground(s_mentionbox);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
// One entry point for the composer's typing-time suggestion overlays: the
|
|
|
|
|
// @-mention picker takes priority over the accent box (never both at once).
|
|
|
|
|
static void composerSuggestRefresh() {
|
|
|
|
|
if (mentionBoxMaybeShow()) { accentBoxHide(); return; }
|
|
|
|
|
mentionBoxHide();
|
|
|
|
|
accentBoxMaybeShow();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void keyboardCb(lv_event_t* e) {
|
|
|
|
|
lv_event_code_t code = lv_event_get_code(e);
|
|
|
|
|
if (code == LV_EVENT_READY || code == LV_EVENT_CANCEL) { accentExit(); accentBoxHide(); hideKb(); }
|
|
|
|
@@ -3644,7 +3817,7 @@ static void keyboardCb(lv_event_t* e) {
|
|
|
|
|
else if (code == LV_EVENT_VALUE_CHANGED) {
|
|
|
|
|
kbSetRotateArrowsOpa(LV_OPA_20);
|
|
|
|
|
accentHandleValueChanged();
|
|
|
|
|
accentBoxMaybeShow(); // letter with accents -> show the tap-to-pick box
|
|
|
|
|
composerSuggestRefresh(); // @mention contact picker, else the accent box
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -4651,6 +4824,13 @@ static void settingsFieldFocusCb(lv_event_t* e) {
|
|
|
|
|
s_kb_panel = nullptr;
|
|
|
|
|
kbMirrorBind(ta);
|
|
|
|
|
noteKbActivity(); // focusing/tapping a field lights the auto backlight
|
|
|
|
|
#if defined(HAS_TANMATSU) || defined(HAS_TDECK_TRACKBALL)
|
|
|
|
|
// A deliberate TAP enters edit mode immediately — even when the field was already the
|
|
|
|
|
// selected (nav-focused) one, where no focus change fires navFocusCb (so tapping it again
|
|
|
|
|
// wouldn't otherwise make it typable). CLICKED only fires on a real tap (past the scroll
|
|
|
|
|
// guards above); keyboard-nav onto a field still starts in navigate mode (press Enter).
|
|
|
|
|
if (code == LV_EVENT_CLICKED) s_nav_ta_editing = true;
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#if defined(HAS_TDECK_KEYBOARD)
|
|
|
|
@@ -17009,6 +17189,12 @@ static void renderMapTiles() {
|
|
|
|
|
// Wi-Fi worker holds a socket pushed the heap into an OOM reboot. When tiles
|
|
|
|
|
// are arriving from the network, let LVGL batch one redraw at the end.
|
|
|
|
|
if (s_tile_fetch_pending == 0) lv_refr_now(NULL);
|
|
|
|
|
// Yield to the IDLE task after each tile's read + decode + paint. This loop
|
|
|
|
|
// runs on loopTask, which is what the task watchdog watches via IDLE1; a full
|
|
|
|
|
// 9-tile load (or a few slow SD reads) can otherwise pin the core long enough
|
|
|
|
|
// to starve IDLE1 and panic (the loopTask Task-WDT reboot this fixes — the
|
|
|
|
|
// download worker already does the same vTaskDelay(1) on its side).
|
|
|
|
|
vTaskDelay(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#if defined(ESP32) && defined(MULTI_TRANSPORT_COMPANION)
|
|
|
|
@@ -18771,6 +18957,21 @@ static void onMapTabActivated() {
|
|
|
|
|
refreshMapInfoLabel();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Idle power-save indicator (iPhone Low-Power-Mode style, T-Deck only): instead of a separate moon
|
|
|
|
|
// glyph, the status-bar battery turns amber while idle power-save is enabled. s_batt_base holds the
|
|
|
|
|
// colour the theme/map chrome wants (off-white off-map, black/white over light tiles); applyBattColor
|
|
|
|
|
// overlays the amber when power-save is on, so the map-chrome setter and the per-tick refresh share
|
|
|
|
|
// one writer and never fight over the battery colour.
|
|
|
|
|
static lv_color_t s_batt_base = lv_color_hex(COLOR_TEXT);
|
|
|
|
|
static void applyBattColor() {
|
|
|
|
|
if (!g_statusbar.batt_icon) return;
|
|
|
|
|
lv_color_t c = s_batt_base;
|
|
|
|
|
#if defined(HAS_TDECK_GT911)
|
|
|
|
|
if (touchSleep::enabled()) c = lv_color_hex(0xFFD60A); // iOS systemYellow ≈ Low Power Mode
|
|
|
|
|
#endif
|
|
|
|
|
lv_obj_set_style_text_color(g_statusbar.batt_icon, c, LV_PART_MAIN);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Immersive map chrome: on the Map tab the status bar, bottom info strip and
|
|
|
|
|
// tab bar all go transparent so the (full-screen) map shows through behind them.
|
|
|
|
|
// Because OSM tiles are LIGHT, the status-bar text/icons are switched to BLACK
|
|
|
|
@@ -18801,7 +19002,8 @@ static void applyMapChrome(bool on) {
|
|
|
|
|
const lv_color_t fg = lv_color_hex(!on ? COLOR_TEXT : (night ? 0xF0F0F0 : 0x000000));
|
|
|
|
|
const lv_color_t fg_sub = lv_color_hex(!on ? COLOR_SUB : (night ? 0xC8CCD0 : 0x000000));
|
|
|
|
|
if (g_statusbar.left_label) lv_obj_set_style_text_color(g_statusbar.left_label, fg, LV_PART_MAIN);
|
|
|
|
|
if (g_statusbar.batt_icon) lv_obj_set_style_text_color(g_statusbar.batt_icon, fg, LV_PART_MAIN);
|
|
|
|
|
s_batt_base = fg; // theme/map-driven battery colour...
|
|
|
|
|
applyBattColor(); // ...with the power-save amber overlaid if enabled
|
|
|
|
|
if (g_statusbar.batt_pct) lv_obj_set_style_text_color(g_statusbar.batt_pct, fg_sub, LV_PART_MAIN);
|
|
|
|
|
if (g_statusbar.clock) lv_obj_set_style_text_color(g_statusbar.clock, fg_sub, LV_PART_MAIN);
|
|
|
|
|
if (g_statusbar.conn_icon) lv_obj_set_style_text_color(g_statusbar.conn_icon, fg_sub, LV_PART_MAIN);
|
|
|
|
@@ -22923,7 +23125,7 @@ static void handleHwKey(int key) {
|
|
|
|
|
} else {
|
|
|
|
|
lv_textarea_add_char(ta, (uint32_t)key);
|
|
|
|
|
}
|
|
|
|
|
accentBoxMaybeShow(); // letter with accents -> show the tap-to-pick box
|
|
|
|
|
composerSuggestRefresh(); // @mention contact picker, else the accent box
|
|
|
|
|
}
|
|
|
|
|
if (g_lv.task) g_lv.task->noteUserInput();
|
|
|
|
|
}
|
|
|
|
@@ -24967,20 +25169,10 @@ static void updateGlobalStatusBar() {
|
|
|
|
|
#if defined(HAS_TDECK_GT911)
|
|
|
|
|
// ---- Idle power-save icon ---- (T-Deck only; predicates live in the same
|
|
|
|
|
// HAS_TDECK_GT911 build environment as the actual light-sleep execution)
|
|
|
|
|
if (g_statusbar.sleep_icon) {
|
|
|
|
|
if (!touchSleep::enabled()) {
|
|
|
|
|
// Feature off — hide entirely; no status to convey.
|
|
|
|
|
lv_obj_add_flag(g_statusbar.sleep_icon, LV_OBJ_FLAG_HIDDEN);
|
|
|
|
|
} else {
|
|
|
|
|
lv_obj_clear_flag(g_statusbar.sleep_icon, LV_OBJ_FLAG_HIDDEN);
|
|
|
|
|
const char* reason = tsBlockReason();
|
|
|
|
|
// Ready = accent colour (same warm highlight used for active glyphs elsewhere
|
|
|
|
|
// in the bar). Blocked = muted grey (COLOR_SUB) so it reads as "paused".
|
|
|
|
|
lv_obj_set_style_text_color(g_statusbar.sleep_icon,
|
|
|
|
|
lv_color_hex(reason == nullptr ? COLOR_ACCENT : (uint32_t)COLOR_SUB),
|
|
|
|
|
LV_PART_MAIN);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Idle power-save state is now shown by tinting the status-bar battery glyph amber
|
|
|
|
|
// (iPhone-style, see applyBattColor), so the separate moon icon stays hidden. Kept in
|
|
|
|
|
// the tree (hidden) to preserve the bar layout and its tap handler.
|
|
|
|
|
if (g_statusbar.sleep_icon) lv_obj_add_flag(g_statusbar.sleep_icon, LV_OBJ_FLAG_HIDDEN);
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
// ---- Mesh signal strength ---- (SNR of the last packet we heard; dims when
|
|
|
|
@@ -25044,6 +25236,14 @@ static void updateGlobalStatusBar() {
|
|
|
|
|
lv_label_set_text(g_statusbar.batt_icon, g);
|
|
|
|
|
s_last_glyph = g;
|
|
|
|
|
}
|
|
|
|
|
#if defined(HAS_TDECK_GT911)
|
|
|
|
|
// Re-tint the battery amber/normal when idle power-save toggles (the toggle cb routes through
|
|
|
|
|
// here via updateGlobalStatusBar). Only on change, so no per-tick style churn.
|
|
|
|
|
{ static int s_last_pwr = -1;
|
|
|
|
|
const int pwr = touchSleep::enabled() ? 1 : 0;
|
|
|
|
|
if (pwr != s_last_pwr) { s_last_pwr = pwr; applyBattColor(); }
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
// ---- Clock ----
|
|
|
|
|
#if defined(ESP32)
|
|
|
|
@@ -29865,6 +30065,29 @@ void UITask::newRoomMsgFromPubWithMeta(uint8_t path_len, bool is_flood,
|
|
|
|
|
syncThreadMeshSlots(from_name, false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void UITask::appSentMsgToContact(const uint8_t* to_pub, const char* to_name, const char* text,
|
|
|
|
|
uint32_t ack_hash) {
|
|
|
|
|
if (!to_name || !to_name[0] || !text || !text[0]) return;
|
|
|
|
|
const char* sender = (_node_prefs && _node_prefs->node_name[0]) ? _node_prefs->node_name : "me";
|
|
|
|
|
// Mirror an app-originated DM as a local outgoing bubble in the recipient's thread (#46) — the
|
|
|
|
|
// on-device UI is otherwise the only message consumer that never sees sends made from the
|
|
|
|
|
// companion app. appendMessage creates the thread if it doesn't exist and refreshes the view.
|
|
|
|
|
appendMessage(to_name, sender, text, false /*channel*/, true /*outgoing*/, false /*mark_unread*/,
|
|
|
|
|
ack_hash, DELIV_SENT);
|
|
|
|
|
// Lock the thread to the contact's pubkey so a reply typed on-device resolves it (mirrors the
|
|
|
|
|
// receive path above).
|
|
|
|
|
const int t = findThreadByName(to_name, false);
|
|
|
|
|
if (t < 0 || !to_pub) return;
|
|
|
|
|
_ui_threads[t].mesh_contact_idx = -1;
|
|
|
|
|
memcpy(_ui_threads[t].mesh_contact_pub, to_pub, PUB_KEY_SIZE);
|
|
|
|
|
memcpy(_ui_threads[t].mesh_contact_key6, to_pub, 6);
|
|
|
|
|
if (!_ui_threads[t].channel && t == _active_thread_idx) {
|
|
|
|
|
_active_dm_contact_set = true;
|
|
|
|
|
memcpy(_active_dm_contact_pub, to_pub, sizeof(_active_dm_contact_pub));
|
|
|
|
|
}
|
|
|
|
|
syncThreadMeshSlots(to_name, false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) {
|
|
|
|
|
newMsgImpl(path_len, from_name, text, msgcount, 0 /*meta_flags*/, 0 /*snr*/, 0 /*rssi*/);
|
|
|
|
|
}
|
|
|
|
|