#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; 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(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); } 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."); 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(g, "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); 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(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) { 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; } }