mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-26 14:58:12 +00:00
refactor(ui): trail — 4 views always cyclable, hint sep, action popup w/ Reset
User feedback drove three changes: 1. Browsing after stop. The old V_CONFIG-snap pinned the user to the settings screen when tracking ended; you could see the map only while active. Drop the snap: Summary/Map/List are reachable from Config and from each other at any time, LEFT/RIGHT cycles all four. Enter from any view toggles tracking; the alert reads "Tracking started/stopped" / "Waiting for GPS fix" as before. Hint shows `<>N/4` so the user knows which view they're on. 2. Hint overlap. A 1-px horizontal separator sits two pixels above the hint line; Summary, List and Map are sized to leave that band clear (`avail` shrinks by an extra 2 px; the map's bottom moves up by 2). No more text touching the hint on OLED. 3. KEY_CONTEXT_MENU (Hold Enter) splits off from KEY_CANCEL and now opens an action popup instead of going back to Tools. Cancel keeps the back-to-Tools role. First action available: "Reset trail" (only when the ring isn't empty). Phase 4 will add Save / Load / Export GPX to the same menu. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,11 +11,10 @@ class TrailScreen : public UIScreen {
|
||||
UITask* _task;
|
||||
TrailStore* _store;
|
||||
|
||||
// V_CONFIG is the resting screen — shown whenever the trail is stopped.
|
||||
// Once tracking starts, the user cycles between V_SUMMARY/V_MAP/V_LIST with
|
||||
// LEFT/RIGHT. Stop returns to V_CONFIG.
|
||||
enum View { V_CONFIG = 0, V_SUMMARY = 1, V_MAP = 2, V_LIST = 3 };
|
||||
static const uint8_t ACTIVE_VIEW_COUNT = 3; // Summary, Map, List
|
||||
// All four views are always cyclable with LEFT/RIGHT. Config holds the
|
||||
// settings + stored-trail summary; the other three render the live or
|
||||
// last-recorded trail in different ways.
|
||||
enum View { V_CONFIG = 0, V_SUMMARY = 1, V_MAP = 2, V_LIST = 3, V_COUNT };
|
||||
|
||||
uint8_t _view = V_CONFIG;
|
||||
int _summary_scroll = 0; // top visible item index in Summary view
|
||||
@@ -27,6 +26,11 @@ class TrailScreen : public UIScreen {
|
||||
enum CfgRow { CFG_MIN_DIST = 0, CFG_UNITS = 1, CFG_ROW_COUNT };
|
||||
uint8_t _cfg_sel = CFG_MIN_DIST;
|
||||
|
||||
// Action popup (Hold Enter): Reset trail, and later phase-4 entries
|
||||
// (Save/Load/Export GPX). Owns the labels because PopupMenu stores
|
||||
// const char* pointers verbatim.
|
||||
PopupMenu _action_menu;
|
||||
|
||||
static const int SUMMARY_ITEM_COUNT = 5;
|
||||
|
||||
public:
|
||||
@@ -37,17 +41,13 @@ public:
|
||||
_summary_scroll = 0;
|
||||
_list_scroll = 0;
|
||||
_cfg_dirty = false;
|
||||
_action_menu.active = false;
|
||||
}
|
||||
|
||||
int render(DisplayDriver& display) override {
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
|
||||
// Snap _view to whatever the active-state allows. Stopped → always V_CONFIG;
|
||||
// active and stuck on V_CONFIG → flip to Summary.
|
||||
if (!_store->isActive() && _view != V_CONFIG) _view = V_CONFIG;
|
||||
if ( _store->isActive() && _view == V_CONFIG) _view = V_SUMMARY;
|
||||
|
||||
const char* title = (_view == V_MAP) ? "TRAIL MAP"
|
||||
: (_view == V_LIST) ? "TRAIL LIST"
|
||||
: "TRAIL";
|
||||
@@ -59,28 +59,54 @@ public:
|
||||
else if (_view == V_LIST) renderList(display);
|
||||
else renderSummary(display);
|
||||
|
||||
// Bottom hint
|
||||
// Bottom hint with a thin separator above so the line never touches the
|
||||
// content rendered just above it.
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
int hint_y = display.height() - display.lineStep();
|
||||
const int hint_y = display.height() - display.lineStep();
|
||||
display.fillRect(0, hint_y - 2, display.width(), 1);
|
||||
display.setCursor(2, hint_y);
|
||||
char hint[28];
|
||||
if (_view == V_CONFIG) {
|
||||
display.print(_store->empty() ? "<>dist [Ent] start" : "<>dist [Ent] resume");
|
||||
snprintf(hint, sizeof(hint), "<>1/%d [Ent] %s", (int)V_COUNT,
|
||||
_store->isActive() ? "stop" : (_store->empty() ? "start" : "resume"));
|
||||
} else {
|
||||
char hint[28];
|
||||
snprintf(hint, sizeof(hint), "<>%d/%d [Ent] stop",
|
||||
(int)(_view - V_SUMMARY + 1), (int)ACTIVE_VIEW_COUNT);
|
||||
display.print(hint);
|
||||
snprintf(hint, sizeof(hint), "<>%d/%d [Ent] %s",
|
||||
(int)_view + 1, (int)V_COUNT,
|
||||
_store->isActive() ? "stop" : "start");
|
||||
}
|
||||
display.print(hint);
|
||||
|
||||
// Action popup overlay (Hold Enter): Reset / Save / Load / Export GPX.
|
||||
if (_action_menu.active) _action_menu.render(display);
|
||||
|
||||
return _store->isActive() ? 1000 : 5000;
|
||||
}
|
||||
|
||||
bool handleInput(char c) override {
|
||||
if (c == KEY_CANCEL || c == KEY_CONTEXT_MENU) {
|
||||
// Action popup overrides everything else while open.
|
||||
if (_action_menu.active) {
|
||||
auto res = _action_menu.handleInput(c);
|
||||
if (res == PopupMenu::SELECTED) {
|
||||
// Item 0 = "Reset trail"; future Save/Load/Export to come.
|
||||
if (_action_menu.selectedIndex() == 0) {
|
||||
if (_store->isActive()) _store->setActive(false);
|
||||
_store->clear();
|
||||
_view = V_CONFIG;
|
||||
_task->showAlert("Trail reset", 800);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (c == KEY_CANCEL) {
|
||||
if (_cfg_dirty) { the_mesh.savePrefs(); _cfg_dirty = false; }
|
||||
_task->gotoToolsScreen();
|
||||
return true;
|
||||
}
|
||||
if (c == KEY_CONTEXT_MENU) {
|
||||
openActionMenu();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_view == V_CONFIG) {
|
||||
NodePrefs* p = _task->getNodePrefs();
|
||||
@@ -90,25 +116,26 @@ public:
|
||||
if ((c == KEY_RIGHT || c == KEY_NEXT) && p) { cycleCfg(p, +1); return true; }
|
||||
if (c == KEY_ENTER) {
|
||||
if (_cfg_dirty) { the_mesh.savePrefs(); _cfg_dirty = false; }
|
||||
_store->setActive(true);
|
||||
_view = V_SUMMARY;
|
||||
_task->showAlert(gpsHasFix() ? "Tracking started" : "Waiting for GPS fix", 1000);
|
||||
bool was_active = _store->isActive();
|
||||
_store->setActive(!was_active);
|
||||
if (!was_active) {
|
||||
_view = V_SUMMARY;
|
||||
_task->showAlert(gpsHasFix() ? "Tracking started" : "Waiting for GPS fix", 1000);
|
||||
} else {
|
||||
_task->showAlert("Tracking stopped", 800);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Active views: Summary / Map / List
|
||||
// Summary / Map / List — cycle through all 4 views (including back to Config).
|
||||
if (c == KEY_LEFT || c == KEY_PREV) {
|
||||
int v = (int)_view - V_SUMMARY;
|
||||
v = (v + ACTIVE_VIEW_COUNT - 1) % ACTIVE_VIEW_COUNT;
|
||||
_view = (uint8_t)(V_SUMMARY + v);
|
||||
_view = (uint8_t)((_view + V_COUNT - 1) % V_COUNT);
|
||||
return true;
|
||||
}
|
||||
if (c == KEY_RIGHT || c == KEY_NEXT) {
|
||||
int v = (int)_view - V_SUMMARY;
|
||||
v = (v + 1) % ACTIVE_VIEW_COUNT;
|
||||
_view = (uint8_t)(V_SUMMARY + v);
|
||||
_view = (uint8_t)((_view + 1) % V_COUNT);
|
||||
return true;
|
||||
}
|
||||
if (_view == V_SUMMARY && c == KEY_UP && _summary_scroll > 0) { _summary_scroll--; return true; }
|
||||
@@ -116,14 +143,28 @@ public:
|
||||
if (_view == V_LIST && c == KEY_UP && _list_scroll > 0) { _list_scroll--; return true; }
|
||||
if (_view == V_LIST && c == KEY_DOWN) { _list_scroll++; return true; }
|
||||
if (c == KEY_ENTER) {
|
||||
_store->setActive(false);
|
||||
_view = V_CONFIG;
|
||||
_task->showAlert("Tracking stopped", 800);
|
||||
bool was_active = _store->isActive();
|
||||
_store->setActive(!was_active);
|
||||
_task->showAlert(was_active ? "Tracking stopped"
|
||||
: (gpsHasFix() ? "Tracking started" : "Waiting for GPS fix"),
|
||||
was_active ? 800 : 1000);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Build and open the action popup. Items depend on what makes sense for the
|
||||
// current state: Reset is only useful when there's something to reset.
|
||||
void openActionMenu() {
|
||||
_action_menu.begin("Trail actions", 3);
|
||||
if (!_store->empty()) _action_menu.addItem("Reset trail");
|
||||
// Phase 4 will add: Save trail, Load trail, Export GPX.
|
||||
if (_action_menu._count == 0) {
|
||||
_action_menu.active = false;
|
||||
_task->showAlert("No actions yet", 800);
|
||||
}
|
||||
}
|
||||
|
||||
// True when the global SensorManager has a usable GPS fix.
|
||||
static bool gpsHasFix() {
|
||||
#if ENV_INCLUDE_GPS == 1
|
||||
@@ -267,7 +308,7 @@ private:
|
||||
const int y0 = display.listStart();
|
||||
const int step = display.lineStep();
|
||||
const int hint_h = step;
|
||||
const int avail = display.height() - y0 - hint_h - 2;
|
||||
const int avail = display.height() - y0 - hint_h - 4;
|
||||
int visible = avail / step;
|
||||
if (visible < 1) visible = 1;
|
||||
if (visible > SUMMARY_ITEM_COUNT) visible = SUMMARY_ITEM_COUNT;
|
||||
@@ -303,7 +344,7 @@ private:
|
||||
const int top = display.listStart();
|
||||
const int step = display.lineStep();
|
||||
const int hint_h = step;
|
||||
const int avail = display.height() - top - hint_h - 2;
|
||||
const int avail = display.height() - top - hint_h - 4;
|
||||
|
||||
if (_store->empty()) {
|
||||
display.drawTextCentered(display.width() / 2, top + avail / 2, "No trail yet");
|
||||
@@ -363,7 +404,7 @@ private:
|
||||
// longitude is scaled by cos(avg_lat) so high-latitude trails don't stretch.
|
||||
void renderMap(DisplayDriver& display) {
|
||||
const int top = display.listStart();
|
||||
const int bottom = display.height() - display.lineStep() - 1;
|
||||
const int bottom = display.height() - display.lineStep() - 3;
|
||||
|
||||
if (_store->empty()) {
|
||||
display.drawTextCentered(display.width() / 2, (top + bottom) / 2, "No trail yet");
|
||||
|
||||
Reference in New Issue
Block a user