refactor: one copy of shared logic and widgets; GPS is asked for, not switched on silently

Duplicates found between the L1 and L2 frontends, and inside the L2 one:
- geo::fmtLocShare / fmtLatLon / turnDeg / isAhead (GeoUtils.h): the [LOC]
  text was built in five places, the turn angle and its "ahead" threshold in
  both navigations, coordinates by hand in nine. Tested.
- LiveShareEngine::setEnabled: live share was switched in three places.
- L1 waypoint share uses WaypointModel::shareText; Trail's "Share my pos"
  goes through UITask::quickShareMyLocation, so it now sends to a running
  live-share session as the home Map page does.
- L2 Widgets.h: switchCreate (one switch size, was four), switchRow with a
  state and callback (16 hand-wired copies), switchRowOpens (Bluetooth, WiFi,
  GPS), primaryButton (one height for Save / Measure / New area / Update).

GPS: with it off, starting the trail, live share or track back, sharing or
marking a position, or centring the map asks first ("GPS is off") on both
L1 and L2; Cancel leaves it undone. A share or a mark waits for a fresh fix
(a switched-off module may still report its last one), which also fixes the
L1 "Share my pos" sending a stale position. SOS still turns the GPS on itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-11-02 10:32:23 +01:00
co-authored by Claude Opus 5.5
parent 781a480aef
commit ada5b8e545
35 changed files with 301 additions and 208 deletions
@@ -1614,6 +1614,7 @@ public:
display.setTextSize(1);
display.drawTextCentered(display.width() / 2, content_y + step, "hibernating..."); // the panel's Hibernate row
}
if (_page == HomePage::MAP && _tile_menu.active) _tile_menu.render(display); // "GPS is off"
if (slide >= 0) { // page turn: the page left behind slides out, this one in
display.slideCompose(_slide_dir * (int)(display.width() * slide + 0.5f));
anim_ms = 15;
@@ -1634,6 +1635,14 @@ public:
}
bool handleInput(char c) override {
// Map page: "GPS is off" before a share (the tile menu, free on this page).
if (_page == HomePage::MAP && _tile_menu.active) {
if (_tile_menu.handleInput(c) == PopupMenu::SELECTED && _tile_menu.selectedIndex() == 0) {
_task->toggleGPS(false);
_task->showAlert("GPS on, share once it has a fix", 1500);
}
return true;
}
// Favourites grid claims joystick UP/DOWN and inner LEFT/RIGHT; LEFT at the
// left column and RIGHT at the right column fall through to page nav so the
// user can still leave the page sideways.
@@ -1759,6 +1768,12 @@ public:
return true;
}
if (c == KEY_CONTEXT_MENU && _page == HomePage::MAP) {
if (_task->hasGPS() && !_task->getGPSState()) { // its last fix may be stale: asked first
_tile_menu.begin("GPS is off", 2);
_tile_menu.addItem("Enable GPS");
_tile_menu.addItem("Cancel");
return true;
}
_task->quickShareMyLocation();
return true;
}
@@ -11,6 +11,7 @@
#include "InfoKit.h"
#include "../NodePrefs.h"
#include "icons.h" // drawList (shared scrolling-list helper)
#include "PopupMenu.h" // "GPS is off" before Auto share starts
class LiveShareScreen : public UIScreen {
UITask* _task;
@@ -18,6 +19,7 @@ class LiveShareScreen : public UIScreen {
bool _dirty = false;
int _sel = 0;
int _scroll = 0;
PopupMenu _gps_ask;
enum Kind : uint8_t { K_TRACK, K_AUTO, K_DURATION, K_TARGET, K_SCOPE, K_MOVE, K_GAP, K_HB };
struct Row { Kind kind; const char* label; };
@@ -39,7 +41,7 @@ class LiveShareScreen : public UIScreen {
public:
LiveShareScreen(UITask* task, NodePrefs* prefs) : _task(task), _prefs(prefs) {}
void onShow() override { _dirty = false; _sel = 0; _scroll = 0; }
void onShow() override { _dirty = false; _sel = 0; _scroll = 0; _gps_ask.active = false; }
// Resolve the configured target's display name (channel name or contact name).
void currentTargetName(char* buf, int n) {
@@ -112,6 +114,7 @@ public:
int mqr = info::listRow(display, y, r.label, val, sel, reserve);
if (mqr > 0) mq_delay = mqr;
});
if (_gps_ask.active) _gps_ask.render(display);
return (mq_delay > 0 && mq_delay < 500) ? mq_delay : 500;
}
@@ -124,8 +127,14 @@ public:
switch (rows(_sel).kind) {
case K_TRACK: _prefs->track_shared_loc ^= 1; _dirty = true; break;
case K_AUTO:
_prefs->loc_share_enabled ^= 1;
if (_prefs->loc_share_enabled) _task->restartLocShareSession();
// With the GPS off nothing would go out: asked first, as Trail does.
if (!_prefs->loc_share_enabled && _task->hasGPS() && !_task->getGPSState()) {
_gps_ask.begin("GPS is off", 2); // the action first, as Trail's: nothing is lost
_gps_ask.addItem("Enable GPS & share");
_gps_ask.addItem("Cancel");
return;
}
_task->core().live_share.setEnabled(!_prefs->loc_share_enabled);
_dirty = true; break;
case K_DURATION:
_prefs->loc_share_duration_idx = (uint8_t)((_prefs->loc_share_duration_idx + (dir >= 0 ? 1 : NodePrefs::LOC_SHARE_DURATION_COUNT - 1)) % NodePrefs::LOC_SHARE_DURATION_COUNT);
@@ -192,6 +201,15 @@ public:
}
bool handleInput(char c) override {
if (_gps_ask.active) {
if (_gps_ask.handleInput(c) == PopupMenu::SELECTED && _gps_ask.selectedIndex() == 0) {
_task->toggleGPS(false);
_task->core().live_share.setEnabled(true);
_dirty = true;
_task->showAlert("GPS on, sharing", 1200);
}
return true;
}
if (c == KEY_CANCEL) {
_task->savePrefsIfDirty(_dirty);
_task->leaveTool();
+2 -4
View File
@@ -14,8 +14,6 @@
namespace navview {
static const int AHEAD_DEG = 12; // within this of straight on: "ahead"
// own_valid : is there a usable GPS fix for the device right now
// cog_valid : is there a stable course-over-ground (UITask::currentCourse)
// eta : optional closing-speed/ETA tracker; when supplied a third line is
@@ -89,8 +87,8 @@ inline void draw(DisplayDriver& d,
}
}
int turn = 0; // -180..179 degrees to turn, right positive; with a course
if (cog_valid) turn = ((to_deg - cog_deg + 540) % 360) - 180;
const bool ahead = cog_valid && turn <= AHEAD_DEG && turn >= -AHEAD_DEG;
if (cog_valid) turn = geo::turnDeg(to_deg, cog_deg);
const bool ahead = cog_valid && geo::isAhead(turn);
// Distance — emphasised at size 2.
d.setTextSize(2);
@@ -861,7 +861,7 @@ class NearbyScreen : public UIScreen, protected NearbyModel {
// row, unlabelled: then there are five rows either way.
const bool one_pos = top + step * 5 + display.getLineHeight() > bottom;
if (one_pos) {
snprintf(buf, sizeof(buf), "%.5f,%.5f", e.lat_e6 / 1e6, e.lon_e6 / 1e6);
geo::fmtLatLon(buf, sizeof(buf), e.lat_e6, e.lon_e6);
display.drawTextEllipsized(tx, top, w - 4, buf);
} else {
snprintf(buf, sizeof(buf), "Lat: %.5f", e.lat_e6 / 1e6);
@@ -964,7 +964,7 @@ public:
} else if (sel == 0) { // Type in
int32_t lat, lon;
char cur[32] = "";
if (_task->core().fixedPosition(lat, lon)) snprintf(cur, sizeof(cur), "%.5f, %.5f", lat / 1e6, lon / 1e6);
if (_task->core().fixedPosition(lat, lon)) geo::fmtLatLon(cur, sizeof(cur), lat, lon, true);
_edit_pos = true;
_kb->begin(cur, 31);
_kb->prompt = "Lat, lon";
+19 -18
View File
@@ -81,7 +81,7 @@ class TrailScreen : public UIScreen {
// correctly (settings rows cycle with LEFT/RIGHT; everything else is Enter).
// Live-share *config* lives in its own tool (Tools › Live Share); the map only
// keeps the one-shot "Share my pos" action.
enum MenuLevel { ML_MAIN, ML_FILE, ML_SETTINGS, ML_CONFIRM_GPS, ML_CONFIRM_RESET };
enum MenuLevel { ML_MAIN, ML_FILE, ML_SETTINGS, ML_CONFIRM_GPS, ML_CONFIRM_GPS_SHARE, ML_CONFIRM_RESET };
PopupMenu _action_menu;
uint8_t _menu_level = ML_MAIN;
uint8_t _act_map[16]; // max rows on any one level; pushAction guards the cap
@@ -179,6 +179,11 @@ public:
_menu_level = ML_MAIN; // popup already closed by handleInput()
return true;
}
if (_menu_level == ML_CONFIRM_GPS_SHARE) { // "Enable GPS": a fix takes a while, so no send yet
if (_action_menu.selectedIndex() == 0) { _task->toggleGPS(false); _task->showAlert("GPS on, share once it has a fix", 1500); }
_menu_level = ML_MAIN;
return true;
}
// Reset confirmation popup: same shape, but defaults to Cancel (row 1)
// since unlike the GPS prompt above, this one is destructive.
if (_menu_level == ML_CONFIRM_RESET) {
@@ -198,13 +203,16 @@ public:
case ACT_MARK_AVG:
case ACT_AUTOSAVE:
case ACT_AUTOPAUSE: return true;
case ACT_SHARE_NOW: shareMyLocationNow(); break;
case ACT_SHARE_NOW:
if (_task->hasGPS() && !_task->getGPSState()) { buildGpsConfirmMenu(true); return true; } // its last fix may be stale
shareMyLocationNow();
break;
case ACT_TOGGLE:
// Starting a trail with GPS switched off logs nothing and just
// sits on "Waiting for GPS fix" forever -- prompt to enable it
// first (only on boards that actually have a toggleable GPS).
if (!_store->isActive() && _task->hasGPS() && !_task->getGPSState()) {
buildGpsConfirmMenu();
buildGpsConfirmMenu(false);
return true;
}
handleToggle();
@@ -379,13 +387,14 @@ private:
pushAction(ACT_SETTINGS, "Settings...");
}
// Confirmation shown when "Start tracking" is chosen with GPS off. Rows are
// plain (not ActionIds); handled by _menu_level in the SELECTED branch.
void buildGpsConfirmMenu() {
_menu_level = ML_CONFIRM_GPS;
// Confirmation shown when "Start tracking" or "Share my pos" is chosen with
// GPS off. Rows are plain (not ActionIds); handled by _menu_level in the
// SELECTED branch.
void buildGpsConfirmMenu(bool share) {
_menu_level = share ? ML_CONFIRM_GPS_SHARE : ML_CONFIRM_GPS;
_act_count = 0;
_action_menu.begin("GPS is off", 2);
_action_menu.addItem("Enable GPS & start");
_action_menu.addItem(share ? "Enable GPS" : "Enable GPS & start");
_action_menu.addItem("Cancel");
}
@@ -471,16 +480,8 @@ private:
p->gps_avg_idx = idx;
}
// One-shot manual share: build "[LOC]lat,lon" and hand it to the Messages
// screen, where the user picks a DM or channel recipient. (Auto live-share
// config lives in Tools › Live Share.)
void shareMyLocationNow() {
int32_t lat, lon;
if (!_task->currentLocation(lat, lon)) { _task->showAlert("No GPS fix", 1000); return; }
char text[40];
snprintf(text, sizeof(text), LOCATION_MSG_TAG "%.5f,%.5f", lat / 1e6, lon / 1e6);
_task->shareToMessage(text);
}
// One-shot manual share, as the home Map page's: UITask::quickShareMyLocation.
void shareMyLocationNow() { _task->quickShareMyLocation(); }
// Render the average speed (or pace) in the globally-selected unit system.
void formatAvgPaceOrSpeed(char* buf, size_t n, NodePrefs* p) const {
+1 -2
View File
@@ -1907,7 +1907,6 @@ bool UITask::currentCourse(int& deg_out) const { return _core->course.currentCou
bool UITask::currentLocation(int32_t& lat, int32_t& lon) const { return _core->course.currentLocation(lat, lon); }
bool UITask::sendLocationShare(int32_t lat, int32_t lon) { return _core->live_share.send(lat, lon); }
void UITask::restartLocShareSession() { _core->live_share.restartSession(); }
void UITask::restartLocShareClock() { _core->live_share.restartClock(); }
LiveTrackStore& UITask::liveTrack() { return _core->live_share.track(); }
@@ -1923,7 +1922,7 @@ void UITask::quickShareMyLocation() {
return;
}
char text[40];
snprintf(text, sizeof(text), LOCATION_MSG_TAG "%.5f,%.5f", lat / 1e6, lon / 1e6);
geo::fmtLocShare(text, sizeof(text), lat, lon);
shareToMessage(text);
}
-1
View File
@@ -366,7 +366,6 @@ public:
void gotoLiveShareScreen();
// Live share session control (ui-core/LiveShareEngine.h): restart = new
// session (re-announce + fresh clock); clock-only = changed "Stop after".
void restartLocShareSession();
void restartLocShareClock();
void gotoLocatorScreen();
// Re-arm the locator state machine so the next evaluation initialises
@@ -388,11 +388,8 @@ public:
}
} else if (sel == 2) { // Send (share in a message)
if (wi >= 0 && wi < _task->waypoints().count()) {
const Waypoint& w = _task->waypoints().at(wi);
double lat = w.lat_1e6 / 1000000.0, lon = w.lon_1e6 / 1000000.0;
char text[80];
if (w.label[0]) snprintf(text, sizeof(text), WAYPOINT_MSG_TAG "%.5f,%.5f %s", lat, lon, w.label);
else snprintf(text, sizeof(text), WAYPOINT_MSG_TAG "%.5f,%.5f", lat, lon);
_task->core().waypoints.shareText(wi, text, sizeof(text));
_task->shareToMessage(text); // hands off to the Messages screen
}
} else { // Set as target