mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-10-09 03:06:39 +00:00
feat(ui-lvgl): settings kit, choice picker, reorganised Settings
group() / settingRow / listRow / actionRow / choiceRow / groupNote: one card per section, hairlines between rows, hints that wrap. Dropdowns replaced by a centred picker list. Settings regrouped (Device, Connections, Map & data, System); Display & power split into Display, Power and Time on L2 only (the shared schema is unchanged), Keyboard and About get their own pages. Radio and Repeater share one LoRa parameter block. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -90,11 +90,12 @@ void UITask::buildBot() {
|
||||
lv_obj_add_event_cb(tabs, onBotTab, LV_EVENT_VALUE_CHANGED, NULL);
|
||||
|
||||
lv_obj_t* list = scrollList(body);
|
||||
lv_obj_t* g = group(list, nullptr);
|
||||
|
||||
for (int i = 0; i < botcfg::rowCount(s_tab); i++) {
|
||||
const botcfg::Row& r = botcfg::row(s_tab, i);
|
||||
uint8_t* fl = botcfg::flag(p, r.kind);
|
||||
lv_obj_t* row = settingRow(list, r.label, fl || botcfg::isHour(r.kind) ? r.hint : nullptr);
|
||||
lv_obj_t* row = settingRow(g, r.label, fl || botcfg::isHour(r.kind) ? r.hint : nullptr);
|
||||
if (fl && r.kind == botcfg::DM_SCOPE) {
|
||||
static const char* SCOPE[] = { "All", "Fav", "" };
|
||||
lv_obj_t* seg = segmented(row, SCOPE, *fl ? 1 : 0, 110, 32);
|
||||
@@ -120,11 +121,12 @@ void UITask::buildBot() {
|
||||
else snprintf(val, sizeof(val), "%02d:00", botcfg::hour(p, r.kind));
|
||||
lv_obj_t* v = label(row, val, THEME_FONT_BODY, theme::ACCENT);
|
||||
lv_label_set_long_mode(v, LV_LABEL_LONG_DOT);
|
||||
lv_obj_set_width(v, botcfg::isHour(r.kind) ? 80 : 190); // fixed: LONG_DOT needs a width
|
||||
lv_obj_set_width(v, botcfg::isHour(r.kind) ? 60 : 150); // fixed: LONG_DOT needs a width
|
||||
lv_obj_set_style_text_align(v, LV_TEXT_ALIGN_RIGHT, 0);
|
||||
lv_obj_align(v, LV_ALIGN_RIGHT_MID, -theme::PAD, 0);
|
||||
label(row, LV_SYMBOL_RIGHT, THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_obj_add_flag(row, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_set_style_bg_color(row, lv_color_hex(theme::SURFACE_2), LV_STATE_PRESSED);
|
||||
lv_obj_set_style_bg_opa(row, LV_OPA_COVER, LV_STATE_PRESSED);
|
||||
lv_obj_add_event_cb(row, onBotRow, LV_EVENT_CLICKED, (void*)(uintptr_t)i);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,11 +39,7 @@ static void clearForm() {
|
||||
static void onChanAction(lv_event_t* e) { s_ui->channelAction((uint8_t)(uintptr_t)lv_event_get_user_data(e)); }
|
||||
static void onChanDropdown(lv_event_t* e) {
|
||||
s_ui->channelSet((uint8_t)(uintptr_t)lv_event_get_user_data(e),
|
||||
(int)lv_dropdown_get_selected((lv_obj_t*)lv_event_get_target(e)));
|
||||
}
|
||||
static void onChanSegment(lv_event_t* e) { // user data: C_NOTIF / C_MELODY
|
||||
s_ui->channelSet((uint8_t)(uintptr_t)lv_event_get_user_data(e),
|
||||
(int)lv_buttonmatrix_get_selected_button((lv_obj_t*)lv_event_get_target(e)));
|
||||
choiceSelected((lv_obj_t*)lv_event_get_target(e)));
|
||||
}
|
||||
static void onChanRowHold(lv_event_t* e) {
|
||||
lv_indev_wait_release(lv_indev_active()); // the hold isn't also a tap that opens the channel
|
||||
@@ -79,22 +75,18 @@ void UITask::channelMenu(int idx) {
|
||||
snprintf(title, sizeof(title), "%s", ch.name); // as named: "#" marks a hashtag channel
|
||||
lv_obj_t* panel = navPopupPanel(title, false);
|
||||
|
||||
// Notifications: three segments in the row (a dropdown's list ran off the
|
||||
// bottom of the screen from a popup this low).
|
||||
static const char* NOTIF[] = { "Default", "Muted", "Always", "" };
|
||||
static const char* MELODY[] = { "Default", "Melody 1", "Melody 2", "" };
|
||||
lv_obj_t* row = settingRow(panel, "Alerts", NULL);
|
||||
radioview::rowSegmented(row, NOTIF, chanctl::notif(_prefs, idx), 200, onChanSegment, C_NOTIF);
|
||||
row = settingRow(panel, "Sound", NULL);
|
||||
radioview::rowSegmented(row, MELODY, chanctl::melody(_prefs, idx), 200, onChanSegment, C_MELODY);
|
||||
lv_obj_t* g = group(panel, nullptr);
|
||||
choiceRow(g, "Alerts", nullptr, "Default\nMuted\nAlways", chanctl::notif(_prefs, idx), onChanDropdown,
|
||||
(void*)(uintptr_t)C_NOTIF);
|
||||
choiceRow(g, "Sound", nullptr, "Default\nMelody 1\nMelody 2", chanctl::melody(_prefs, idx), onChanDropdown,
|
||||
(void*)(uintptr_t)C_MELODY);
|
||||
const ScopeList& sl = the_mesh.scopeList();
|
||||
if (sl.count > 0) { // only once regions are set up (Settings in the app)
|
||||
row = settingRow(panel, "Scope", "Region it's sent in");
|
||||
int o = 0;
|
||||
for (uint8_t i = 0; i <= sl.count && o < (int)sizeof(s_scope_opts) - 24; i++)
|
||||
o += snprintf(s_scope_opts + o, sizeof(s_scope_opts) - o, "%s%s", i ? "\n" : "", sl.name(i));
|
||||
lv_obj_t* dd = radioview::rowDropdown(row, s_scope_opts, chanctl::scope(_prefs, idx), 112, onChanDropdown, C_SCOPE);
|
||||
lv_obj_set_style_max_height(lv_dropdown_get_list(dd), 100, 0); // stays on screen; scrolls if long
|
||||
choiceRow(g, "Scope", "Region it's sent in", s_scope_opts, chanctl::scope(_prefs, idx), onChanDropdown,
|
||||
(void*)(uintptr_t)C_SCOPE);
|
||||
}
|
||||
|
||||
lv_obj_t* acts = lv_obj_create(panel);
|
||||
|
||||
@@ -92,7 +92,7 @@ static void onAlarmSwitch(lv_event_t* e) {
|
||||
s_ui->setAlarm(2, lv_obj_has_state((lv_obj_t*)lv_event_get_target(e), LV_STATE_CHECKED) ? 1 : 0);
|
||||
}
|
||||
static void onAlarmRepeat(lv_event_t* e) {
|
||||
s_ui->setAlarm(3, (int)lv_dropdown_get_selected((lv_obj_t*)lv_event_get_target(e)));
|
||||
s_ui->setAlarm(3, choiceSelected((lv_obj_t*)lv_event_get_target(e)));
|
||||
}
|
||||
static void onTimerRoller(lv_event_t* e) {
|
||||
int v = (int)lv_roller_get_selected((lv_obj_t*)lv_event_get_target(e));
|
||||
@@ -160,17 +160,12 @@ void UITask::buildClock() {
|
||||
if (_prefs->alarm_on) lv_obj_add_state(sw, LV_STATE_CHECKED);
|
||||
lv_obj_add_event_cb(sw, onAlarmSwitch, LV_EVENT_VALUE_CHANGED, NULL);
|
||||
label(col, "Repeat", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_obj_t* dd = lv_dropdown_create(col);
|
||||
char opts[48];
|
||||
int o = 0;
|
||||
for (uint8_t i = 0; i < NodePrefs::ALARM_REPEAT_COUNT; i++)
|
||||
o += snprintf(opts + o, sizeof(opts) - o, "%s%s", i ? "\n" : "", i ? NodePrefs::alarmRepeatLabel(i) : "Once");
|
||||
lv_dropdown_set_options(dd, opts);
|
||||
lv_dropdown_set_selected(dd, NodePrefs::alarmRepeatIdxForMask(_prefs->alarm_repeat_mask));
|
||||
lv_obj_t* dd = choiceCreate(col, opts, NodePrefs::alarmRepeatIdxForMask(_prefs->alarm_repeat_mask), "Repeat");
|
||||
lv_obj_set_width(dd, 112);
|
||||
lv_obj_t* list = lv_dropdown_get_list(dd); // opens upwards: tighter so all four fit under the status bar
|
||||
lv_obj_set_style_pad_ver(list, 4, 0);
|
||||
lv_obj_set_style_text_line_space(list, 12, 0);
|
||||
lv_obj_add_event_cb(dd, onAlarmRepeat, LV_EVENT_VALUE_CHANGED, NULL);
|
||||
bool synced = rtc_clock.getCurrentTime() > 1000000000UL;
|
||||
label(body, synced ? "Rings at this local time (Settings time zone)." : "The clock isn't set yet: the alarm waits for a time sync.",
|
||||
|
||||
@@ -52,10 +52,10 @@ static lv_obj_t* actionButton(lv_obj_t* acts, const char* text, lv_event_cb_t cb
|
||||
|
||||
static void onConvAction(lv_event_t* e) { s_ui->conversationAction((uint8_t)(uintptr_t)lv_event_get_user_data(e)); }
|
||||
static void onConvMelody(lv_event_t* e) {
|
||||
s_ui->conversationMelody((int)lv_buttonmatrix_get_selected_button((lv_obj_t*)lv_event_get_target(e)));
|
||||
s_ui->conversationMelody(choiceSelected((lv_obj_t*)lv_event_get_target(e)));
|
||||
}
|
||||
static void onConvNotif(lv_event_t* e) {
|
||||
s_ui->conversationNotif((int)lv_buttonmatrix_get_selected_button((lv_obj_t*)lv_event_get_target(e)));
|
||||
s_ui->conversationNotif(choiceSelected((lv_obj_t*)lv_event_get_target(e)));
|
||||
}
|
||||
static void onMsgAction(lv_event_t* e) { s_ui->messageAction((uint8_t)(uintptr_t)lv_event_get_user_data(e)); }
|
||||
static void onRoomLoginKb(lv_event_t* e) { s_ui->roomLoginDone(lv_event_get_code(e) == LV_EVENT_READY); }
|
||||
@@ -161,13 +161,10 @@ void UITask::conversationMenu(const uint8_t* pub_key) {
|
||||
lv_obj_t* panel = navPopupPanel(ci.name, false);
|
||||
bool room = ci.type == ADV_TYPE_ROOM;
|
||||
|
||||
if (!room) { // alerts: three segments (a dropdown's list runs off screen this low)
|
||||
static const char* NOTIF[] = { "Default", "Muted", "Always", "" };
|
||||
lv_obj_t* row = settingRow(panel, "Alerts", NULL);
|
||||
radioview::rowSegmented(row, NOTIF, contactctl::notif(_prefs, ci.id.pub_key), 200, onConvNotif, 0);
|
||||
static const char* MELODY[] = { "Default", "Melody 1", "Melody 2", "" };
|
||||
row = settingRow(panel, "Sound", NULL);
|
||||
radioview::rowSegmented(row, MELODY, contactctl::melody(_prefs, ci.id.pub_key), 200, onConvMelody, 0);
|
||||
if (!room) {
|
||||
lv_obj_t* g = group(panel, nullptr);
|
||||
choiceRow(g, "Alerts", nullptr, "Default\nMuted\nAlways", contactctl::notif(_prefs, ci.id.pub_key), onConvNotif);
|
||||
choiceRow(g, "Sound", nullptr, "Default\nMelody 1\nMelody 2", contactctl::melody(_prefs, ci.id.pub_key), onConvMelody);
|
||||
} else {
|
||||
bool in = _core->rooms.isLoggedIn(ci.id.pub_key);
|
||||
lv_obj_t* st = label(panel, in ? LV_SYMBOL_OK " Logged in: you can post"
|
||||
|
||||
@@ -407,15 +407,12 @@ void UITask::pinRowRefresh() {
|
||||
static void onAccent(lv_event_t* e) { s_ui->setAccent((int)(uintptr_t)lv_event_get_user_data(e)); }
|
||||
|
||||
// A row of colour swatches; the current one ringed.
|
||||
void UITask::accentRow(lv_obj_t* body) {
|
||||
lv_obj_t* card = lv_obj_create(body);
|
||||
styleSurface(card, theme::SURFACE);
|
||||
lv_obj_remove_flag(card, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_radius(card, theme::RADIUS, 0);
|
||||
lv_obj_set_style_pad_all(card, theme::PAD, 0);
|
||||
lv_obj_set_style_pad_row(card, 8, 0);
|
||||
void UITask::accentRow(lv_obj_t* body) { // a row of a group: the label over the swatches
|
||||
lv_obj_t* card = groupLine(body, false);
|
||||
lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_flex_align(card, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
|
||||
lv_obj_set_style_pad_row(card, 8, 0);
|
||||
lv_obj_set_style_pad_bottom(card, 10, 0);
|
||||
label(card, "Accent colour", THEME_FONT_BODY, theme::TEXT);
|
||||
lv_obj_t* row = lv_obj_create(card);
|
||||
lv_obj_remove_style_all(row);
|
||||
|
||||
@@ -878,7 +878,7 @@ static void onLiveTiles(lv_event_t* e) {
|
||||
}
|
||||
static void onNavTool(lv_event_t* e) { s_ui->navToolAction((uint8_t)(uintptr_t)lv_event_get_user_data(e)); }
|
||||
static void onNavShareTarget(lv_event_t* e) {
|
||||
s_ui->navSetShareTarget((int)lv_dropdown_get_selected((lv_obj_t*)lv_event_get_target(e)));
|
||||
s_ui->navSetShareTarget(choiceSelected((lv_obj_t*)lv_event_get_target(e)));
|
||||
}
|
||||
|
||||
static lv_obj_t* toolRow(lv_obj_t* parent) {
|
||||
@@ -950,18 +950,7 @@ void UITask::navToolsPopup() {
|
||||
o += snprintf(opts + o, sizeof(opts) - o, "%s" UI_SYMBOL_STAR " %s", o ? "\n" : "", c.name);
|
||||
favs++;
|
||||
}
|
||||
lv_obj_t* tr = lv_obj_create(list);
|
||||
styleSurface(tr, theme::SURFACE);
|
||||
lv_obj_remove_flag(tr, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_size(tr, LV_PCT(100), 40);
|
||||
lv_obj_set_style_radius(tr, theme::RADIUS, 0);
|
||||
lv_obj_align(label(tr, "Send to", THEME_FONT_BODY, theme::TEXT), LV_ALIGN_LEFT_MID, theme::PAD, 0);
|
||||
lv_obj_t* dd = lv_dropdown_create(tr);
|
||||
lv_dropdown_set_options(dd, o ? opts : "(no channels)");
|
||||
lv_dropdown_set_selected(dd, sel);
|
||||
lv_obj_set_width(dd, 170);
|
||||
lv_obj_align(dd, LV_ALIGN_RIGHT_MID, -4, 0);
|
||||
lv_obj_add_event_cb(dd, onNavShareTarget, LV_EVENT_VALUE_CHANGED, NULL);
|
||||
choiceRow(group(list, nullptr), "Send to", nullptr, o ? opts : "(no channels)", sel, onNavShareTarget);
|
||||
r = toolRow(list);
|
||||
_nav_share_btn = toolButton(r, "", navmap::TL_SHARE_TOGGLE, true);
|
||||
toolButton(r, LV_SYMBOL_UPLOAD " Send once", navmap::TL_SHARE_ONCE, false);
|
||||
@@ -974,14 +963,12 @@ void UITask::navToolsPopup() {
|
||||
else snprintf(al, sizeof(al), "Off");
|
||||
label(list, al, THEME_FONT_SMALL, theme::TEXT);
|
||||
|
||||
sectionTitle(list, "MAP");
|
||||
lv_obj_t* lsw = switchRow(list, "Live tiles", "Load missing tiles over WiFi", nullptr);
|
||||
lv_obj_t* g = group(list, "MAP");
|
||||
lv_obj_t* lsw = switchRow(g, "Live tiles", "Load missing tiles over WiFi", nullptr);
|
||||
if (lvport::liveTiles()) lv_obj_add_state(lsw, LV_STATE_CHECKED);
|
||||
lv_obj_add_event_cb(lsw, onLiveTiles, LV_EVENT_VALUE_CHANGED, NULL);
|
||||
r = toolRow(list);
|
||||
toolButton(r, LV_SYMBOL_DOWNLOAD " Download this area", navmap::TL_DOWNLOAD, false);
|
||||
r = toolRow(list);
|
||||
toolButton(r, LV_SYMBOL_SETTINGS " Options: trail, sharing, alert", navmap::TL_OPTIONS, false);
|
||||
listRow(g, "Download this area", "For use offline", onNavTool, (void*)(uintptr_t)navmap::TL_DOWNLOAD);
|
||||
listRow(g, "Map options", "Trail, live sharing, arrival alert", onNavTool, (void*)(uintptr_t)navmap::TL_OPTIONS);
|
||||
|
||||
_nav_reset_armed_ms = 0;
|
||||
refreshNavTools();
|
||||
|
||||
@@ -204,10 +204,8 @@ void UITask::buildOta() {
|
||||
_ota_btn_lbl = label(b, "", THEME_FONT_BODY, theme::TEXT);
|
||||
lv_obj_center(_ota_btn_lbl);
|
||||
stylePrimary(b);
|
||||
lv_obj_t* h = label(body, "From github.com/" OTA_REPO " releases, over WiFi. About a minute; keep the device "
|
||||
"charged. Settings, contacts and messages are kept.", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_label_set_long_mode(h, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_width(h, LV_PCT(100));
|
||||
groupNote(body, "From github.com/" OTA_REPO " releases, over WiFi. About a minute; keep the device "
|
||||
"charged. Settings, contacts and messages are kept.");
|
||||
refreshOta();
|
||||
}
|
||||
|
||||
|
||||
@@ -107,17 +107,15 @@ void UITask::showQuickMsgs() {
|
||||
void UITask::buildQuickMsgs() {
|
||||
using namespace qview;
|
||||
lv_obj_t* body = newScreen("Quick messages", true);
|
||||
lv_obj_t* t = label(body, "Sent from a chat's \"+\"; placeholders are filled in.",
|
||||
THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_label_set_long_mode(t, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_width(t, LV_PCT(100));
|
||||
lv_obj_t* g = group(body, nullptr);
|
||||
for (int i = 0; i < msgtext::QUICK_COUNT; i++) {
|
||||
const char* q = msgtext::quick(_prefs, i);
|
||||
char title[msgtext::QUICK_LEN + 8];
|
||||
snprintf(title, sizeof(title), "%d %s", i + 1, q[0] ? q : "(empty)");
|
||||
lv_obj_t* row = listRow(body, title, NULL, onQuickSlot, (void*)(uintptr_t)i);
|
||||
if (!q[0]) lv_obj_set_style_text_color(lv_obj_get_child(row, 0), lv_color_hex(theme::TEXT_MUTED), 0);
|
||||
lv_obj_t* row = listRow(g, title, NULL, onQuickSlot, (void*)(uintptr_t)i);
|
||||
if (!q[0]) lv_obj_set_style_text_color(rowTitle(row), lv_color_hex(theme::TEXT_MUTED), 0);
|
||||
}
|
||||
groupNote(body, "Sent from a chat's \"+\"; placeholders are filled in.");
|
||||
}
|
||||
|
||||
// A field and the placeholders over the keyboard (a titled popup doesn't fit
|
||||
@@ -193,9 +191,9 @@ void UITask::advertPopup() {
|
||||
lv_obj_set_flex_grow(list, 1);
|
||||
lv_obj_set_flex_flow(list, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(list, theme::GAP, 0);
|
||||
sectionTitle(list, "SEND NOW");
|
||||
listRow(list, "Zero hop", "Direct range only", onAdvertGo, (void*)(uintptr_t)0);
|
||||
listRow(list, "Flood", "Across the mesh", onAdvertGo, (void*)(uintptr_t)1);
|
||||
lv_obj_t* g = group(list, "SEND NOW");
|
||||
listRow(g, "Zero hop", "Direct range only", onAdvertGo, (void*)(uintptr_t)0);
|
||||
listRow(g, "Flood", "Across the mesh", onAdvertGo, (void*)(uintptr_t)1);
|
||||
schemaRows(list, settings::PG_ADVERT);
|
||||
}
|
||||
|
||||
|
||||
@@ -54,27 +54,25 @@ static void onScopeAction(lv_event_t* e) { s_ui->scopeAction((uint8_t)(uintptr_
|
||||
// The Radio screen's lower sections, under the parameters.
|
||||
void UITask::buildRadioExtras(lv_obj_t* body) {
|
||||
NodePrefs* p = _prefs;
|
||||
sectionTitle(body, "MY PRESETS");
|
||||
lv_obj_t* g = group(body, "MY PRESETS");
|
||||
for (int i = 0; i < NodePrefs::USER_RADIO_PRESET_MAX; i++) {
|
||||
const NodePrefs::UserRadioPreset& u = p->user_radio_presets[i];
|
||||
if (!u.name[0]) continue;
|
||||
char sub[48];
|
||||
snprintf(sub, sizeof(sub), "%.3f MHz SF%u BW%g 4/%u", u.freq, (unsigned)u.sf, (double)u.bw, (unsigned)u.cr);
|
||||
lv_obj_t* row = listRow(body, u.name, sub, onPresetRow, (void*)(uintptr_t)i);
|
||||
if (radioParamsMatchPreset(p->freq, p->bw, p->sf, p->cr, u.freq, u.bw, u.sf, u.cr))
|
||||
lv_obj_align(label(row, LV_SYMBOL_OK, THEME_FONT_BODY, theme::ACCENT), LV_ALIGN_RIGHT_MID, -theme::PAD, 0);
|
||||
lv_obj_t* row = listRow(g, u.name, sub, onPresetRow, (void*)(uintptr_t)i);
|
||||
if (radioParamsMatchPreset(p->freq, p->bw, p->sf, p->cr, u.freq, u.bw, u.sf, u.cr)) rowCheck(row);
|
||||
}
|
||||
listRow(body, LV_SYMBOL_PLUS " Save current settings",
|
||||
radioctl::userPresetsFull(p) ? "All 4 used - a new name replaces the oldest" : "As a preset of your own",
|
||||
onPresetSave, NULL);
|
||||
actionRow(g, LV_SYMBOL_PLUS " Save current settings", onPresetSave, NULL, theme::ACCENT);
|
||||
if (radioctl::userPresetsFull(p)) groupNote(body, "All 4 used - a new name replaces the oldest.");
|
||||
|
||||
sectionTitle(body, "SCOPE");
|
||||
g = group(body, "SCOPE");
|
||||
const ScopeList& sl = the_mesh.scopeList();
|
||||
char sub[48];
|
||||
if (sl.count == 0) snprintf(sub, sizeof(sub), "None set up - messages go everywhere");
|
||||
else snprintf(sub, sizeof(sub), "Default: %s (%u set up)", sl.default_idx ? sl.name(sl.default_idx) : "no scope",
|
||||
(unsigned)sl.count);
|
||||
listRow(body, LV_SYMBOL_LIST " Scopes", sub, onOpenScopes, NULL);
|
||||
listRow(g, "Scopes", sub, onOpenScopes, NULL);
|
||||
}
|
||||
|
||||
// ── My presets ────────────────────────────────────────────────────────────────
|
||||
@@ -184,14 +182,15 @@ void UITask::buildScopes() {
|
||||
lv_obj_t* body = newScreen("Scopes", true);
|
||||
const ScopeList& sl = the_mesh.scopeList();
|
||||
if (_header && sl.count < ScopeList::MAX_SCOPE_ENTRIES) headerButton(_header, LV_SYMBOL_PLUS " Add", onScopeAdd, 4, NULL);
|
||||
radiox::note(body, "A scope keeps messages within a region's repeaters. The default one is used for direct messages "
|
||||
"and relaying; each channel picks its own in its options.");
|
||||
lv_obj_t* g = group(body, nullptr);
|
||||
for (uint8_t i = 0; i <= sl.count; i++) {
|
||||
bool def = i == sl.default_idx;
|
||||
const char* sub = def ? "Default" : i == 0 ? "Reaches every repeater" : nullptr;
|
||||
lv_obj_t* row = listRow(body, i == 0 ? "* (no scope)" : sl.name(i), sub, onScopeRow, (void*)(uintptr_t)i);
|
||||
if (def) lv_obj_align(label(row, LV_SYMBOL_OK, THEME_FONT_BODY, theme::ACCENT), LV_ALIGN_RIGHT_MID, -theme::PAD, 0);
|
||||
lv_obj_t* row = listRow(g, i == 0 ? "* (no scope)" : sl.name(i), sub, onScopeRow, (void*)(uintptr_t)i);
|
||||
if (def) rowCheck(row);
|
||||
}
|
||||
groupNote(body, "A scope keeps messages within a region's repeaters. The default is used for direct messages "
|
||||
"and relaying; each channel picks its own in its options.");
|
||||
}
|
||||
|
||||
void UITask::scopeMenu(int idx) {
|
||||
|
||||
@@ -23,16 +23,39 @@ static bool s_freq_rpt = false; // the entry is for the repeater profile
|
||||
static const int OPTS_LEN = 1024;
|
||||
static char* s_opts = psramBuf<char>(OPTS_LEN); // dropdown options (LVGL copies them)
|
||||
|
||||
// Label + optional hint on the left of a settings row; the control goes on the right.
|
||||
|
||||
// A choice at the right of a settings row (its label titles the picker).
|
||||
static lv_obj_t* rowDropdown(lv_obj_t* row, const char* opts, int sel, int width, lv_event_cb_t cb, uintptr_t which) {
|
||||
lv_obj_t* dd = lv_dropdown_create(row);
|
||||
lv_dropdown_set_options(dd, opts);
|
||||
if (sel >= 0) lv_dropdown_set_selected(dd, sel);
|
||||
lv_obj_set_width(dd, width);
|
||||
lv_obj_align(dd, LV_ALIGN_RIGHT_MID, -4, 0);
|
||||
lv_obj_add_event_cb(dd, cb, LV_EVENT_VALUE_CHANGED, (void*)which);
|
||||
return dd;
|
||||
(void)width;
|
||||
lv_obj_t* t = rowTitle(row);
|
||||
lv_obj_t* c = choiceCreate(row, opts, sel, t ? lv_label_get_text(t) : "");
|
||||
lv_obj_align(c, LV_ALIGN_RIGHT_MID, -4, 0);
|
||||
lv_obj_add_event_cb(c, cb, LV_EVENT_VALUE_CHANGED, (void*)which);
|
||||
return c;
|
||||
}
|
||||
|
||||
// The LoRa parameters, as Radio and the repeater's own profile both set them:
|
||||
// preset (0 = "Custom"), frequency (typed in a popup), SF, bandwidth, coding
|
||||
// rate. `ids` are the radioSet / repeaterSet codes for preset, SF, BW, CR.
|
||||
static void paramRows(lv_obj_t* g, const NodePrefs* p, int preset, float freq, uint8_t sf_v, float bw_v, uint8_t cr_v,
|
||||
lv_event_cb_t cb, const uint8_t ids[4], lv_event_cb_t freq_cb, const char* sf_hint) {
|
||||
int o = snprintf(s_opts, OPTS_LEN, "Custom");
|
||||
const char* name; float f, b; uint8_t sf, cr;
|
||||
for (int i = 0; radioctl::presetAt(p, i, name, f, b, sf, cr) && o < OPTS_LEN - 24; i++)
|
||||
o += snprintf(s_opts + o, OPTS_LEN - o, "\n%s", name);
|
||||
rowDropdown(settingRow(g, "Preset", nullptr), s_opts, preset + 1, 0, cb, ids[0]);
|
||||
char fs[16];
|
||||
snprintf(fs, sizeof(fs), "%.3f MHz", freq);
|
||||
rowValue(settingRow(g, "Frequency", nullptr), fs, freq_cb);
|
||||
o = 0;
|
||||
for (int v = 5; v <= 12; v++) o += snprintf(s_opts + o, OPTS_LEN - o, v > 5 ? "\n%d" : "%d", v);
|
||||
rowDropdown(settingRow(g, "Spreading factor", sf_hint), s_opts, sf_v >= 5 && sf_v <= 12 ? sf_v - 5 : 0, 0, cb, ids[1]);
|
||||
o = 0;
|
||||
for (int i = 0; i < LORA_BW_OPT_COUNT; i++)
|
||||
o += snprintf(s_opts + o, OPTS_LEN - o, "%s%g kHz", i ? "\n" : "", (double)LORA_BW_OPTS[i]);
|
||||
rowDropdown(settingRow(g, "Bandwidth", nullptr), s_opts, nearestBwIndex(bw_v), 0, cb, ids[2]);
|
||||
o = 0;
|
||||
for (int v = 5; v <= 8; v++) o += snprintf(s_opts + o, OPTS_LEN - o, v > 5 ? "\n4/%d" : "4/%d", v);
|
||||
rowDropdown(settingRow(g, "Coding rate", nullptr), s_opts, cr_v >= 5 && cr_v <= 8 ? cr_v - 5 : 0, 0, cb, ids[3]);
|
||||
}
|
||||
|
||||
// A few choices side by side at the right of a row (a dropdown's list can run
|
||||
@@ -47,7 +70,7 @@ static lv_obj_t* rowSegmented(lv_obj_t* row, const char** map, int sel, int widt
|
||||
} // namespace radioview
|
||||
|
||||
static void onRadioDropdown(lv_event_t* e) {
|
||||
s_ui->radioSet((int)(uintptr_t)lv_event_get_user_data(e), (int)lv_dropdown_get_selected((lv_obj_t*)lv_event_get_target(e)));
|
||||
s_ui->radioSet((int)(uintptr_t)lv_event_get_user_data(e), choiceSelected((lv_obj_t*)lv_event_get_target(e)));
|
||||
}
|
||||
static void onRadioSwitch(lv_event_t* e) {
|
||||
s_ui->radioSet((int)(uintptr_t)lv_event_get_user_data(e),
|
||||
@@ -68,60 +91,23 @@ void UITask::buildRadio() {
|
||||
s_overlay = s_ta = nullptr;
|
||||
NodePrefs* p = _prefs;
|
||||
|
||||
// Preset: "Custom" first (current params match none), then the list.
|
||||
int o = snprintf(s_opts, OPTS_LEN, "Custom");
|
||||
const char* name; float f, b; uint8_t sf, cr;
|
||||
for (int i = 0; radioctl::presetAt(p, i, name, f, b, sf, cr) && o < OPTS_LEN - 24; i++)
|
||||
o += snprintf(s_opts + o, OPTS_LEN - o, "\n%s", name);
|
||||
lv_obj_t* row = settingRow(body, "Preset", nullptr);
|
||||
lv_obj_t* dd = rowDropdown(row, s_opts, radioctl::currentPreset(p) + 1, 200, onRadioDropdown, R_PRESET);
|
||||
lv_dropdown_set_dir(dd, LV_DIR_BOTTOM);
|
||||
lv_obj_t* g = group(body, "LORA");
|
||||
static const uint8_t IDS[4] = { R_PRESET, R_SF, R_BW, R_CR };
|
||||
paramRows(g, p, radioctl::currentPreset(p), p->freq, p->sf, p->bw, p->cr, onRadioDropdown, IDS, onRadioFreq,
|
||||
"Higher = longer range");
|
||||
groupNote(body, "Everyone you talk to needs the same settings.");
|
||||
|
||||
// Frequency: tap to type it.
|
||||
row = settingRow(body, "Frequency", "MHz");
|
||||
lv_obj_t* fb = lv_button_create(row);
|
||||
lv_obj_set_size(fb, 120, 34);
|
||||
lv_obj_align(fb, LV_ALIGN_RIGHT_MID, -4, 0);
|
||||
lv_obj_set_style_shadow_width(fb, 0, 0);
|
||||
lv_obj_set_style_radius(fb, theme::RADIUS, 0);
|
||||
lv_obj_set_style_bg_color(fb, lv_color_hex(theme::SURFACE_2), 0);
|
||||
lv_obj_add_event_cb(fb, onRadioFreq, LV_EVENT_CLICKED, NULL);
|
||||
char fs[16];
|
||||
snprintf(fs, sizeof(fs), "%.3f", p->freq);
|
||||
lv_obj_center(label(fb, fs, THEME_FONT_BODY, theme::TEXT));
|
||||
|
||||
o = 0;
|
||||
for (int v = 5; v <= 12; v++) o += snprintf(s_opts + o, OPTS_LEN - o, v > 5 ? "\n%d" : "%d", v);
|
||||
rowDropdown(settingRow(body, "Spreading factor", "Higher = longer range"), s_opts,
|
||||
p->sf >= 5 && p->sf <= 12 ? p->sf - 5 : 0, 90, onRadioDropdown, R_SF);
|
||||
o = 0;
|
||||
for (int i = 0; i < LORA_BW_OPT_COUNT; i++)
|
||||
o += snprintf(s_opts + o, OPTS_LEN - o, "%s%g kHz", i ? "\n" : "", (double)LORA_BW_OPTS[i]);
|
||||
rowDropdown(settingRow(body, "Bandwidth", nullptr), s_opts, nearestBwIndex(p->bw), 130, onRadioDropdown, R_BW);
|
||||
o = 0;
|
||||
for (int v = 5; v <= 8; v++) o += snprintf(s_opts + o, OPTS_LEN - o, v > 5 ? "\n4/%d" : "4/%d", v);
|
||||
rowDropdown(settingRow(body, "Coding rate", nullptr), s_opts, p->cr >= 5 && p->cr <= 8 ? p->cr - 5 : 0, 90,
|
||||
onRadioDropdown, R_CR);
|
||||
|
||||
sectionTitle(body, "TRANSMIT");
|
||||
o = 0;
|
||||
g = group(body, "TRANSMIT");
|
||||
int o = 0;
|
||||
for (int v = TX_MIN; v <= TX_MAX; v++) o += snprintf(s_opts + o, OPTS_LEN - o, v > TX_MIN ? "\n%d dBm" : "%d dBm", v);
|
||||
int tx = p->tx_power_dbm < TX_MIN ? TX_MIN : p->tx_power_dbm > TX_MAX ? TX_MAX : p->tx_power_dbm;
|
||||
rowDropdown(settingRow(body, "TX power", p->tx_apc ? "Ceiling for auto power" : nullptr), s_opts, tx - TX_MIN, 110,
|
||||
rowDropdown(settingRow(g, "TX power", p->tx_apc ? "Ceiling for auto power" : nullptr), s_opts, tx - TX_MIN, 0,
|
||||
onRadioDropdown, R_TX);
|
||||
row = settingRow(body, "Auto power", p->client_repeat ? "Off while repeating" : "Lowers power on good links");
|
||||
lv_obj_t* sw = lv_switch_create(row);
|
||||
lv_obj_set_size(sw, 46, 24);
|
||||
lv_obj_align(sw, LV_ALIGN_RIGHT_MID, -theme::PAD, 0);
|
||||
lv_obj_t* sw = switchRow(g, "Auto power", p->client_repeat ? "Off while repeating" : "Lowers power on good links", nullptr);
|
||||
if (p->tx_apc) lv_obj_add_state(sw, LV_STATE_CHECKED);
|
||||
if (p->client_repeat) lv_obj_add_state(sw, LV_STATE_DISABLED);
|
||||
lv_obj_add_event_cb(sw, onRadioSwitch, LV_EVENT_VALUE_CHANGED, (void*)(uintptr_t)R_APC);
|
||||
|
||||
lv_obj_t* note = label(body, "Everyone you talk to needs the same settings.",
|
||||
THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_label_set_long_mode(note, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_width(note, LV_PCT(100));
|
||||
|
||||
buildRadioExtras(body); // my presets, scopes (RadioExtras.h)
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ static lv_obj_t* s_scopes_sub = nullptr; // "Extra scopes" row's count, update
|
||||
|
||||
static void onOpenRepeater(lv_event_t* e) { (void)e; s_ui->showRepeater(); }
|
||||
static void onRptDropdown(lv_event_t* e) {
|
||||
s_ui->repeaterSet((int)(uintptr_t)lv_event_get_user_data(e), (int)lv_dropdown_get_selected((lv_obj_t*)lv_event_get_target(e)));
|
||||
s_ui->repeaterSet((int)(uintptr_t)lv_event_get_user_data(e), choiceSelected((lv_obj_t*)lv_event_get_target(e)));
|
||||
}
|
||||
static void onRptSwitch(lv_event_t* e) {
|
||||
s_ui->repeaterSet((int)(uintptr_t)lv_event_get_user_data(e),
|
||||
@@ -52,87 +52,50 @@ void UITask::buildRepeater() {
|
||||
lv_obj_t* body = newScreen("Repeater", true);
|
||||
NodePrefs* p = _prefs;
|
||||
|
||||
lv_obj_t* row = settingRow(body, "Repeater", p->client_repeat ? "Relaying for others" : "Relay others' messages");
|
||||
lv_obj_t* sw = lv_switch_create(row);
|
||||
lv_obj_set_size(sw, 46, 24);
|
||||
lv_obj_align(sw, LV_ALIGN_RIGHT_MID, -theme::PAD, 0);
|
||||
lv_obj_t* g = group(body, nullptr);
|
||||
lv_obj_t* sw = switchRow(g, "Repeater", p->client_repeat ? "Relaying for others" : "Relay others' messages", nullptr);
|
||||
if (p->client_repeat) lv_obj_add_state(sw, LV_STATE_CHECKED);
|
||||
lv_obj_add_event_cb(sw, onRptSwitch, LV_EVENT_VALUE_CHANGED, (void*)(uintptr_t)RP_ON);
|
||||
lv_obj_t* t = label(body, "Uses more battery; auto power pauses.",
|
||||
THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_label_set_long_mode(t, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_width(t, LV_PCT(100));
|
||||
groupNote(body, "Uses more battery; auto power pauses.");
|
||||
|
||||
sectionTitle(body, "NETWORK");
|
||||
row = settingRow(body, "Relay on", p->repeater_use_profile ? "Its own frequency" : "Your chat frequency");
|
||||
g = group(body, "NETWORK");
|
||||
lv_obj_t* row = settingRow(g, "Relay on", p->repeater_use_profile ? "Its own frequency" : "Your chat frequency");
|
||||
static const char* NET[] = { "Current", "Custom", "" };
|
||||
lv_obj_t* seg = segmented(row, NET, p->repeater_use_profile ? 1 : 0, 150, 34);
|
||||
lv_obj_align(seg, LV_ALIGN_RIGHT_MID, -4, 0);
|
||||
lv_obj_add_event_cb(seg, onRptNetwork, LV_EVENT_VALUE_CHANGED, NULL);
|
||||
radioview::rowSegmented(row, NET, p->repeater_use_profile ? 1 : 0, 150, onRptNetwork, 0);
|
||||
if (p->repeater_use_profile) {
|
||||
int o = snprintf(s_opts, OPTS_LEN, "Custom");
|
||||
const char* name; float f, b; uint8_t sf, cr;
|
||||
for (int i = 0; radioctl::presetAt(p, i, name, f, b, sf, cr) && o < OPTS_LEN - 24; i++)
|
||||
o += snprintf(s_opts + o, OPTS_LEN - o, "\n%s", name);
|
||||
lv_obj_t* dd = rowDropdown(settingRow(body, "Preset", nullptr), s_opts, rptctl::currentPreset(p) + 1, 200,
|
||||
onRptDropdown, RP_PRESET);
|
||||
lv_dropdown_set_dir(dd, LV_DIR_BOTTOM);
|
||||
|
||||
row = settingRow(body, "Frequency", "MHz");
|
||||
lv_obj_t* fb = lv_button_create(row);
|
||||
lv_obj_set_size(fb, 120, 34);
|
||||
lv_obj_align(fb, LV_ALIGN_RIGHT_MID, -4, 0);
|
||||
lv_obj_set_style_shadow_width(fb, 0, 0);
|
||||
lv_obj_set_style_radius(fb, theme::RADIUS, 0);
|
||||
lv_obj_set_style_bg_color(fb, lv_color_hex(theme::SURFACE_2), 0);
|
||||
lv_obj_add_event_cb(fb, onRptFreq, LV_EVENT_CLICKED, NULL);
|
||||
char fs[16];
|
||||
snprintf(fs, sizeof(fs), "%.3f", p->repeater_freq);
|
||||
lv_obj_center(label(fb, fs, THEME_FONT_BODY, theme::TEXT));
|
||||
|
||||
o = 0;
|
||||
for (int v = 5; v <= 12; v++) o += snprintf(s_opts + o, OPTS_LEN - o, v > 5 ? "\n%d" : "%d", v);
|
||||
rowDropdown(settingRow(body, "Spreading factor", nullptr), s_opts,
|
||||
p->repeater_sf >= 5 && p->repeater_sf <= 12 ? p->repeater_sf - 5 : 0, 90, onRptDropdown, RP_SF);
|
||||
o = 0;
|
||||
for (int i = 0; i < LORA_BW_OPT_COUNT; i++)
|
||||
o += snprintf(s_opts + o, OPTS_LEN - o, "%s%g kHz", i ? "\n" : "", (double)LORA_BW_OPTS[i]);
|
||||
rowDropdown(settingRow(body, "Bandwidth", nullptr), s_opts, nearestBwIndex(p->repeater_bw), 130, onRptDropdown, RP_BW);
|
||||
o = 0;
|
||||
for (int v = 5; v <= 8; v++) o += snprintf(s_opts + o, OPTS_LEN - o, v > 5 ? "\n4/%d" : "4/%d", v);
|
||||
rowDropdown(settingRow(body, "Coding rate", nullptr), s_opts,
|
||||
p->repeater_cr >= 5 && p->repeater_cr <= 8 ? p->repeater_cr - 5 : 0, 90, onRptDropdown, RP_CR);
|
||||
static const uint8_t IDS[4] = { RP_PRESET, RP_SF, RP_BW, RP_CR };
|
||||
radioview::paramRows(g, p, rptctl::currentPreset(p), p->repeater_freq, p->repeater_sf, p->repeater_bw,
|
||||
p->repeater_cr, onRptDropdown, IDS, onRptFreq, nullptr);
|
||||
}
|
||||
|
||||
sectionTitle(body, "WHAT IT RELAYS");
|
||||
switchRow(body, "Skip adverts", "Relay messages, not adverts", &p->repeat_skip_adverts);
|
||||
g = group(body, "WHAT IT RELAYS");
|
||||
switchRow(g, "Skip adverts", "Relay messages, not adverts", &p->repeat_skip_adverts);
|
||||
char v[12];
|
||||
int o = 0;
|
||||
for (int i = 0; i <= rptctl::MAX_HOPS; i++) {
|
||||
rptctl::fmtHops(v, sizeof(v), (uint8_t)i);
|
||||
o += snprintf(s_opts + o, OPTS_LEN - o, "%s%s", i ? "\n" : "", v);
|
||||
}
|
||||
rowDropdown(settingRow(body, "Max hops", "Hop limit for floods"), s_opts, p->repeat_max_hops, 90,
|
||||
onRptDropdown, RP_HOPS);
|
||||
rowDropdown(settingRow(g, "Max hops", "Hop limit for floods"), s_opts, p->repeat_max_hops, 0, onRptDropdown, RP_HOPS);
|
||||
o = 0;
|
||||
for (int i = 0; i <= rptctl::MAX_YIELD; i++) {
|
||||
rptctl::fmtYield(v, sizeof(v), (uint8_t)i);
|
||||
o += snprintf(s_opts + o, OPTS_LEN - o, "%s%s", i ? "\n" : "", v);
|
||||
}
|
||||
rowDropdown(settingRow(body, "Yield", "Let fixed repeaters go first"), s_opts, p->repeat_delay_boost, 90,
|
||||
rowDropdown(settingRow(g, "Yield", "Let fixed repeaters go first"), s_opts, p->repeat_delay_boost, 0,
|
||||
onRptDropdown, RP_YIELD);
|
||||
o = 0;
|
||||
for (int i = 0; i < rptctl::snrChoiceCount(); i++) {
|
||||
rptctl::fmtSnr(v, sizeof(v), rptctl::snrFromChoice(i));
|
||||
o += snprintf(s_opts + o, OPTS_LEN - o, "%s%s", i ? "\n" : "", v);
|
||||
}
|
||||
rowDropdown(settingRow(body, "Min SNR", "Ignore weaker packets"), s_opts, rptctl::snrToChoice(p->repeat_min_snr), 100,
|
||||
rowDropdown(settingRow(g, "Min SNR", "Ignore weaker packets"), s_opts, rptctl::snrToChoice(p->repeat_min_snr), 0,
|
||||
onRptDropdown, RP_SNR);
|
||||
switchRow(body, "Skip duplicates", "Skip if already relayed", &p->repeat_suppress_dup);
|
||||
switchRow(body, "Scope only", "Only floods in your scopes", &p->repeat_scope_only);
|
||||
switchRow(g, "Skip duplicates", "Skip if already relayed", &p->repeat_suppress_dup);
|
||||
switchRow(g, "Scope only", "Only floods in your scopes", &p->repeat_scope_only);
|
||||
char sub[48];
|
||||
extraScopesSummary(p, sub, sizeof(sub));
|
||||
s_scopes_sub = lv_obj_get_child(listRow(body, LV_SYMBOL_LIST " Extra scopes", sub, onRptScopes, NULL), 1);
|
||||
s_scopes_sub = rowSub(listRow(g, "Extra scopes", sub, onRptScopes, NULL));
|
||||
}
|
||||
|
||||
// Rebuilds the screen where it was scrolled to.
|
||||
|
||||
@@ -133,7 +133,7 @@ static void onMelMatrix(lv_event_t* e) {
|
||||
(int)lv_buttonmatrix_get_selected_button((lv_obj_t*)lv_event_get_target(e)));
|
||||
}
|
||||
static void onMelBpm(lv_event_t* e) {
|
||||
s_ui->melodySet(MS_BPM, (int)lv_dropdown_get_selected((lv_obj_t*)lv_event_get_target(e)));
|
||||
s_ui->melodySet(MS_BPM, choiceSelected((lv_obj_t*)lv_event_get_target(e)));
|
||||
}
|
||||
static void onMelAction(lv_event_t* e) { s_ui->melodyAction((uint8_t)(uintptr_t)lv_event_get_user_data(e)); }
|
||||
static void onMelPlay(lv_event_t* e) { (void)e; s_ui->melodyAction(MA_PLAY); }
|
||||
@@ -272,19 +272,16 @@ void UITask::buildSoundRows(lv_obj_t* body, bool top) {
|
||||
radioview::rowSegmented(row, MODES, soundctl::mode(_prefs), 150, onSoundMode, 0);
|
||||
return;
|
||||
}
|
||||
sectionTitle(body, "MELODIES");
|
||||
lv_obj_t* g = group(body, "MELODIES");
|
||||
for (int slot = 0; slot < 2; slot++) {
|
||||
soundctl::Melody m;
|
||||
soundctl::load(_prefs, slot, m);
|
||||
char sub[40];
|
||||
if (m.len) snprintf(sub, sizeof(sub), "%d note%s, %u BPM", m.len, m.len == 1 ? "" : "s", soundctl::bpm(m.bpm_idx));
|
||||
else snprintf(sub, sizeof(sub), "Empty - tap to compose");
|
||||
listRow(body, slot ? LV_SYMBOL_AUDIO " Melody 2" : LV_SYMBOL_AUDIO " Melody 1", sub, onMelodyRow, (void*)(uintptr_t)slot);
|
||||
listRow(g, slot ? "Melody 2" : "Melody 1", sub, onMelodyRow, (void*)(uintptr_t)slot);
|
||||
}
|
||||
lv_obj_t* t = label(body, "Per chat: hold it in Chats for its options.",
|
||||
THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_label_set_long_mode(t, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_width(t, LV_PCT(100));
|
||||
groupNote(body, "Per chat: hold it in Chats for its options.");
|
||||
}
|
||||
|
||||
// ── Melody editor ─────────────────────────────────────────────────────────────
|
||||
@@ -312,11 +309,8 @@ void UITask::buildMelodies() {
|
||||
lv_obj_t* top = flexRow(body);
|
||||
lv_obj_t* slots = segmented(top, SLOTS, s_slot, 150, 32);
|
||||
lv_obj_add_event_cb(slots, onMelSlot, LV_EVENT_VALUE_CHANGED, NULL);
|
||||
s_bpm = lv_dropdown_create(top);
|
||||
lv_dropdown_set_options_static(s_bpm, "60 BPM\n90 BPM\n120 BPM\n150 BPM\n180 BPM");
|
||||
lv_dropdown_set_selected(s_bpm, s_mel.bpm_idx);
|
||||
s_bpm = choiceCreate(top, "60 BPM\n90 BPM\n120 BPM\n150 BPM\n180 BPM", s_mel.bpm_idx, "Tempo");
|
||||
lv_obj_set_size(s_bpm, 102, 32);
|
||||
lv_obj_set_style_pad_ver(s_bpm, 6, 0);
|
||||
lv_obj_add_event_cb(s_bpm, onMelBpm, LV_EVENT_VALUE_CHANGED, NULL);
|
||||
smallButton(top, LV_SYMBOL_PLUS, MA_ADD); // (fits: 150 + 102 + 40 + gaps)
|
||||
|
||||
@@ -377,7 +371,7 @@ void UITask::melodySlot(int slot) {
|
||||
s_slot = slot == 1 ? 1 : 0;
|
||||
soundctl::load(_prefs, s_slot, s_mel);
|
||||
s_sel = 0;
|
||||
if (s_bpm) lv_dropdown_set_selected(s_bpm, s_mel.bpm_idx);
|
||||
if (s_bpm) choiceSetSelected(s_bpm, s_mel.bpm_idx);
|
||||
rebuildStrip();
|
||||
syncControls();
|
||||
refreshMelody();
|
||||
|
||||
@@ -131,23 +131,19 @@ static uint64_t treeBytes(const char* root, int depth = 0) {
|
||||
return sum;
|
||||
}
|
||||
|
||||
// A bar with "x free of y" under it.
|
||||
static lv_obj_t* usageBar(lv_obj_t* parent, lv_obj_t** lbl) {
|
||||
lv_obj_t* card = lv_obj_create(parent);
|
||||
styleSurface(card, theme::SURFACE);
|
||||
lv_obj_remove_flag(card, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_radius(card, theme::RADIUS, 0);
|
||||
lv_obj_set_style_pad_all(card, theme::PAD, 0);
|
||||
lv_obj_set_style_pad_row(card, 6, 0);
|
||||
lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_t* bar = lv_bar_create(card);
|
||||
// A group row: a bar with "x used of y" under it.
|
||||
static lv_obj_t* usageBar(lv_obj_t* g, lv_obj_t** lbl) {
|
||||
lv_obj_t* row = infoLine(g);
|
||||
lv_obj_set_style_pad_ver(row, 10, 0);
|
||||
lv_obj_set_flex_flow(row, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(row, 6, 0);
|
||||
lv_obj_t* bar = lv_bar_create(row);
|
||||
lv_obj_set_size(bar, LV_PCT(100), 10);
|
||||
lv_bar_set_range(bar, 0, 1000);
|
||||
lv_obj_set_style_bg_color(bar, lv_color_hex(theme::BG), LV_PART_MAIN);
|
||||
lv_obj_set_style_bg_opa(bar, LV_OPA_COVER, LV_PART_MAIN);
|
||||
lv_obj_set_style_bg_color(bar, lv_color_hex(theme::ACCENT), LV_PART_INDICATOR);
|
||||
*lbl = label(card, "", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
*lbl = label(row, "", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
return bar;
|
||||
}
|
||||
|
||||
@@ -181,11 +177,11 @@ static void refreshSizes() {
|
||||
|
||||
static void onOpenStorage(lv_event_t* e) { (void)e; s_ui->showStorage(); }
|
||||
static void onHistKeep(lv_event_t* e) {
|
||||
s_ui->storageKeep((int)lv_dropdown_get_selected((lv_obj_t*)lv_event_get_target(e)));
|
||||
s_ui->storageKeep((int)choiceSelected((lv_obj_t*)lv_event_get_target(e)));
|
||||
}
|
||||
static void onHistClear(lv_event_t* e) { (void)e; s_ui->storageClearHistory(); }
|
||||
static void onLiveCap(lv_event_t* e) {
|
||||
s_ui->storageLiveCap((int)lv_dropdown_get_selected((lv_obj_t*)lv_event_get_target(e)));
|
||||
s_ui->storageLiveCap((int)choiceSelected((lv_obj_t*)lv_event_get_target(e)));
|
||||
}
|
||||
static void onLiveClear(lv_event_t* e) { (void)e; s_ui->storageClearLive(); }
|
||||
|
||||
@@ -201,22 +197,21 @@ void UITask::buildStorage() {
|
||||
memset(s_cat_val, 0, sizeof(s_cat_val));
|
||||
s_clear_armed_ms = 0;
|
||||
|
||||
sectionTitle(body, "SD CARD");
|
||||
lv_obj_t* g = group(body, "SD CARD");
|
||||
if (!lvport::mountStorage()) {
|
||||
label(body, "No SD card.", THEME_FONT_BODY, theme::TEXT_MUTED);
|
||||
infoRow(g, "Card", "None", theme::TEXT_MUTED);
|
||||
} else {
|
||||
s_sd_bar = usageBar(body, &s_sd_lbl);
|
||||
s_sd_bar = usageBar(g, &s_sd_lbl);
|
||||
lv_label_set_text(s_sd_lbl, "Reading the card...");
|
||||
lv_obj_t* cats = infoCard(body); // what takes the space
|
||||
for (int c = 0; c < C_COUNT; c++) s_cat_val[c] = infoRow(cats, CAT_NAME[c], "...", theme::ACCENT);
|
||||
s_status = infoRow(cats, "Files", "");
|
||||
for (int c = 0; c < C_COUNT; c++) s_cat_val[c] = infoRow(g, CAT_NAME[c], "...", theme::ACCENT); // what takes the space
|
||||
s_status = infoRow(g, "Files", "");
|
||||
s_sd_read = false; // read in pollStorage(): the first read of a big card takes a moment
|
||||
startWalk();
|
||||
}
|
||||
|
||||
sectionTitle(body, "DEVICE");
|
||||
g = group(body, "DEVICE");
|
||||
lv_obj_t* flbl;
|
||||
lv_obj_t* fbar = usageBar(body, &flbl);
|
||||
lv_obj_t* fbar = usageBar(g, &flbl);
|
||||
uint64_t ft = 0, fu = 0;
|
||||
if (lvport::flashInfo(ft, fu)) {
|
||||
if (fu == 0) fu = treeBytes(lvport::FLASH_ROOT); // the sim only counts files
|
||||
@@ -224,37 +219,25 @@ void UITask::buildStorage() {
|
||||
} else {
|
||||
lv_label_set_text(flbl, "Not available");
|
||||
}
|
||||
label(body, "Contacts, channels, settings, saved trail, waypoints.", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
groupNote(body, "Contacts, channels, settings, saved trail, waypoints.");
|
||||
|
||||
sectionTitle(body, "MESSAGES");
|
||||
g = group(body, "MESSAGES");
|
||||
int keep_idx = histstore::KEEP_DEFAULT;
|
||||
for (int i = 0; i < histstore::KEEP_COUNT; i++) if (histstore::KEEP[i] == s_archive.keep()) keep_idx = i;
|
||||
lv_obj_t* dd = dropdownRow(body, "Kept per chat", histstore::KEEP_OPTS, keep_idx);
|
||||
lv_obj_remove_event_cb(dd, onKeyboardAlphabet);
|
||||
lv_obj_add_event_cb(dd, onHistKeep, LV_EVENT_VALUE_CHANGED, NULL);
|
||||
char note[96];
|
||||
snprintf(note, sizeof(note), "Each channel and contact keeps its newest ones on the card (%u KB per 100).",
|
||||
char hint[48];
|
||||
snprintf(hint, sizeof(hint), "Newest on the card, %u KB per 100",
|
||||
(unsigned)((sizeof(ChHistEntry) * 100 + 512) / 1024));
|
||||
lv_obj_t* n = label(body, note, THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_label_set_long_mode(n, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_width(n, LV_PCT(100));
|
||||
lv_obj_t* row = listRow(body, LV_SYMBOL_TRASH " Delete message history", "Every chat, on the card and in memory",
|
||||
onHistClear, NULL);
|
||||
s_clear_lbl = lv_obj_get_child(row, 0);
|
||||
choiceRow(g, "Kept per chat", hint, histstore::KEEP_OPTS, keep_idx, onHistKeep);
|
||||
s_clear_lbl = actionRow(g, LV_SYMBOL_TRASH " Delete message history", onHistClear, NULL, theme::FAIL);
|
||||
|
||||
sectionTitle(body, "LIVE MAP TILES");
|
||||
g = group(body, "LIVE MAP TILES");
|
||||
int cap_idx = mapview::LIVE_CAP_DEFAULT;
|
||||
for (int i = 0; i < mapview::LIVE_CAP_COUNT; i++)
|
||||
if (((uint64_t)mapview::LIVE_CAP_MB[i] << 20) == mapview::s_live_cache.limit()) cap_idx = i;
|
||||
lv_obj_t* cd = dropdownRow(body, "Keep up to", mapview::LIVE_CAP_OPTS, cap_idx);
|
||||
lv_obj_remove_event_cb(cd, onKeyboardAlphabet);
|
||||
lv_obj_add_event_cb(cd, onLiveCap, LV_EVENT_VALUE_CHANGED, NULL);
|
||||
s_live_info = label(body, "", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_label_set_long_mode(s_live_info, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_width(s_live_info, LV_PCT(100));
|
||||
lv_obj_t* lrow = listRow(body, LV_SYMBOL_TRASH " Delete live map tiles", "Downloaded maps stay",
|
||||
onLiveClear, NULL);
|
||||
s_live_lbl = lv_obj_get_child(lrow, 0);
|
||||
s_live_info = infoRow(g, "Fetched while browsing", "");
|
||||
choiceRow(g, "Keep up to", "Past it, the oldest go first", mapview::LIVE_CAP_OPTS, cap_idx, onLiveCap);
|
||||
s_live_lbl = actionRow(g, LV_SYMBOL_TRASH " Delete live map tiles", onLiveClear, NULL, theme::FAIL);
|
||||
groupNote(body, "Downloaded areas stay.");
|
||||
s_live_armed_ms = 0;
|
||||
s_shown_ms = 0;
|
||||
}
|
||||
@@ -289,8 +272,7 @@ void UITask::pollStorage() {
|
||||
char b[16], t[96];
|
||||
fmtBytes(b, sizeof(b), mapview::s_live_cache.bytes());
|
||||
if (mapview::s_live_cache.clearing()) snprintf(t, sizeof(t), "Deleting... %lu left", (unsigned long)mapview::s_live_cache.count());
|
||||
else snprintf(t, sizeof(t), "Fetched while browsing online: %lu tiles, %s. Past the limit, the oldest go first.",
|
||||
(unsigned long)mapview::s_live_cache.count(), b);
|
||||
else snprintf(t, sizeof(t), "%lu tiles, %s", (unsigned long)mapview::s_live_cache.count(), b);
|
||||
lv_label_set_text(s_live_info, t);
|
||||
}
|
||||
if (s_live_armed_ms && millis() - s_live_armed_ms > 4000) {
|
||||
|
||||
@@ -150,24 +150,6 @@ static lv_obj_t* dimOverlay(lv_obj_t* parent) {
|
||||
return o;
|
||||
}
|
||||
|
||||
// A settings row: the label left (a muted hint under it, cut at `hint_w`),
|
||||
// room on the right for a switch / dropdown / slider.
|
||||
static lv_obj_t* settingRow(lv_obj_t* parent, const char* text, const char* hint, int hint_w = 150) {
|
||||
lv_obj_t* row = lv_obj_create(parent);
|
||||
styleSurface(row, theme::SURFACE);
|
||||
lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_size(row, LV_PCT(100), theme::ROW_H);
|
||||
lv_obj_set_style_radius(row, theme::RADIUS, 0);
|
||||
lv_obj_align(label(row, text, THEME_FONT_BODY, theme::TEXT), LV_ALIGN_TOP_LEFT, theme::PAD, hint ? 5 : 13);
|
||||
if (hint) {
|
||||
lv_obj_t* h = label(row, hint, THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_label_set_long_mode(h, LV_LABEL_LONG_DOT);
|
||||
lv_obj_set_size(h, hint_w, 15); // fixed height: cut, don't wrap onto the label
|
||||
lv_obj_align(h, LV_ALIGN_BOTTOM_LEFT, theme::PAD, -5);
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
// ── Info cards ──
|
||||
// Label / value rows on a card (Diagnostics, GPS, node detail, About): the
|
||||
// label small and muted on the left, the value on the right -- wrapping,
|
||||
@@ -214,6 +196,7 @@ static lv_obj_t* infoLine(lv_obj_t* card) { // one row's box, the hairline abo
|
||||
lv_obj_remove_flag(r, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_size(r, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_ver(r, 5, 0);
|
||||
if (lv_obj_has_flag(card, LV_OBJ_FLAG_USER_1)) lv_obj_set_style_pad_hor(r, theme::PAD, 0); // in a group()
|
||||
if (lv_obj_get_index(r) > 0) {
|
||||
lv_obj_set_style_border_side(r, LV_BORDER_SIDE_TOP, 0);
|
||||
lv_obj_set_style_border_width(r, 1, 0);
|
||||
@@ -252,6 +235,8 @@ static lv_obj_t* infoNote(lv_obj_t* card, const char* key, const char* text) {
|
||||
return t;
|
||||
}
|
||||
|
||||
static lv_obj_t* sectionTitle(lv_obj_t* parent, const char* text);
|
||||
|
||||
static void infoSet(lv_obj_t* value, const char* text) {
|
||||
lv_obj_t* row = lv_obj_get_parent(value);
|
||||
if (!text || !text[0]) { lv_obj_add_flag(row, LV_OBJ_FLAG_HIDDEN); return; }
|
||||
@@ -269,6 +254,322 @@ static lv_obj_t* flexBox(lv_obj_t* parent, lv_flex_flow_t flow) {
|
||||
return b;
|
||||
}
|
||||
|
||||
// ── Settings kit ──
|
||||
// Every settings page is built from the same few pieces: group() -- a section
|
||||
// title and the card its rows sit on, hairlines between them -- and rows on
|
||||
// it: settingRow() (label, hint under it, a control on the right: switch,
|
||||
// choice, slider, segmented), listRow() (opens something: a chevron),
|
||||
// actionRow() (does something now) and groupNote() (one line under a card).
|
||||
// Rows built outside a group keep the older look: a card of their own.
|
||||
|
||||
static inline bool isGroup(lv_obj_t* o) { return o && lv_obj_has_flag(o, LV_OBJ_FLAG_USER_1); }
|
||||
|
||||
static lv_obj_t* group(lv_obj_t* parent, const char* title) {
|
||||
if (title) sectionTitle(parent, title);
|
||||
lv_obj_t* c = infoCard(parent);
|
||||
lv_obj_set_style_pad_hor(c, 0, 0); // rows pad themselves: a pressed row lights edge to edge
|
||||
lv_obj_set_style_clip_corner(c, true, 0);
|
||||
lv_obj_add_flag(c, LV_OBJ_FLAG_USER_1);
|
||||
return c;
|
||||
}
|
||||
|
||||
static lv_obj_t* groupNote(lv_obj_t* parent, const char* text) {
|
||||
lv_obj_t* l = label(parent, text, THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_width(l, LV_PCT(100));
|
||||
lv_obj_set_style_pad_hor(l, theme::PAD, 0);
|
||||
return l;
|
||||
}
|
||||
|
||||
// A group row's box: full width, at least a touch target tall, a flex row
|
||||
// with the text column growing and whatever follows it on the right.
|
||||
static lv_obj_t* groupLine(lv_obj_t* card, bool tappable) {
|
||||
lv_obj_t* r = infoLine(card);
|
||||
lv_obj_set_style_pad_hor(r, theme::PAD, 0);
|
||||
lv_obj_set_style_pad_ver(r, 6, 0);
|
||||
lv_obj_set_style_min_height(r, theme::ROW_H, 0);
|
||||
lv_obj_set_flex_flow(r, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(r, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_style_pad_column(r, 8, 0);
|
||||
if (tappable) {
|
||||
lv_obj_add_flag(r, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_set_style_bg_color(r, lv_color_hex(theme::SURFACE_2), LV_STATE_PRESSED);
|
||||
lv_obj_set_style_bg_opa(r, LV_OPA_COVER, LV_STATE_PRESSED);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
// The label (and the hint under it, wrapping) filling a group row's left side.
|
||||
static lv_obj_t* groupText(lv_obj_t* row, const char* text, const char* hint, uint32_t col = theme::TEXT) {
|
||||
lv_obj_t* t = flexBox(row, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_width(t, 1);
|
||||
lv_obj_set_flex_grow(t, 1);
|
||||
lv_obj_set_style_pad_row(t, 1, 0);
|
||||
// The row's touch-target height lives here: a row's own min height comes
|
||||
// after its flex layout, which then sits everything at the top.
|
||||
lv_obj_set_style_min_height(t, theme::ROW_H - 12, 0);
|
||||
lv_obj_set_flex_align(t, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
|
||||
lv_obj_t* l = label(t, text, THEME_FONT_BODY, col);
|
||||
lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_width(l, LV_PCT(100));
|
||||
if (hint) {
|
||||
lv_obj_t* h = label(t, hint, THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_label_set_long_mode(h, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_width(h, LV_PCT(100));
|
||||
}
|
||||
lv_obj_set_user_data(row, l); // rowTitle()
|
||||
return l;
|
||||
}
|
||||
|
||||
// The title label of a settingRow() / listRow(), to recolour or rename it.
|
||||
static lv_obj_t* rowTitle(lv_obj_t* row) { return (lv_obj_t*)lv_obj_get_user_data(row); }
|
||||
|
||||
// A settings row: the label left (a muted hint under it, cut at `hint_w`),
|
||||
// room on the right for a switch / dropdown / slider.
|
||||
static lv_obj_t* settingRow(lv_obj_t* parent, const char* text, const char* hint, int hint_w = 150) {
|
||||
if (isGroup(parent)) {
|
||||
lv_obj_t* row = groupLine(parent, false);
|
||||
groupText(row, text, hint);
|
||||
return row;
|
||||
}
|
||||
lv_obj_t* row = lv_obj_create(parent);
|
||||
styleSurface(row, theme::SURFACE);
|
||||
lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_size(row, LV_PCT(100), theme::ROW_H);
|
||||
lv_obj_set_style_radius(row, theme::RADIUS, 0);
|
||||
lv_obj_t* t = label(row, text, THEME_FONT_BODY, theme::TEXT);
|
||||
lv_obj_align(t, LV_ALIGN_TOP_LEFT, theme::PAD, hint ? 5 : 13);
|
||||
lv_obj_set_user_data(row, t);
|
||||
if (hint) {
|
||||
lv_obj_t* h = label(row, hint, THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_label_set_long_mode(h, LV_LABEL_LONG_DOT);
|
||||
lv_obj_set_size(h, hint_w, 15); // fixed height: cut, don't wrap onto the label
|
||||
lv_obj_align(h, LV_ALIGN_BOTTOM_LEFT, theme::PAD, -5);
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
// Does something at once (Reboot, Delete ...): its text in `col`, no chevron.
|
||||
// Returns the label, for a confirm-by-second-tap text.
|
||||
static lv_obj_t* actionRow(lv_obj_t* card, const char* text, lv_event_cb_t cb, void* user,
|
||||
uint32_t col = theme::TEXT) {
|
||||
lv_obj_t* r = groupLine(card, true);
|
||||
lv_obj_t* l = groupText(r, text, nullptr, col);
|
||||
lv_obj_add_event_cb(r, cb, LV_EVENT_CLICKED, user);
|
||||
return l;
|
||||
}
|
||||
|
||||
// ── Choice: a value picked from a list ──
|
||||
// In place of a dropdown: the value and a chevron; a tap opens the options
|
||||
// over the screen, and a pick sends LV_EVENT_VALUE_CHANGED like a dropdown.
|
||||
struct Choice {
|
||||
char* opts; // "\n"-separated, owned
|
||||
uint16_t sel, count;
|
||||
char title[32];
|
||||
lv_obj_t* value;
|
||||
};
|
||||
static lv_obj_t* s_pick_overlay = nullptr;
|
||||
static lv_obj_t* s_pick_target = nullptr;
|
||||
|
||||
static Choice* choiceOf(lv_obj_t* c) { return (Choice*)lv_obj_get_user_data(c); }
|
||||
|
||||
// Option `i` of a "\n"-list, into buf.
|
||||
static const char* choiceItem(const Choice* ch, int i, char* buf, size_t n) {
|
||||
const char* p = ch->opts;
|
||||
for (int k = 0; k < i && p; k++) { p = strchr(p, '\n'); if (p) p++; }
|
||||
if (!p) { buf[0] = '\0'; return buf; }
|
||||
const char* e = strchr(p, '\n');
|
||||
size_t len = e ? (size_t)(e - p) : strlen(p);
|
||||
if (len >= n) len = n - 1;
|
||||
memcpy(buf, p, len);
|
||||
buf[len] = '\0';
|
||||
return buf;
|
||||
}
|
||||
|
||||
static void choiceShow(lv_obj_t* c) {
|
||||
Choice* ch = choiceOf(c);
|
||||
char v[48];
|
||||
lv_label_set_text(ch->value, choiceItem(ch, ch->sel, v, sizeof(v)));
|
||||
}
|
||||
|
||||
static void choiceSetOptions(lv_obj_t* c, const char* opts) {
|
||||
Choice* ch = choiceOf(c);
|
||||
lv_free(ch->opts);
|
||||
size_t n = strlen(opts);
|
||||
ch->opts = (char*)lv_malloc(n + 1);
|
||||
memcpy(ch->opts, opts, n + 1);
|
||||
ch->count = 1;
|
||||
for (const char* p = opts; *p; p++) if (*p == '\n') ch->count++;
|
||||
if (ch->sel >= ch->count) ch->sel = 0;
|
||||
choiceShow(c);
|
||||
}
|
||||
|
||||
static int choiceSelected(lv_obj_t* c) { return choiceOf(c)->sel; }
|
||||
static void choiceSetSelected(lv_obj_t* c, int i) {
|
||||
Choice* ch = choiceOf(c);
|
||||
if (i < 0 || i >= ch->count) return;
|
||||
ch->sel = (uint16_t)i;
|
||||
choiceShow(c);
|
||||
}
|
||||
|
||||
static void pickerClose() {
|
||||
if (s_pick_overlay) lv_obj_delete_async(s_pick_overlay);
|
||||
s_pick_overlay = s_pick_target = nullptr;
|
||||
}
|
||||
|
||||
static void onPickOption(lv_event_t* e) {
|
||||
lv_obj_t* c = s_pick_target;
|
||||
pickerClose();
|
||||
if (!c) return;
|
||||
choiceSetSelected(c, (int)(uintptr_t)lv_event_get_user_data(e));
|
||||
lv_obj_send_event(c, LV_EVENT_VALUE_CHANGED, NULL); // may rebuild the screen: c is gone after this
|
||||
}
|
||||
|
||||
static void pickerOpen(lv_obj_t* c) {
|
||||
Choice* ch = choiceOf(c);
|
||||
pickerClose();
|
||||
s_pick_target = c;
|
||||
s_pick_overlay = dimOverlay(lv_layer_top());
|
||||
lv_obj_add_event_cb(s_pick_overlay, [](lv_event_t* e) { // a tap beside the list: nothing changes
|
||||
if (lv_event_get_target(e) == lv_event_get_current_target(e)) pickerClose();
|
||||
}, LV_EVENT_CLICKED, NULL);
|
||||
lv_obj_t* panel = lv_obj_create(s_pick_overlay);
|
||||
styleSurface(panel, theme::BG);
|
||||
lv_obj_set_width(panel, lv_display_get_horizontal_resolution(NULL) - 48);
|
||||
lv_obj_set_height(panel, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_max_height(panel, lv_display_get_vertical_resolution(NULL) - theme::STATUS_H - 16, 0);
|
||||
lv_obj_align(panel, LV_ALIGN_CENTER, 0, theme::STATUS_H / 2);
|
||||
lv_obj_set_style_radius(panel, theme::RADIUS, 0);
|
||||
lv_obj_set_style_border_color(panel, lv_color_hex(theme::ACCENT), 0);
|
||||
lv_obj_set_style_border_width(panel, 1, 0);
|
||||
lv_obj_set_style_pad_all(panel, 6, 0);
|
||||
lv_obj_set_style_pad_row(panel, 4, 0);
|
||||
lv_obj_set_flex_flow(panel, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_scrollbar_mode(panel, LV_SCROLLBAR_MODE_ACTIVE);
|
||||
lv_obj_t* t = label(panel, ch->title, THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_obj_set_style_pad_hor(t, 4, 0);
|
||||
lv_obj_t* card = group(panel, nullptr);
|
||||
lv_obj_t* sel_row = nullptr;
|
||||
for (int i = 0; i < ch->count; i++) {
|
||||
char v[48];
|
||||
bool cur = i == ch->sel;
|
||||
lv_obj_t* r = groupLine(card, true);
|
||||
lv_obj_set_style_min_height(r, 38, 0);
|
||||
groupText(r, choiceItem(ch, i, v, sizeof(v)), nullptr, cur ? theme::ACCENT : theme::TEXT);
|
||||
if (cur) { label(r, LV_SYMBOL_OK, THEME_FONT_BODY, theme::ACCENT); sel_row = r; }
|
||||
lv_obj_add_event_cb(r, onPickOption, LV_EVENT_CLICKED, (void*)(uintptr_t)i);
|
||||
}
|
||||
anim::popup(s_pick_overlay);
|
||||
if (sel_row) { lv_obj_update_layout(panel); lv_obj_scroll_to_view_recursive(sel_row, LV_ANIM_OFF); }
|
||||
}
|
||||
|
||||
// A choice: in a group row the value is bare text (accent) and a chevron;
|
||||
// elsewhere it sits on a small surface of its own.
|
||||
static lv_obj_t* choiceCreate(lv_obj_t* parent, const char* opts, int sel, const char* title) {
|
||||
lv_obj_t* c = lv_obj_create(parent);
|
||||
lv_obj_remove_style_all(c);
|
||||
lv_obj_add_flag(c, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_remove_flag(c, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_size(c, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(c, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(c, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); // END + SIZE_CONTENT mis-sizes
|
||||
lv_obj_set_style_pad_column(c, 6, 0);
|
||||
lv_obj_set_ext_click_area(c, 10);
|
||||
lv_obj_set_style_opa(c, LV_OPA_60, LV_STATE_PRESSED);
|
||||
if (!isGroup(lv_obj_get_parent(parent))) {
|
||||
lv_obj_set_style_bg_color(c, lv_color_hex(theme::SURFACE_2), 0);
|
||||
lv_obj_set_style_bg_opa(c, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_radius(c, theme::RADIUS_SM, 0);
|
||||
lv_obj_set_style_pad_hor(c, 10, 0);
|
||||
lv_obj_set_style_pad_ver(c, 8, 0);
|
||||
}
|
||||
Choice* ch = (Choice*)lv_malloc(sizeof(Choice));
|
||||
memset(ch, 0, sizeof(*ch));
|
||||
snprintf(ch->title, sizeof(ch->title), "%s", title ? title : "");
|
||||
ch->value = label(c, "", THEME_FONT_BODY, theme::ACCENT);
|
||||
lv_obj_set_style_max_width(ch->value, 140, 0); // a long value wraps (LONG_DOT mis-sizes with SIZE_CONTENT)
|
||||
lv_obj_set_style_text_align(ch->value, LV_TEXT_ALIGN_RIGHT, 0);
|
||||
label(c, LV_SYMBOL_DOWN, THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_obj_set_user_data(c, ch);
|
||||
ch->sel = sel < 0 ? 0 : (uint16_t)sel;
|
||||
choiceSetOptions(c, opts);
|
||||
lv_obj_add_event_cb(c, [](lv_event_t* e) { pickerOpen((lv_obj_t*)lv_event_get_current_target(e)); },
|
||||
LV_EVENT_CLICKED, NULL);
|
||||
lv_obj_add_event_cb(c, [](lv_event_t* e) {
|
||||
lv_obj_t* o = (lv_obj_t*)lv_event_get_current_target(e);
|
||||
if (s_pick_target == o) pickerClose();
|
||||
Choice* ch = choiceOf(o);
|
||||
lv_free(ch->opts);
|
||||
lv_free(ch);
|
||||
}, LV_EVENT_DELETE, NULL);
|
||||
return c;
|
||||
}
|
||||
|
||||
|
||||
// One settings row: label left, a choice right.
|
||||
static lv_obj_t* choiceRow(lv_obj_t* parent, const char* text, const char* hint, const char* options, int sel,
|
||||
lv_event_cb_t cb, void* user = nullptr) {
|
||||
lv_obj_t* row = settingRow(parent, text, hint);
|
||||
lv_obj_t* c = choiceCreate(row, options, sel, text);
|
||||
lv_obj_align(c, LV_ALIGN_RIGHT_MID, -4, 0);
|
||||
lv_obj_add_event_cb(c, cb, LV_EVENT_VALUE_CHANGED, user);
|
||||
return c;
|
||||
}
|
||||
|
||||
// A value typed in a popup (frequency, name ...): shown as a choice is, with
|
||||
// a pencil; the tap is the caller's.
|
||||
static lv_obj_t* rowValue(lv_obj_t* row, const char* text, lv_event_cb_t cb, void* user = nullptr) {
|
||||
lv_obj_t* c = flexBox(row, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(c, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_style_pad_column(c, 6, 0);
|
||||
lv_obj_add_flag(c, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_set_ext_click_area(c, 10);
|
||||
lv_obj_set_style_opa(c, LV_OPA_60, LV_STATE_PRESSED);
|
||||
label(c, text, THEME_FONT_BODY, theme::ACCENT);
|
||||
label(c, LV_SYMBOL_EDIT, THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_obj_add_event_cb(c, cb, LV_EVENT_CLICKED, user);
|
||||
return c;
|
||||
}
|
||||
|
||||
// A tick on a listRow() (the one in use), left of its chevron.
|
||||
static void rowCheck(lv_obj_t* row) {
|
||||
lv_obj_t* l = label(row, LV_SYMBOL_OK, THEME_FONT_BODY, theme::ACCENT);
|
||||
if (isGroup(lv_obj_get_parent(row))) lv_obj_move_to_index(l, (int32_t)lv_obj_get_child_count(row) - 2);
|
||||
else lv_obj_align(l, LV_ALIGN_RIGHT_MID, -theme::PAD, 0);
|
||||
}
|
||||
|
||||
// The subtitle label of a listRow() / settingRow() with one, to update it.
|
||||
static lv_obj_t* rowSub(lv_obj_t* row) {
|
||||
if (isGroup(lv_obj_get_parent(row))) return lv_obj_get_child(lv_obj_get_child(row, 0), 1);
|
||||
return lv_obj_get_child(row, 1);
|
||||
}
|
||||
|
||||
// Settings pages: the schema's, then this UI's own. Display, Power and Time
|
||||
// split the schema's "Display & power" page (shared with L1, left as it is)
|
||||
// into what their titles say.
|
||||
enum : uint8_t { PG_KEYBOARD = settings::PG_COUNT, PG_ABOUT, PG_DISPLAY, PG_POWER, PG_TIME, PG_ALL };
|
||||
|
||||
// A schema setting by its NodePrefs field; -1 if there's none.
|
||||
static int settingIdx(uint16_t offset) {
|
||||
for (int i = 0; i < settings::COUNT; i++) if (settings::ALL[i].offset == offset) return i;
|
||||
return -1;
|
||||
}
|
||||
#define SETTING(field) settingIdx(offsetof(NodePrefs, field))
|
||||
|
||||
// A setting's current value as its choice shows it ("UTC+2", "3.4 V" ...).
|
||||
static const char* settingText(const NodePrefs& p, int idx, char* buf, int n) {
|
||||
buf[0] = '\0';
|
||||
if (idx < 0) return buf;
|
||||
const settings::Setting& st = settings::ALL[idx];
|
||||
uint8_t v = settings::get(p, st);
|
||||
if (st.option) st.option(v, buf, n, p);
|
||||
else snprintf(buf, n, "%s", v ? "On" : "Off");
|
||||
return buf;
|
||||
}
|
||||
static lv_obj_t* s_sec_card[settings::SEC_COUNT]; // schemaRows()' groups, by section
|
||||
static bool s_opts_from_map = false; // Map options opened from the map: back returns there
|
||||
static int32_t s_settings_y = 0; // Settings' scroll, kept while a page of it is open
|
||||
|
||||
// A one-of-N choice: a row (or grid, with "\n" in the map) of checkable
|
||||
// buttons, `sel` checked (-1: none). `item` is the unchecked button colour —
|
||||
// SURFACE on a SURFACE_2 panel, SURFACE_2 on the page.
|
||||
@@ -929,6 +1230,8 @@ static void onBack(lv_event_t* e) { (void)e; s_ui->back(); }
|
||||
lv_obj_t* UITask::newScreen(const char* title, bool with_back) {
|
||||
_home_clock = _home_date = _home_unread = nullptr;
|
||||
_thread_list = _compose_ta = _keyboard = nullptr;
|
||||
if (_shown_screen == SCR_SETTINGS && _body) s_settings_y = lv_obj_get_scroll_y(_body);
|
||||
pickerClose();
|
||||
_header = _body = nullptr;
|
||||
_nearby_list = _nearby_status = _nearby_sort_lbl = _nearby_chips = nullptr;
|
||||
_node_info = _node_ping = _node_delete_lbl = nullptr;
|
||||
@@ -1022,7 +1325,7 @@ void UITask::back() {
|
||||
case SCR_ADMIN: if (_nav_overlay) navClosePopup(); else adminLeave(); break;
|
||||
case SCR_SETTINGS: if (_nav_overlay) navClosePopup(); else showHome(); break;
|
||||
case SCR_SETTINGS_NAV: // the map's options go back to the map
|
||||
if (_settings_page == settings::PG_NAV) openMap(true); else showSettings();
|
||||
if (_settings_page == settings::PG_NAV && s_opts_from_map) openMap(true); else showSettings();
|
||||
break;
|
||||
case SCR_QUICK: // back to Messages & contacts' bottom, where the row is
|
||||
if (_nav_overlay) { navClosePopup(); break; }
|
||||
@@ -1322,8 +1625,16 @@ static void onChatFilter(lv_event_t* e);
|
||||
enum : uint8_t { CF_CHANNELS, CF_ROOMS, CF_CONTACTS }; // favourites-only filters
|
||||
|
||||
// One tappable list row: title, optional muted subtitle, optional badge.
|
||||
// In a group(): a flat row, the subtitle wrapping, a chevron on the right.
|
||||
static lv_obj_t* listRow(lv_obj_t* parent, const char* title, const char* sub,
|
||||
lv_event_cb_t cb, void* user) {
|
||||
if (isGroup(parent)) {
|
||||
lv_obj_t* row = groupLine(parent, true);
|
||||
groupText(row, title, sub);
|
||||
label(row, LV_SYMBOL_RIGHT, THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_obj_add_event_cb(row, cb, LV_EVENT_CLICKED, user);
|
||||
return row;
|
||||
}
|
||||
lv_obj_t* row = lv_button_create(parent);
|
||||
lv_obj_set_size(row, LV_PCT(100), theme::ROW_H);
|
||||
lv_obj_set_style_bg_color(row, lv_color_hex(theme::SURFACE), 0);
|
||||
@@ -1336,6 +1647,7 @@ static lv_obj_t* listRow(lv_obj_t* parent, const char* title, const char* sub,
|
||||
lv_label_set_long_mode(t, LV_LABEL_LONG_DOT);
|
||||
lv_obj_set_width(t, 230);
|
||||
lv_obj_align(t, LV_ALIGN_TOP_LEFT, theme::PAD, sub ? 5 : 13);
|
||||
lv_obj_set_user_data(row, t);
|
||||
if (sub) {
|
||||
lv_obj_t* s = label(row, sub, THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_label_set_long_mode(s, LV_LABEL_LONG_DOT);
|
||||
@@ -1754,7 +2066,7 @@ void UITask::refreshNearbyList() {
|
||||
snprintf(sub, sizeof(sub), "%s%s%s%s", kind, e.is_live ? " - live" : "",
|
||||
(e.dist_km >= 0.0f && age[0]) ? " - " : "", (e.dist_km >= 0.0f && age[0]) ? age : "");
|
||||
lv_obj_t* row = listRow(_nearby_list, title, sub, onNearbyRow, (void*)(uintptr_t)i);
|
||||
if (e.fav) lv_obj_set_style_text_color(lv_obj_get_child(row, 0), lv_color_hex(theme::ACCENT), 0);
|
||||
if (e.fav) lv_obj_set_style_text_color(rowTitle(row), lv_color_hex(theme::ACCENT), 0);
|
||||
if (right[0]) {
|
||||
lv_obj_t* r = label(row, right, THEME_FONT_SMALL, e.is_live ? theme::OK : theme::TEXT_MUTED);
|
||||
lv_obj_align(r, LV_ALIGN_RIGHT_MID, -theme::PAD, 0);
|
||||
@@ -2531,19 +2843,7 @@ static lv_obj_t* s_kb_alt_dd = nullptr;
|
||||
|
||||
static void onKeyboardAlphabet(lv_event_t* e) {
|
||||
(void)e;
|
||||
s_ui->setKeyboardAlphabets(lv_dropdown_get_selected(s_kb_main_dd), lv_dropdown_get_selected(s_kb_alt_dd));
|
||||
}
|
||||
|
||||
// One settings row: label left, dropdown right.
|
||||
static lv_obj_t* dropdownRow(lv_obj_t* parent, const char* text, const char* options, int sel) {
|
||||
lv_obj_t* row = settingRow(parent, text, NULL);
|
||||
lv_obj_t* dd = lv_dropdown_create(row);
|
||||
lv_dropdown_set_options_static(dd, options);
|
||||
lv_dropdown_set_selected(dd, sel);
|
||||
lv_obj_set_width(dd, 130);
|
||||
lv_obj_align(dd, LV_ALIGN_RIGHT_MID, -4, 0);
|
||||
lv_obj_add_event_cb(dd, onKeyboardAlphabet, LV_EVENT_VALUE_CHANGED, NULL);
|
||||
return dd;
|
||||
s_ui->setKeyboardAlphabets(choiceSelected(s_kb_main_dd), choiceSelected(s_kb_alt_dd));
|
||||
}
|
||||
|
||||
// A preference toggle (0/1 byte in NodePrefs), saved on change.
|
||||
@@ -2578,7 +2878,7 @@ static void onSchemaSwitch(lv_event_t* e) {
|
||||
}
|
||||
static void onSchemaDropdown(lv_event_t* e) {
|
||||
s_ui->setSchemaValue((int)(uintptr_t)lv_event_get_user_data(e),
|
||||
(int)lv_dropdown_get_selected((lv_obj_t*)lv_event_get_target(e)));
|
||||
choiceSelected((lv_obj_t*)lv_event_get_target(e)));
|
||||
}
|
||||
static void onOpenSchemaPage(lv_event_t* e) { s_ui->showSchemaSettings((int)(uintptr_t)lv_event_get_user_data(e)); }
|
||||
static void onPruneContacts(lv_event_t* e) { (void)e; s_ui->pruneContacts(); }
|
||||
@@ -2590,9 +2890,11 @@ static void onVolumeSlider(lv_event_t* e) {
|
||||
s_ui->setSoundVolume((int)lv_slider_get_value((lv_obj_t*)lv_event_get_target(e)));
|
||||
}
|
||||
|
||||
|
||||
void UITask::showSchemaSettings(int page) {
|
||||
if (page == settings::PG_NAV && !_nav_back) s_opts_from_map = _screen == SCR_MAP;
|
||||
_screen = SCR_SETTINGS_NAV;
|
||||
_settings_page = (uint8_t)(page < settings::PG_COUNT ? page : 0);
|
||||
_settings_page = (uint8_t)(page < PG_ALL ? page : 0);
|
||||
buildSchemaSettings();
|
||||
}
|
||||
|
||||
@@ -2646,108 +2948,155 @@ void UITask::setTapWake(bool on) {
|
||||
}
|
||||
|
||||
void UITask::buildSchemaSettings() {
|
||||
lv_obj_t* body = newScreen(settings::pageTitle(_settings_page), true);
|
||||
static const char* const OWN[] = { "Keyboard", "About", "Display", "Power", "Time" };
|
||||
const char* title = _settings_page >= PG_KEYBOARD ? OWN[_settings_page - PG_KEYBOARD] : settings::pageTitle(_settings_page);
|
||||
lv_obj_t* body = newScreen(title, true);
|
||||
_prune_lbl = nullptr;
|
||||
_prune_armed_ms = 0;
|
||||
switch (_settings_page) {
|
||||
case PG_KEYBOARD: buildKeyboardPage(body); return;
|
||||
case PG_ABOUT: buildAboutPage(body); return;
|
||||
case PG_DISPLAY: {
|
||||
lv_obj_t* g = group(body, "SCREEN");
|
||||
schemaRow(g, SETTING(display_brightness));
|
||||
schemaRow(g, SETTING(auto_off_secs));
|
||||
schemaRow(g, SETTING(msg_wake_screen_off));
|
||||
lv_obj_t* tw = switchRow(g, "Tap to wake", "Off: only the top button wakes it", nullptr); // in NVS
|
||||
if (_tap_wake) lv_obj_add_state(tw, LV_STATE_CHECKED);
|
||||
lv_obj_add_event_cb(tw, onTapWake, LV_EVENT_VALUE_CHANGED, NULL);
|
||||
g = group(body, "LOCK");
|
||||
schemaRow(g, SETTING(auto_lock));
|
||||
listRow(g, "Screen PIN", _pin[0] ? "On - asked when the screen wakes" : "Off", onPinSetup, NULL);
|
||||
accentRow(group(body, "LOOK")); // DeviceScreen.h
|
||||
return;
|
||||
}
|
||||
case PG_POWER: {
|
||||
lv_obj_t* g = group(body, "BATTERY");
|
||||
schemaRow(g, SETTING(batt_display_mode));
|
||||
schemaRow(g, SETTING(low_batt_mv));
|
||||
g = group(body, "GPS");
|
||||
schemaRow(g, SETTING(gps_interval));
|
||||
return;
|
||||
}
|
||||
case PG_TIME: {
|
||||
lv_obj_t* g = group(body, nullptr);
|
||||
for (int i = 0; i < settings::COUNT; i++) if (settings::ALL[i].section == settings::SEC_TIME) schemaRow(g, i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
schemaRows(body, _settings_page);
|
||||
if (_settings_page == settings::PG_MESSAGES) { // the action that goes with "Contact expiry"
|
||||
lv_obj_t* b = lv_button_create(body);
|
||||
lv_obj_set_size(b, LV_PCT(100), 38);
|
||||
lv_obj_set_style_shadow_width(b, 0, 0);
|
||||
lv_obj_set_style_radius(b, theme::RADIUS, 0);
|
||||
lv_obj_set_style_bg_color(b, lv_color_hex(theme::SURFACE), 0);
|
||||
lv_obj_add_event_cb(b, onPruneContacts, LV_EVENT_CLICKED, NULL);
|
||||
_prune_lbl = label(b, LV_SYMBOL_TRASH " Remove inactive contacts now", THEME_FONT_BODY, theme::TEXT);
|
||||
lv_obj_center(_prune_lbl);
|
||||
}
|
||||
if (_settings_page == settings::PG_DEVICE) { // after Lock screen's section, in NVS not the schema
|
||||
sectionTitle(body, "WAKE");
|
||||
lv_obj_t* tw = switchRow(body, "Tap to wake", "Off: only the top button turns it on", nullptr);
|
||||
if (_tap_wake) lv_obj_add_state(tw, LV_STATE_CHECKED);
|
||||
lv_obj_add_event_cb(tw, onTapWake, LV_EVENT_VALUE_CHANGED, NULL);
|
||||
sectionTitle(body, "SECURITY");
|
||||
listRow(body, LV_SYMBOL_EYE_CLOSE " Screen PIN", _pin[0] ? "On - asked when the screen wakes" : "Off",
|
||||
onPinSetup, NULL);
|
||||
sectionTitle(body, "LOOK");
|
||||
accentRow(body); // DeviceScreen.h
|
||||
}
|
||||
if (_settings_page == settings::PG_SOUND) buildSoundRows(body, false); // the melodies
|
||||
if (_settings_page == settings::PG_MESSAGES) {
|
||||
sectionTitle(body, "QUICK MESSAGES");
|
||||
lv_obj_t* c = s_sec_card[settings::SEC_CONTACTS]; // the action that goes with "Contact expiry"
|
||||
if (c) _prune_lbl = actionRow(c, LV_SYMBOL_TRASH " Remove inactive contacts now", onPruneContacts, NULL, theme::FAIL);
|
||||
char sub[48];
|
||||
snprintf(sub, sizeof(sub), "%d of %d set - sent with one tap", msgtext::quickUsed(_prefs), msgtext::QUICK_COUNT);
|
||||
listRow(body, LV_SYMBOL_EDIT " Quick messages", sub, onOpenQuickMsgs, NULL);
|
||||
listRow(s_sec_card[settings::SEC_MESSAGES], "Quick messages", sub, onOpenQuickMsgs, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
// A page's schema rows under their section titles (Settings pages and the tools' options).
|
||||
// Settings > Keyboard: the two scripts the keyboard switches between.
|
||||
void UITask::buildKeyboardPage(lv_obj_t* body) {
|
||||
uint8_t main_a = _prefs ? _prefs->keyboard_main_alphabet : 0;
|
||||
uint8_t alt_a = _prefs ? _prefs->keyboard_alt_alphabet : 0;
|
||||
if (main_a >= NodePrefs::KB_ALPHABET_COUNT) main_a = 0;
|
||||
if (alt_a >= NodePrefs::KB_ALPHABET_COUNT) alt_a = main_a;
|
||||
lv_obj_t* g = group(body, "SCRIPTS");
|
||||
// Order matches NodePrefs::KB_ALPHABET_* (Latin, Cyrillic, Greek).
|
||||
s_kb_main_dd = choiceRow(g, "Main", nullptr, "Latin\nCyrillic\nGreek", main_a, onKeyboardAlphabet);
|
||||
// "None" = no second script (stored as alt == main, as ui-new does).
|
||||
s_kb_alt_dd = choiceRow(g, "Additional", "The globe key switches to it", "None\nLatin\nCyrillic\nGreek",
|
||||
alt_a == main_a ? 0 : alt_a + 1, onKeyboardAlphabet);
|
||||
groupNote(body, "Hold a letter for accents and other variants.");
|
||||
}
|
||||
|
||||
// Settings > About: this node, the firmware, credits.
|
||||
void UITask::buildAboutPage(lv_obj_t* body) {
|
||||
lv_obj_t* about = infoCard(body);
|
||||
infoRow(about, "Node", the_mesh.getNodeName());
|
||||
infoRow(about, "Firmware", FIRMWARE_VERSION);
|
||||
if (!strstr(FIRMWARE_VERSION, FIRMWARE_BUILD_DATE)) infoRow(about, "Built", FIRMWARE_BUILD_DATE);
|
||||
sectionTitle(body, "CREDITS");
|
||||
lv_obj_t* cr = infoCard(body);
|
||||
infoNote(cr, "Map data", creditText((lvport::mountStorage() && mapview::s_provider->available())
|
||||
? mapview::s_provider->attribution() : "\xC2\xA9 OpenStreetMap contributors"));
|
||||
infoNote(cr, "Emoji", creditText("Twemoji \xC2\xA9 Twitter, Inc. and contributors (CC-BY\xC2\xA0" "4.0)"));
|
||||
}
|
||||
|
||||
// A page's schema rows, one group per section (Settings pages and the tools'
|
||||
// options). Each section's card is kept in s_sec_card for rows added after.
|
||||
|
||||
void UITask::schemaRows(lv_obj_t* body, uint8_t page) {
|
||||
uint8_t sec = 0xFF;
|
||||
lv_obj_t* card = nullptr;
|
||||
for (lv_obj_t*& c : s_sec_card) c = nullptr;
|
||||
for (int i = 0; i < settings::COUNT; i++) {
|
||||
const settings::Setting& st = settings::ALL[i];
|
||||
if (settings::sectionPage(st.section) != page) continue;
|
||||
if (st.section != sec) {
|
||||
sec = st.section;
|
||||
sectionTitle(body, settings::sectionTitle(sec));
|
||||
if (sec == settings::SEC_SOUND) buildSoundRows(body, true); // On / Off / Auto
|
||||
card = s_sec_card[sec] = group(body, settings::sectionTitle(sec));
|
||||
if (sec == settings::SEC_SOUND) buildSoundRows(card, true); // On / Off / Auto
|
||||
}
|
||||
uint8_t v = settings::get(*_prefs, st);
|
||||
if (st.offset == offsetof(NodePrefs, buzzer_volume)) { // a five-step slider, heard on release
|
||||
lv_obj_t* row = settingRow(body, st.label, NULL);
|
||||
lv_obj_t* sl = lv_slider_create(row);
|
||||
lv_slider_set_range(sl, 0, 4);
|
||||
lv_slider_set_value(sl, v, LV_ANIM_OFF);
|
||||
lv_obj_set_size(sl, 170, 10);
|
||||
lv_obj_align(sl, LV_ALIGN_RIGHT_MID, -18, 0);
|
||||
lv_obj_set_ext_click_area(sl, 14);
|
||||
lv_obj_add_event_cb(sl, onVolumeSlider, LV_EVENT_RELEASED, NULL);
|
||||
continue;
|
||||
}
|
||||
if (st.offset == offsetof(NodePrefs, display_brightness)) { // a slider here instead of five steps
|
||||
lv_obj_t* row = settingRow(body, st.label, NULL);
|
||||
lv_obj_t* sl = lv_slider_create(row);
|
||||
lv_slider_set_range(sl, 5, 100);
|
||||
uint8_t pct = _prefs->display_brightness_pct ? _prefs->display_brightness_pct
|
||||
: (uint8_t)(_prefs->display_brightness * 25 > 5 ? _prefs->display_brightness * 25 : 5);
|
||||
lv_slider_set_value(sl, pct, LV_ANIM_OFF);
|
||||
lv_obj_set_size(sl, 170, 10);
|
||||
lv_obj_align(sl, LV_ALIGN_RIGHT_MID, -18, 0);
|
||||
lv_obj_set_ext_click_area(sl, 14);
|
||||
lv_obj_add_event_cb(sl, onBrightnessSlider, LV_EVENT_VALUE_CHANGED, NULL);
|
||||
lv_obj_add_event_cb(sl, onBrightnessSlider, LV_EVENT_RELEASED, NULL);
|
||||
continue;
|
||||
}
|
||||
if (!st.option) {
|
||||
lv_obj_t* sw = switchRow(body, st.label, st.hint, nullptr);
|
||||
if (v) lv_obj_add_state(sw, LV_STATE_CHECKED);
|
||||
lv_obj_add_event_cb(sw, onSchemaSwitch, LV_EVENT_VALUE_CHANGED, (void*)(uintptr_t)i);
|
||||
continue;
|
||||
}
|
||||
char opts[160];
|
||||
int o = 0;
|
||||
for (uint8_t k = 0; k < st.count && o < (int)sizeof(opts) - 16; k++) {
|
||||
if (k) opts[o++] = '\n';
|
||||
st.option(k, opts + o, sizeof(opts) - o, *_prefs);
|
||||
o += strlen(opts + o);
|
||||
}
|
||||
opts[o] = '\0';
|
||||
lv_obj_t* row = settingRow(body, st.label, st.hint, 176);
|
||||
lv_obj_t* dd = lv_dropdown_create(row);
|
||||
lv_dropdown_set_options(dd, opts);
|
||||
lv_dropdown_set_selected(dd, v);
|
||||
lv_obj_set_width(dd, 112);
|
||||
lv_obj_align(dd, LV_ALIGN_RIGHT_MID, -4, 0);
|
||||
lv_obj_add_event_cb(dd, onSchemaDropdown, LV_EVENT_VALUE_CHANGED, (void*)(uintptr_t)i);
|
||||
schemaRow(card, i);
|
||||
}
|
||||
}
|
||||
|
||||
// One schema setting as a row of `card`: a switch, a choice, or (brightness,
|
||||
// volume) a slider.
|
||||
void UITask::schemaRow(lv_obj_t* card, int i) {
|
||||
if (i < 0 || i >= settings::COUNT) return;
|
||||
const settings::Setting& st = settings::ALL[i];
|
||||
uint8_t v = settings::get(*_prefs, st);
|
||||
if (st.offset == offsetof(NodePrefs, buzzer_volume)) { // a five-step slider, heard on release
|
||||
lv_obj_t* row = settingRow(card, st.label, NULL);
|
||||
lv_obj_t* sl = lv_slider_create(row);
|
||||
lv_slider_set_range(sl, 0, 4);
|
||||
lv_slider_set_value(sl, v, LV_ANIM_OFF);
|
||||
lv_obj_set_size(sl, 150, 8);
|
||||
lv_obj_set_style_margin_right(sl, 8, 0);
|
||||
lv_obj_set_ext_click_area(sl, 14);
|
||||
lv_obj_add_event_cb(sl, onVolumeSlider, LV_EVENT_RELEASED, NULL);
|
||||
return;
|
||||
}
|
||||
if (st.offset == offsetof(NodePrefs, display_brightness)) { // a slider here instead of five steps
|
||||
lv_obj_t* row = settingRow(card, st.label, NULL);
|
||||
lv_obj_t* sl = lv_slider_create(row);
|
||||
lv_slider_set_range(sl, 5, 100);
|
||||
uint8_t pct = _prefs->display_brightness_pct ? _prefs->display_brightness_pct
|
||||
: (uint8_t)(_prefs->display_brightness * 25 > 5 ? _prefs->display_brightness * 25 : 5);
|
||||
lv_slider_set_value(sl, pct, LV_ANIM_OFF);
|
||||
lv_obj_set_size(sl, 150, 8);
|
||||
lv_obj_set_style_margin_right(sl, 8, 0);
|
||||
lv_obj_set_ext_click_area(sl, 14);
|
||||
lv_obj_add_event_cb(sl, onBrightnessSlider, LV_EVENT_VALUE_CHANGED, NULL);
|
||||
lv_obj_add_event_cb(sl, onBrightnessSlider, LV_EVENT_RELEASED, NULL);
|
||||
return;
|
||||
}
|
||||
if (!st.option) {
|
||||
lv_obj_t* sw = switchRow(card, st.label, st.hint, nullptr);
|
||||
if (v) lv_obj_add_state(sw, LV_STATE_CHECKED);
|
||||
lv_obj_add_event_cb(sw, onSchemaSwitch, LV_EVENT_VALUE_CHANGED, (void*)(uintptr_t)i);
|
||||
return;
|
||||
}
|
||||
char opts[160];
|
||||
int o = 0;
|
||||
for (uint8_t k = 0; k < st.count && o < (int)sizeof(opts) - 16; k++) {
|
||||
if (k) opts[o++] = '\n';
|
||||
st.option(k, opts + o, sizeof(opts) - o, *_prefs);
|
||||
o += strlen(opts + o);
|
||||
}
|
||||
opts[o] = '\0';
|
||||
choiceRow(card, st.label, st.hint, opts, v, onSchemaDropdown, (void*)(uintptr_t)i);
|
||||
}
|
||||
|
||||
void UITask::setSchemaValue(int idx, int v) {
|
||||
if (!_prefs || idx < 0 || idx >= settings::COUNT) return;
|
||||
const settings::Setting& st = settings::ALL[idx];
|
||||
settings::set(*_prefs, st, (uint8_t)v);
|
||||
if (st.changed) st.changed(*_core);
|
||||
the_mesh.savePrefs();
|
||||
if (st.offset == offsetof(NodePrefs, units_imperial)) { // other labels depend on it
|
||||
if (st.offset == offsetof(NodePrefs, units_imperial) && _screen == SCR_SETTINGS_NAV) { // other labels depend on it
|
||||
lv_obj_t* body = _body;
|
||||
int32_t y = body ? lv_obj_get_scroll_y(body) : 0;
|
||||
buildSchemaSettings();
|
||||
@@ -2763,68 +3112,77 @@ void UITask::showSettings() {
|
||||
static void onOpenRadio(lv_event_t* e); // RadioScreen.h
|
||||
static void onOpenStorage(lv_event_t* e); // StorageScreen.h
|
||||
|
||||
// Settings: groups of rows, each opening a page (chevron) or switching
|
||||
// something on the spot. What the app is for first, the system last.
|
||||
void UITask::buildSettings() {
|
||||
// The order of the original (ui-new): display, sound, radio, system,
|
||||
// keyboard, contacts and messages. Tools have their own Home tiles.
|
||||
bool restore = _nav_back; // back from one of its pages: where it was
|
||||
lv_obj_t* body = newScreen("Settings", true);
|
||||
char sub[48];
|
||||
lv_obj_t* g;
|
||||
char v1[24], v2[24];
|
||||
if (_prefs) {
|
||||
sectionTitle(body, "DISPLAY");
|
||||
listRow(body, LV_SYMBOL_EYE_OPEN " Display & power", "Screen, battery, time, units",
|
||||
onOpenSchemaPage, (void*)(uintptr_t)settings::PG_DEVICE);
|
||||
sectionTitle(body, "SOUND");
|
||||
char sub[48], vol[12];
|
||||
g = group(body, "DEVICE");
|
||||
listRow(g, LV_SYMBOL_EYE_OPEN " Display", "Brightness, screen off, lock, colour",
|
||||
onOpenSchemaPage, (void*)(uintptr_t)PG_DISPLAY);
|
||||
snprintf(sub, sizeof(sub), "Shutdown %s - GPS: %s", settingText(*_prefs, SETTING(low_batt_mv), v1, sizeof(v1)),
|
||||
settingText(*_prefs, SETTING(gps_interval), v2, sizeof(v2)));
|
||||
listRow(g, LV_SYMBOL_BATTERY_FULL " Power", sub, onOpenSchemaPage, (void*)(uintptr_t)PG_POWER);
|
||||
char vol[12];
|
||||
settings::optVolume(_prefs->buzzer_volume, vol, sizeof(vol), *_prefs);
|
||||
snprintf(sub, sizeof(sub), "%s, %s - alerts, melodies", soundctl::modeLabel(soundctl::mode(_prefs)), vol);
|
||||
listRow(body, LV_SYMBOL_VOLUME_MAX " Sound", sub, onOpenSchemaPage, (void*)(uintptr_t)settings::PG_SOUND);
|
||||
sectionTitle(body, "RADIO");
|
||||
snprintf(sub, sizeof(sub), "%s, %s", soundctl::modeLabel(soundctl::mode(_prefs)), vol);
|
||||
listRow(g, LV_SYMBOL_VOLUME_MAX " Sound", sub, onOpenSchemaPage, (void*)(uintptr_t)settings::PG_SOUND);
|
||||
static const char* const SCRIPT[] = { "Latin", "Cyrillic", "Greek" };
|
||||
uint8_t ma = _prefs->keyboard_main_alphabet % NodePrefs::KB_ALPHABET_COUNT;
|
||||
uint8_t aa = _prefs->keyboard_alt_alphabet % NodePrefs::KB_ALPHABET_COUNT;
|
||||
snprintf(sub, sizeof(sub), aa == ma ? "%s" : "%s + %s", SCRIPT[ma], SCRIPT[aa]);
|
||||
listRow(g, LV_SYMBOL_KEYBOARD " Keyboard", sub, onOpenSchemaPage, (void*)(uintptr_t)PG_KEYBOARD);
|
||||
listRow(g, LV_SYMBOL_ENVELOPE " Messages & contacts", "Resend, expiry, quick messages",
|
||||
onOpenSchemaPage, (void*)(uintptr_t)settings::PG_MESSAGES);
|
||||
}
|
||||
|
||||
g = group(body, "CONNECTIONS");
|
||||
if (_prefs) {
|
||||
int pi = radioctl::currentPreset(_prefs);
|
||||
const char* pn = "Custom"; float f, b; uint8_t sf, cr;
|
||||
if (pi >= 0) radioctl::presetAt(_prefs, pi, pn, f, b, sf, cr);
|
||||
snprintf(sub, sizeof(sub), "%s - %.3f MHz, %d dBm", pn, _prefs->freq, _prefs->tx_power_dbm);
|
||||
listRow(body, UI_SYMBOL_RADIO " Radio", sub, onOpenRadio, NULL);
|
||||
} else {
|
||||
sectionTitle(body, "RADIO");
|
||||
listRow(g, UI_SYMBOL_RADIO " Radio", sub, onOpenRadio, NULL);
|
||||
}
|
||||
bluetoothRow(body);
|
||||
wifiRow(body);
|
||||
sectionTitle(body, "SYSTEM");
|
||||
listRow(body, LV_SYMBOL_EDIT " Name", the_mesh.getNodeName(), onNodeName, NULL);
|
||||
listRow(body, LV_SYMBOL_DOWNLOAD " Firmware update", FIRMWARE_VERSION, onOpenOta, NULL);
|
||||
if (_core->gpsAvailable()) {
|
||||
lv_obj_t* sw = switchRow(body, "GPS", "For maps and sharing", nullptr);
|
||||
bluetoothRow(g);
|
||||
wifiRow(g);
|
||||
if (_core->gpsAvailable()) { // as WiFi: the switch turns it on / off, the row opens its details
|
||||
lv_obj_t* sw = switchRow(g, LV_SYMBOL_GPS " GPS", "Tap for satellites and signal", nullptr);
|
||||
if (_core->gpsEnabled()) lv_obj_add_state(sw, LV_STATE_CHECKED);
|
||||
lv_obj_add_event_cb(sw, onGpsSwitch, LV_EVENT_VALUE_CHANGED, NULL);
|
||||
listRow(body, LV_SYMBOL_GPS " GPS details", "Satellites, signal, sky view", onOpenGpsFromSettings, NULL);
|
||||
lv_obj_t* row = lv_obj_get_parent(sw);
|
||||
lv_obj_add_flag(row, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_set_style_bg_color(row, lv_color_hex(theme::SURFACE_2), LV_STATE_PRESSED);
|
||||
lv_obj_set_style_bg_opa(row, LV_OPA_COVER, LV_STATE_PRESSED);
|
||||
lv_obj_add_event_cb(row, [](lv_event_t* e) {
|
||||
if (lv_event_get_target(e) == lv_event_get_current_target(e)) onOpenGpsFromSettings(e); // not the switch
|
||||
}, LV_EVENT_CLICKED, NULL);
|
||||
}
|
||||
listRow(body, LV_SYMBOL_SD_CARD " Storage", "SD card, message history", onOpenStorage, NULL);
|
||||
listRow(body, LV_SYMBOL_REFRESH " Reboot", NULL, onPowerRow, (void*)(uintptr_t)1);
|
||||
listRow(body, LV_SYMBOL_POWER " Power off", NULL, onPowerRow, (void*)(uintptr_t)0);
|
||||
sectionTitle(body, "KEYBOARD");
|
||||
uint8_t main_a = _prefs ? _prefs->keyboard_main_alphabet : 0;
|
||||
uint8_t alt_a = _prefs ? _prefs->keyboard_alt_alphabet : 0;
|
||||
if (main_a >= NodePrefs::KB_ALPHABET_COUNT) main_a = 0;
|
||||
if (alt_a >= NodePrefs::KB_ALPHABET_COUNT) alt_a = main_a;
|
||||
// Order matches NodePrefs::KB_ALPHABET_* (Latin, Cyrillic, Greek).
|
||||
s_kb_main_dd = dropdownRow(body, "Main", "Latin\nCyrillic\nGreek", main_a);
|
||||
// "None" = no second script (stored as alt == main, as ui-new does).
|
||||
s_kb_alt_dd = dropdownRow(body, "Additional", "None\nLatin\nCyrillic\nGreek",
|
||||
alt_a == main_a ? 0 : alt_a + 1);
|
||||
label(body, "Hold a letter for accents and other variants.", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
|
||||
g = group(body, "MAP & DATA");
|
||||
if (_prefs) listRow(g, UI_SYMBOL_MAP " Map", "Trail, live sharing, arrival alert",
|
||||
onOpenSchemaPage, (void*)(uintptr_t)settings::PG_NAV);
|
||||
listRow(g, LV_SYMBOL_SD_CARD " Storage", "SD card, message history", onOpenStorage, NULL);
|
||||
|
||||
g = group(body, "SYSTEM");
|
||||
listRow(g, LV_SYMBOL_EDIT " Name", the_mesh.getNodeName(), onNodeName, NULL);
|
||||
if (_prefs) {
|
||||
sectionTitle(body, "CONTACTS & MESSAGES");
|
||||
listRow(body, LV_SYMBOL_ENVELOPE " Messages & contacts", "Resend, expiry, quick messages",
|
||||
onOpenSchemaPage, (void*)(uintptr_t)settings::PG_MESSAGES);
|
||||
snprintf(sub, sizeof(sub), "%s, %s", settingText(*_prefs, SETTING(tz_offset_hours), v1, sizeof(v1)),
|
||||
_prefs->clock_12h ? "12 h" : "24 h");
|
||||
listRow(g, UI_SYMBOL_CLOCK " Time", sub, onOpenSchemaPage, (void*)(uintptr_t)PG_TIME);
|
||||
schemaRow(g, SETTING(units_imperial));
|
||||
}
|
||||
listRow(g, LV_SYMBOL_DOWNLOAD " Firmware update", FIRMWARE_VERSION, onOpenOta, NULL);
|
||||
listRow(g, LV_SYMBOL_LIST " About", "Node, firmware, credits", onOpenSchemaPage, (void*)(uintptr_t)PG_ABOUT);
|
||||
|
||||
sectionTitle(body, "ABOUT");
|
||||
lv_obj_t* about = infoCard(body);
|
||||
infoRow(about, "Node", the_mesh.getNodeName());
|
||||
infoRow(about, "Firmware", FIRMWARE_VERSION);
|
||||
if (!strstr(FIRMWARE_VERSION, FIRMWARE_BUILD_DATE)) infoRow(about, "Built", FIRMWARE_BUILD_DATE);
|
||||
infoNote(about, "Map data", creditText((lvport::mountStorage() && mapview::s_provider->available())
|
||||
? mapview::s_provider->attribution() : "\xC2\xA9 OpenStreetMap contributors"));
|
||||
infoNote(about, "Emoji", creditText("Twemoji \xC2\xA9 Twitter, Inc. and contributors (CC-BY\xC2\xA0" "4.0)"));
|
||||
g = group(body, nullptr);
|
||||
actionRow(g, LV_SYMBOL_REFRESH " Reboot", onPowerRow, (void*)(uintptr_t)1);
|
||||
actionRow(g, LV_SYMBOL_POWER " Power off", onPowerRow, (void*)(uintptr_t)0, theme::FAIL);
|
||||
if (restore) { lv_obj_update_layout(body); lv_obj_scroll_to_y(body, s_settings_y, LV_ANIM_OFF); }
|
||||
}
|
||||
|
||||
void UITask::setKeyboardAlphabets(int main_idx, int alt_sel) {
|
||||
@@ -2860,5 +3218,25 @@ extern "C" EMSCRIPTEN_KEEPALIVE void sim_open(const char* name) {
|
||||
{ "quick", &UITask::showQuickMsgs },
|
||||
};
|
||||
for (auto& s : SCREENS) if (!strcmp(s.n, name)) { (s_ui->*s.fn)(); return; }
|
||||
if (!strncmp(name, "page", 4)) { s_ui->showSchemaSettings(atoi(name + 4)); return; }
|
||||
if (!strcmp(name, "map")) { s_ui->openMap(true); return; }
|
||||
if (!strcmp(name, "maptools")) { s_ui->navToolsPopup(); return; }
|
||||
if (!strcmp(name, "advert")) { s_ui->advertPopup(); return; }
|
||||
}
|
||||
// Scrolls the screen's (or a popup's) main list by dy; returns what was left to scroll.
|
||||
static void dbgScrollable(lv_obj_t* o, lv_obj_t*& best, int& h) {
|
||||
if (lv_obj_has_flag(o, LV_OBJ_FLAG_HIDDEN) || lv_obj_check_type(o, &lv_roller_class) || lv_obj_check_type(o, &lv_dropdownlist_class)) return;
|
||||
int v = lv_obj_get_scroll_bottom(o) + lv_obj_get_scroll_y(o);
|
||||
if (lv_obj_has_flag(o, LV_OBJ_FLAG_SCROLLABLE) && v > h) { h = v; best = o; }
|
||||
for (uint32_t i = 0; i < lv_obj_get_child_count(o); i++) dbgScrollable(lv_obj_get_child(o, i), best, h);
|
||||
}
|
||||
extern "C" EMSCRIPTEN_KEEPALIVE int sim_scroll(int dy) {
|
||||
lv_obj_t* o = nullptr; int h = 10;
|
||||
dbgScrollable(lv_layer_top(), o, h);
|
||||
if (!o) dbgScrollable(lv_screen_active(), o, h);
|
||||
if (!o) return -1;
|
||||
int left = lv_obj_get_scroll_bottom(o);
|
||||
lv_obj_scroll_by(o, 0, -(dy < left ? dy : left), LV_ANIM_OFF);
|
||||
return left;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -271,6 +271,9 @@ private:
|
||||
void buildSettings();
|
||||
void buildSchemaSettings();
|
||||
void schemaRows(lv_obj_t* body, uint8_t page);
|
||||
void schemaRow(lv_obj_t* card, int idx);
|
||||
void buildKeyboardPage(lv_obj_t* body);
|
||||
void buildAboutPage(lv_obj_t* body);
|
||||
void buildAdminPick();
|
||||
void buildOta();
|
||||
void refreshOta();
|
||||
|
||||
@@ -128,6 +128,8 @@ static void wifiRow(lv_obj_t* body) {
|
||||
lv_obj_add_event_cb(sw, onWifiAllowed, LV_EVENT_VALUE_CHANGED, NULL);
|
||||
lv_obj_t* row = lv_obj_get_parent(sw);
|
||||
lv_obj_add_flag(row, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_set_style_bg_color(row, lv_color_hex(theme::SURFACE_2), LV_STATE_PRESSED);
|
||||
lv_obj_set_style_bg_opa(row, LV_OPA_COVER, LV_STATE_PRESSED);
|
||||
lv_obj_add_event_cb(row, [](lv_event_t* e) {
|
||||
if (lv_event_get_target(e) == lv_event_get_current_target(e)) s_ui->showWifi(); // not the switch
|
||||
}, LV_EVENT_CLICKED, NULL);
|
||||
|
||||
Reference in New Issue
Block a user