mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-01 10:21:59 +00:00
feat(l1): one list row for settings and tool screens: values flush right, switches for on/off
InfoKit gains valueRow (value at the right edge, cut and scrolling when selected), switchRow and listRow (exact ON/OFF becomes a switch). Settings, Live Share, Locator, Repeater, Bot, Auto-Advert, alarm, GPIO and popup value rows use them; units after a space, "Off" in title case, Trail summary as blocks with a big distance and a points meter, "+ Send" as a soft pill. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -46,18 +46,20 @@ static inline const char* bearingCardinal(int deg) {
|
||||
}
|
||||
|
||||
// Human distance string. Metric: "850m" / "2.3km" / "140km". Imperial:
|
||||
// "850ft" / "2.3mi" / "140mi" (feet below ~1000 ft, then miles).
|
||||
static inline void fmtDist(char* buf, int n, float km, bool imperial) {
|
||||
// "850ft" / "2.3mi" / "140mi" (feet below ~1000 ft, then miles). `spaced`
|
||||
// puts a space before the unit ("850 m"), for value rows with room for it.
|
||||
static inline void fmtDist(char* buf, int n, float km, bool imperial, bool spaced = false) {
|
||||
const char* sp = spaced ? " " : "";
|
||||
if (imperial) {
|
||||
float ft = km * 3280.84f;
|
||||
if (ft < 1000.0f) { snprintf(buf, n, "%dft", (int)(ft + 0.5f)); return; }
|
||||
if (ft < 1000.0f) { snprintf(buf, n, "%d%sft", (int)(ft + 0.5f), sp); return; }
|
||||
float mi = km * 0.621371f;
|
||||
if (mi < 100.0f) snprintf(buf, n, "%.1fmi", mi);
|
||||
else snprintf(buf, n, "%dmi", (int)(mi + 0.5f));
|
||||
if (mi < 100.0f) snprintf(buf, n, "%.1f%smi", mi, sp);
|
||||
else snprintf(buf, n, "%d%smi", (int)(mi + 0.5f), sp);
|
||||
} else {
|
||||
if (km < 1.0f) snprintf(buf, n, "%dm", (int)(km * 1000 + 0.5f));
|
||||
else if (km < 100.0f) snprintf(buf, n, "%.1fkm", km);
|
||||
else snprintf(buf, n, "%dkm", (int)(km + 0.5f));
|
||||
if (km < 1.0f) snprintf(buf, n, "%d%sm", (int)(km * 1000 + 0.5f), sp);
|
||||
else if (km < 100.0f) snprintf(buf, n, "%.1f%skm", km, sp);
|
||||
else snprintf(buf, n, "%d%skm", (int)(km + 0.5f), sp);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -310,7 +310,7 @@ struct NodePrefs { // persisted to file
|
||||
}
|
||||
}
|
||||
static const char* alarmRepeatLabel(uint8_t idx) {
|
||||
static const char* L[ALARM_REPEAT_COUNT] = { "OFF", "Daily", "Weekdays", "Weekends" };
|
||||
static const char* L[ALARM_REPEAT_COUNT] = { "Once", "Daily", "Weekdays", "Weekends" }; // 0: no repeat (as the L2 shows it)
|
||||
return L[idx < ALARM_REPEAT_COUNT ? idx : 0];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#pragma once
|
||||
#include "InfoKit.h"
|
||||
// Configures periodic automatic 0-hop advert with GPS position.
|
||||
// Included by UITask.cpp after DashboardConfigScreen.h.
|
||||
|
||||
@@ -28,14 +29,10 @@ public:
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawCenteredHeader("AUTO-ADVERT");
|
||||
const int valx = display.width() / 2 + 6;
|
||||
int sel = 0, scroll = 0;
|
||||
drawList(display, 1, sel, scroll, [&](int, int y, bool is_sel, int reserve) {
|
||||
drawRowSelection(display, y, is_sel, reserve);
|
||||
display.setCursor(4, y);
|
||||
display.print("Interval");
|
||||
display.setCursor(valx, y);
|
||||
display.print(OPT_LABELS[currentIdx()]);
|
||||
info::valueRow(display, y, "Interval", OPT_LABELS[currentIdx()], is_sel, reserve);
|
||||
});
|
||||
return 5000;
|
||||
}
|
||||
@@ -60,4 +57,4 @@ public:
|
||||
};
|
||||
|
||||
const uint32_t AutoAdvertScreen::OPTS[AutoAdvertScreen::OPT_COUNT] = { 0, 30, 60, 120, 300, 600, 1800, 3600 };
|
||||
const char* AutoAdvertScreen::OPT_LABELS[AutoAdvertScreen::OPT_COUNT] = { "OFF", "30s", "1min", "2min", "5min", "10min", "30min", "1h" };
|
||||
const char* AutoAdvertScreen::OPT_LABELS[AutoAdvertScreen::OPT_COUNT] = { "Off", "30 s", "1 min", "2 min", "5 min", "10 min", "30 min", "1 h" };
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Custom screen — not part of upstream UITask.cpp
|
||||
// Included by UITask.cpp after KeyboardWidget.h is defined.
|
||||
|
||||
#include "InfoKit.h"
|
||||
#include "TabBar.h"
|
||||
#include "../ui-core/BotConfig.h"
|
||||
|
||||
@@ -89,8 +90,6 @@ public:
|
||||
return _kb->render(display);
|
||||
}
|
||||
|
||||
int val_x = display.valCol();
|
||||
|
||||
// Reply counter, right-aligned in the header — reserve its width from the
|
||||
// tab bar before drawing so a right-side tab can't run under it.
|
||||
uint16_t sent = the_mesh.botReplyCount();
|
||||
@@ -112,44 +111,33 @@ public:
|
||||
drawList(display, n, _sel, _scroll, [&](int i, int y, bool sel, int reserve) {
|
||||
Row r = tabRow(_tab, i);
|
||||
drawRowSelection(display, y, sel, reserve);
|
||||
display.setCursor(2, y);
|
||||
display.print(r.label);
|
||||
display.setCursor(val_x, y);
|
||||
|
||||
int cap;
|
||||
const char* txt = botcfg::text(_prefs, r.kind, cap);
|
||||
const uint8_t* fl = botcfg::flag(_prefs, r.kind);
|
||||
char name[32];
|
||||
const char* shown = "";
|
||||
if (fl) {
|
||||
display.print(r.kind == botcfg::DM_SCOPE ? (*fl ? "Fav" : "All") : (*fl ? "ON" : "OFF"));
|
||||
shown = r.kind == botcfg::DM_SCOPE ? (*fl ? "Fav" : "All") : (*fl ? "ON" : "OFF");
|
||||
} else if (txt) {
|
||||
int mqr = display.drawTextEllipsized(val_x, y, display.width() - val_x - 1 - reserve,
|
||||
botcfg::shownText(txt, botcfg::isTrigger(r.kind)), sel);
|
||||
if (sel && mqr > 0) mq_delay = mqr;
|
||||
shown = botcfg::shownText(txt, botcfg::isTrigger(r.kind));
|
||||
} else if (r.kind == botcfg::CHANNEL || r.kind == botcfg::ROOM) {
|
||||
bool any = r.kind == botcfg::CHANNEL ? _num_channels > 0 : _num_rooms > 0;
|
||||
bool ok = r.kind == botcfg::CHANNEL ? botcfg::channelName(_prefs, name, sizeof(name))
|
||||
: botcfg::roomName(_prefs, name, sizeof(name));
|
||||
if (!any) display.print("(none)");
|
||||
else if (!ok) display.print("?");
|
||||
else {
|
||||
int mqr = display.drawTextEllipsized(val_x, y, display.width() - val_x - 1 - reserve, name, sel);
|
||||
if (sel && mqr > 0) mq_delay = mqr;
|
||||
}
|
||||
shown = !any ? "(none)" : !ok ? "?" : name;
|
||||
} else if (botcfg::isHour(r.kind)) {
|
||||
char hb[10];
|
||||
if (botcfg::quietOff(_prefs)) strcpy(hb, "OFF");
|
||||
if (botcfg::quietOff(_prefs)) strcpy(hb, "Off");
|
||||
else snprintf(hb, sizeof(hb), "%02d:00", botcfg::hour(_prefs, r.kind));
|
||||
// Bracket the value while the stepper sub-mode is open on this row,
|
||||
// as a visual cue that UP/DOWN now steps it instead of moving rows.
|
||||
if (_stepper_row == i) {
|
||||
char bracketed[14];
|
||||
snprintf(bracketed, sizeof(bracketed), "[%s]", hb);
|
||||
display.print(bracketed);
|
||||
} else {
|
||||
display.print(hb);
|
||||
}
|
||||
if (_stepper_row == i) snprintf(name, sizeof(name), "[%s]", hb);
|
||||
else snprintf(name, sizeof(name), "%s", hb);
|
||||
shown = name;
|
||||
}
|
||||
int mqr = info::listRow(display, y, r.label, shown, sel, reserve);
|
||||
if (mqr > 0) mq_delay = mqr;
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
});
|
||||
return 2000;
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
// (and immediately on any key); the millis() timing underneath is unaffected.
|
||||
// Included by UITask.cpp after the other screen fragments.
|
||||
|
||||
#include "InfoKit.h"
|
||||
#include "../NodePrefs.h"
|
||||
#include "icons.h" // drawList / drawRowSelection
|
||||
|
||||
@@ -166,19 +167,18 @@ class ClockToolsScreen : public UIScreen {
|
||||
if (!_prefs) return 60000;
|
||||
const char* rows[3] = { "Time", "Repeat", "Armed" };
|
||||
if (_sel > 2) _sel = 2;
|
||||
const int valx = d.width() / 2 + 6;
|
||||
int mq_delay = 0;
|
||||
drawList(d, 3, _sel, _scroll, [&](int i, int y, bool sel, int reserve) {
|
||||
drawRowSelection(d, y, sel, reserve);
|
||||
d.setCursor(4, y); d.print(rows[i]);
|
||||
if (i == 0) {
|
||||
drawAlarmTime(d, y, valx, sel && _alarm_editing);
|
||||
if (i == 0) { // the time, flush right like every value (digits edited in place)
|
||||
d.setCursor(2, y); d.print(rows[i]);
|
||||
drawAlarmTime(d, y, d.width() - reserve - 2 - d.getTextWidth("00:00"), sel && _alarm_editing);
|
||||
} else if (i == 1) {
|
||||
int mqr = d.drawTextEllipsized(valx, y, d.width() - valx - reserve,
|
||||
NodePrefs::alarmRepeatLabel(NodePrefs::alarmRepeatIdxForMask(_prefs->alarm_repeat_mask)), sel);
|
||||
if (sel && mqr > 0) mq_delay = mqr;
|
||||
int mqr = info::valueRow(d, y, rows[i],
|
||||
NodePrefs::alarmRepeatLabel(NodePrefs::alarmRepeatIdxForMask(_prefs->alarm_repeat_mask)), sel, reserve);
|
||||
if (mqr > 0) mq_delay = mqr;
|
||||
} else {
|
||||
d.drawTextEllipsized(valx, y, d.width() - valx - reserve, _prefs->alarm_on ? "ON" : "OFF");
|
||||
info::switchRow(d, y, rows[i], _prefs->alarm_on, sel, reserve);
|
||||
}
|
||||
});
|
||||
if (_alarm_editing) return 50;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
// Included by UITask.cpp.
|
||||
#if defined(PIN_GPIO1)
|
||||
|
||||
#include "InfoKit.h"
|
||||
#include <helpers/ui/DisplayDriver.h>
|
||||
#include <helpers/ui/UIScreen.h>
|
||||
#include "icons.h"
|
||||
@@ -65,11 +66,11 @@ class GpioScreen : public UIScreen {
|
||||
case 3: strncpy(buf, "Output", n); break;
|
||||
case 4: {
|
||||
int mv = 0;
|
||||
if (_task->botGetGPIOAnalog(pin + 1, mv)) snprintf(buf, n, "%dmV", mv);
|
||||
if (_task->botGetGPIOAnalog(pin + 1, mv)) snprintf(buf, n, "%d mV", mv);
|
||||
else strncpy(buf, "Analog", n);
|
||||
break;
|
||||
}
|
||||
default: strncpy(buf, "OFF", n); break;
|
||||
default: strncpy(buf, "Off", n); break;
|
||||
}
|
||||
buf[n - 1] = '\0';
|
||||
}
|
||||
@@ -116,20 +117,17 @@ public:
|
||||
drawList(display, _item_count, _sel, _scroll, [&](int row, int y, bool sel, int reserve) {
|
||||
const Row& it = _items[row];
|
||||
drawRowSelection(display, y, sel, reserve);
|
||||
display.setCursor(2, y);
|
||||
char val[16];
|
||||
if (it.kind == ROW_MODE) {
|
||||
char lbl[8];
|
||||
snprintf(lbl, sizeof(lbl), "GPIO%d", it.pin + 1);
|
||||
display.print(lbl);
|
||||
modeRowValue(it.pin, val, sizeof(val));
|
||||
uint8_t m = gpioModeOf(it.pin);
|
||||
if (m == 1 || m == 4) any_live = true; // Input / Analog need a live-refreshed reading
|
||||
} else {
|
||||
display.print(" State"); // indented: reads as GPIOn's sub-row
|
||||
strncpy(val, gpioModeOf(it.pin) == 3 ? "ON" : "OFF", sizeof(val));
|
||||
info::valueRow(display, y, lbl, val, sel, reserve);
|
||||
} else { // indented: reads as GPIOn's sub-row
|
||||
info::switchRow(display, y, " State", gpioModeOf(it.pin) == 3, sel, reserve);
|
||||
}
|
||||
display.drawTextRightAlign(display.width() - reserve - 2, y, val);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
});
|
||||
return any_live ? 500 : 2000; // faster refresh only while a row needs a live reading
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
// fixed 128x64, so a larger-font e-ink layout gets them scaled for free.
|
||||
|
||||
#include <helpers/ui/DisplayDriver.h>
|
||||
#include <string.h>
|
||||
#include "icons.h"
|
||||
|
||||
namespace info {
|
||||
@@ -124,19 +125,58 @@ inline int bigH(DisplayDriver& d) { return 2 * d.getLineHeight(); }
|
||||
|
||||
// An on/off switch, right edge at x_right, centred on a text line at y: a
|
||||
// filled pill with the knob right when on, an outline with the knob left off.
|
||||
// `inv` draws it in a selected row's colours (dark on the light bar) and
|
||||
// leaves the ink dark, as the row found it.
|
||||
inline int switchW(DisplayDriver& d) { return 13 * miniIconScale(d); }
|
||||
inline void toggle(DisplayDriver& d, int x_right, int y, bool on) {
|
||||
inline void toggle(DisplayDriver& d, int x_right, int y, bool on, bool inv = false) {
|
||||
const DisplayDriver::Color ink = inv ? DisplayDriver::DARK : DisplayDriver::LIGHT;
|
||||
const DisplayDriver::Color paper = inv ? DisplayDriver::LIGHT : DisplayDriver::DARK;
|
||||
const int s = miniIconScale(d), w = switchW(d), h = 7 * s;
|
||||
const int x = x_right - w, ty = y + (d.getLineHeight() - h) / 2;
|
||||
d.setColor(ink);
|
||||
if (on) {
|
||||
d.fillSoftRect(x, ty, w, h);
|
||||
d.setColor(DisplayDriver::DARK);
|
||||
d.setColor(paper);
|
||||
d.fillRect(x + w - 5 * s, ty + s, 3 * s, h - 2 * s);
|
||||
d.setColor(DisplayDriver::LIGHT);
|
||||
} else {
|
||||
d.drawSoftRect(x, ty, w, h);
|
||||
d.fillRect(x + 2 * s, ty + 2 * s, 3 * s, h - 4 * s);
|
||||
}
|
||||
d.setColor(ink);
|
||||
}
|
||||
|
||||
// ── List rows (call after drawRowSelection(); text is already in the row's ink)
|
||||
// A setting or field: label at x0, value flush right. A value too long for the
|
||||
// room beside the label is cut, and scrolls while the row is selected.
|
||||
// Returns that marquee's next-step delay (0: nothing moving).
|
||||
inline int valueRow(DisplayDriver& d, int y, const char* label, const char* value, bool sel,
|
||||
int reserve, int x0 = 2) {
|
||||
d.setCursor(x0, y);
|
||||
d.print(label);
|
||||
if (!value || !*value) return 0;
|
||||
const int right = d.width() - reserve - 2;
|
||||
const int lx = x0 + (*label ? d.getTextWidth(label) + d.getCharWidth() : 0);
|
||||
if (lx + (int)d.getTextWidth(value) <= right) { d.drawTextRightAlign(right, y, value); return 0; }
|
||||
const int r = d.drawTextEllipsized(lx, y, right - lx, value, sel);
|
||||
return sel && r > 0 ? r : 0;
|
||||
}
|
||||
// A row from a screen's value formatter: exactly "ON" / "OFF" is a yes/no
|
||||
// setting and gets the switch; anything else (an option that can be "Off"
|
||||
// among numbers included) is a value. Returns valueRow()'s marquee delay.
|
||||
inline void switchRow(DisplayDriver& d, int y, const char* label, bool on, bool sel, int reserve, int x0 = 2);
|
||||
inline int listRow(DisplayDriver& d, int y, const char* label, const char* value, bool sel,
|
||||
int reserve, int x0 = 2) {
|
||||
if (value && (!strcmp(value, "ON") || !strcmp(value, "OFF"))) {
|
||||
switchRow(d, y, label, value[1] == 'N', sel, reserve, x0);
|
||||
return 0;
|
||||
}
|
||||
return valueRow(d, y, label, value, sel, reserve, x0);
|
||||
}
|
||||
// An on/off row: the label, then a switch at the right edge.
|
||||
inline void switchRow(DisplayDriver& d, int y, const char* label, bool on, bool sel, int reserve, int x0) {
|
||||
d.setCursor(x0, y);
|
||||
d.print(label);
|
||||
toggle(d, d.width() - reserve - 2, y, on, sel);
|
||||
}
|
||||
|
||||
// The last N samples of one reading, oldest first, for a history line.
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
// one-shot "Share my pos" lives on the Map screen.
|
||||
// Included by UITask.cpp after AutoAdvertScreen.h.
|
||||
|
||||
#include "InfoKit.h"
|
||||
#include "../NodePrefs.h"
|
||||
#include "icons.h" // drawList (shared scrolling-list helper)
|
||||
|
||||
@@ -65,7 +66,7 @@ public:
|
||||
snprintf(buf, n, "%s", (_prefs && _prefs->loc_share_enabled) ? "ON" : "OFF");
|
||||
break;
|
||||
case K_DURATION:
|
||||
snprintf(buf, n, "%uh", (unsigned)(NodePrefs::locShareDurationMins(_prefs ? _prefs->loc_share_duration_idx : 0) / 60));
|
||||
snprintf(buf, n, "%u h", (unsigned)(NodePrefs::locShareDurationMins(_prefs ? _prefs->loc_share_duration_idx : 0) / 60));
|
||||
break;
|
||||
case K_TARGET:
|
||||
currentTargetName(buf, n);
|
||||
@@ -79,18 +80,18 @@ public:
|
||||
break;
|
||||
}
|
||||
case K_MOVE:
|
||||
snprintf(buf, n, "%um", (unsigned)NodePrefs::locShareMoveMeters(_prefs ? _prefs->loc_share_move_idx : 1));
|
||||
snprintf(buf, n, "%u m", (unsigned)NodePrefs::locShareMoveMeters(_prefs ? _prefs->loc_share_move_idx : 1));
|
||||
break;
|
||||
case K_GAP: {
|
||||
uint16_t s = NodePrefs::locShareIntervalSecs(_prefs ? _prefs->loc_share_interval_idx : 1);
|
||||
if (s < 60) snprintf(buf, n, "%us", (unsigned)s);
|
||||
else snprintf(buf, n, "%um", (unsigned)(s / 60));
|
||||
if (s < 60) snprintf(buf, n, "%u s", (unsigned)s);
|
||||
else snprintf(buf, n, "%u min", (unsigned)(s / 60));
|
||||
break;
|
||||
}
|
||||
case K_HB: {
|
||||
uint16_t s = NodePrefs::locShareHeartbeatSecs(_prefs ? _prefs->loc_share_heartbeat_idx : 0);
|
||||
if (s == 0) snprintf(buf, n, "OFF");
|
||||
else snprintf(buf, n, "%um", (unsigned)(s / 60));
|
||||
if (s == 0) snprintf(buf, n, "Off");
|
||||
else snprintf(buf, n, "%u min", (unsigned)(s / 60));
|
||||
break;
|
||||
}
|
||||
default: buf[0] = '\0';
|
||||
@@ -102,19 +103,14 @@ public:
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawCenteredHeader("LIVE SHARE");
|
||||
|
||||
const int valx = display.width() / 2 + 6;
|
||||
int mq_delay = 0;
|
||||
drawList(display, ROW_COUNT, _sel, _scroll, [&](int i, int y, bool sel, int reserve) {
|
||||
Row r = rows(i);
|
||||
drawRowSelection(display, y, sel, reserve);
|
||||
display.setCursor(4, y);
|
||||
display.print(r.label);
|
||||
char val[24];
|
||||
valueLabel(r.kind, val, sizeof(val));
|
||||
if (val[0]) {
|
||||
int mqr = display.drawTextEllipsized(valx, y, display.width() - valx - reserve, val, sel);
|
||||
if (sel && mqr > 0) mq_delay = mqr;
|
||||
}
|
||||
int mqr = info::listRow(display, y, r.label, val, sel, reserve);
|
||||
if (mqr > 0) mq_delay = mqr;
|
||||
});
|
||||
return (mq_delay > 0 && mq_delay < 500) ? mq_delay : 500;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
// (UITask::setTargetNow()), bypassing this picker entirely.
|
||||
// Included by UITask.cpp after LiveShareScreen.h.
|
||||
|
||||
#include "InfoKit.h"
|
||||
#include "../NodePrefs.h"
|
||||
#include "../Waypoint.h"
|
||||
#include "../LiveTrack.h"
|
||||
@@ -92,7 +93,7 @@ public:
|
||||
break;
|
||||
case K_RADIUS: {
|
||||
uint16_t r = NodePrefs::locatorRadiusMeters(_prefs ? _prefs->locator_radius_idx : 1);
|
||||
geo::fmtDist(buf, n, r / 1000.0f, _task && _task->useImperial());
|
||||
geo::fmtDist(buf, n, r / 1000.0f, _task && _task->useImperial(), true);
|
||||
break;
|
||||
}
|
||||
case K_MODE:
|
||||
@@ -113,19 +114,14 @@ public:
|
||||
|
||||
const int rc = visibleRows();
|
||||
if (_sel >= rc) _sel = rc - 1; // beeper row may have just been hidden
|
||||
const int valx = display.width() / 2 + 6;
|
||||
int mq_delay = 0;
|
||||
drawList(display, rc, _sel, _scroll, [&](int i, int y, bool sel, int reserve) {
|
||||
Row r = rows(i);
|
||||
drawRowSelection(display, y, sel, reserve);
|
||||
display.setCursor(4, y);
|
||||
display.print(r.label);
|
||||
char val[24];
|
||||
valueLabel(r.kind, val, sizeof(val));
|
||||
if (val[0]) {
|
||||
int mqr = display.drawTextEllipsized(valx, y, display.width() - valx - reserve, val, sel);
|
||||
if (sel && mqr > 0) mq_delay = mqr;
|
||||
}
|
||||
int mqr = info::listRow(display, y, r.label, val, sel, reserve);
|
||||
if (mqr > 0) mq_delay = mqr;
|
||||
});
|
||||
return (mq_delay > 0 && mq_delay < 500) ? mq_delay : 500;
|
||||
}
|
||||
|
||||
@@ -554,25 +554,25 @@ class MessagesScreen : public UIScreen {
|
||||
return { outgoing ? (full_avail - w) : 0, w };
|
||||
}
|
||||
|
||||
// Bottom-right "[+ send]" compose button, shared by both history lists:
|
||||
// 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
|
||||
// sits under your own messages rather than the other side's. Box is exactly
|
||||
// lh tall (no added border padding) — the text has no descenders ("[+
|
||||
// send]"), so padding sized against the font's full ascent+descent cell
|
||||
// lh tall (no added border padding) — the text has no descenders ("+
|
||||
// Send"), so padding sized against the font's full ascent+descent cell
|
||||
// read as a lopsided 1px-top/3px-bottom gap instead of an even border;
|
||||
// tight framing avoids that and gives the history list back the 2px. Always
|
||||
// reset to LIGHT first so it stays visible regardless of the ink the
|
||||
// message loop left.
|
||||
static void drawComposeButton(DisplayDriver& d, int cby, int lh, bool sel) {
|
||||
const char* ctxt = "[+ send]";
|
||||
const char* ctxt = "+ Send";
|
||||
int ctw = d.getTextWidth(ctxt);
|
||||
int bw = ctw + 4;
|
||||
int bw = ctw + 6;
|
||||
int bx = d.width() - bw;
|
||||
d.setColor(DisplayDriver::LIGHT);
|
||||
if (sel) { d.fillSoftRect(bx, cby, bw, lh); d.setColor(DisplayDriver::DARK); }
|
||||
else d.drawSoftRect(bx, cby, bw, lh);
|
||||
d.setCursor(bx + 2, cby);
|
||||
d.setCursor(bx + 3, cby);
|
||||
d.print(ctxt);
|
||||
d.setColor(DisplayDriver::LIGHT);
|
||||
}
|
||||
@@ -2535,5 +2535,5 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
const char* const MessagesScreen::NOTIF_LABELS[3] = { "Default", "OFF", "ON" };
|
||||
const char* const MessagesScreen::NOTIF_LABELS[3] = { "Default", "Off", "On" }; // three states: text, not a switch
|
||||
const char* const MessagesScreen::MELODY_LABELS[3] = { "Global", "M1", "M2" };
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#pragma once
|
||||
#include "InfoKit.h"
|
||||
#include <helpers/ui/DisplayDriver.h>
|
||||
#include <Arduino.h>
|
||||
#include "icons.h" // scalable scroll indicator (track + thumb), matches the rest of the UI
|
||||
@@ -88,6 +89,21 @@ struct PopupMenu {
|
||||
setSelected(1);
|
||||
}
|
||||
|
||||
// A value row reads "Label: value": split it so the value can stand at the
|
||||
// right edge (a switch for ON / OFF), like a settings row.
|
||||
bool splitValue(int i, char* lab, int n, const char*& val) const {
|
||||
if (!(_value_mask & (1u << i))) return false;
|
||||
const char* c = strstr(_items[i], ": ");
|
||||
if (!c || c - _items[i] >= n) return false;
|
||||
memcpy(lab, _items[i], c - _items[i]);
|
||||
lab[c - _items[i]] = '\0';
|
||||
val = c + 2;
|
||||
return true;
|
||||
}
|
||||
static int valueWidth(DisplayDriver& d, const char* v) {
|
||||
return (!strcmp(v, "ON") || !strcmp(v, "OFF")) ? info::switchW(d) : d.getTextWidth(v);
|
||||
}
|
||||
|
||||
int render(DisplayDriver& display) {
|
||||
// Everything is derived from the live font metrics so the box fits its
|
||||
// content on every display — including landscape e-ink, where the font (and
|
||||
@@ -130,7 +146,10 @@ struct PopupMenu {
|
||||
// gutter; clamped to the screen with a sane minimum.
|
||||
int content_w = _title ? display.getTextWidth(_title) : 0;
|
||||
for (int i = 0; i < _count; i++) {
|
||||
int w = display.getTextWidth(_items[i]);
|
||||
char lab[32]; const char* val;
|
||||
int w = splitValue(i, lab, sizeof(lab), val)
|
||||
? display.getTextWidth(lab) + 2 * cw + valueWidth(display, val)
|
||||
: display.getTextWidth(_items[i]);
|
||||
if (w > content_w) content_w = w;
|
||||
}
|
||||
int box_w = _has_checkboxes ? (checkboxWidth(display) + pad) : 0;
|
||||
@@ -171,7 +190,11 @@ struct PopupMenu {
|
||||
}
|
||||
// Return value not needed here: this popup already redraws every 50ms
|
||||
// (below), faster than any marquee step, so the animation is already smooth.
|
||||
display.drawTextEllipsized(bx + pad, py, text_w, _items[idx], idx == _sel);
|
||||
char lab[32]; const char* val;
|
||||
if (splitValue(idx, lab, sizeof(lab), val)) // "Label: value" -> label left, value / switch right
|
||||
info::listRow(display, py, lab, val, idx == _sel, display.width() - (bx + pad + text_w) - 2, bx + pad);
|
||||
else
|
||||
display.drawTextEllipsized(bx + pad, py, text_w, _items[idx], idx == _sel);
|
||||
if (_has_checkboxes) drawCheckbox(display, bx + bw - arrow_w - pad - checkboxWidth(display), py, isChecked(idx));
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
// can't land outside what's legal for the operator's region).
|
||||
// Included by UITask.cpp.
|
||||
|
||||
#include "InfoKit.h"
|
||||
#include <helpers/ui/DisplayDriver.h>
|
||||
#include <helpers/ui/UIScreen.h>
|
||||
#include "icons.h"
|
||||
@@ -113,12 +114,12 @@ class RepeaterScreen : public UIScreen {
|
||||
case IT_RPRESET: strncpy(buf, _picker.currentName(p, rptTarget(p)), n); break;
|
||||
case IT_RFREQ: snprintf(buf, n, "%.3f", p->repeater_freq); break;
|
||||
case IT_RSF: snprintf(buf, n, "%d", (int)p->repeater_sf); break;
|
||||
case IT_RBW: snprintf(buf, n, "%.1f", p->repeater_bw); break;
|
||||
case IT_RBW: snprintf(buf, n, "%.1f kHz", p->repeater_bw); break;
|
||||
case IT_RCR: snprintf(buf, n, "%d", (int)p->repeater_cr); break;
|
||||
case IT_SKIP: strncpy(buf, p->repeat_skip_adverts ? "ON" : "OFF", n); break;
|
||||
case IT_HOPS: rptctl::fmtHops(buf, n, p->repeat_max_hops, "OFF"); break;
|
||||
case IT_YIELD: rptctl::fmtYield(buf, n, p->repeat_delay_boost, "OFF"); break;
|
||||
case IT_SNR: rptctl::fmtSnr(buf, n, p->repeat_min_snr, "OFF"); break;
|
||||
case IT_HOPS: rptctl::fmtHops(buf, n, p->repeat_max_hops); break; // "Off": a value, not a switch
|
||||
case IT_YIELD: rptctl::fmtYield(buf, n, p->repeat_delay_boost); break;
|
||||
case IT_SNR: rptctl::fmtSnr(buf, n, p->repeat_min_snr); break;
|
||||
case IT_SUPPRESS: strncpy(buf, p->repeat_suppress_dup ? "ON" : "OFF", n); break;
|
||||
case IT_SCOPE: strncpy(buf, p->repeat_scope_only ? "ON" : "OFF", n); break;
|
||||
case IT_SCOPE_EXTRA: {
|
||||
@@ -151,18 +152,18 @@ public:
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawCenteredHeader("REPEATER");
|
||||
|
||||
// Config only — live forwarding stats live on Tools › Diagnostics.
|
||||
// Config only — live forwarding stats live on Status › Mesh.
|
||||
drawList(display, _item_count, _sel, _scroll, [&](int row, int y, bool sel, int reserve) {
|
||||
int item = _items[row];
|
||||
drawRowSelection(display, y, sel, reserve);
|
||||
display.setCursor(2, y);
|
||||
display.print(itemLabel(item));
|
||||
if (item == IT_RFREQ && sel && _editor.active()) {
|
||||
_editor.render(display, display.valCol(), y);
|
||||
if (item == IT_RFREQ && sel && _editor.active()) { // the digit editor where the value stands
|
||||
display.setCursor(2, y);
|
||||
display.print(itemLabel(item));
|
||||
_editor.render(display, display.width() - reserve - 2 - display.getTextWidth("000.000"), y);
|
||||
} else {
|
||||
char val[16];
|
||||
char val[24];
|
||||
itemValue(item, p, val, sizeof(val));
|
||||
display.drawTextRightAlign(display.width() - reserve - 2, y, val);
|
||||
info::listRow(display, y, itemLabel(item), val, sel, reserve);
|
||||
}
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "RadioPresetPicker.h"
|
||||
#include "AccordionList.h"
|
||||
#include "PopupMenu.h" // scope list management's per-row action menu
|
||||
#include "InfoKit.h" // value / switch rows
|
||||
|
||||
class SettingsScreen : public UIScreen {
|
||||
UITask* _task;
|
||||
@@ -107,13 +108,16 @@ class SettingsScreen : public UIScreen {
|
||||
return L[r & 3];
|
||||
}
|
||||
|
||||
// Level boxes ending at the row's right edge, like every other value.
|
||||
void renderBar(DisplayDriver& display, int x, int y, int value, int max_val) {
|
||||
const int gap = 2;
|
||||
const int avail = display.width() - x - _reserve;
|
||||
const int right = display.width() - _reserve - 2;
|
||||
const int avail = right - x;
|
||||
const int raw = (avail - (max_val - 1) * gap) / max_val;
|
||||
const int cap = display.getLineHeight() - 2;
|
||||
const int box_h = raw < cap ? (raw < 2 ? 2 : raw) : cap;
|
||||
const int box_w = box_h;
|
||||
x = right - (max_val * box_w + (max_val - 1) * gap);
|
||||
for (int i = 0; i < max_val; i++) {
|
||||
int bx = x + i * (box_w + gap);
|
||||
display.drawRect(bx, y, box_w, box_h);
|
||||
@@ -219,22 +223,20 @@ class SettingsScreen : public UIScreen {
|
||||
|
||||
int renderSchema(DisplayDriver& display, const settings::Setting& st, NodePrefs* p, int y, bool sel) {
|
||||
const char* label = settings::shortLabel(st);
|
||||
display.print(label);
|
||||
if (!p) return 0;
|
||||
if (!p) { display.setCursor(2, y); display.print(label); return 0; }
|
||||
if (st.offset == offsetof(NodePrefs, display_brightness) || st.offset == offsetof(NodePrefs, buzzer_volume)) {
|
||||
display.setCursor(2, y);
|
||||
display.print(label);
|
||||
renderBar(display, valCol(display), y, settings::get(*p, st) + 1, st.count);
|
||||
return 0;
|
||||
}
|
||||
char v[24];
|
||||
settings::text(*p, st, v, sizeof(v), "ON", "OFF");
|
||||
int right = display.width() - _reserve;
|
||||
int x = valCol(display), w = display.getTextWidth(v);
|
||||
if (x + w > right) {
|
||||
int min_x = 2 + display.getTextWidth(label) + display.getCharWidth();
|
||||
x = right - w > min_x ? right - w : min_x;
|
||||
if (!st.option) { // a switch
|
||||
info::switchRow(display, y, label, settings::get(*p, st) != 0, sel, _reserve);
|
||||
return 0;
|
||||
}
|
||||
int r = display.drawTextEllipsized(x, y, right - x, v, sel);
|
||||
return sel && r > 0 ? r : 0;
|
||||
char v[24];
|
||||
settings::text(*p, st, v, sizeof(v));
|
||||
return info::valueRow(display, y, label, v, sel, _reserve);
|
||||
}
|
||||
|
||||
// (Re)load the section sizes into the accordion (folds all, resets the cursor).
|
||||
@@ -445,173 +447,117 @@ class SettingsScreen : public UIScreen {
|
||||
// to keep a marquee animation going (see DisplayDriver::drawTextEllipsized).
|
||||
int renderItem(DisplayDriver& display, int item, int y, bool sel) {
|
||||
NodePrefs* p = _task->getNodePrefs();
|
||||
int mq_delay = 0;
|
||||
int mq = 0;
|
||||
char buf[24];
|
||||
auto val = [&](const char* label, const char* v) { mq = info::valueRow(display, y, label, v, sel, _reserve); };
|
||||
auto sw = [&](const char* label, bool on) { info::switchRow(display, y, label, on, sel, _reserve); };
|
||||
|
||||
drawRowSelection(display, y, sel, _reserve);
|
||||
|
||||
display.setCursor(2, y);
|
||||
|
||||
if (item >= SCHEMA_ITEM) {
|
||||
mq_delay = renderSchema(display, settings::ALL[item - SCHEMA_ITEM], p, y, sel);
|
||||
mq = renderSchema(display, settings::ALL[item - SCHEMA_ITEM], p, y, sel);
|
||||
} else if (item == BUZZER) {
|
||||
display.print("Buzzer");
|
||||
display.setCursor(valCol(display), y);
|
||||
#ifdef PIN_BUZZER
|
||||
{ static const char* labels[] = { "ON", "OFF", "Auto" };
|
||||
int m = _task->getBuzzerMode();
|
||||
display.print(labels[m < 3 ? m : 0]); }
|
||||
static const char* labels[] = { "On", "Off", "Auto" };
|
||||
int m = _task->getBuzzerMode();
|
||||
val("Buzzer", labels[m < 3 ? m : 0]);
|
||||
#else
|
||||
display.print("N/A");
|
||||
val("Buzzer", "N/A");
|
||||
#endif
|
||||
} else if (isHomePage(item)) {
|
||||
if (p) ensurePageOrderInit(p);
|
||||
int pos = homePagePosition(item, p);
|
||||
if (pos > 0) {
|
||||
char pb[5]; snprintf(pb, sizeof(pb), "%2d ", pos);
|
||||
display.print(pb);
|
||||
}
|
||||
display.print(homePageLabel(item));
|
||||
display.setCursor(display.width() - 6 * display.getCharWidth() - _reserve, y);
|
||||
if (!homePageToggleable(item))
|
||||
display.print("always");
|
||||
else
|
||||
display.print(homePageVisible(item, p) ? "ON" : "OFF");
|
||||
char label[24];
|
||||
if (pos > 0) snprintf(label, sizeof(label), "%2d %s", pos, homePageLabel(item));
|
||||
else snprintf(label, sizeof(label), "%s", homePageLabel(item));
|
||||
if (!homePageToggleable(item)) val(label, "always");
|
||||
else sw(label, homePageVisible(item, p));
|
||||
} else if (item == TX_POWER) {
|
||||
display.print("TX Pwr");
|
||||
char buf[8];
|
||||
snprintf(buf, sizeof(buf),"%ddBm", p ? p->tx_power_dbm : 0);
|
||||
display.setCursor(valCol(display), y);
|
||||
display.print(buf);
|
||||
snprintf(buf, sizeof(buf), "%d dBm", p ? p->tx_power_dbm : 0);
|
||||
val("TX Pwr", buf);
|
||||
} else if (item == RADIO_PRESET) {
|
||||
display.print("Preset");
|
||||
const char* name = p ? _picker.currentName(p, radioTarget(p)) : "Custom";
|
||||
int xc = valCol(display);
|
||||
int r = display.drawTextEllipsized(xc, y, display.width() - xc - _reserve, name, sel);
|
||||
if (sel && r > 0) mq_delay = r;
|
||||
val("Preset", p ? _picker.currentName(p, radioTarget(p)) : "Custom");
|
||||
} else if (item == CUSTOM_FREQ) {
|
||||
display.print("Freq");
|
||||
int xc = valCol(display);
|
||||
if (sel && _editor.active()) {
|
||||
_editor.render(display, xc, y);
|
||||
if (sel && _editor.active()) { // the digit editor where the value stands
|
||||
display.print("Freq");
|
||||
_editor.render(display, display.width() - _reserve - 2 - display.getTextWidth("000.000"), y);
|
||||
} else {
|
||||
char buf[10];
|
||||
snprintf(buf, sizeof(buf), "%.3f", p ? p->freq : 0.0f);
|
||||
display.setCursor(xc, y);
|
||||
display.print(buf);
|
||||
val("Freq", buf);
|
||||
}
|
||||
} else if (item == CUSTOM_SF) {
|
||||
display.print("SF");
|
||||
char buf[6];
|
||||
snprintf(buf, sizeof(buf), "%d", p ? (int)p->sf : 0);
|
||||
display.setCursor(valCol(display), y);
|
||||
display.print(buf);
|
||||
val("SF", buf);
|
||||
} else if (item == CUSTOM_BW) {
|
||||
display.print("BW");
|
||||
char buf[10];
|
||||
snprintf(buf, sizeof(buf), "%.1f", p ? p->bw : 0.0f);
|
||||
display.setCursor(valCol(display), y);
|
||||
display.print(buf);
|
||||
snprintf(buf, sizeof(buf), "%.1f kHz", p ? p->bw : 0.0f);
|
||||
val("BW", buf);
|
||||
} else if (item == CUSTOM_CR) {
|
||||
display.print("CR");
|
||||
char buf[6];
|
||||
snprintf(buf, sizeof(buf), "%d", p ? (int)p->cr : 0);
|
||||
display.setCursor(valCol(display), y);
|
||||
display.print(buf);
|
||||
val("CR", buf);
|
||||
#if FEAT_RX_POWERSAVE
|
||||
} else if (item == POWER_SAVE) {
|
||||
display.print("Pwr save");
|
||||
display.setCursor(valCol(display), y);
|
||||
// Forced off (and locked) while the repeater is on — it must hear all traffic.
|
||||
if (p && p->client_repeat) display.print("--");
|
||||
else display.print((p && p->rx_powersave) ? "ON" : "OFF");
|
||||
if (p && p->client_repeat) val("Pwr save", "--");
|
||||
else sw("Pwr save", p && p->rx_powersave);
|
||||
#endif
|
||||
} else if (item == TX_APC) {
|
||||
display.print("Auto pwr");
|
||||
display.setCursor(valCol(display), y);
|
||||
// Suppressed (and locked) while repeating — a repeater holds full TX power.
|
||||
if (p && p->client_repeat) display.print("--");
|
||||
else display.print((p && p->tx_apc) ? "ON" : "OFF");
|
||||
if (p && p->client_repeat) val("Auto pwr", "--");
|
||||
else sw("Auto pwr", p && p->tx_apc);
|
||||
} else if (item == SCOPE_NAME) {
|
||||
display.print("Scope");
|
||||
int vx = valCol(display);
|
||||
const ScopeList& sl = the_mesh.scopeList();
|
||||
int r = display.drawTextEllipsized(vx, y, display.width() - vx - _reserve,
|
||||
sl.name(sl.default_idx), sel);
|
||||
if (sel && r > 0) mq_delay = r;
|
||||
val("Scope", sl.name(sl.default_idx));
|
||||
} else if (item == LOCK_PIN) {
|
||||
display.print("Lock PIN");
|
||||
display.setCursor(valCol(display), y);
|
||||
display.print(_task->passwordLockEnabled() ? "ON" : "OFF");
|
||||
sw("Lock PIN", _task->passwordLockEnabled());
|
||||
} else if (item == DEVICE_NAME) {
|
||||
display.print("Name");
|
||||
int vx = valCol(display);
|
||||
int r = display.drawTextEllipsized(vx, y, display.width() - vx - _reserve, the_mesh.getNodeName(), sel);
|
||||
if (sel && r > 0) mq_delay = r;
|
||||
val("Name", the_mesh.getNodeName());
|
||||
} else if (item == REBOOT) {
|
||||
display.print("Reboot"); // action row: Enter reboots this device
|
||||
} else if (item == KEYBOARD_TYPE) {
|
||||
display.print("Type");
|
||||
display.setCursor(valCol(display), y);
|
||||
display.print((p && p->keyboard_type) ? "T9" : "ABC");
|
||||
val("Type", (p && p->keyboard_type) ? "T9" : "ABC");
|
||||
} else if (item == KEYBOARD_MAIN_ALPHABET) {
|
||||
display.print("Main");
|
||||
display.setCursor(valCol(display), y);
|
||||
display.print(NodePrefs::keyboardAlphabetLabel(p ? p->keyboard_main_alphabet : 0));
|
||||
val("Main", NodePrefs::keyboardAlphabetLabel(p ? p->keyboard_main_alphabet : 0));
|
||||
} else if (item == KEYBOARD_ALPHABET) {
|
||||
display.print("Additional");
|
||||
display.setCursor(valCol(display), y);
|
||||
display.print(NodePrefs::keyboardAlphabetLabel(p ? p->keyboard_alt_alphabet : 0));
|
||||
val("Additional", NodePrefs::keyboardAlphabetLabel(p ? p->keyboard_alt_alphabet : 0));
|
||||
#if defined(CARDKB_I2C)
|
||||
} else if (item == KEYBOARD_CARDKB_COMPACT) {
|
||||
display.print("Ext. KB");
|
||||
display.setCursor(valCol(display), y);
|
||||
display.print((p && p->keyboard_cardkb_compact) ? "Compact" : "Full");
|
||||
val("Ext. KB", (p && p->keyboard_cardkb_compact) ? "Compact" : "Full");
|
||||
#endif
|
||||
#if FEAT_DISPLAY_ROTATION_SETTING
|
||||
} else if (item == ROTATION) {
|
||||
display.print("Rotation");
|
||||
display.setCursor(valCol(display), y);
|
||||
display.print(rotLabel(p ? p->display_rotation : 0));
|
||||
val("Rotation", rotLabel(p ? p->display_rotation : 0));
|
||||
#endif
|
||||
#if FEAT_JOYSTICK_ROTATION_SETTING
|
||||
} else if (item == JOY_ROTATION) {
|
||||
display.print("Joystick");
|
||||
display.setCursor(valCol(display), y);
|
||||
display.print(rotLabel(p ? p->joystick_rotation : 0));
|
||||
val("Joystick", rotLabel(p ? p->joystick_rotation : 0));
|
||||
#endif
|
||||
#if FEAT_FULL_REFRESH_SETTING
|
||||
} else if (item == EINK_FULL_REFRESH) {
|
||||
display.print("Full rfsh");
|
||||
display.setCursor(valCol(display), y);
|
||||
{ uint8_t idx = p ? p->eink_full_refresh_every : 0;
|
||||
if (idx >= EINK_FULL_REFRESH_COUNT) idx = 0;
|
||||
display.print(EINK_FULL_REFRESH_LABELS[idx]); }
|
||||
uint8_t idx = p ? p->eink_full_refresh_every : 0;
|
||||
if (idx >= EINK_FULL_REFRESH_COUNT) idx = 0;
|
||||
val("Full rfsh", EINK_FULL_REFRESH_LABELS[idx]);
|
||||
#endif
|
||||
} else if (item == DM_FILTER) {
|
||||
display.print("DMs");
|
||||
display.setCursor(valCol(display), y);
|
||||
display.print((p && p->dm_show_all) ? "All" : "Fav");
|
||||
val("DMs", (p && p->dm_show_all) ? "All" : "Fav");
|
||||
} else if (item == CH_FILTER) {
|
||||
display.print("Channels");
|
||||
display.setCursor(valCol(display), y);
|
||||
display.print((p && p->ch_fav_only) ? "Fav" : "All");
|
||||
val("Channels", (p && p->ch_fav_only) ? "Fav" : "All");
|
||||
} else if (item == ROOM_FILTER) {
|
||||
display.print("Rooms");
|
||||
display.setCursor(valCol(display), y);
|
||||
display.print((p && p->room_fav_only) ? "Fav" : "All");
|
||||
val("Rooms", (p && p->room_fav_only) ? "Fav" : "All");
|
||||
} else if (item == PRUNE_NOW) {
|
||||
display.print("Prune now"); // action row: Enter counts + confirms + removes
|
||||
} else if (isMsgSlot(item)) {
|
||||
// A message template reads from the left, cut at the edge (scrolls when selected).
|
||||
int slot = msgSlotIndex(item);
|
||||
char label[5];
|
||||
snprintf(label, sizeof(label), "Q%d:", slot + 1);
|
||||
display.print(label);
|
||||
const char* tmpl = (p && p->custom_msgs[slot][0]) ? p->custom_msgs[slot] : "(empty)";
|
||||
int xm = 8 + display.getCharWidth() * 4;
|
||||
int r = display.drawTextEllipsized(xm, y, display.width() - xm - _reserve, tmpl, sel);
|
||||
if (sel && r > 0) mq_delay = r;
|
||||
int r = display.drawTextEllipsized(xm, y, display.width() - xm - _reserve - 2, tmpl, sel);
|
||||
if (sel && r > 0) mq = r;
|
||||
}
|
||||
return mq_delay;
|
||||
return mq;
|
||||
}
|
||||
|
||||
// Keyboard state for editing message slots
|
||||
@@ -656,7 +602,7 @@ class SettingsScreen : public UIScreen {
|
||||
} else {
|
||||
display.print(sl.name((uint8_t)idx));
|
||||
if ((uint8_t)idx == sl.default_idx)
|
||||
display.drawTextRightAlign(display.width() - reserve - 2, y, "[default]");
|
||||
display.drawTextRightAlign(display.width() - reserve - 2, y, "default");
|
||||
}
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
});
|
||||
@@ -1138,5 +1084,5 @@ public:
|
||||
};
|
||||
|
||||
#if FEAT_FULL_REFRESH_SETTING
|
||||
const char* SettingsScreen::EINK_FULL_REFRESH_LABELS[5] = { "OFF", "5", "10", "20", "30" };
|
||||
const char* SettingsScreen::EINK_FULL_REFRESH_LABELS[5] = { "Off", "5", "10", "20", "30" };
|
||||
#endif
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
// accidentally stops tracking.
|
||||
// Included by UITask.cpp after Trail store + ToolsScreen.
|
||||
|
||||
#include "InfoKit.h"
|
||||
#include "../Trail.h"
|
||||
#include "../GeoUtils.h"
|
||||
#include "GfxUtils.h"
|
||||
@@ -92,7 +93,6 @@ class TrailScreen : public UIScreen {
|
||||
char _act_autosave_label[24];
|
||||
char _act_toggle_label[20];
|
||||
|
||||
static const int SUMMARY_ITEM_COUNT = 5;
|
||||
|
||||
public:
|
||||
TrailScreen(UITask* task, TrailStore* store)
|
||||
@@ -505,67 +505,51 @@ private:
|
||||
snprintf(buf, n, "Pace: %u:%02u %s", pm, ps, unit);
|
||||
}
|
||||
|
||||
void summaryItem(int i, char* buf, size_t n) const {
|
||||
switch (i) {
|
||||
case 0: {
|
||||
const char* st;
|
||||
if (!_store->isActive()) st = "stopped";
|
||||
else if (_store->isPaused()) st = "paused";
|
||||
else if (_store->empty()) st = "waiting fix";
|
||||
else st = "tracking";
|
||||
snprintf(buf, n, "Status: %s", st);
|
||||
break;
|
||||
}
|
||||
case 1:
|
||||
snprintf(buf, n, "Points: %d / %d", _store->count(), TrailStore::CAPACITY);
|
||||
break;
|
||||
case 2: {
|
||||
uint32_t d = _store->totalDistanceMeters();
|
||||
char ds[12];
|
||||
geo::fmtDist(ds, sizeof(ds), d / 1000.0f, useImperial());
|
||||
snprintf(buf, n, "Dist: %s", ds);
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
uint32_t es = _store->elapsedSeconds();
|
||||
// Unit-suffixed so "1h 05m" can't be misread as "1m 05s".
|
||||
if (es < 3600) snprintf(buf, n, "Time: %lum %02lus",
|
||||
(unsigned long)(es / 60), (unsigned long)(es % 60));
|
||||
else snprintf(buf, n, "Time: %luh %02lum",
|
||||
(unsigned long)(es / 3600), (unsigned long)((es % 3600) / 60));
|
||||
break;
|
||||
}
|
||||
case 4:
|
||||
formatAvgPaceOrSpeed(buf, n, _task->getNodePrefs());
|
||||
break;
|
||||
default:
|
||||
buf[0] = '\0';
|
||||
// The summary: the distance large with the time and state beside it, the
|
||||
// points stored as a meter, then the average speed (or pace).
|
||||
void summaryBlocks(DisplayDriver& d, info::Flow& f, int rsv) {
|
||||
const int lh = d.getLineHeight(), right = d.width() - rsv - 2;
|
||||
char a[28], b[28];
|
||||
if (f.place(info::bigH(d) + 3)) {
|
||||
geo::fmtDist(a, sizeof(a), _store->totalDistanceMeters() / 1000.0f, useImperial());
|
||||
char* u = a; // "2.4km" -> "2.4" + "km"
|
||||
while (*u && (isdigit((unsigned char)*u) || *u == '.')) u++;
|
||||
snprintf(b, sizeof(b), "%s", u);
|
||||
*u = 0;
|
||||
info::big(d, 1, f.at, a, b);
|
||||
const uint32_t es = _store->elapsedSeconds();
|
||||
// Unit-suffixed so "1h 05m" can't be misread as "1m 05s".
|
||||
if (es < 3600) snprintf(a, sizeof(a), "%lum %02lus", (unsigned long)(es / 60), (unsigned long)(es % 60));
|
||||
else snprintf(a, sizeof(a), "%luh %02lum", (unsigned long)(es / 3600), (unsigned long)((es % 3600) / 60));
|
||||
d.drawTextRightAlign(right, f.at, a);
|
||||
const char* st = !_store->isActive() ? "stopped" : _store->isPaused() ? "paused"
|
||||
: _store->empty() ? "waiting fix" : "tracking";
|
||||
d.drawTextRightAlign(right, f.at + lh + 1, st);
|
||||
}
|
||||
if (f.place(d.lineStep())) {
|
||||
snprintf(a, sizeof(a), "%d", _store->count());
|
||||
info::row(d, f.at, "Points", a, rsv);
|
||||
const int x0 = 1 + d.getTextWidth("Points") + 6, x1 = right - d.getTextWidth("000") - 6;
|
||||
if (x1 - x0 > 10) info::meter(d, x0, f.at, x1 - x0, lh - 1, (float)_store->count() / TrailStore::CAPACITY);
|
||||
}
|
||||
if (f.place(d.lineStep())) {
|
||||
formatAvgPaceOrSpeed(a, sizeof(a), _task->getNodePrefs());
|
||||
char* c = strstr(a, ": "); // "Avg: 5 km/h" -> label + value
|
||||
if (c) { *c = 0; info::row(d, f.at, a, c + 2, rsv); }
|
||||
else info::row(d, f.at, a, nullptr, rsv);
|
||||
}
|
||||
}
|
||||
|
||||
void renderSummary(DisplayDriver& display) {
|
||||
const int y0 = display.listStart();
|
||||
const int step = display.lineStep();
|
||||
const int avail = display.height() - y0 - 2;
|
||||
int visible = avail / step;
|
||||
if (visible < 1) visible = 1;
|
||||
if (visible > SUMMARY_ITEM_COUNT) visible = SUMMARY_ITEM_COUNT;
|
||||
|
||||
int max_scroll = SUMMARY_ITEM_COUNT - visible;
|
||||
if (_summary_scroll > max_scroll) _summary_scroll = max_scroll;
|
||||
if (_summary_scroll < 0) _summary_scroll = 0;
|
||||
_summary_max_scroll = max_scroll;
|
||||
|
||||
for (int i = 0; i < visible; i++) {
|
||||
int idx = _summary_scroll + i;
|
||||
if (idx >= SUMMARY_ITEM_COUNT) break;
|
||||
char buf[28];
|
||||
summaryItem(idx, buf, sizeof(buf));
|
||||
display.setCursor(2, y0 + i * step);
|
||||
display.print(buf);
|
||||
}
|
||||
|
||||
drawScrollIndicator(display, y0, visible * step, SUMMARY_ITEM_COUNT, visible, _summary_scroll);
|
||||
const int top = display.listStart(), bottom = display.height();
|
||||
info::Flow m = info::Flow::measure();
|
||||
summaryBlocks(display, m, 0);
|
||||
const long view = bottom - top;
|
||||
const int rsv = m.total_px > view ? scrollIndicatorColWidth(display) : 0;
|
||||
info::Flow f(top, bottom, _summary_scroll);
|
||||
summaryBlocks(display, f, rsv);
|
||||
_summary_max_scroll = f.more ? _summary_scroll + 1 : _summary_scroll;
|
||||
if (rsv) drawScrollIndicatorPx(display, top, view, f.total_px, view, f.skipped_px);
|
||||
}
|
||||
|
||||
void renderList(DisplayDriver& display) {
|
||||
|
||||
Reference in New Issue
Block a user