mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-08-02 10:16:11 +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:
co-authored by
Claude Sonnet 4.6
parent
e6d9b8449b
commit
c07d7fed50
@@ -11,11 +11,10 @@ class TrailScreen : public UIScreen {
|
|||||||
UITask* _task;
|
UITask* _task;
|
||||||
TrailStore* _store;
|
TrailStore* _store;
|
||||||
|
|
||||||
// V_CONFIG is the resting screen — shown whenever the trail is stopped.
|
// All four views are always cyclable with LEFT/RIGHT. Config holds the
|
||||||
// Once tracking starts, the user cycles between V_SUMMARY/V_MAP/V_LIST with
|
// settings + stored-trail summary; the other three render the live or
|
||||||
// LEFT/RIGHT. Stop returns to V_CONFIG.
|
// last-recorded trail in different ways.
|
||||||
enum View { V_CONFIG = 0, V_SUMMARY = 1, V_MAP = 2, V_LIST = 3 };
|
enum View { V_CONFIG = 0, V_SUMMARY = 1, V_MAP = 2, V_LIST = 3, V_COUNT };
|
||||||
static const uint8_t ACTIVE_VIEW_COUNT = 3; // Summary, Map, List
|
|
||||||
|
|
||||||
uint8_t _view = V_CONFIG;
|
uint8_t _view = V_CONFIG;
|
||||||
int _summary_scroll = 0; // top visible item index in Summary view
|
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 };
|
enum CfgRow { CFG_MIN_DIST = 0, CFG_UNITS = 1, CFG_ROW_COUNT };
|
||||||
uint8_t _cfg_sel = CFG_MIN_DIST;
|
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;
|
static const int SUMMARY_ITEM_COUNT = 5;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@@ -37,17 +41,13 @@ public:
|
|||||||
_summary_scroll = 0;
|
_summary_scroll = 0;
|
||||||
_list_scroll = 0;
|
_list_scroll = 0;
|
||||||
_cfg_dirty = false;
|
_cfg_dirty = false;
|
||||||
|
_action_menu.active = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int render(DisplayDriver& display) override {
|
int render(DisplayDriver& display) override {
|
||||||
display.setTextSize(1);
|
display.setTextSize(1);
|
||||||
display.setColor(DisplayDriver::LIGHT);
|
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"
|
const char* title = (_view == V_MAP) ? "TRAIL MAP"
|
||||||
: (_view == V_LIST) ? "TRAIL LIST"
|
: (_view == V_LIST) ? "TRAIL LIST"
|
||||||
: "TRAIL";
|
: "TRAIL";
|
||||||
@@ -59,28 +59,54 @@ public:
|
|||||||
else if (_view == V_LIST) renderList(display);
|
else if (_view == V_LIST) renderList(display);
|
||||||
else renderSummary(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);
|
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);
|
display.setCursor(2, hint_y);
|
||||||
|
char hint[28];
|
||||||
if (_view == V_CONFIG) {
|
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 {
|
} else {
|
||||||
char hint[28];
|
snprintf(hint, sizeof(hint), "<>%d/%d [Ent] %s",
|
||||||
snprintf(hint, sizeof(hint), "<>%d/%d [Ent] stop",
|
(int)_view + 1, (int)V_COUNT,
|
||||||
(int)(_view - V_SUMMARY + 1), (int)ACTIVE_VIEW_COUNT);
|
_store->isActive() ? "stop" : "start");
|
||||||
display.print(hint);
|
|
||||||
}
|
}
|
||||||
|
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;
|
return _store->isActive() ? 1000 : 5000;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool handleInput(char c) override {
|
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; }
|
if (_cfg_dirty) { the_mesh.savePrefs(); _cfg_dirty = false; }
|
||||||
_task->gotoToolsScreen();
|
_task->gotoToolsScreen();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (c == KEY_CONTEXT_MENU) {
|
||||||
|
openActionMenu();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (_view == V_CONFIG) {
|
if (_view == V_CONFIG) {
|
||||||
NodePrefs* p = _task->getNodePrefs();
|
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_RIGHT || c == KEY_NEXT) && p) { cycleCfg(p, +1); return true; }
|
||||||
if (c == KEY_ENTER) {
|
if (c == KEY_ENTER) {
|
||||||
if (_cfg_dirty) { the_mesh.savePrefs(); _cfg_dirty = false; }
|
if (_cfg_dirty) { the_mesh.savePrefs(); _cfg_dirty = false; }
|
||||||
_store->setActive(true);
|
bool was_active = _store->isActive();
|
||||||
_view = V_SUMMARY;
|
_store->setActive(!was_active);
|
||||||
_task->showAlert(gpsHasFix() ? "Tracking started" : "Waiting for GPS fix", 1000);
|
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 true;
|
||||||
}
|
}
|
||||||
return false;
|
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) {
|
if (c == KEY_LEFT || c == KEY_PREV) {
|
||||||
int v = (int)_view - V_SUMMARY;
|
_view = (uint8_t)((_view + V_COUNT - 1) % V_COUNT);
|
||||||
v = (v + ACTIVE_VIEW_COUNT - 1) % ACTIVE_VIEW_COUNT;
|
|
||||||
_view = (uint8_t)(V_SUMMARY + v);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (c == KEY_RIGHT || c == KEY_NEXT) {
|
if (c == KEY_RIGHT || c == KEY_NEXT) {
|
||||||
int v = (int)_view - V_SUMMARY;
|
_view = (uint8_t)((_view + 1) % V_COUNT);
|
||||||
v = (v + 1) % ACTIVE_VIEW_COUNT;
|
|
||||||
_view = (uint8_t)(V_SUMMARY + v);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (_view == V_SUMMARY && c == KEY_UP && _summary_scroll > 0) { _summary_scroll--; 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_UP && _list_scroll > 0) { _list_scroll--; return true; }
|
||||||
if (_view == V_LIST && c == KEY_DOWN) { _list_scroll++; return true; }
|
if (_view == V_LIST && c == KEY_DOWN) { _list_scroll++; return true; }
|
||||||
if (c == KEY_ENTER) {
|
if (c == KEY_ENTER) {
|
||||||
_store->setActive(false);
|
bool was_active = _store->isActive();
|
||||||
_view = V_CONFIG;
|
_store->setActive(!was_active);
|
||||||
_task->showAlert("Tracking stopped", 800);
|
_task->showAlert(was_active ? "Tracking stopped"
|
||||||
|
: (gpsHasFix() ? "Tracking started" : "Waiting for GPS fix"),
|
||||||
|
was_active ? 800 : 1000);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
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.
|
// True when the global SensorManager has a usable GPS fix.
|
||||||
static bool gpsHasFix() {
|
static bool gpsHasFix() {
|
||||||
#if ENV_INCLUDE_GPS == 1
|
#if ENV_INCLUDE_GPS == 1
|
||||||
@@ -267,7 +308,7 @@ private:
|
|||||||
const int y0 = display.listStart();
|
const int y0 = display.listStart();
|
||||||
const int step = display.lineStep();
|
const int step = display.lineStep();
|
||||||
const int hint_h = step;
|
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;
|
int visible = avail / step;
|
||||||
if (visible < 1) visible = 1;
|
if (visible < 1) visible = 1;
|
||||||
if (visible > SUMMARY_ITEM_COUNT) visible = SUMMARY_ITEM_COUNT;
|
if (visible > SUMMARY_ITEM_COUNT) visible = SUMMARY_ITEM_COUNT;
|
||||||
@@ -303,7 +344,7 @@ private:
|
|||||||
const int top = display.listStart();
|
const int top = display.listStart();
|
||||||
const int step = display.lineStep();
|
const int step = display.lineStep();
|
||||||
const int hint_h = step;
|
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()) {
|
if (_store->empty()) {
|
||||||
display.drawTextCentered(display.width() / 2, top + avail / 2, "No trail yet");
|
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.
|
// longitude is scaled by cos(avg_lat) so high-latitude trails don't stretch.
|
||||||
void renderMap(DisplayDriver& display) {
|
void renderMap(DisplayDriver& display) {
|
||||||
const int top = display.listStart();
|
const int top = display.listStart();
|
||||||
const int bottom = display.height() - display.lineStep() - 1;
|
const int bottom = display.height() - display.lineStep() - 3;
|
||||||
|
|
||||||
if (_store->empty()) {
|
if (_store->empty()) {
|
||||||
display.drawTextCentered(display.width() / 2, (top + bottom) / 2, "No trail yet");
|
display.drawTextCentered(display.width() / 2, (top + bottom) / 2, "No trail yet");
|
||||||
|
|||||||
Reference in New Issue
Block a user