Files
MeshCore-Solo/examples/companion_radio/ui-lvgl/QuickScreen.h
T
JakubandClaude Opus 5.5 4f5357031f feat(l2): Arduino 3.x as the published env, limits sized for the PSRAM, stage 7 findings
- Wio_Tracker_L2_companion_solo_lvgl now builds on Arduino-ESP32 3.3.12
  (pioarduino); the 2.0.17 build stays as _solo_lvgl_arduino2 (not published)
- Per-board limits (defaults unchanged, L1 byte-for-byte the same):
  TRAIL_CAPACITY, WAYPOINT_CAPACITY, HIST_CH_MAX / HIST_DM_MAX, NEARBY_MAX;
  the L2 sets 4096 / 64 / 256 / 128 / 64 (in PSRAM with UiCore; internal
  heap 154 KB free after). ui-lvgl: 32 trail segments, contact list 256,
  rooms 32, 50 bubbles per conversation, pickers 128, WiFi scan 20
- Map trail drawing drops points under 2 px apart
- "Nearby" is "Nodes"; advert options "Zero hop" / "Flood"
- Roadmap stage 7: LVGL on other displays -- e-ink stays ui-new, an OLED
  spike after this roadmap, Cardputer as a colour candidate

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

244 lines
10 KiB
C++

