Files
MeshCore-Solo/examples/companion_radio/ui-new/RadioPresetPicker.h
MarekZegare4 0e333aeb5e refactor(ui): share radio preset picker between Settings and Repeater
Settings › Radio and Tools › Repeater had near-identical preset-picker
logic (name/list-index lookup, save/delete, popup build, selection
handling) plus their own copy of the nearest-bandwidth search — only the
target fields (companion params vs. dedicated repeater profile) and the
apply call differed.

Extract the shared logic into RadioPresetPicker (a Target of field
pointers + a Result the screen acts on), with no dependency on UITask so
each screen keeps its own apply/dirty/keyboard/alert handling. Move the
nearest-bandwidth search to nearestBwIndex() in RadioPresets.h. Removes
~6 duplicated methods and 4 state fields from each screen.

Verified: WioTrackerL1_companion_solo_dual builds clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 22:32:47 +02:00

163 lines
6.3 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#pragma once
#include <string.h>
#include "PopupMenu.h"
#include "../RadioPresets.h"
#include "../NodePrefs.h"
// Shared "radio preset" picker used by both Settings Radio and Tools
// Repeater. Both edit the same 4 user preset slots in NodePrefs and build the
// same popup layout; they differ only in which freq/bw/sf/cr fields they target
// (the companion's own params vs. the dedicated repeater profile) and in what
// runs when a value is applied. This struct owns the popup + save/delete state
// and the lookup/save logic; the owning screen passes a Target pointing at its
// fields and handles apply()/dirty/keyboard/alert on the Result it returns.
//
// Popup layout (built by open()):
// [0] = "+ Save current..."
// [1 .. RADIO_PRESET_COUNT] = built-in presets
// [.. + user_count] = saved user presets (empty slots skipped)
// [last, if user_count > 0] = "- Delete preset..."
struct RadioPresetPicker {
// The four radio fields this picker reads and writes.
struct Target {
float* freq;
float* bw;
uint8_t* sf;
uint8_t* cr;
};
// What the owning screen should do after a SELECTED item is handled.
enum Result {
NONE, // nothing to do (sub-menu opened, or a no-op row)
START_SAVE, // open the keyboard to name a new preset, then call save()
APPLIED, // target fields changed — apply to the radio + mark dirty
DELETED // a saved preset was removed — mark dirty + alert
};
PopupMenu menu;
uint8_t user_slot[NodePrefs::USER_RADIO_PRESET_MAX]; // popup row → NodePrefs slot
int user_count = 0;
bool saving = false; // keyboard is open to name a new preset
bool deleting = false; // menu is showing the delete sub-list
static bool matches(const Target& t, float freq, float bw, uint8_t sf, uint8_t cr) {
return radioParamsMatchPreset(*t.freq, *t.bw, *t.sf, *t.cr, freq, bw, sf, cr);
}
// Display name for the target: a built-in preset, a saved user preset, or
// "Custom" if the params don't exactly match any of them.
const char* currentName(NodePrefs* p, const Target& t) const {
if (!p) return "Custom";
for (int i = 0; i < RADIO_PRESET_COUNT; i++) {
const RadioPreset& r = RADIO_PRESETS[i];
if (matches(t, r.freq, r.bw, r.sf, r.cr)) return r.name;
}
for (int i = 0; i < NodePrefs::USER_RADIO_PRESET_MAX; i++) {
const auto& u = p->user_radio_presets[i];
if (u.name[0] && matches(t, u.freq, u.bw, u.sf, u.cr)) return u.name;
}
return "Custom";
}
// Position of the target within the popup list built by open(), or -1
// ("Custom") if nothing matches.
int currentListIndex(NodePrefs* p, const Target& t) const {
if (!p) return -1;
for (int i = 0; i < RADIO_PRESET_COUNT; i++) {
const RadioPreset& r = RADIO_PRESETS[i];
if (matches(t, r.freq, r.bw, r.sf, r.cr)) return i + 1;
}
int pos = RADIO_PRESET_COUNT + 1;
for (int i = 0; i < NodePrefs::USER_RADIO_PRESET_MAX; i++) {
const auto& u = p->user_radio_presets[i];
if (!u.name[0]) continue;
if (matches(t, u.freq, u.bw, u.sf, u.cr)) return pos;
pos++;
}
return -1;
}
// Save the target's params as a named user preset: overwrite a slot with the
// same name if one exists, else the first empty slot, else slot 0 (oldest).
// Returns true if anything was written (caller alerts + marks dirty).
bool save(NodePrefs* p, const char* name, const Target& t) {
if (!p || !name || !name[0]) return false;
int slot = -1;
for (int i = 0; i < NodePrefs::USER_RADIO_PRESET_MAX; i++)
if (strcmp(p->user_radio_presets[i].name, name) == 0) { slot = i; break; }
if (slot < 0)
for (int i = 0; i < NodePrefs::USER_RADIO_PRESET_MAX; i++)
if (!p->user_radio_presets[i].name[0]) { slot = i; break; }
if (slot < 0) slot = 0;
NodePrefs::UserRadioPreset& u = p->user_radio_presets[slot];
strncpy(u.name, name, sizeof(u.name) - 1);
u.name[sizeof(u.name) - 1] = '\0';
u.freq = *t.freq; u.bw = *t.bw; u.sf = *t.sf; u.cr = *t.cr;
return true;
}
void open(NodePrefs* p, const Target& t, const char* title) {
deleting = false;
menu.begin(title, 6);
menu.addItem("+ Save current...");
for (int i = 0; i < RADIO_PRESET_COUNT; i++) menu.addItem(RADIO_PRESETS[i].name);
user_count = 0;
if (p) {
for (int i = 0; i < NodePrefs::USER_RADIO_PRESET_MAX; i++) {
if (!p->user_radio_presets[i].name[0]) continue;
menu.addItem(p->user_radio_presets[i].name);
user_slot[user_count++] = (uint8_t)i;
}
}
if (user_count > 0) menu.addItem("- Delete preset...");
int idx = currentListIndex(p, t);
menu.setSelected(idx >= 0 ? idx : 0);
}
// Reuses `menu` for a second-level list of just the saved user presets (their
// slot mapping in user_slot is still the one open() built, since this is only
// reached from within that same popup).
void openDelete(NodePrefs* p) {
menu.begin("Delete Preset", 6);
for (int i = 0; i < user_count; i++)
menu.addItem(p->user_radio_presets[user_slot[i]].name);
deleting = true;
}
// Handle the index the popup reports as SELECTED. Mutates target fields on a
// built-in/user pick; deletes a slot in the delete sub-list. See Result.
Result onSelected(int idx, NodePrefs* p, const Target& t) {
if (!p) return NONE;
if (deleting) {
Result r = NONE;
if (idx >= 0 && idx < user_count) {
p->user_radio_presets[user_slot[idx]].name[0] = '\0';
r = DELETED;
}
deleting = false;
return r;
}
const int builtin_base = 1;
const int user_base = builtin_base + RADIO_PRESET_COUNT;
const int delete_idx = user_base + user_count;
if (idx == 0) {
saving = true;
return START_SAVE;
}
if (idx >= builtin_base && idx < user_base) {
const RadioPreset& r = RADIO_PRESETS[idx - builtin_base];
*t.freq = r.freq; *t.bw = r.bw; *t.sf = r.sf; *t.cr = r.cr;
return APPLIED;
}
if (idx >= user_base && idx < delete_idx) {
const NodePrefs::UserRadioPreset& u = p->user_radio_presets[user_slot[idx - user_base]];
*t.freq = u.freq; *t.bw = u.bw; *t.sf = u.sf; *t.cr = u.cr;
return APPLIED;
}
if (user_count > 0 && idx == delete_idx) {
openDelete(p);
}
return NONE;
}
};