mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-01 02:11:57 +00:00
refactor: shared text-entry popup (ui-lvgl) and chat bubble row (ui-new); GPS search clock keeps a found fix
- ui-lvgl: navTextEntry()/TextEntry replace eight copies of popup + field + keyboard wiring; one byte-capped insert callback instead of two. - ui-new: drawHeadedBubble()/BubbleMark replace the duplicated DM and channel bubble drawing in MessagesScreen. - GpsSky::halt(): stopping the receiver ends an unfinished search but keeps the first-fix time; keyboard cursor blink restarts after a key. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VeBrxKyHHxWsmttVkUdBrj
This commit is contained in:
@@ -350,21 +350,12 @@ void UITask::adminValuePopup() {
|
||||
void UITask::adminTextPopup(const char* text, bool digits) {
|
||||
AdminSession& S = _core->admin;
|
||||
const admin::Field* f = S.currentField();
|
||||
lv_obj_t* panel = navPopupPanel(f ? f->label : "Command", false);
|
||||
lv_obj_align(panel, LV_ALIGN_TOP_MID, 0, theme::STATUS_H + 4); // above the keyboard
|
||||
_nav_ta = textField(panel);
|
||||
lv_textarea_set_max_length(_nav_ta, 150);
|
||||
if (digits) lv_textarea_set_accepted_chars(_nav_ta, "0123456789.");
|
||||
if (f && f->isCustom()) lv_textarea_set_placeholder_text(_nav_ta, "e.g. get advert.interval");
|
||||
lv_textarea_set_text(_nav_ta, text);
|
||||
lv_obj_add_state(_nav_ta, LV_STATE_FOCUSED);
|
||||
_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);
|
||||
if (digits) kb::apply(_nav_kb, kb::L_SYM);
|
||||
lv_obj_add_event_cb(_nav_kb, onAdminTextKb, LV_EVENT_READY, NULL);
|
||||
lv_obj_add_event_cb(_nav_kb, onAdminTextKb, LV_EVENT_CANCEL, NULL);
|
||||
TextEntry t = { f ? f->label : "Command", onAdminTextKb };
|
||||
t.text = text;
|
||||
t.max_bytes = 150;
|
||||
if (digits) { t.accepted = "0123456789."; t.symbols = true; }
|
||||
if (f && f->isCustom()) t.hint = "e.g. get advert.interval";
|
||||
navTextEntry(t);
|
||||
}
|
||||
|
||||
void UITask::adminTextDone(bool ok) {
|
||||
|
||||
@@ -153,13 +153,11 @@ void UITask::botRow(int row) {
|
||||
int cap;
|
||||
if (char* t = botcfg::text(p, r.kind, cap)) {
|
||||
bool reply = !botcfg::isTrigger(r.kind);
|
||||
lv_obj_t* panel = navPopupPanel(r.label, false);
|
||||
lv_obj_align(panel, LV_ALIGN_TOP_MID, 0, theme::STATUS_H + 4); // above the keyboard
|
||||
_nav_ta = textField(panel);
|
||||
lv_textarea_set_max_length(_nav_ta, cap - 1);
|
||||
lv_textarea_set_text(_nav_ta, t);
|
||||
lv_textarea_set_placeholder_text(_nav_ta, botcfg::isTrigger(r.kind) ? "e.g. !hi, or * for any message" : "Reply text");
|
||||
lv_obj_add_state(_nav_ta, LV_STATE_FOCUSED);
|
||||
TextEntry te = { r.label, onBotTextKb };
|
||||
te.text = t;
|
||||
te.max_bytes = cap - 1;
|
||||
te.hint = botcfg::isTrigger(r.kind) ? "e.g. !hi, or * for any message" : "Reply text";
|
||||
lv_obj_t* panel = navTextEntry(te);
|
||||
if (reply) {
|
||||
// Placeholders the bot fills in, as buttons in place of the title (no
|
||||
// room for another row above the keyboard; the field says "Reply").
|
||||
@@ -180,12 +178,6 @@ void UITask::botRow(int row) {
|
||||
if (strcmp(ph, "{batt}") != 0) botChip(ph, c); // already there
|
||||
}, chips);
|
||||
}
|
||||
_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, onBotTextKb, LV_EVENT_READY, NULL);
|
||||
lv_obj_add_event_cb(_nav_kb, onBotTextKb, LV_EVENT_CANCEL, NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -122,18 +122,11 @@ void UITask::roomLoginPopup(const uint8_t* pub_key) {
|
||||
ContactInfo ci;
|
||||
if (!MessageHistory::contactByPrefix(pub_key, ci)) return;
|
||||
memcpy(s_login_key, ci.id.pub_key, PUB_KEY_SIZE);
|
||||
lv_obj_t* panel = navPopupPanel(ci.name, false);
|
||||
lv_obj_align(panel, LV_ALIGN_TOP_MID, 0, theme::STATUS_H + 4); // above the keyboard
|
||||
_nav_ta = wifiField(panel, "Room password (may be empty)", true);
|
||||
lv_obj_remove_event_cb(_nav_ta, onWifiField);
|
||||
lv_textarea_set_max_length(_nav_ta, 15);
|
||||
lv_obj_add_state(_nav_ta, LV_STATE_FOCUSED); // draws the cursor
|
||||
_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, onRoomLoginKb, LV_EVENT_READY, NULL);
|
||||
lv_obj_add_event_cb(_nav_kb, onRoomLoginKb, LV_EVENT_CANCEL, NULL);
|
||||
TextEntry t = { ci.name, onRoomLoginKb };
|
||||
t.hint = "Room password (may be empty)";
|
||||
t.password = true;
|
||||
t.max_bytes = 15;
|
||||
navTextEntry(t);
|
||||
}
|
||||
|
||||
void UITask::roomLoginDone(bool ok) {
|
||||
|
||||
@@ -39,14 +39,6 @@ static uint8_t s_pin_key[PUB_KEY_SIZE];
|
||||
|
||||
enum : uint8_t { F_CHANGE, F_REMOVE };
|
||||
|
||||
// Text field whose limit is in UTF-8 bytes (user data: the byte cap).
|
||||
static void onByteCapInsert(lv_event_t* e) {
|
||||
lv_obj_t* ta = (lv_obj_t*)lv_event_get_target(e);
|
||||
const char* ins = (const char*)lv_event_get_param(e);
|
||||
size_t cap = (size_t)(uintptr_t)lv_event_get_user_data(e);
|
||||
if (ins && strlen(lv_textarea_get_text(ta)) + strlen(ins) > cap) lv_textarea_set_insert_replace(ta, "");
|
||||
}
|
||||
|
||||
} // namespace devview
|
||||
|
||||
static void onNodeName(lv_event_t* e) { (void)e; s_ui->nodeNamePopup(); }
|
||||
@@ -116,20 +108,11 @@ static bool pinEdit(char* entry, const char* key) {
|
||||
|
||||
void UITask::nodeNamePopup() {
|
||||
if (!_prefs) return;
|
||||
lv_obj_t* panel = navPopupPanel("Node name", false);
|
||||
lv_obj_align(panel, LV_ALIGN_TOP_MID, 0, theme::STATUS_H + 4); // above the keyboard
|
||||
_nav_ta = wifiField(panel, "Shown to others in adverts", false);
|
||||
lv_obj_remove_event_cb(_nav_ta, onWifiField);
|
||||
lv_textarea_set_text(_nav_ta, the_mesh.getNodeName());
|
||||
lv_obj_add_event_cb(_nav_ta, devview::onByteCapInsert, LV_EVENT_INSERT,
|
||||
(void*)(uintptr_t)(sizeof(_prefs->node_name) - 1));
|
||||
lv_obj_add_state(_nav_ta, LV_STATE_FOCUSED); // draws the cursor
|
||||
_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, onNodeNameKb, LV_EVENT_READY, NULL);
|
||||
lv_obj_add_event_cb(_nav_kb, onNodeNameKb, LV_EVENT_CANCEL, NULL);
|
||||
TextEntry t = { "Node name", onNodeNameKb };
|
||||
t.hint = "Shown to others in adverts";
|
||||
t.text = the_mesh.getNodeName();
|
||||
t.max_bytes = sizeof(_prefs->node_name) - 1;
|
||||
navTextEntry(t);
|
||||
}
|
||||
|
||||
void UITask::nodeNameDone(bool ok) {
|
||||
|
||||
@@ -155,6 +155,7 @@ static const char* s_var_map[13];
|
||||
// on another object) can take that state away: put it back after a pick.
|
||||
static void keepCursor(lv_obj_t* ta) {
|
||||
lv_obj_add_state(ta, LV_STATE_FOCUSED);
|
||||
lv_textarea_set_cursor_pos(ta, lv_textarea_get_cursor_pos(ta)); // restarts the blink (see fieldFocus)
|
||||
lv_obj_invalidate(ta);
|
||||
}
|
||||
|
||||
|
||||
@@ -452,18 +452,10 @@ void UITask::mapAreaAction(uint8_t act) {
|
||||
bool trails = m.flags & mapview::AreaStore::F_TRAILS;
|
||||
switch (act) {
|
||||
case A_RENAME: {
|
||||
lv_obj_t* panel = navPopupPanel("Area name", false);
|
||||
lv_obj_align(panel, LV_ALIGN_TOP_MID, 0, theme::STATUS_H + 4); // above the keyboard
|
||||
_nav_ta = textField(panel);
|
||||
lv_textarea_set_max_length(_nav_ta, sizeof(m.name) - 1);
|
||||
lv_textarea_set_text(_nav_ta, m.name);
|
||||
lv_obj_add_state(_nav_ta, LV_STATE_FOCUSED);
|
||||
_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, onRenameKb, LV_EVENT_READY, NULL);
|
||||
lv_obj_add_event_cb(_nav_kb, onRenameKb, LV_EVENT_CANCEL, NULL);
|
||||
TextEntry t = { "Area name", onRenameKb };
|
||||
t.text = m.name;
|
||||
t.max_bytes = sizeof(m.name) - 1;
|
||||
navTextEntry(t);
|
||||
return;
|
||||
}
|
||||
case A_FILL:
|
||||
|
||||
@@ -92,15 +92,6 @@ static void onTrailDraw(lv_event_t* e) {
|
||||
lv_draw_line(d.base.layer, &d);
|
||||
}
|
||||
|
||||
// Waypoint labels are 11 bytes of UTF-8 (Waypoint.h): reject input past that,
|
||||
// so a two-byte letter is never cut in half.
|
||||
static void onLabelInsert(lv_event_t* e) {
|
||||
lv_obj_t* ta = (lv_obj_t*)lv_event_get_target(e);
|
||||
const char* ins = (const char*)lv_event_get_param(e);
|
||||
if (ins && strlen(lv_textarea_get_text(ta)) + strlen(ins) > WAYPOINT_LABEL_LEN - 1)
|
||||
lv_textarea_set_insert_replace(ta, "");
|
||||
}
|
||||
|
||||
} // namespace navmap
|
||||
|
||||
static void onNavList(lv_event_t* e) { (void)e; s_ui->navTargetsPopup(); }
|
||||
@@ -603,6 +594,24 @@ void UITask::navClosePopup() {
|
||||
_nav_trail_lbl = _nav_trail_btn = _nav_reset_lbl = _nav_share_lbl = _nav_share_btn = _nav_tb_btn = nullptr;
|
||||
}
|
||||
|
||||
lv_obj_t* UITask::navTextEntry(const TextEntry& t) {
|
||||
lv_obj_t* panel = navPopupPanel(t.title, false);
|
||||
lv_obj_align(panel, LV_ALIGN_TOP_MID, 0, theme::STATUS_H + 4); // above the keyboard
|
||||
_nav_ta = textField(panel, t.hint);
|
||||
if (t.password) lv_textarea_set_password_mode(_nav_ta, true);
|
||||
if (t.accepted) lv_textarea_set_accepted_chars(_nav_ta, t.accepted);
|
||||
if (t.text) lv_textarea_set_text(_nav_ta, t.text); // before the cap: a longer stored text is shown, not wiped
|
||||
if (t.max_bytes) lv_obj_add_event_cb(_nav_ta, onByteCapInsert, LV_EVENT_INSERT, (void*)(uintptr_t)t.max_bytes);
|
||||
_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); // focuses the field: the cursor
|
||||
if (t.symbols) kb::apply(_nav_kb, kb::L_SYM);
|
||||
lv_obj_add_event_cb(_nav_kb, t.done, LV_EVENT_READY, NULL);
|
||||
lv_obj_add_event_cb(_nav_kb, t.done, LV_EVENT_CANCEL, NULL);
|
||||
return panel;
|
||||
}
|
||||
|
||||
static void navRowRight(lv_obj_t* row, const char* text, uint32_t col, int right) {
|
||||
lv_obj_align(label(row, text, THEME_FONT_SMALL, col), LV_ALIGN_RIGHT_MID, -right, 0);
|
||||
}
|
||||
@@ -742,19 +751,10 @@ void UITask::navWaypointAction(uint8_t act) {
|
||||
|
||||
void UITask::navRenamePopup(int idx) {
|
||||
_nav_wp = idx;
|
||||
lv_obj_t* panel = navPopupPanel("Waypoint name", false);
|
||||
lv_obj_align(panel, LV_ALIGN_TOP_MID, 0, theme::STATUS_H + 4); // above the keyboard
|
||||
_nav_ta = textField(panel);
|
||||
lv_textarea_set_text(_nav_ta, _core->waypoints.at(idx).label);
|
||||
lv_obj_add_state(_nav_ta, LV_STATE_FOCUSED); // draws the cursor
|
||||
lv_obj_add_event_cb(_nav_ta, navmap::onLabelInsert, LV_EVENT_INSERT, NULL);
|
||||
|
||||
_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, onNavRenameKb, LV_EVENT_READY, NULL);
|
||||
lv_obj_add_event_cb(_nav_kb, onNavRenameKb, LV_EVENT_CANCEL, NULL);
|
||||
TextEntry t = { "Waypoint name", onNavRenameKb };
|
||||
t.text = _core->waypoints.at(idx).label;
|
||||
t.max_bytes = WAYPOINT_LABEL_LEN - 1;
|
||||
navTextEntry(t);
|
||||
}
|
||||
|
||||
void UITask::navRenameDone(bool ok) {
|
||||
@@ -1460,19 +1460,10 @@ void UITask::navWaypointsPopup() {
|
||||
// name is optional) -> a new waypoint.
|
||||
void UITask::navCoordsPopup() {
|
||||
if (_core->waypoints.full()) { showToast(waypointsFull()); return; }
|
||||
lv_obj_t* panel = navPopupPanel("Add by coordinates", false);
|
||||
lv_obj_align(panel, LV_ALIGN_TOP_MID, 0, theme::STATUS_H + 4); // above the keyboard
|
||||
_nav_ta = textField(panel);
|
||||
lv_textarea_set_placeholder_text(_nav_ta, "50.06142, 19.93721 Name");
|
||||
lv_obj_add_state(_nav_ta, LV_STATE_FOCUSED); // draws the cursor
|
||||
|
||||
_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);
|
||||
kb::apply(_nav_kb, kb::L_SYM); // digits first; "abc" gives letters for the name
|
||||
lv_obj_add_event_cb(_nav_kb, onNavCoordsKb, LV_EVENT_READY, NULL);
|
||||
lv_obj_add_event_cb(_nav_kb, onNavCoordsKb, LV_EVENT_CANCEL, NULL);
|
||||
TextEntry t = { "Add by coordinates", onNavCoordsKb };
|
||||
t.hint = "50.06142, 19.93721 Name";
|
||||
t.symbols = true; // digits first; "abc" gives letters for the name
|
||||
navTextEntry(t);
|
||||
}
|
||||
|
||||
void UITask::navCoordsDone(bool ok) {
|
||||
|
||||
@@ -151,7 +151,7 @@ void UITask::quickEdit(int slot) {
|
||||
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_event_cb(_nav_ta, 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);
|
||||
|
||||
@@ -112,19 +112,11 @@ void UITask::radioNamePopup(uint8_t what) {
|
||||
const char* title = what == N_PRESET ? "Preset name" : what == N_SCOPE_ADD ? "New scope" : "Scope name";
|
||||
const char* hint = what == N_PRESET ? "e.g. Hiking" : "Region name, e.g. pl-maz";
|
||||
size_t cap = what == N_PRESET ? sizeof(_prefs->user_radio_presets[0].name) - 1 : sizeof(ScopeEntry::name) - 1;
|
||||
lv_obj_t* panel = navPopupPanel(title, false);
|
||||
lv_obj_align(panel, LV_ALIGN_TOP_MID, 0, theme::STATUS_H + 4); // above the keyboard
|
||||
_nav_ta = wifiField(panel, hint, false);
|
||||
lv_obj_remove_event_cb(_nav_ta, onWifiField);
|
||||
if (what == N_SCOPE_RENAME) lv_textarea_set_text(_nav_ta, the_mesh.scopeList().name((uint8_t)s_scope));
|
||||
lv_obj_add_event_cb(_nav_ta, devview::onByteCapInsert, LV_EVENT_INSERT, (void*)(uintptr_t)cap);
|
||||
lv_obj_add_state(_nav_ta, LV_STATE_FOCUSED); // draws the cursor
|
||||
_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, onRadioNameKb, LV_EVENT_READY, NULL);
|
||||
lv_obj_add_event_cb(_nav_kb, onRadioNameKb, LV_EVENT_CANCEL, NULL);
|
||||
TextEntry t = { title, onRadioNameKb };
|
||||
t.hint = hint;
|
||||
t.max_bytes = cap;
|
||||
if (what == N_SCOPE_RENAME) t.text = the_mesh.scopeList().name((uint8_t)s_scope);
|
||||
navTextEntry(t);
|
||||
}
|
||||
|
||||
void UITask::radioNameDone(bool ok) {
|
||||
|
||||
@@ -1056,6 +1056,15 @@ static void fieldFocus(lv_obj_t* ta, bool on) {
|
||||
lv_textarea_set_cursor_pos(ta, lv_textarea_get_cursor_pos(ta));
|
||||
}
|
||||
|
||||
// A field limited in UTF-8 bytes (user data: the cap) -- names and other text
|
||||
// that is stored in a fixed buffer. LVGL's own limit counts characters.
|
||||
static void onByteCapInsert(lv_event_t* e) {
|
||||
lv_obj_t* ta = (lv_obj_t*)lv_event_get_target(e);
|
||||
const char* ins = (const char*)lv_event_get_param(e);
|
||||
size_t cap = (size_t)(uintptr_t)lv_event_get_user_data(e);
|
||||
if (ins && strlen(lv_textarea_get_text(ta)) + strlen(ins) > cap) lv_textarea_set_insert_replace(ta, "");
|
||||
}
|
||||
|
||||
// The primary action of a screen or popup: full amber, dark text (Theme.h);
|
||||
// call after its label is made.
|
||||
static void stylePrimary(lv_obj_t* b) {
|
||||
|
||||
@@ -24,6 +24,20 @@ struct UiEvent;
|
||||
|
||||
namespace mapview { struct TileArea; }
|
||||
|
||||
// A popup that asks for one line of text: the field over the keyboard. `done`
|
||||
// gets the keyboard's READY (OK) and CANCEL; the field is _nav_ta, the keyboard
|
||||
// _nav_kb, until navClosePopup().
|
||||
struct TextEntry {
|
||||
const char* title;
|
||||
lv_event_cb_t done;
|
||||
const char* text = nullptr; // what the field starts with
|
||||
const char* hint = nullptr; // placeholder
|
||||
size_t max_bytes = 0; // UTF-8 bytes the field takes; 0 = no limit
|
||||
bool password = false;
|
||||
const char* accepted = nullptr; // only these characters (lv_textarea_set_accepted_chars)
|
||||
bool symbols = false; // open on the digits / symbols page
|
||||
};
|
||||
|
||||
class UITask : public UITaskBase, public UiCoreHost {
|
||||
public:
|
||||
UITask(mesh::MainBoard* board, BaseSerialInterface* serial) : UITaskBase(board, serial) {}
|
||||
@@ -385,6 +399,7 @@ private:
|
||||
void navFrameTarget();
|
||||
void navSetTarget(uint8_t kind, const uint8_t* key, int32_t lat, int32_t lon, const char* name);
|
||||
lv_obj_t* navPopupPanel(const char* title, bool full, bool bottom = false);
|
||||
lv_obj_t* navTextEntry(const TextEntry& t); // popup + one-line field + keyboard (NavMap.h)
|
||||
void navRenamePopup(int idx);
|
||||
void navAddWaypoint(int32_t lat, int32_t lon);
|
||||
void navDropAt(int x, int y);
|
||||
|
||||
@@ -562,6 +562,83 @@ class MessagesScreen : public UIScreen {
|
||||
return { outgoing ? (full_avail - w) : 0, w };
|
||||
}
|
||||
|
||||
// The marker in a bubble's header, after the sender: a delivery glyph (an
|
||||
// outgoing DM, or a channel post a repeater echoed), or the hop count an
|
||||
// incoming message took; none if neither applies.
|
||||
struct BubbleMark {
|
||||
enum Kind : uint8_t { NONE, ACK, HOPS } kind = NONE;
|
||||
AckState status = ACK_OK;
|
||||
int sends = 1, relay = 0, hops = 0;
|
||||
static BubbleMark ack(AckState s, int sends, int relay) { BubbleMark m; m.kind = ACK; m.status = s; m.sends = sends; m.relay = relay; return m; }
|
||||
static BubbleMark hop(int n) { BubbleMark m; if (n > 0) { m.kind = HOPS; m.hops = n; } return m; }
|
||||
int width(DisplayDriver& d) const {
|
||||
return kind == ACK ? 3 + ackGlyphWidth(d, status, sends, relay)
|
||||
: kind == HOPS ? 3 + miniIconNumberWidth(d, hops) : 0;
|
||||
}
|
||||
};
|
||||
|
||||
// One history row as a bubble with a header (sender, marker, age) over its
|
||||
// body, sized to its own content -- shared by the DM/room and channel lists.
|
||||
// `expand` wraps the body over several lines; otherwise one line that the
|
||||
// selected row marquees. `mq_delay` takes the marquee's redraw request.
|
||||
void drawHeadedBubble(DisplayDriver& display, int y, int bh, int lh, bool sel, int reserve, bool outgoing,
|
||||
const char* sender, const char* body, const char* age, const BubbleMark& mark,
|
||||
bool expand, int& mq_delay) {
|
||||
const int age_w = age[0] ? display.getTextWidth(age) + 3 : 0;
|
||||
// Size the bubble to its own content before drawing anything (see
|
||||
// computeBubbleBox): the header (sender+marker+age) vs the body, measured
|
||||
// once here and reused below instead of re-wrapping.
|
||||
const int full_avail = display.width() - reserve;
|
||||
const int max_w = bubbleMaxW(display, full_avail);
|
||||
const int ack_w = mark.width(display);
|
||||
// A little air between a marker and the age, so "12" + "1s" doesn't read as "121s".
|
||||
const int mk_gap = (ack_w > 0 && age_w > 0) ? 2 : 0;
|
||||
const int header_w = 3 + (sender[0] ? display.getTextWidth(sender) : -3) + ack_w + mk_gap + age_w + 3;
|
||||
int body_w, nl = 0;
|
||||
if (expand) {
|
||||
display.translateUTF8ToBlocks(s_wrap_trans, body, sizeof(s_wrap_trans));
|
||||
nl = FullscreenMsgView::wrapLines(display, s_wrap_trans, max_w - 6, s_wrap_lines, 8);
|
||||
body_w = 0;
|
||||
for (int li = 0; li < nl; li++) { int w = display.getTextWidth(s_wrap_lines[li]); if (w > body_w) body_w = w; }
|
||||
body_w += 6;
|
||||
} else {
|
||||
int raw_w = display.getTextWidth(body);
|
||||
body_w = (raw_w > max_w - 6 ? max_w - 6 : raw_w) + 6;
|
||||
}
|
||||
const BubbleBox box = computeBubbleBox(full_avail, max_w, outgoing, header_w, body_w);
|
||||
|
||||
drawHistRowFrame(display, box.x, box.w, y, bh, lh, sel);
|
||||
// Only the body marquees, not the sender too: both share the single
|
||||
// marquee slot on DisplayDriver, and if two texts in the same row both
|
||||
// qualified they'd keep resetting each other's animation every frame.
|
||||
// The name gives way to the marker and the age (a long name is
|
||||
// ellipsized), and the marker sits right after whatever width the name
|
||||
// actually got -- not after its full width, which would run it into the age.
|
||||
int name_avail = box.w - 6 - age_w - ack_w - mk_gap;
|
||||
if (name_avail < display.getCharWidth()) name_avail = display.getCharWidth();
|
||||
int name_w = sender[0] ? display.getTextWidth(sender) : 0;
|
||||
if (name_w > name_avail) name_w = name_avail;
|
||||
if (sender[0]) display.drawTextEllipsized(box.x + 3, y + 1, name_avail, sender);
|
||||
int gx = box.x + 3 + (name_w ? name_w + 3 : 0);
|
||||
if (mark.kind == BubbleMark::ACK) drawAckGlyph(display, gx, y + 1, mark.status, mark.sends, mark.relay);
|
||||
else if (mark.kind == BubbleMark::HOPS) miniIconDrawNumber(display, gx, y + 1, mark.hops);
|
||||
if (age[0]) { display.setCursor(box.x + box.w - age_w, y + 1); display.print(age); }
|
||||
// Ink is already LIGHT (unselected) or DARK (selected) from
|
||||
// drawHistRowFrame above, and nothing since has changed it.
|
||||
if (expand) {
|
||||
for (int li = 0; li < nl; li++) { display.setCursor(box.x + 3, y + (li + 1) * lh + 1); display.print(s_wrap_lines[li]); }
|
||||
} else {
|
||||
// Suppress this row's own marquee while _ctx_menu (Path/Relayed by,
|
||||
// etc.) sits on top of it -- both would otherwise fight over
|
||||
// DisplayDriver's single shared marquee slot every frame (this row
|
||||
// redraws every frame regardless of the popup), each is_new-resetting
|
||||
// the other's animation and producing a stuck-then-jumpy scroll.
|
||||
const bool body_marquee = sel && !_ctx_menu.active;
|
||||
const int r_body = display.drawTextEllipsized(box.x + 3, y + lh + 1, box.w - 6, body, body_marquee);
|
||||
if (body_marquee && r_body > 0) mq_delay = r_body;
|
||||
}
|
||||
}
|
||||
|
||||
// Bottom-right "+ Send" compose button, shared by both history lists:
|
||||
// bordered when idle, inverted (filled) when selected. Right-aligned to
|
||||
// match the outgoing ("Me") bubbles anchored on that side, so composing
|
||||
@@ -1511,71 +1588,12 @@ public:
|
||||
// time at write time), but if it ever does, show the receipt time
|
||||
// (now_ts) rather than leave the row with no age at all.
|
||||
char age[6]; geo::fmtAgeShort(age, sizeof(age), now_ts, e.timestamp ? e.timestamp : now_ts);
|
||||
int age_w = age[0] ? display.getTextWidth(age) + 3 : 0;
|
||||
|
||||
// Size the bubble to its own content before drawing anything (see
|
||||
// computeBubbleBox): the header (sender+ack+age) vs the body, measured
|
||||
// once here and reused below instead of re-wrapping.
|
||||
int full_avail = display.width() - reserve;
|
||||
int max_w = bubbleMaxW(display, full_avail);
|
||||
// Incoming: the hop count the DM actually took to reach us, shown as
|
||||
// the same tiny digit icon an outgoing send uses for its relay/echo
|
||||
// count -- no ack glyph exists for incoming (there's nothing to
|
||||
// deliver), so this slot is otherwise empty.
|
||||
int in_hop_count = !e.outgoing ? (e.path_len & 63) : 0;
|
||||
int ack_w = e.outgoing ? (3 + ackGlyphWidth(display, _history.dmEffectiveStatus(e), e.attempt + 1))
|
||||
: (in_hop_count > 0 ? (3 + miniIconNumberWidth(display, in_hop_count)) : 0);
|
||||
// A little air between a marker and the age, so "12" + "1s" doesn't read as "121s".
|
||||
int mk_gap = (ack_w > 0 && age_w > 0) ? 2 : 0;
|
||||
int header_w = 3 + (sender[0] ? display.getTextWidth(sender) : -3) + ack_w + mk_gap + age_w + 3;
|
||||
int body_w, nl = 0;
|
||||
if (expand) {
|
||||
display.translateUTF8ToBlocks(s_wrap_trans, body, sizeof(s_wrap_trans));
|
||||
nl = FullscreenMsgView::wrapLines(display, s_wrap_trans, max_w - 6, s_wrap_lines, 8);
|
||||
body_w = 0;
|
||||
for (int li = 0; li < nl; li++) { int w = display.getTextWidth(s_wrap_lines[li]); if (w > body_w) body_w = w; }
|
||||
body_w += 6;
|
||||
} else {
|
||||
int raw_w = display.getTextWidth(body);
|
||||
body_w = (raw_w > max_w - 6 ? max_w - 6 : raw_w) + 6;
|
||||
}
|
||||
BubbleBox box = computeBubbleBox(full_avail, max_w, e.outgoing, header_w, body_w);
|
||||
|
||||
drawHistRowFrame(display, box.x, box.w, y, bh, lh, sel);
|
||||
// Only the body marquees, not the sender too: both share the single
|
||||
// marquee slot on DisplayDriver, and if two texts in the same row both
|
||||
// qualified they'd keep resetting each other's animation every frame.
|
||||
// The name gives way to the ack/hop marker and the age (a long name is
|
||||
// ellipsized), and the marker sits right after whatever width the name
|
||||
// actually got -- not after its full width, which would run it into the age.
|
||||
int name_avail = box.w - 6 - age_w - ack_w - mk_gap;
|
||||
if (name_avail < display.getCharWidth()) name_avail = display.getCharWidth();
|
||||
int name_w = sender[0] ? display.getTextWidth(sender) : 0;
|
||||
if (name_w > name_avail) name_w = name_avail;
|
||||
if (sender[0]) display.drawTextEllipsized(box.x + 3, y + 1, name_avail, sender);
|
||||
const int mark_x = box.x + 3 + (name_w ? name_w + 3 : 0);
|
||||
if (e.outgoing) { // delivery marker after "Me"
|
||||
int gx = mark_x;
|
||||
drawAckGlyph(display, gx, y + 1, _history.dmEffectiveStatus(e), e.attempt + 1);
|
||||
} else if (in_hop_count > 0) { // hop count after the sender name
|
||||
int gx = mark_x;
|
||||
miniIconDrawNumber(display, gx, y + 1, in_hop_count);
|
||||
}
|
||||
if (age[0]) { display.setCursor(box.x + box.w - age_w, y + 1); display.print(age); }
|
||||
// Ink is already LIGHT (unselected) or DARK (selected) from
|
||||
// drawHistRowFrame above, and nothing since has changed it.
|
||||
if (expand) {
|
||||
for (int li = 0; li < nl; li++) { display.setCursor(box.x + 3, y + (li + 1) * lh + 1); display.print(s_wrap_lines[li]); }
|
||||
} else {
|
||||
// Suppress this row's own marquee while _ctx_menu (Path/Relayed by,
|
||||
// etc.) sits on top of it -- both would otherwise fight over
|
||||
// DisplayDriver's single shared marquee slot every frame (this row
|
||||
// redraws every frame regardless of the popup), each is_new-resetting
|
||||
// the other's animation and producing a stuck-then-jumpy scroll.
|
||||
bool body_marquee = sel && !_ctx_menu.active;
|
||||
int r_body = display.drawTextEllipsized(box.x + 3, y + lh + 1, box.w - 6, body, body_marquee);
|
||||
if (body_marquee && r_body > 0) mq_delay = r_body;
|
||||
}
|
||||
// Outgoing: the delivery marker. Incoming: the hop count the DM
|
||||
// actually took to reach us, as the same tiny digit icon (no ack
|
||||
// glyph exists for incoming -- there's nothing to deliver).
|
||||
const BubbleMark mark = e.outgoing ? BubbleMark::ack(_history.dmEffectiveStatus(e), e.attempt + 1, 0)
|
||||
: BubbleMark::hop(e.path_len & 63);
|
||||
drawHeadedBubble(display, y, bh, lh, sel, reserve, e.outgoing, sender, body, age, mark, expand, mq_delay);
|
||||
}
|
||||
|
||||
if (dm_count == 0) {
|
||||
@@ -1734,70 +1752,18 @@ public:
|
||||
// now_ts here too rather than leave the row with no age at all.
|
||||
uint32_t ch_ts = _history.chAtPos(ring_pos).timestamp;
|
||||
char age[6]; geo::fmtAgeShort(age, sizeof(age), now_ts, ch_ts ? ch_ts : now_ts);
|
||||
int age_w = age[0] ? display.getTextWidth(age) + 3 : 0;
|
||||
const char* body = skipReplyPrefix(msg_part);
|
||||
|
||||
// Size the bubble to its own content before drawing anything (see
|
||||
// computeBubbleBox): the header (sender+ack+age) vs the body, measured
|
||||
// once here and reused below instead of re-wrapping. Channels have no
|
||||
// recipient ACK — only show ✓ once a repeater echo confirms the send
|
||||
// was relayed into the mesh; otherwise no marker (absence is normal).
|
||||
bool outgoing = strcmp(sender, "Me") == 0;
|
||||
bool show_ack = outgoing && _history.chAtPos(ring_pos).relay_status == ACK_OK;
|
||||
int relay_count = show_ack ? (_history.chAtPos(ring_pos).path_len & 63) : 0;
|
||||
// Incoming: the hop count this post actually took to reach us, shown
|
||||
// the same way an outgoing post's repeater-echo count is.
|
||||
int in_hop_count = !outgoing ? (_history.chAtPos(ring_pos).path_len & 63) : 0;
|
||||
int full_avail = display.width() - reserve;
|
||||
int max_w = bubbleMaxW(display, full_avail);
|
||||
int ack_w = show_ack ? (3 + ackGlyphWidth(display, ACK_OK, 1, relay_count))
|
||||
: (in_hop_count > 0 ? (3 + miniIconNumberWidth(display, in_hop_count)) : 0);
|
||||
// A little air between a marker and the age, so "12" + "1s" doesn't read as "121s".
|
||||
int mk_gap = (ack_w > 0 && age_w > 0) ? 2 : 0;
|
||||
int header_w = 3 + (sender[0] ? display.getTextWidth(sender) : -3) + ack_w + mk_gap + age_w + 3;
|
||||
int body_w, nl = 0;
|
||||
if (expand) {
|
||||
display.translateUTF8ToBlocks(s_wrap_trans, body, sizeof(s_wrap_trans));
|
||||
nl = FullscreenMsgView::wrapLines(display, s_wrap_trans, max_w - 6, s_wrap_lines, 8);
|
||||
body_w = 0;
|
||||
for (int li = 0; li < nl; li++) { int w = display.getTextWidth(s_wrap_lines[li]); if (w > body_w) body_w = w; }
|
||||
body_w += 6;
|
||||
} else {
|
||||
int raw_w = display.getTextWidth(body);
|
||||
body_w = (raw_w > max_w - 6 ? max_w - 6 : raw_w) + 6;
|
||||
}
|
||||
BubbleBox box = computeBubbleBox(full_avail, max_w, outgoing, header_w, body_w);
|
||||
|
||||
drawHistRowFrame(display, box.x, box.w, y, bh, lh, sel);
|
||||
// Only the body marquees, not the sender — see the DM history block above.
|
||||
// Name yields to the marker and age -- see the DM history block above.
|
||||
int name_avail = box.w - 6 - age_w - ack_w - mk_gap;
|
||||
if (name_avail < display.getCharWidth()) name_avail = display.getCharWidth();
|
||||
int name_w = display.getTextWidth(sender);
|
||||
if (name_w > name_avail) name_w = name_avail;
|
||||
display.drawTextEllipsized(box.x + 3, y + 1, name_avail, sender);
|
||||
if (show_ack) {
|
||||
int gx = box.x + 3 + name_w + 3;
|
||||
drawAckGlyph(display, gx, y + 1, ACK_OK, 1, relay_count);
|
||||
} else if (in_hop_count > 0) {
|
||||
int gx = box.x + 3 + name_w + 3;
|
||||
miniIconDrawNumber(display, gx, y + 1, in_hop_count);
|
||||
}
|
||||
if (age[0]) { display.setCursor(box.x + box.w - age_w, y + 1); display.print(age); }
|
||||
// Ink is already LIGHT (unselected) or DARK (selected) from
|
||||
// drawHistRowFrame above, and nothing since has changed it.
|
||||
if (expand) {
|
||||
for (int li = 0; li < nl; li++) { display.setCursor(box.x + 3, y + (li + 1) * lh + 1); display.print(s_wrap_lines[li]); }
|
||||
} else {
|
||||
// Suppress this row's own marquee while _ctx_menu (Path/Relayed by,
|
||||
// etc.) sits on top of it -- both would otherwise fight over
|
||||
// DisplayDriver's single shared marquee slot every frame (this row
|
||||
// redraws every frame regardless of the popup), each is_new-resetting
|
||||
// the other's animation and producing a stuck-then-jumpy scroll.
|
||||
bool body_marquee = sel && !_ctx_menu.active;
|
||||
int r_body = display.drawTextEllipsized(box.x + 3, y + lh + 1, box.w - 6, body, body_marquee);
|
||||
if (body_marquee && r_body > 0) mq_delay = r_body;
|
||||
}
|
||||
// Channels have no recipient ACK -- only show the glyph once a
|
||||
// repeater echo confirms the send was relayed into the mesh; otherwise
|
||||
// no marker (absence is normal). Incoming: the hop count this post
|
||||
// actually took to reach us.
|
||||
const bool outgoing = strcmp(sender, "Me") == 0;
|
||||
const ChHistEntry& cm = _history.chAtPos(ring_pos);
|
||||
const BubbleMark mark = !outgoing ? BubbleMark::hop(cm.path_len & 63)
|
||||
: cm.relay_status == ACK_OK ? BubbleMark::ack(ACK_OK, 1, cm.path_len & 63)
|
||||
: BubbleMark();
|
||||
drawHeadedBubble(display, y, bh, lh, sel, reserve, outgoing, sender, body, age, mark, expand, mq_delay);
|
||||
}
|
||||
|
||||
if (ch_hist_count == 0) {
|
||||
|
||||
@@ -56,6 +56,10 @@ public:
|
||||
_len = 0;
|
||||
}
|
||||
|
||||
// The receiver was switched off: a search that never ended stops counting
|
||||
// (a fix already found keeps its time-to-first-fix to look at).
|
||||
void halt() { if (!ttff_ms) start_ms = 0; }
|
||||
|
||||
void feed(char c) {
|
||||
if (c == '$') { _len = 0; _buf[_len++] = c; return; }
|
||||
if (_len == 0) return;
|
||||
|
||||
@@ -105,7 +105,7 @@ public :
|
||||
|
||||
void stop() override {
|
||||
#ifdef GPS_SKYVIEW
|
||||
_sky.reset(); // a receiver that is off is not searching: stop the TTFF clock
|
||||
_sky.halt(); // a receiver that is off is not searching: stop the search clock
|
||||
#endif
|
||||
if (_pin_en != -1) {
|
||||
digitalWrite(_pin_en, !GPS_EN_ACTIVE);
|
||||
|
||||
Reference in New Issue
Block a user