// Quick messages and placeholders (the compose bar's "+", Settings > Messages
// & contacts > Quick messages), Settings > Send advert and > Bluetooth, and
// Messages' "Read all". The text side is ui-core/MessageText.h.
// Included by UITask.cpp (single translation unit).
namespace qview {
static const int PH_MAX = 12;
static const char* s_ph[PH_MAX]; // placeholders on offer (static strings)
static int s_ph_n = 0;
static int s_edit = -1; // quick message being edited
static void collect() {
s_ph_n = 0;
msgtext::placeholders(&sensors, [](const char* ph, void*) { if (s_ph_n < PH_MAX) s_ph[s_ph_n++] = ph; }, nullptr);
}
// The placeholders as small buttons; `cb` gets the index as user data.
static lv_obj_t* chips(lv_obj_t* parent, lv_event_cb_t cb, bool wrap) {
lv_obj_t* row = lv_obj_create(parent);
styleSurface(row, theme::BG);
lv_obj_set_size(row, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_flex_flow(row, wrap ? LV_FLEX_FLOW_ROW_WRAP : LV_FLEX_FLOW_ROW);
lv_obj_set_style_pad_column(row, 4, 0);
lv_obj_set_style_pad_row(row, 4, 0);
if (wrap) lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE);
else { lv_obj_set_scroll_dir(row, LV_DIR_HOR); lv_obj_set_scrollbar_mode(row, LV_SCROLLBAR_MODE_OFF); }
for (int i = 0; i < s_ph_n; i++) {
lv_obj_t* b = lv_button_create(row);
lv_obj_set_size(b, LV_SIZE_CONTENT, 28);
lv_obj_set_style_pad_hor(b, 8, 0);
lv_obj_set_style_pad_ver(b, 0, 0);
lv_obj_set_style_radius(b, theme::RADIUS_SM, 0);
lv_obj_set_style_shadow_width(b, 0, 0);
lv_obj_set_style_bg_color(b, lv_color_hex(theme::SURFACE_2), 0);
lv_obj_add_event_cb(b, cb, LV_EVENT_CLICKED, (void*)(uintptr_t)i);
lv_obj_center(label(b, s_ph[i], THEME_FONT_SMALL, theme::TEXT));
}
return row;
}
static void onQuickSend(lv_event_t* e) { s_ui->quickSend((int)(uintptr_t)lv_event_get_user_data(e)); }
static void onQuickInsert(lv_event_t* e) { s_ui->quickInsert((int)(uintptr_t)lv_event_get_user_data(e)); }
static void onQuickSlot(lv_event_t* e) { s_ui->quickEdit((int)(uintptr_t)lv_event_get_user_data(e)); }
static void onQuickEditPh(lv_event_t* e) { s_ui->quickEditInsert((int)(uintptr_t)lv_event_get_user_data(e)); }
static void onQuickEditKb(lv_event_t* e) { s_ui->quickEditDone(lv_event_get_code(e) == LV_EVENT_READY); }
static void onQuickList(lv_event_t* e) { (void)e; s_ui->showQuickMsgs(); }
static void onAdvertGo(lv_event_t* e) { s_ui->advertSend((uintptr_t)lv_event_get_user_data(e) != 0); }
static void onBtSwitch(lv_event_t* e) {
s_ui->setBluetooth(lv_obj_has_state((lv_obj_t*)lv_event_get_target(e), LV_STATE_CHECKED));
}
} // namespace qview
static void onAdvertRow(lv_event_t* e) { (void)e; s_ui->advertPopup(); }
static void onOpenQuickMsgs(lv_event_t* e) { (void)e; s_ui->showQuickMsgs(); }
// ── The compose bar's "+" ─────────────────────────────────────────────────────
void UITask::quickPopup() {
using namespace qview;
if (!_compose_ta) return;
collect();
lv_obj_t* panel = navPopupPanel("Quick", true);
lv_obj_add_flag(panel, LV_OBJ_FLAG_SCROLLABLE);
label(panel, "Insert, filled in when sent:", THEME_FONT_SMALL, theme::TEXT_MUTED);
chips(panel, onQuickInsert, true);
label(panel, "Send now:", THEME_FONT_SMALL, theme::TEXT_MUTED);
int shown = 0;
for (int i = 0; i < msgtext::QUICK_COUNT; i++) {
const char* t = msgtext::quick(_prefs, i);
if (!t[0]) continue;
char sent[MSG_TEXT_BUF];
msgtext::expand(t, sent, sizeof(sent), _prefs);
listRow(panel, t, strcmp(sent, t) ? sent : NULL, onQuickSend, (void*)(uintptr_t)i); // what goes out
shown++;
}
if (!shown) label(panel, "No quick messages yet.", THEME_FONT_SMALL, theme::TEXT_MUTED);
listRow(panel, LV_SYMBOL_EDIT " Edit quick messages", NULL, onQuickList, NULL);
}
void UITask::quickSend(int slot) {
const char* t = msgtext::quick(_prefs, slot);
if (!t[0]) return;
char text[MSG_TEXT_BUF];
msgtext::expand(t, text, sizeof(text), _prefs);
navClosePopup();
if (sendThreadText(text)) refreshThread();
}
void UITask::quickInsert(int ph) {
using namespace qview;
if (ph < 0 || ph >= s_ph_n || !_compose_ta) return;
navClosePopup();
lv_textarea_add_text(_compose_ta, s_ph[ph]); // the byte cap still applies (onComposeInsert)
setKeyboardVisible(true);
}
// ── Settings > Messages & contacts > Quick messages ───────────────────────────
void UITask::showQuickMsgs() {
navClosePopup();
_screen = SCR_QUICK;
buildQuickMsgs();
}
void UITask::buildQuickMsgs() {
using namespace qview;
lv_obj_t* body = newScreen("Quick messages", true);
lv_obj_t* t = label(body, "Sent from a chat's \"+\"; placeholders are filled in.",
THEME_FONT_SMALL, theme::TEXT_MUTED);
lv_label_set_long_mode(t, LV_LABEL_LONG_WRAP);
lv_obj_set_width(t, LV_PCT(100));
for (int i = 0; i < msgtext::QUICK_COUNT; i++) {
const char* q = msgtext::quick(_prefs, i);
char title[msgtext::QUICK_LEN + 8];
snprintf(title, sizeof(title), "%d %s", i + 1, q[0] ? q : "(empty)");
lv_obj_t* row = listRow(body, title, NULL, onQuickSlot, (void*)(uintptr_t)i);
if (!q[0]) lv_obj_set_style_text_color(lv_obj_get_child(row, 0), lv_color_hex(theme::TEXT_MUTED), 0);
}
}
// A field and the placeholders over the keyboard (a titled popup doesn't fit
// above 124 px of keys); the field's placeholder says which slot it is.
void UITask::quickEdit(int slot) {
using namespace qview;
if (!_prefs || slot < 0 || slot >= msgtext::QUICK_COUNT) return;
collect();
s_edit = slot;
navClosePopup();
_nav_overlay = lv_obj_create(screen());
lv_obj_remove_style_all(_nav_overlay);
lv_obj_set_size(_nav_overlay, LV_PCT(100), LV_PCT(100));
lv_obj_set_style_bg_color(_nav_overlay, lv_color_hex(0x000000), 0);
lv_obj_set_style_bg_opa(_nav_overlay, LV_OPA_80, 0);
lv_obj_add_flag(_nav_overlay, LV_OBJ_FLAG_CLICKABLE);
lv_obj_remove_flag(_nav_overlay, LV_OBJ_FLAG_SCROLLABLE);
int w = lv_display_get_horizontal_resolution(NULL) - 16;
char ph[40];
snprintf(ph, sizeof(ph), "Quick message %d (empty clears it)", slot + 1);
_nav_ta = wifiField(_nav_overlay, ph, false);
lv_obj_remove_event_cb(_nav_ta, onWifiField);
lv_obj_set_width(_nav_ta, w);
lv_obj_set_pos(_nav_ta, 8, theme::STATUS_H + 4);
lv_textarea_set_text(_nav_ta, msgtext::quick(_prefs, slot));
lv_obj_add_event_cb(_nav_ta, devview::onByteCapInsert, LV_EVENT_INSERT, (void*)(uintptr_t)(msgtext::QUICK_LEN - 1));
lv_obj_add_state(_nav_ta, LV_STATE_FOCUSED); // draws the cursor
lv_obj_t* row = chips(_nav_overlay, onQuickEditPh, false);
lv_obj_set_width(row, w);
lv_obj_set_pos(row, 8, theme::STATUS_H + 4 + 34 + 6);
_nav_kb = kb::create(_nav_overlay, _prefs);
lv_obj_set_size(_nav_kb, LV_PCT(100), 124);
lv_obj_align(_nav_kb, LV_ALIGN_BOTTOM_MID, 0, 0);
lv_keyboard_set_textarea(_nav_kb, _nav_ta);
lv_obj_add_event_cb(_nav_kb, onQuickEditKb, LV_EVENT_READY, NULL);
lv_obj_add_event_cb(_nav_kb, onQuickEditKb, LV_EVENT_CANCEL, NULL);
}
void UITask::quickEditInsert(int ph) {
using namespace qview;
if (ph >= 0 && ph < s_ph_n && _nav_ta) lv_textarea_add_text(_nav_ta, s_ph[ph]);
}
void UITask::quickEditDone(bool ok) {
using namespace qview;
if (ok && _nav_ta && _prefs && s_edit >= 0) {
msgtext::setQuick(_prefs, s_edit, lv_textarea_get_text(_nav_ta));
the_mesh.savePrefs();
showToast(msgtext::quick(_prefs, s_edit)[0] ? "Quick message saved" : "Quick message cleared", 1200);
}
navClosePopup();
if (ok && _screen == SCR_QUICK) {
lv_obj_t* body = _body;
int32_t y = body ? lv_obj_get_scroll_y(body) : 0;
buildQuickMsgs();
if (_body) { lv_obj_update_layout(_body); lv_obj_scroll_to_y(_body, y, LV_ANIM_OFF); }
}
}
// ── Nearby > advert ───────────────────────────────────────────────────────────
// Shows you (and your position, if shared) to other nodes: now, or on a timer
// (the schema's PG_ADVERT page, ui-new's Tools > Auto-Advert).
void UITask::advertPopup() {
using namespace qview;
lv_obj_t* panel = navPopupPanel("Advert", true);
lv_obj_t* list = lv_obj_create(panel);
styleSurface(list, theme::BG);
lv_obj_set_width(list, LV_PCT(100));
lv_obj_set_flex_grow(list, 1);
lv_obj_set_flex_flow(list, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_row(list, theme::GAP, 0);
sectionTitle(list, "SEND NOW");
listRow(list, "Zero hop", "Direct range only", onAdvertGo, (void*)(uintptr_t)0);
listRow(list, "Flood", "Across the mesh", onAdvertGo, (void*)(uintptr_t)1);
schemaRows(list, settings::PG_ADVERT);
}
void UITask::advertSend(bool flood) {
navClosePopup();
bool ok = flood ? the_mesh.advertFlood() : the_mesh.advert();
if (ok) notify(UIEventType::ack);
showToast(!ok ? "Advert failed" : flood ? "Flood advert sent" : "Zero-hop advert sent");
}
// ── Settings > Bluetooth ──────────────────────────────────────────────────────
// BLE only: the USB link to a computer stays on. Not kept over a reboot
// (Bluetooth starts on, as on L1).
static void bluetoothRow(lv_obj_t* body) {
char sub[48];
bool on = s_ui->isSerialEnabled();
if (!on) snprintf(sub, sizeof(sub), "Off");
else if (s_ui->hasConnection()) snprintf(sub, sizeof(sub), "App connected");
else if (the_mesh.getBLEPin()) snprintf(sub, sizeof(sub), "PIN %06lu", (unsigned long)the_mesh.getBLEPin());
else snprintf(sub, sizeof(sub), "Waiting for the app");
lv_obj_t* sw = switchRow(body, LV_SYMBOL_BLUETOOTH " Bluetooth", sub, nullptr);
if (on) lv_obj_add_state(sw, LV_STATE_CHECKED);
lv_obj_add_event_cb(sw, qview::onBtSwitch, LV_EVENT_VALUE_CHANGED, NULL);
}
void UITask::setBluetooth(bool on) {
if (on) enableSerial(); else disableSerial();
showToast(on ? "Bluetooth on" : "Bluetooth off - USB still works", 1500);
if (_screen == SCR_SETTINGS) { // the row's subtitle
lv_obj_t* body = _body;
int32_t y = body ? lv_obj_get_scroll_y(body) : 0;
buildSettings();
if (_body) { lv_obj_update_layout(_body); lv_obj_scroll_to_y(_body, y, LV_ANIM_OFF); }
}
refreshStatusBar();
}
// ── Messages > Read all ───────────────────────────────────────────────────────
void UITask::markAllRead() {
_core->markAllRead();
showToast("All marked read", 1200);
if (_screen == SCR_CHATS) buildChats();
}