diff --git a/examples/companion_radio/RadioPresets.h b/examples/companion_radio/RadioPresets.h index 3446b7b8..46ae3cc5 100644 --- a/examples/companion_radio/RadioPresets.h +++ b/examples/companion_radio/RadioPresets.h @@ -41,6 +41,18 @@ static const int RADIO_PRESET_COUNT = sizeof(RADIO_PRESETS) / sizeof(RADIO_PRESE static const float LORA_BW_OPTS[] = { 7.8f, 10.4f, 15.6f, 20.8f, 31.25f, 41.7f, 62.5f, 125.0f, 250.0f, 500.0f }; static const int LORA_BW_OPT_COUNT = sizeof(LORA_BW_OPTS) / sizeof(LORA_BW_OPTS[0]); +// Index of the LORA_BW_OPTS entry nearest `bw` — used both to render the current +// BW and to step left/right through the table without drifting off-grid. +static inline int nearestBwIndex(float bw) { + int best = 0; + float best_diff = 1e9f; + for (int i = 0; i < LORA_BW_OPT_COUNT; i++) { + float diff = fabsf(bw - LORA_BW_OPTS[i]); + if (diff < best_diff) { best_diff = diff; best = i; } + } + return best; +} + // True if (freq, bw, sf, cr) matches the radio's currently-applied (cur_*) params. // Floats are compared with a small epsilon since they may have round-tripped // through unit conversions (e.g. kHz integers from the companion app). diff --git a/examples/companion_radio/ui-new/RadioPresetPicker.h b/examples/companion_radio/ui-new/RadioPresetPicker.h new file mode 100644 index 00000000..dd0af15d --- /dev/null +++ b/examples/companion_radio/ui-new/RadioPresetPicker.h @@ -0,0 +1,162 @@ +#pragma once +#include +#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; + } +}; diff --git a/examples/companion_radio/ui-new/RepeaterScreen.h b/examples/companion_radio/ui-new/RepeaterScreen.h index 8cd3bb56..ec358d67 100644 --- a/examples/companion_radio/ui-new/RepeaterScreen.h +++ b/examples/companion_radio/ui-new/RepeaterScreen.h @@ -1,9 +1,9 @@ #pragma once -// Tools › Repeater — consolidates the repeater toggle, its flood "politeness" -// filters, an optional dedicated radio profile, and live forwarding stats on one -// screen. A dedicated screen (vs. the old Settings › Radio sub-items) gives -// full-width rows, so the longer labels no longer collide with the value column, -// and keeps the relaying controls next to the numbers that show them working. +// Tools › Repeater — consolidates the repeater toggle, its flood forwarding +// filters, and an optional dedicated radio profile on one screen. A dedicated +// screen (vs. the old Settings › Radio sub-items) gives full-width rows, so the +// longer labels no longer collide with the value column. Live forwarding stats +// live separately on Tools › Diagnostics. // // Network modes: // - Current — repeat on the companion's current frequency. Opt-in only: @@ -19,8 +19,8 @@ #include #include #include "icons.h" -#include "PopupMenu.h" #include "DigitEditor.h" +#include "RadioPresetPicker.h" #include "../RadioPresets.h" #include "../MyMesh.h" @@ -40,22 +40,13 @@ class RepeaterScreen : public UIScreen { uint8_t _items[12]; int _item_count; - PopupMenu _preset_menu; + RadioPresetPicker _picker; DigitEditor _freq_editor; - uint8_t _preset_user_slot[NodePrefs::USER_RADIO_PRESET_MAX]; - int _preset_user_count = 0; - bool _preset_saving = false; // keyboard is open to name a new preset - bool _preset_deleting = false; // _preset_menu is showing the delete sub-list - // Nearest entry in LORA_BW_OPTS to the repeater profile's bw. - int rptBwIndex(NodePrefs* p) const { - int best = 0; - float best_diff = 1e9f; - for (int i = 0; i < LORA_BW_OPT_COUNT; i++) { - float diff = fabsf(p->repeater_bw - LORA_BW_OPTS[i]); - if (diff < best_diff) { best_diff = diff; best = i; } - } - return best; + // The dedicated repeater profile's fields, as the shared preset picker's target + // (Settings › Radio points the same picker at the companion's own params). + RadioPresetPicker::Target rptTarget(NodePrefs* p) const { + return { &p->repeater_freq, &p->repeater_bw, &p->repeater_sf, &p->repeater_cr }; } void buildItems(NodePrefs* p) { @@ -98,69 +89,12 @@ class RepeaterScreen : public UIScreen { return ""; } - // Name of the preset matching the repeater profile, or "Custom". - const char* rptPresetName(NodePrefs* p) const { - for (int i = 0; i < RADIO_PRESET_COUNT; i++) { - const RadioPreset& r = RADIO_PRESETS[i]; - if (radioParamsMatchPreset(p->repeater_freq, p->repeater_bw, p->repeater_sf, p->repeater_cr, r.freq, r.bw, r.sf, r.cr)) - return r.name; - } - 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; - if (radioParamsMatchPreset(p->repeater_freq, p->repeater_bw, p->repeater_sf, p->repeater_cr, u.freq, u.bw, u.sf, u.cr)) - return u.name; - } - return "Custom"; - } - - // Position of the repeater profile within the popup list built by - // openPresetMenu() below ("Save current..." at 0, then built-ins, then - // non-empty user slots in slot order) — or -1 ("Custom") if nothing matches. - int currentPresetListIndex(NodePrefs* p) const { - for (int i = 0; i < RADIO_PRESET_COUNT; i++) { - const RadioPreset& r = RADIO_PRESETS[i]; - if (radioParamsMatchPreset(p->repeater_freq, p->repeater_bw, p->repeater_sf, p->repeater_cr, 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 (radioParamsMatchPreset(p->repeater_freq, p->repeater_bw, p->repeater_sf, p->repeater_cr, u.freq, u.bw, u.sf, u.cr)) - return pos; - pos++; - } - return -1; - } - - // Save the current repeater profile as a named user preset: overwrite a slot - // with the same name if one exists, else the first empty slot, else slot 0. - // Shared with Settings' freq/bw/sf/cr — these are the same 4 user slots, just - // populated from the repeater profile instead of the companion's own params. - void saveCurrentAsPreset(NodePrefs* p, const char* name) { - if (!p || !name || !name[0]) return; - 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 = p->repeater_freq; u.bw = p->repeater_bw; u.sf = p->repeater_sf; u.cr = p->repeater_cr; - _dirty = true; - _task->showAlert("Preset saved", 800); - } - void itemValue(int item, NodePrefs* p, char* buf, size_t n) const { if (!p) { strncpy(buf, "OFF", n); buf[n-1]=0; return; } switch (item) { case IT_REPEATER: strncpy(buf, p->client_repeat ? "ON" : "OFF", n); break; case IT_NETWORK: strncpy(buf, p->repeater_use_profile ? "Custom" : "Current", n); break; - case IT_RPRESET: strncpy(buf, rptPresetName(p), n); break; + case IT_RPRESET: strncpy(buf, _picker.currentName(p, rptTarget(p)), n); break; case IT_RFREQ: snprintf(buf, n, "%.3f", p->repeater_freq); break; case IT_RSF: snprintf(buf, n, "%d", (int)p->repeater_sf); break; case IT_RBW: snprintf(buf, n, "%.1f", p->repeater_bw); break; @@ -191,45 +125,17 @@ class RepeaterScreen : public UIScreen { p->repeater_sf = p->sf; p->repeater_cr = p->cr; } - // Layout: [0]="+ Save current...", [1..RADIO_PRESET_COUNT]=built-ins, - // [..+_preset_user_count]=saved user presets, ["- Delete preset..." if any]. - void openPresetMenu(NodePrefs* p) { - _preset_deleting = false; - _preset_menu.begin("Repeater Preset", 6); - _preset_menu.addItem("+ Save current..."); - for (int i = 0; i < RADIO_PRESET_COUNT; i++) _preset_menu.addItem(RADIO_PRESETS[i].name); - _preset_user_count = 0; - for (int i = 0; i < NodePrefs::USER_RADIO_PRESET_MAX; i++) { - if (!p->user_radio_presets[i].name[0]) continue; - _preset_menu.addItem(p->user_radio_presets[i].name); - _preset_user_slot[_preset_user_count++] = (uint8_t)i; - } - if (_preset_user_count > 0) _preset_menu.addItem("- Delete preset..."); - int idx = currentPresetListIndex(p); - _preset_menu.setSelected(idx >= 0 ? idx : 0); - } - - // Reuses _preset_menu for a second-level list of just the saved user presets - // (their slot mapping in _preset_user_slot is still the one openPresetMenu() - // just built, since this is only reached from within that same popup). - void openDeletePresetMenu(NodePrefs* p) { - _preset_menu.begin("Delete Preset", 6); - for (int i = 0; i < _preset_user_count; i++) - _preset_menu.addItem(p->user_radio_presets[_preset_user_slot[i]].name); - _preset_deleting = true; - } - public: RepeaterScreen(UITask* task) : _task(task), _dirty(false), _sel(0), _scroll(0), _item_count(1) {} void enter() { _dirty = false; _sel = 0; _scroll = 0; - _preset_menu.active = false; _freq_editor.active = false; - _preset_saving = false; _preset_deleting = false; + _picker.menu.active = false; _freq_editor.active = false; + _picker.saving = false; _picker.deleting = false; } int render(DisplayDriver& display) override { - if (_preset_saving) return _task->keyboard().render(display); + if (_picker.saving) return _task->keyboard().render(display); NodePrefs* p = _task->getNodePrefs(); buildItems(p); @@ -271,61 +177,49 @@ public: } drawScrollIndicator(display, start_y, visible * item_h, total, visible, _scroll); display.setColor(DisplayDriver::LIGHT); - if (_preset_menu.active) _preset_menu.render(display); - return (_preset_menu.active || _freq_editor.active) ? 50 : 500; + if (_picker.menu.active) _picker.menu.render(display); + return (_picker.menu.active || _freq_editor.active) ? 50 : 500; } bool handleInput(char c) override { NodePrefs* p = _task->getNodePrefs(); // Keyboard editing mode for naming a new saved preset - if (_preset_saving) { + if (_picker.saving) { auto res = _task->keyboard().handleInput(c); if (res == KeyboardWidget::DONE) { - saveCurrentAsPreset(p, _task->keyboard().buf); - _preset_saving = false; + if (_picker.save(p, _task->keyboard().buf, rptTarget(p))) { + _dirty = true; + _task->showAlert("Preset saved", 800); + } + _picker.saving = false; } else if (res == KeyboardWidget::CANCELLED) { - _preset_saving = false; + _picker.saving = false; } return true; } // Modal overlays first. - if (_preset_menu.active) { - auto res = _preset_menu.handleInput(c); + if (_picker.menu.active) { + auto res = _picker.menu.handleInput(c); if (res == PopupMenu::SELECTED && p) { - int idx = _preset_menu.selectedIndex(); - if (_preset_deleting) { - if (idx >= 0 && idx < _preset_user_count) { - p->user_radio_presets[_preset_user_slot[idx]].name[0] = '\0'; - _dirty = true; - _task->showAlert("Preset deleted", 800); - } - _preset_deleting = false; - } else { - int save_idx = 0; - int builtin_base = 1; - int user_base = builtin_base + RADIO_PRESET_COUNT; - int delete_idx = user_base + _preset_user_count; - if (idx == save_idx) { - _preset_saving = true; + switch (_picker.onSelected(_picker.menu.selectedIndex(), p, rptTarget(p))) { + case RadioPresetPicker::START_SAVE: _task->keyboard().begin("", (int)sizeof(p->user_radio_presets[0].name) - 1); - } else if (idx >= builtin_base && idx < user_base) { - const RadioPreset& r = RADIO_PRESETS[idx - builtin_base]; - p->repeater_freq = r.freq; p->repeater_bw = r.bw; p->repeater_sf = r.sf; p->repeater_cr = r.cr; + break; + case RadioPresetPicker::APPLIED: the_mesh.applyRepeaterRadio(); // live if currently relaying on the profile _dirty = true; - } else if (idx >= user_base && idx < delete_idx) { - const NodePrefs::UserRadioPreset& up = p->user_radio_presets[_preset_user_slot[idx - user_base]]; - p->repeater_freq = up.freq; p->repeater_bw = up.bw; p->repeater_sf = up.sf; p->repeater_cr = up.cr; - the_mesh.applyRepeaterRadio(); + break; + case RadioPresetPicker::DELETED: _dirty = true; - } else if (_preset_user_count > 0 && idx == delete_idx) { - openDeletePresetMenu(p); - } + _task->showAlert("Preset deleted", 800); + break; + case RadioPresetPicker::NONE: + break; } } else if (res == PopupMenu::CANCELLED) { - _preset_deleting = false; + _picker.deleting = false; } return true; } @@ -372,7 +266,7 @@ public: _dirty = true; return true; } - if (item == IT_RPRESET && enter) { openPresetMenu(p); return true; } + if (item == IT_RPRESET && enter) { _picker.open(p, rptTarget(p), "Repeater Preset"); return true; } if (item == IT_RFREQ && enter) { float lo, hi; radio_driver.getFreqBounds(lo, hi); _freq_editor.begin(p->repeater_freq, lo, hi, 3, 3); @@ -383,7 +277,7 @@ public: if (left && p->repeater_sf > 5) { p->repeater_sf--; the_mesh.applyRepeaterRadio(); _dirty = true; return true; } } if (item == IT_RBW) { - int idx = rptBwIndex(p); + int idx = nearestBwIndex(p->repeater_bw); if (right && idx < LORA_BW_OPT_COUNT - 1) { p->repeater_bw = LORA_BW_OPTS[idx + 1]; the_mesh.applyRepeaterRadio(); _dirty = true; return true; } if (left && idx > 0) { p->repeater_bw = LORA_BW_OPTS[idx - 1]; the_mesh.applyRepeaterRadio(); _dirty = true; return true; } } diff --git a/examples/companion_radio/ui-new/SettingsScreen.h b/examples/companion_radio/ui-new/SettingsScreen.h index c84265f4..4789406a 100644 --- a/examples/companion_radio/ui-new/SettingsScreen.h +++ b/examples/companion_radio/ui-new/SettingsScreen.h @@ -5,6 +5,7 @@ #include "../Features.h" #include "../RadioPresets.h" #include "DigitEditor.h" +#include "RadioPresetPicker.h" class SettingsScreen : public UIScreen { UITask* _task; @@ -115,77 +116,10 @@ class SettingsScreen : public UIScreen { return 0; } - static bool matchesPreset(NodePrefs* p, float freq, float bw, uint8_t sf, uint8_t cr) { - return radioParamsMatchPreset(p->freq, p->bw, p->sf, p->cr, freq, bw, sf, cr); - } - - // Display name for the Preset row: a built-in preset, a saved user preset, or - // "Custom" if freq/sf/bw/cr don't exactly match any of them. - const char* currentPresetName() { - NodePrefs* p = _task->getNodePrefs(); - if (!p) return "Custom"; - for (int i = 0; i < RADIO_PRESET_COUNT; i++) { - const RadioPreset& r = RADIO_PRESETS[i]; - if (matchesPreset(p, 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] && matchesPreset(p, u.freq, u.bw, u.sf, u.cr)) return u.name; - } - return "Custom"; - } - - // Position of the current settings within the popup list built by - // openPresetMenu() below ("Save current..." at 0, then built-ins, then - // non-empty user slots in slot order) — or -1 ("Custom") if nothing matches. - int currentPresetListIndex() { - NodePrefs* p = _task->getNodePrefs(); - if (!p) return -1; - for (int i = 0; i < RADIO_PRESET_COUNT; i++) { - const RadioPreset& r = RADIO_PRESETS[i]; - if (matchesPreset(p, 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 (matchesPreset(p, u.freq, u.bw, u.sf, u.cr)) return pos; - pos++; - } - return -1; - } - - // Save the current freq/sf/bw/cr as a named user preset: overwrite a slot with - // the same name if one exists, else the first empty slot, else slot 0 (oldest). - void saveCurrentAsPreset(const char* name) { - NodePrefs* p = _task->getNodePrefs(); - if (!p || !name || !name[0]) return; - 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 = p->freq; u.bw = p->bw; u.sf = p->sf; u.cr = p->cr; - _dirty = true; - _task->showAlert("Preset saved", 800); - } - - // Nearest entry in LORA_BW_OPTS to the current bw (also used to step left/right). - int customBwIndex() { - NodePrefs* p = _task->getNodePrefs(); - float bw = p ? p->bw : 62.5f; - int best = 0; - float best_diff = 1e9f; - for (int i = 0; i < LORA_BW_OPT_COUNT; i++) { - float diff = fabsf(bw - LORA_BW_OPTS[i]); - if (diff < best_diff) { best_diff = diff; best = i; } - } - return best; + // The companion's own radio fields, as the shared preset picker's target + // (Tools › Repeater points the same picker at the dedicated repeater profile). + RadioPresetPicker::Target radioTarget(NodePrefs* p) const { + return { &p->freq, &p->bw, &p->sf, &p->cr }; } // Value column start, pulled left by the scrollbar gutter so right-side @@ -568,7 +502,7 @@ class SettingsScreen : public UIScreen { display.print(buf); } else if (item == RADIO_PRESET) { display.print("Preset"); - const char* name = currentPresetName(); + const char* name = p ? _picker.currentName(p, radioTarget(p)) : "Custom"; int xc = valCol(display); display.drawTextEllipsized(xc, y, display.width() - xc - _reserve, name); } else if (item == CUSTOM_FREQ) { @@ -711,51 +645,15 @@ class SettingsScreen : public UIScreen { KeyboardWidget* _kb; // Radio preset picker — names are too long for the value column, so Enter on - // RADIO_PRESET opens this as a full-width scrollable list instead of cycling. - PopupMenu _preset_menu; - // Maps a user-preset row's position in the open popup back to its slot in - // NodePrefs::user_radio_presets (empty slots are skipped when building the list). - uint8_t _preset_user_slot[NodePrefs::USER_RADIO_PRESET_MAX]; - int _preset_user_count = 0; - bool _preset_saving = false; // _kb is open to name a new preset, not a msg slot - bool _preset_deleting = false; // _preset_menu is showing the delete sub-list + // RADIO_PRESET opens it as a full-width scrollable list instead of cycling. + // Shared with Tools › Repeater (see RadioPresetPicker.h). _picker.saving means + // _kb is open to name a new preset, not a message slot. + RadioPresetPicker _picker; // Digit-by-digit Freq editor (see DigitEditor.h) — only this field uses it; // SF/BW/CR are small discrete sets where a plain left/right cycle is fine. DigitEditor _freq_editor; - // Layout: [0]="+ Save current...", [1..RADIO_PRESET_COUNT]=built-ins, - // [..+_preset_user_count]=saved user presets, ["- Delete preset..." if any]. - void openPresetMenu() { - NodePrefs* p = _task->getNodePrefs(); - _preset_deleting = false; - _preset_menu.begin("Radio Preset", 6); - _preset_menu.addItem("+ Save current..."); - for (int i = 0; i < RADIO_PRESET_COUNT; i++) _preset_menu.addItem(RADIO_PRESETS[i].name); - _preset_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; - _preset_menu.addItem(p->user_radio_presets[i].name); - _preset_user_slot[_preset_user_count++] = (uint8_t)i; - } - } - if (_preset_user_count > 0) _preset_menu.addItem("- Delete preset..."); - int idx = currentPresetListIndex(); - _preset_menu.setSelected(idx >= 0 ? idx : 0); - } - - // Reuses _preset_menu for a second-level list of just the saved user presets - // (their slot mapping in _preset_user_slot is still the one openPresetMenu() - // just built, since this is only reached from within that same popup). - void openDeletePresetMenu() { - NodePrefs* p = _task->getNodePrefs(); - _preset_menu.begin("Delete Preset", 6); - for (int i = 0; i < _preset_user_count; i++) - _preset_menu.addItem(p->user_radio_presets[_preset_user_slot[i]].name); - _preset_deleting = true; - } - public: SettingsScreen(UITask* task, KeyboardWidget* kb) : _task(task), _kb(kb), _selected(SECTION_DISPLAY), _scroll(0), _visible(4), _dirty(false), _edit_slot(-1) { @@ -775,7 +673,7 @@ public: int render(DisplayDriver& display) override { display.setTextSize(1); - if (_edit_slot >= 0 || _preset_saving) { + if (_edit_slot >= 0 || _picker.saving) { return _kb->render(display); } @@ -792,7 +690,7 @@ public: drawScrollIndicator(display, start_y, _visible * item_h, _vis_count, _visible, _scroll); - if (_preset_menu.active) _preset_menu.render(display); + if (_picker.menu.active) _picker.menu.render(display); return 2000; } @@ -829,53 +727,41 @@ public: } // Keyboard editing mode for naming a new saved preset - if (_preset_saving) { + if (_picker.saving) { auto res = _kb->handleInput(c); if (res == KeyboardWidget::DONE) { - saveCurrentAsPreset(_kb->buf); - _preset_saving = false; + if (p && _picker.save(p, _kb->buf, radioTarget(p))) { + _dirty = true; + _task->showAlert("Preset saved", 800); + } + _picker.saving = false; } else if (res == KeyboardWidget::CANCELLED) { - _preset_saving = false; + _picker.saving = false; } return true; } // Radio preset popup (and its "delete a saved preset" sub-list) - if (_preset_menu.active) { - auto res = _preset_menu.handleInput(c); + if (_picker.menu.active) { + auto res = _picker.menu.handleInput(c); if (res == PopupMenu::SELECTED && p) { - int idx = _preset_menu.selectedIndex(); - if (_preset_deleting) { - if (idx >= 0 && idx < _preset_user_count) { - p->user_radio_presets[_preset_user_slot[idx]].name[0] = '\0'; + switch (_picker.onSelected(_picker.menu.selectedIndex(), p, radioTarget(p))) { + case RadioPresetPicker::START_SAVE: + _kb->begin("", (int)sizeof(p->user_radio_presets[0].name) - 1); + break; + case RadioPresetPicker::APPLIED: + _task->applyRadioParams(); + _dirty = true; + break; + case RadioPresetPicker::DELETED: _dirty = true; _task->showAlert("Preset deleted", 800); - } - _preset_deleting = false; - } else { - int save_idx = 0; - int builtin_base = 1; - int user_base = builtin_base + RADIO_PRESET_COUNT; - int delete_idx = user_base + _preset_user_count; - if (idx == save_idx) { - _preset_saving = true; - _kb->begin("", (int)sizeof(p->user_radio_presets[0].name) - 1); - } else if (idx >= builtin_base && idx < user_base) { - const RadioPreset& r = RADIO_PRESETS[idx - builtin_base]; - p->freq = r.freq; p->bw = r.bw; p->sf = r.sf; p->cr = r.cr; - _task->applyRadioParams(); - _dirty = true; - } else if (idx >= user_base && idx < delete_idx) { - const NodePrefs::UserRadioPreset& u = p->user_radio_presets[_preset_user_slot[idx - user_base]]; - p->freq = u.freq; p->bw = u.bw; p->sf = u.sf; p->cr = u.cr; - _task->applyRadioParams(); - _dirty = true; - } else if (_preset_user_count > 0 && idx == delete_idx) { - openDeletePresetMenu(); - } + break; + case RadioPresetPicker::NONE: + break; } } else if (res == PopupMenu::CANCELLED) { - _preset_deleting = false; + _picker.deleting = false; } return true; } @@ -972,7 +858,7 @@ public: if (left && p->tx_power_dbm > 2) { p->tx_power_dbm--; _task->applyTxPower(); _dirty = true; return true; } } if (_selected == RADIO_PRESET && p && enter) { - openPresetMenu(); + _picker.open(p, radioTarget(p), "Radio Preset"); return true; } // Enter Freq's digit-by-digit editor (see DigitEditor.h). Bounds come from @@ -989,7 +875,7 @@ public: if (left && p->sf > 5) { p->sf--; _task->applyRadioParams(); _dirty = true; return true; } } if (_selected == CUSTOM_BW && p) { - int idx = customBwIndex(); + int idx = nearestBwIndex(p->bw); if (right && idx < LORA_BW_OPT_COUNT - 1) { p->bw = LORA_BW_OPTS[idx + 1]; _task->applyRadioParams(); _dirty = true; return true; } if (left && idx > 0) { p->bw = LORA_BW_OPTS[idx - 1]; _task->applyRadioParams(); _dirty = true; return true; } }