mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-10-09 03:06:39 +00:00
244 lines
11 KiB
C++
244 lines
11 KiB
C++
#pragma once
|
|
// Settings > Radio, the parts beyond the radio parameters -- ui-new's
|
|
// Settings > Radio preset picker ("+ Save current..." / "- Delete preset...")
|
|
// and Settings > Radio > Scope list:
|
|
// - My presets: the user's saved presets (NodePrefs::user_radio_presets, 4
|
|
// slots; saving is ui-core/RadioControl.h). Tap one to use or delete it;
|
|
// "Save current settings" names the current parameters as a new one. They
|
|
// also appear in the Preset dropdown above.
|
|
// - Scopes: the shared named-scope list (ScopeList.h, edited through
|
|
// MyMesh). "*" is no scope; the default one goes on direct messages and
|
|
// relaying, channels pick their own in their options.
|
|
//
|
|
// Single-TU fragment: included at the end of ui-lvgl/UITask.cpp.
|
|
|
|
namespace radiox {
|
|
|
|
enum : uint8_t { N_PRESET, N_SCOPE_ADD, N_SCOPE_RENAME }; // what the name popup names
|
|
enum : uint8_t { P_USE, P_DELETE, P_DELETE_GO }; // preset popup actions
|
|
enum : uint8_t { S_DEFAULT, S_RENAME, S_DELETE, S_DELETE_GO }; // scope popup actions
|
|
|
|
static uint8_t s_naming = N_PRESET;
|
|
static int s_slot = -1; // user preset slot the popup is about
|
|
static int s_scope = -1; // scope list index the popup is about
|
|
|
|
static lv_obj_t* redButton(lv_obj_t* panel, const char* text, lv_event_cb_t cb, uint8_t act) {
|
|
lv_obj_t* b = lv_button_create(panel);
|
|
lv_obj_set_size(b, LV_PCT(100), 40);
|
|
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::FAIL), 0);
|
|
lv_obj_add_event_cb(b, cb, LV_EVENT_CLICKED, (void*)(uintptr_t)act);
|
|
lv_obj_center(label(b, text, THEME_FONT_BODY, theme::TEXT));
|
|
return b;
|
|
}
|
|
|
|
static lv_obj_t* note(lv_obj_t* parent, const char* text) {
|
|
lv_obj_t* t = label(parent, text, THEME_FONT_SMALL, theme::TEXT_MUTED);
|
|
lv_label_set_long_mode(t, LV_LABEL_LONG_WRAP);
|
|
lv_obj_set_width(t, LV_PCT(100));
|
|
return t;
|
|
}
|
|
|
|
} // namespace radiox
|
|
|
|
static void onPresetRow(lv_event_t* e) { s_ui->presetMenu((int)(uintptr_t)lv_event_get_user_data(e)); }
|
|
static void onPresetAction(lv_event_t* e) { s_ui->presetAction((uint8_t)(uintptr_t)lv_event_get_user_data(e)); }
|
|
static void onPresetSave(lv_event_t* e) { (void)e; s_ui->radioNamePopup(radiox::N_PRESET); }
|
|
static void onRadioNameKb(lv_event_t* e) { s_ui->radioNameDone(lv_event_get_code(e) == LV_EVENT_READY); }
|
|
static void onOpenScopes(lv_event_t* e) { (void)e; s_ui->showScopes(); }
|
|
static void onScopeRow(lv_event_t* e) { s_ui->scopeMenu((int)(uintptr_t)lv_event_get_user_data(e)); }
|
|
static void onScopeAdd(lv_event_t* e) { (void)e; s_ui->radioNamePopup(radiox::N_SCOPE_ADD); }
|
|
static void onScopeAction(lv_event_t* e) { s_ui->scopeAction((uint8_t)(uintptr_t)lv_event_get_user_data(e)); }
|
|
|
|
// The Radio screen's lower sections, under the parameters.
|
|
void UITask::buildRadioExtras(lv_obj_t* body) {
|
|
NodePrefs* p = _prefs;
|
|
sectionTitle(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);
|
|
}
|
|
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);
|
|
|
|
sectionTitle(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);
|
|
}
|
|
|
|
// ── My presets ────────────────────────────────────────────────────────────────
|
|
|
|
void UITask::presetMenu(int slot) {
|
|
if (!_prefs || slot < 0 || slot >= NodePrefs::USER_RADIO_PRESET_MAX) return;
|
|
const NodePrefs::UserRadioPreset& u = _prefs->user_radio_presets[slot];
|
|
if (!u.name[0]) return;
|
|
radiox::s_slot = slot;
|
|
lv_obj_t* panel = navPopupPanel(u.name, false);
|
|
char info[64];
|
|
snprintf(info, sizeof(info), "%.3f MHz, SF%u, BW %g kHz, CR 4/%u", u.freq, (unsigned)u.sf, (double)u.bw, (unsigned)u.cr);
|
|
label(panel, info, THEME_FONT_BODY, theme::TEXT);
|
|
lv_obj_t* acts = convview::actionRow(panel);
|
|
convview::actionButton(acts, LV_SYMBOL_OK " Use", onPresetAction, radiox::P_USE, true);
|
|
convview::actionButton(acts, LV_SYMBOL_TRASH " Delete", onPresetAction, radiox::P_DELETE, false);
|
|
}
|
|
|
|
void UITask::presetAction(uint8_t act) {
|
|
using namespace radiox;
|
|
NodePrefs* p = _prefs;
|
|
if (!p || s_slot < 0 || !p->user_radio_presets[s_slot].name[0]) { navClosePopup(); return; }
|
|
NodePrefs::UserRadioPreset& u = p->user_radio_presets[s_slot];
|
|
switch (act) {
|
|
case P_USE:
|
|
p->freq = u.freq; p->bw = u.bw; p->sf = u.sf; p->cr = u.cr;
|
|
radioctl::applyParams();
|
|
the_mesh.savePrefs();
|
|
navClosePopup();
|
|
rebuildRadio();
|
|
showToast("Preset in use");
|
|
break;
|
|
case P_DELETE: {
|
|
char t[40];
|
|
snprintf(t, sizeof(t), "Delete %s?", u.name);
|
|
lv_obj_t* panel = navPopupPanel(t, false);
|
|
note(panel, "Removes the saved preset. The radio settings stay as they are.");
|
|
redButton(panel, LV_SYMBOL_TRASH " Delete", onPresetAction, P_DELETE_GO);
|
|
break;
|
|
}
|
|
case P_DELETE_GO:
|
|
radioctl::deleteUserPreset(p, s_slot);
|
|
the_mesh.savePrefs();
|
|
navClosePopup();
|
|
rebuildRadio();
|
|
showToast("Preset deleted");
|
|
break;
|
|
}
|
|
}
|
|
|
|
// ── Name entry: a new preset, a new scope, a scope's new name ────────────────
|
|
|
|
void UITask::radioNamePopup(uint8_t what) {
|
|
using namespace radiox;
|
|
if (!_prefs) return;
|
|
s_naming = what;
|
|
const char* title = what == N_PRESET ? "Preset name" : what == N_SCOPE_ADD ? "New scope" : "Scope name";
|
|
const char* hint = what == N_PRESET ? "e.g. Hiking" : "Region name, e.g. pl-maz";
|
|
size_t cap = what == N_PRESET ? sizeof(_prefs->user_radio_presets[0].name) - 1 : sizeof(ScopeEntry::name) - 1;
|
|
lv_obj_t* panel = navPopupPanel(title, false);
|
|
lv_obj_align(panel, LV_ALIGN_TOP_MID, 0, theme::STATUS_H + 4); // above the keyboard
|
|
_nav_ta = wifiField(panel, hint, false);
|
|
lv_obj_remove_event_cb(_nav_ta, onWifiField);
|
|
if (what == N_SCOPE_RENAME) lv_textarea_set_text(_nav_ta, the_mesh.scopeList().name((uint8_t)s_scope));
|
|
lv_obj_add_event_cb(_nav_ta, devview::onByteCapInsert, LV_EVENT_INSERT, (void*)(uintptr_t)cap);
|
|
lv_obj_add_state(_nav_ta, LV_STATE_FOCUSED); // draws the cursor
|
|
_nav_kb = kb::create(_nav_overlay, _prefs);
|
|
lv_obj_set_size(_nav_kb, LV_PCT(100), 124);
|
|
lv_obj_align(_nav_kb, LV_ALIGN_BOTTOM_MID, 0, 0);
|
|
lv_keyboard_set_textarea(_nav_kb, _nav_ta);
|
|
lv_obj_add_event_cb(_nav_kb, onRadioNameKb, LV_EVENT_READY, NULL);
|
|
lv_obj_add_event_cb(_nav_kb, onRadioNameKb, LV_EVENT_CANCEL, NULL);
|
|
}
|
|
|
|
void UITask::radioNameDone(bool ok) {
|
|
using namespace radiox;
|
|
if (!ok || !_nav_ta || !_prefs) { navClosePopup(); return; }
|
|
char name[32];
|
|
snprintf(name, sizeof(name), "%s", lv_textarea_get_text(_nav_ta));
|
|
if (!name[0]) { showToast("Name can't be empty"); return; }
|
|
const char* toast = nullptr;
|
|
if (s_naming == N_PRESET) {
|
|
NodePrefs* p = _prefs;
|
|
radioctl::saveUserPreset(p, name, p->freq, p->bw, p->sf, p->cr);
|
|
the_mesh.savePrefs();
|
|
toast = "Preset saved";
|
|
} else if (s_naming == N_SCOPE_ADD) {
|
|
toast = the_mesh.addScope(name) ? "Scope added" : "The list is full";
|
|
} else {
|
|
the_mesh.renameScope((uint8_t)s_scope, name);
|
|
toast = "Scope renamed";
|
|
}
|
|
navClosePopup();
|
|
if (_screen == SCR_RADIO) rebuildRadio();
|
|
else if (_screen == SCR_SCOPES) buildScopes();
|
|
showToast(toast);
|
|
}
|
|
|
|
// ── Scopes ────────────────────────────────────────────────────────────────────
|
|
|
|
void UITask::showScopes() {
|
|
_screen = SCR_SCOPES;
|
|
buildScopes();
|
|
}
|
|
|
|
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.");
|
|
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);
|
|
}
|
|
}
|
|
|
|
void UITask::scopeMenu(int idx) {
|
|
const ScopeList& sl = the_mesh.scopeList();
|
|
if (idx < 0 || idx > sl.count) return;
|
|
radiox::s_scope = idx;
|
|
bool def = idx == sl.default_idx;
|
|
lv_obj_t* panel = navPopupPanel(idx == 0 ? "* (no scope)" : sl.name((uint8_t)idx), false);
|
|
if (def) radiox::note(panel, "This is the default: direct messages and relaying use it.");
|
|
if (idx == 0 && def) return; // nothing else to do with "*"
|
|
lv_obj_t* acts = convview::actionRow(panel);
|
|
if (!def) convview::actionButton(acts, LV_SYMBOL_OK " Default", onScopeAction, radiox::S_DEFAULT, true);
|
|
if (idx > 0) {
|
|
convview::actionButton(acts, LV_SYMBOL_EDIT " Rename", onScopeAction, radiox::S_RENAME, false);
|
|
convview::actionButton(acts, LV_SYMBOL_TRASH " Delete", onScopeAction, radiox::S_DELETE, false);
|
|
}
|
|
}
|
|
|
|
void UITask::scopeAction(uint8_t act) {
|
|
using namespace radiox;
|
|
const ScopeList& sl = the_mesh.scopeList();
|
|
if (s_scope < 0 || s_scope > sl.count) { navClosePopup(); return; }
|
|
switch (act) {
|
|
case S_DEFAULT:
|
|
the_mesh.setDefaultScope((uint8_t)s_scope);
|
|
navClosePopup();
|
|
buildScopes();
|
|
showToast("Default for direct messages and relaying");
|
|
break;
|
|
case S_RENAME:
|
|
if (s_scope > 0) radioNamePopup(N_SCOPE_RENAME);
|
|
break;
|
|
case S_DELETE: {
|
|
if (s_scope == 0) break;
|
|
char t[40];
|
|
snprintf(t, sizeof(t), "Delete %s?", sl.name((uint8_t)s_scope));
|
|
lv_obj_t* panel = navPopupPanel(t, false);
|
|
note(panel, "Channels using it go back to no scope. If it is the default, * becomes the default.");
|
|
redButton(panel, LV_SYMBOL_TRASH " Delete", onScopeAction, S_DELETE_GO);
|
|
break;
|
|
}
|
|
case S_DELETE_GO:
|
|
if (s_scope > 0) the_mesh.removeScope((uint8_t)s_scope);
|
|
navClosePopup();
|
|
buildScopes();
|
|
showToast("Scope deleted");
|
|
break;
|
|
}
|
|
}
|