// 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(); }