mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-09-14 15:16:40 +00:00
fix(ui): one rule for value rows, one meaning for Hold Enter
Three interaction inconsistencies found while auditing the favourites work, all of the same shape: the same gesture meaning different things depending on which screen you were on. Value rows in popup menus. Rows like "Notif: ON" or "Sort: Dist" show a value the user steps through with LEFT/RIGHT, but Enter treated them as ordinary menu picks and closed the popup, so changing two of them meant reopening the menu in between. Trail's settings submenu was the lone exception, working around it by rebuilding and re-selecting after each Enter. PopupMenu now knows the difference: addValueItem() marks a row, and Enter on it returns the new VALUE_NEXT instead of SELECTED, leaving the menu open. Only Back closes a menu now. Applied to the Messages contact/room/channel menus, Nodes, the Ringtone editor and Trail, which drops its reopenSettingsAt() workaround. The LEFT/RIGHT cycling bodies moved into one helper per menu, since Enter and RIGHT now share them. Nodes' Fav row was the worst case: LEFT/RIGHT did nothing there at all, so the only way to toggle a favourite was an Enter that dismissed the menu on every flip. Its label moved to a member buffer (as the Pin row already had) so it can be retitled in place. Settings rows Auto-off, Low battery, GPS pwr and Battery ignored Enter, though their options wrap exactly like the melody/keyboard/clock rows beside them, where Enter has always stepped forward. They accept it now. Rows that ramp between fixed ends (Brightness, Volume, TX Pwr, Timezone, SF/BW/CR) stay LEFT/RIGHT-only -- there is nothing to wrap to. Hold Enter no longer doubles as Back. It quietly meant "go back" on Tools, Locator, Live Share, Repeater, Bot, Auto-Advert, GPIO, Compass, the Dashboard config and the Messages navigate view, while elsewhere the same long press opens a context menu. It now only ever opens a menu, or does nothing where there is none. Same for dismissing an open popup, which it used to do. Checked that this strands nobody: every board that can reach these screens has a real Back key (back_btn on joystick boards, Esc on CardKB/TCA8418/T-Deck). Single-button boards produce no KEY_ENTER at all, so they never leave the home pages in the first place.
This commit is contained in:
@@ -50,7 +50,7 @@ public:
|
||||
}
|
||||
|
||||
bool handleInput(char c) override {
|
||||
if (c == KEY_CANCEL || c == KEY_CONTEXT_MENU) {
|
||||
if (c == KEY_CANCEL) {
|
||||
_task->savePrefsIfDirty(_dirty);
|
||||
_task->gotoToolsScreen();
|
||||
return true;
|
||||
|
||||
@@ -267,7 +267,7 @@ public:
|
||||
bool up = (c == KEY_UP);
|
||||
bool down = (c == KEY_DOWN);
|
||||
bool enter = (c == KEY_ENTER);
|
||||
bool cancel = (c == KEY_CANCEL || c == KEY_CONTEXT_MENU);
|
||||
bool cancel = (c == KEY_CANCEL);
|
||||
|
||||
if (_kb_row >= 0) {
|
||||
auto res = _kb->handleInput(c);
|
||||
|
||||
@@ -120,7 +120,7 @@ public:
|
||||
}
|
||||
|
||||
bool handleInput(char c) override {
|
||||
if (c == KEY_CANCEL || c == KEY_CONTEXT_MENU) { _task->gotoToolsScreen(); return true; }
|
||||
if (c == KEY_CANCEL) { _task->gotoToolsScreen(); return true; }
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -64,7 +64,7 @@ public:
|
||||
}
|
||||
|
||||
bool handleInput(char c) override {
|
||||
if (c == KEY_CANCEL || c == KEY_CONTEXT_MENU) {
|
||||
if (c == KEY_CANCEL) {
|
||||
_task->savePrefsIfDirty(_dirty);
|
||||
_task->gotoHomeScreen();
|
||||
return true;
|
||||
|
||||
@@ -136,7 +136,7 @@ public:
|
||||
}
|
||||
|
||||
bool handleInput(char c) override {
|
||||
if (c == KEY_CANCEL || c == KEY_CONTEXT_MENU) { _task->gotoToolsScreen(); return true; }
|
||||
if (c == KEY_CANCEL) { _task->gotoToolsScreen(); return true; }
|
||||
if (c == KEY_UP) { _sel = (_sel > 0) ? _sel - 1 : _item_count - 1; return true; }
|
||||
if (c == KEY_DOWN) { _sel = (_sel < _item_count - 1) ? _sel + 1 : 0; return true; }
|
||||
if (!_prefs) return false;
|
||||
|
||||
@@ -164,7 +164,7 @@ public:
|
||||
}
|
||||
|
||||
bool handleInput(char c) override {
|
||||
if (c == KEY_CANCEL || c == KEY_CONTEXT_MENU) {
|
||||
if (c == KEY_CANCEL) {
|
||||
_task->savePrefsIfDirty(_dirty);
|
||||
_task->gotoToolsScreen();
|
||||
return true;
|
||||
|
||||
@@ -301,10 +301,10 @@ public:
|
||||
if (c == KEY_UP) { _pick_sel = (_pick_sel > 0) ? _pick_sel - 1 : _target_n - 1; return true; }
|
||||
if (c == KEY_DOWN) { _pick_sel = (_pick_sel < _target_n - 1) ? _pick_sel + 1 : 0; return true; }
|
||||
if (c == KEY_ENTER) { applyTarget(_targets[_pick_sel]); _picking = false; return true; }
|
||||
if (c == KEY_CANCEL || c == KEY_CONTEXT_MENU) { _picking = false; return true; }
|
||||
if (c == KEY_CANCEL) { _picking = false; return true; }
|
||||
return true;
|
||||
}
|
||||
if (c == KEY_CANCEL || c == KEY_CONTEXT_MENU) {
|
||||
if (c == KEY_CANCEL) {
|
||||
_task->savePrefsIfDirty(_dirty); // engine re-seeded per edit
|
||||
_task->gotoToolsScreen();
|
||||
return true;
|
||||
|
||||
@@ -722,6 +722,68 @@ class MessagesScreen : public UIScreen {
|
||||
}
|
||||
}
|
||||
|
||||
// Advance one of the contact menu's value rows. dir is +1 for RIGHT and for
|
||||
// Enter (which PopupMenu reports as VALUE_NEXT on a value row), -1 for LEFT.
|
||||
void cycleContactCtxValue(int sel, int dir) {
|
||||
static const char* NOTIF_LABELS[] = { "Default", "OFF", "ON" };
|
||||
static const char* ML[] = { "Global", "M1", "M2" };
|
||||
ContactInfo ci;
|
||||
if (_num_contacts <= 0 || !the_mesh.getContactByIdx(_sorted[_contact_sel], ci)) return;
|
||||
if (sel == 1) {
|
||||
uint8_t v = dmNotifState(ci.id.pub_key);
|
||||
v = (dir > 0) ? (v + 1) % 3 : (v + 2) % 3;
|
||||
setDmNotifState(ci.id.pub_key, v);
|
||||
snprintf(_ctx_notif_item, sizeof(_ctx_notif_item), "Notif: %s", NOTIF_LABELS[v]);
|
||||
_ctx_dirty = true;
|
||||
} else if (sel == 2) {
|
||||
uint8_t v = dmMelodySlot(ci.id.pub_key);
|
||||
v = (dir > 0) ? (v + 1) % 3 : (v + 2) % 3;
|
||||
setDmMelody(ci.id.pub_key, v);
|
||||
snprintf(_ctx_melody_item, sizeof(_ctx_melody_item), "Melody: %s", ML[v]);
|
||||
_ctx_dirty = true;
|
||||
} else if (sel == _ctx_fav_idx) {
|
||||
toggleContactFav(ci);
|
||||
}
|
||||
}
|
||||
|
||||
// Same, for the room menu -- its only value row is Fav.
|
||||
void cycleRoomCtxValue(int sel) {
|
||||
ContactInfo ci;
|
||||
if (sel != _ctx_fav_idx || _num_contacts <= 0) return;
|
||||
if (the_mesh.getContactByIdx(_sorted[_contact_sel], ci)) toggleContactFav(ci);
|
||||
}
|
||||
|
||||
// Same, for the channel menu. The list rebuild is deferred to menu close: with
|
||||
// the fav-only filter on, un-favouriting removes this channel from the list,
|
||||
// and rebuilding under the open menu would shift _channel_sel onto another one.
|
||||
void cycleChannelCtxValue(int sel, int dir) {
|
||||
static const char* NOTIF_LABELS[] = { "Default", "OFF", "ON" };
|
||||
static const char* ML[] = { "Global", "M1", "M2" };
|
||||
if (_num_channels <= 0) return;
|
||||
uint8_t ch_idx = _ctx_ch_idx; // frozen at menu open -- see declaration
|
||||
if (sel == 1) {
|
||||
uint8_t v = chNotifState(ch_idx);
|
||||
v = (dir > 0) ? (v + 1) % 3 : (v + 2) % 3;
|
||||
setChNotifState(ch_idx, v);
|
||||
snprintf(_ctx_notif_item, sizeof(_ctx_notif_item), "Notif: %s", NOTIF_LABELS[v]);
|
||||
_ctx_dirty = true;
|
||||
} else if (sel == 2) {
|
||||
uint8_t v = chNotifMelody(ch_idx);
|
||||
v = (dir > 0) ? (v + 1) % 3 : (v + 2) % 3;
|
||||
setChNotifMelody(ch_idx, v);
|
||||
snprintf(_ctx_melody_item, sizeof(_ctx_melody_item), "Melody: %s", ML[v]);
|
||||
_ctx_dirty = true;
|
||||
} else if (sel == _ctx_fav_idx) {
|
||||
NodePrefs* p2 = _task->getNodePrefs();
|
||||
if (p2) {
|
||||
p2->ch_fav_bitmask ^= (1ULL << ch_idx);
|
||||
bool is_fav = (p2->ch_fav_bitmask & (1ULL << ch_idx));
|
||||
snprintf(_ctx_fav_item, sizeof(_ctx_fav_item), is_fav ? "Fav: ON" : "Fav: OFF");
|
||||
_ctx_dirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool chIsFav(uint8_t ch_idx) const {
|
||||
NodePrefs* p = _task->getNodePrefs();
|
||||
return p && (p->ch_fav_bitmask & (1ULL << ch_idx)) != 0;
|
||||
@@ -1721,9 +1783,9 @@ public:
|
||||
// Channel Add/Edit form consumes all input while active.
|
||||
if (_ch_view.active()) return _ch_view.handleInput(c);
|
||||
|
||||
// Navigate view: any back key returns to the message it was opened from.
|
||||
// Navigate view: Back or Enter returns to the message it was opened from.
|
||||
if (_nav_active) {
|
||||
if (c == KEY_CANCEL || c == KEY_ENTER || c == KEY_CONTEXT_MENU) _nav_active = false;
|
||||
if (c == KEY_CANCEL || c == KEY_ENTER) _nav_active = false;
|
||||
return true;
|
||||
}
|
||||
if (_phase == MODE_SELECT) {
|
||||
@@ -1787,14 +1849,17 @@ public:
|
||||
return true;
|
||||
}
|
||||
if (_room_mode) {
|
||||
// LEFT/RIGHT toggle Fav in-place (menu stays open), as in the other menus.
|
||||
if ((keyIsPrev(c) || keyIsNext(c)) && _num_contacts > 0 &&
|
||||
_ctx_menu.selectedIndex() == _ctx_fav_idx) {
|
||||
ContactInfo ci;
|
||||
if (the_mesh.getContactByIdx(_sorted[_contact_sel], ci)) toggleContactFav(ci);
|
||||
// LEFT/RIGHT -- and Enter, via VALUE_NEXT -- toggle Fav in place; the
|
||||
// menu stays open and only Back closes it.
|
||||
if (keyIsPrev(c) || keyIsNext(c)) {
|
||||
cycleRoomCtxValue(_ctx_menu.selectedIndex());
|
||||
return true;
|
||||
}
|
||||
auto res = _ctx_menu.handleInput(c);
|
||||
if (res == PopupMenu::VALUE_NEXT) {
|
||||
cycleRoomCtxValue(_ctx_menu.selectedIndex());
|
||||
return true; // still open -- the list rebuild below waits for close
|
||||
}
|
||||
if (res == PopupMenu::SELECTED && _num_contacts > 0) {
|
||||
if (the_mesh.getContactByIdx(_sorted[_contact_sel], _sel_contact)) {
|
||||
int sel = _ctx_menu.selectedIndex();
|
||||
@@ -1812,7 +1877,7 @@ public:
|
||||
forgetRoomLoggedIn(_sel_contact.id.pub_key);
|
||||
_task->showAlert("Logged out", 1000);
|
||||
}
|
||||
// Fav row: already toggled by LEFT/RIGHT, ENTER just closes.
|
||||
// Fav is a value row -- Enter never selects it (see cycleRoomCtxValue).
|
||||
}
|
||||
}
|
||||
if (res != PopupMenu::NONE && _phase == CONTACT_PICK) {
|
||||
@@ -1822,36 +1887,17 @@ public:
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// LEFT/RIGHT cycle Notif/Melody in-place (menu stays open).
|
||||
if (_num_contacts > 0) {
|
||||
bool left = keyIsPrev(c);
|
||||
bool right = keyIsNext(c);
|
||||
if (left || right) {
|
||||
static const char* NOTIF_LABELS[] = { "Default", "OFF", "ON" };
|
||||
static const char* ML[] = { "Global", "M1", "M2" };
|
||||
ContactInfo ci;
|
||||
if (the_mesh.getContactByIdx(_sorted[_contact_sel], ci)) {
|
||||
int sel = _ctx_menu.selectedIndex();
|
||||
if (sel == 1) {
|
||||
uint8_t v = dmNotifState(ci.id.pub_key);
|
||||
v = right ? (v + 1) % 3 : (v + 2) % 3;
|
||||
setDmNotifState(ci.id.pub_key, v);
|
||||
snprintf(_ctx_notif_item, sizeof(_ctx_notif_item), "Notif: %s", NOTIF_LABELS[v]);
|
||||
_ctx_dirty = true;
|
||||
} else if (sel == 2) {
|
||||
uint8_t v = dmMelodySlot(ci.id.pub_key);
|
||||
v = right ? (v + 1) % 3 : (v + 2) % 3;
|
||||
setDmMelody(ci.id.pub_key, v);
|
||||
snprintf(_ctx_melody_item, sizeof(_ctx_melody_item), "Melody: %s", ML[v]);
|
||||
_ctx_dirty = true;
|
||||
} else if (sel == _ctx_fav_idx) {
|
||||
toggleContactFav(ci);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// LEFT/RIGHT -- and Enter, via VALUE_NEXT -- cycle Notif/Melody/Fav in
|
||||
// place; the menu stays open and only Back closes it.
|
||||
if (keyIsPrev(c) || keyIsNext(c)) {
|
||||
cycleContactCtxValue(_ctx_menu.selectedIndex(), keyIsNext(c) ? 1 : -1);
|
||||
return true;
|
||||
}
|
||||
auto res = _ctx_menu.handleInput(c);
|
||||
if (res == PopupMenu::VALUE_NEXT) {
|
||||
cycleContactCtxValue(_ctx_menu.selectedIndex(), 1);
|
||||
return true; // still open -- the save/rebuild below waits for close
|
||||
}
|
||||
if (res == PopupMenu::SELECTED && _num_contacts > 0) {
|
||||
ContactInfo ci;
|
||||
if (the_mesh.getContactByIdx(_sorted[_contact_sel], ci)) {
|
||||
@@ -1863,7 +1909,8 @@ public:
|
||||
} else if (sel == 4) {
|
||||
pinContactAction(ci);
|
||||
}
|
||||
// sel 1 (Notif), 2 (Melody), 3 (Fav): already cycled via LEFT/RIGHT; ENTER just closes.
|
||||
// sel 1 (Notif), 2 (Melody), 3 (Fav) are value rows -- Enter never
|
||||
// selects them (see cycleContactCtxValue).
|
||||
}
|
||||
}
|
||||
if (res != PopupMenu::NONE) {
|
||||
@@ -1935,7 +1982,7 @@ public:
|
||||
_ctx_menu.addItem("Login...");
|
||||
if (logged_in) _ctx_menu.addItem("Logout");
|
||||
_ctx_fav_idx = logged_in ? 2 : 1;
|
||||
_ctx_menu.addItem(_ctx_fav_item);
|
||||
_ctx_menu.addValueItem(_ctx_fav_item);
|
||||
_ctx_pin_idx = _ctx_fav_idx + 1;
|
||||
_ctx_menu.addItem(_ctx_pin_item);
|
||||
return true;
|
||||
@@ -1956,10 +2003,10 @@ public:
|
||||
(ci.flags & 0x01) ? "Fav: ON" : "Fav: OFF");
|
||||
_ctx_menu.begin("Contact options", 3);
|
||||
_ctx_menu.addItem("Mark as read");
|
||||
_ctx_menu.addItem(_ctx_notif_item);
|
||||
_ctx_menu.addItem(_ctx_melody_item);
|
||||
_ctx_menu.addValueItem(_ctx_notif_item);
|
||||
_ctx_menu.addValueItem(_ctx_melody_item);
|
||||
_ctx_fav_idx = 3;
|
||||
_ctx_menu.addItem(_ctx_fav_item);
|
||||
_ctx_menu.addValueItem(_ctx_fav_item);
|
||||
_ctx_menu.addItem(_ctx_pin_item);
|
||||
_ctx_dirty = false;
|
||||
return true;
|
||||
@@ -1968,42 +2015,11 @@ public:
|
||||
} else if (_phase == CHANNEL_PICK) {
|
||||
// Context menu consumes all input while open
|
||||
if (_ctx_menu.active) {
|
||||
// LEFT/RIGHT cycle Notif/Melody/Fav in-place (menu stays open).
|
||||
if (!_pin_picker_active && _num_channels > 0) {
|
||||
bool left = keyIsPrev(c);
|
||||
bool right = keyIsNext(c);
|
||||
if (left || right) {
|
||||
static const char* NOTIF_LABELS[] = { "Default", "OFF", "ON" };
|
||||
static const char* ML[] = { "Global", "M1", "M2" };
|
||||
uint8_t ch_idx = _ctx_ch_idx; // frozen at menu open — see declaration
|
||||
int sel = _ctx_menu.selectedIndex();
|
||||
if (sel == 1) {
|
||||
uint8_t v = chNotifState(ch_idx);
|
||||
v = right ? (v + 1) % 3 : (v + 2) % 3;
|
||||
setChNotifState(ch_idx, v);
|
||||
snprintf(_ctx_notif_item, sizeof(_ctx_notif_item), "Notif: %s", NOTIF_LABELS[v]);
|
||||
_ctx_dirty = true;
|
||||
} else if (sel == 2) {
|
||||
uint8_t v = chNotifMelody(ch_idx);
|
||||
v = right ? (v + 1) % 3 : (v + 2) % 3;
|
||||
setChNotifMelody(ch_idx, v);
|
||||
snprintf(_ctx_melody_item, sizeof(_ctx_melody_item), "Melody: %s", ML[v]);
|
||||
_ctx_dirty = true;
|
||||
} else if (sel == _ctx_fav_idx) {
|
||||
NodePrefs* p2 = _task->getNodePrefs();
|
||||
if (p2) {
|
||||
p2->ch_fav_bitmask ^= (1ULL << ch_idx);
|
||||
bool is_fav = (p2->ch_fav_bitmask & (1ULL << ch_idx));
|
||||
snprintf(_ctx_fav_item, sizeof(_ctx_fav_item), is_fav ? "Fav: ON" : "Fav: OFF");
|
||||
_ctx_dirty = true;
|
||||
// List rebuild is deferred to menu close: with the fav-only
|
||||
// filter on, un-favouriting this channel removes it from the
|
||||
// list, and rebuilding under the open menu would shift
|
||||
// _channel_sel onto a different channel mid-interaction.
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// LEFT/RIGHT -- and Enter, via VALUE_NEXT below -- cycle Notif/Melody/Fav
|
||||
// in place; the menu stays open and only Back closes it.
|
||||
if (!_pin_picker_active && (keyIsPrev(c) || keyIsNext(c))) {
|
||||
cycleChannelCtxValue(_ctx_menu.selectedIndex(), keyIsNext(c) ? 1 : -1);
|
||||
return true;
|
||||
}
|
||||
auto res = _ctx_menu.handleInput(c);
|
||||
if (_pin_picker_active) {
|
||||
@@ -2018,6 +2034,10 @@ public:
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (res == PopupMenu::VALUE_NEXT) {
|
||||
cycleChannelCtxValue(_ctx_menu.selectedIndex(), 1);
|
||||
return true; // still open -- the save/rebuild below waits for close
|
||||
}
|
||||
if (res == PopupMenu::SELECTED && _num_channels > 0) {
|
||||
uint8_t ch_idx = _ctx_ch_idx; // frozen at menu open — see declaration
|
||||
int sel = _ctx_menu.selectedIndex();
|
||||
@@ -2050,7 +2070,8 @@ public:
|
||||
the_mesh.setChannelLocal(ch_idx, ch);
|
||||
_task->showAlert("Channel deleted", 1000);
|
||||
}
|
||||
// sel 1/2/3 already handled by LEFT/RIGHT; ENTER just closes.
|
||||
// sel 1/2/3 are value rows -- Enter never selects them
|
||||
// (see cycleChannelCtxValue).
|
||||
}
|
||||
if (res != PopupMenu::NONE) {
|
||||
_task->savePrefsIfDirty(_ctx_dirty);
|
||||
@@ -2111,10 +2132,10 @@ public:
|
||||
else snprintf(_ctx_pin_item, sizeof(_ctx_pin_item), "Pin to dial"); }
|
||||
_ctx_menu.begin("Channel options", 6);
|
||||
_ctx_menu.addItem("Mark all read");
|
||||
_ctx_menu.addItem(_ctx_notif_item);
|
||||
_ctx_menu.addItem(_ctx_melody_item);
|
||||
_ctx_menu.addValueItem(_ctx_notif_item);
|
||||
_ctx_menu.addValueItem(_ctx_melody_item);
|
||||
_ctx_fav_idx = 3;
|
||||
_ctx_menu.addItem(_ctx_fav_item);
|
||||
_ctx_menu.addValueItem(_ctx_fav_item);
|
||||
_ctx_menu.addItem(_ctx_pin_item);
|
||||
_ctx_menu.addItem("Edit");
|
||||
_ctx_menu.addItem("Delete");
|
||||
|
||||
@@ -567,6 +567,7 @@ class NearbyScreen : public UIScreen {
|
||||
return e && ((e->contact_idx >= 0) || (_source == SRC_SCAN && e->is_known));
|
||||
}
|
||||
|
||||
char _fav_label[12]; // "Fav: ON" / "Fav: OFF" -- rewritten in place by L/R
|
||||
char _pin_label[24]; // "Pin to dial" / "Unpin (slot N)" -- _menu stores the pointer
|
||||
|
||||
void openActionMenu() {
|
||||
@@ -589,6 +590,10 @@ class NearbyScreen : public UIScreen {
|
||||
_menu.addItem(label);
|
||||
_menu_actions[_menu_action_count++] = a;
|
||||
};
|
||||
auto addValue = [&](const char* label, Action a) {
|
||||
_menu.addValueItem(label);
|
||||
_menu_actions[_menu_action_count++] = a;
|
||||
};
|
||||
|
||||
if (has_gps) add("Navigate", ACT_NAV);
|
||||
if (has_key) add("Ping", ACT_PING);
|
||||
@@ -597,7 +602,10 @@ class NearbyScreen : public UIScreen {
|
||||
// by pubkey prefix, so a name-only live-scan/channel row can't offer this.
|
||||
if (has_gps && has_key) add("Set as target", ACT_LOCATOR);
|
||||
if (can_add) add("Add contact", ACT_ADD);
|
||||
if (is_contact && has_key) add(e->fav ? "Fav: ON" : "Fav: OFF", ACT_FAV);
|
||||
if (is_contact && has_key) {
|
||||
snprintf(_fav_label, sizeof(_fav_label), e->fav ? "Fav: ON" : "Fav: OFF");
|
||||
addValue(_fav_label, ACT_FAV);
|
||||
}
|
||||
if (is_contact && has_key) {
|
||||
if (is_pinned) snprintf(_pin_label, sizeof(_pin_label), "Unpin (slot %d)",
|
||||
_task->findFavouriteSlot(e->pub_key) + 1);
|
||||
@@ -606,10 +614,35 @@ class NearbyScreen : public UIScreen {
|
||||
}
|
||||
if (is_admin_target) add("Admin", ACT_ADMIN);
|
||||
if (is_contact && has_key) add("Delete contact", ACT_DELETE);
|
||||
if (stored) add(_sort_label, ACT_SORT); // sort is meaningless for live-scan rows
|
||||
if (stored) addValue(_sort_label, ACT_SORT); // sort is meaningless for live-scan rows
|
||||
add(stored ? "Discover scan" : "Rescan", ACT_SCAN);
|
||||
}
|
||||
|
||||
// Flip the selected contact's favourite flag and retitle the open menu row.
|
||||
// The list re-sorts underneath (favourites first), but refreshKeepingSelection()
|
||||
// re-finds this node, so the popup stays anchored to it.
|
||||
void toggleFavSelected() {
|
||||
const Entry* e = selected();
|
||||
if (!e || !e->has_key) return;
|
||||
bool now_fav = !e->fav;
|
||||
if (!the_mesh.setContactFavourite(e->pub_key, now_fav)) return;
|
||||
snprintf(_fav_label, sizeof(_fav_label), now_fav ? "Fav: ON" : "Fav: OFF");
|
||||
refreshKeepingSelection();
|
||||
}
|
||||
|
||||
// Advance the value on the menu's value rows (Sort, Fav). Both are two-state,
|
||||
// so LEFT and RIGHT do the same thing here and Enter joins them.
|
||||
void cycleMenuValue(int i) {
|
||||
if (i < 0 || i >= _menu_action_count) return;
|
||||
if (_menu_actions[i] == ACT_SORT) {
|
||||
_sort = (_sort == SORT_DIST) ? SORT_TIME : SORT_DIST;
|
||||
buildSortLabel();
|
||||
refresh();
|
||||
} else if (_menu_actions[i] == ACT_FAV) {
|
||||
toggleFavSelected();
|
||||
}
|
||||
}
|
||||
|
||||
void runAction(Action a) {
|
||||
switch (a) {
|
||||
case ACT_NAV: {
|
||||
@@ -644,11 +677,7 @@ class NearbyScreen : public UIScreen {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ACT_FAV: {
|
||||
const Entry* e = selected();
|
||||
if (e && e->has_key && the_mesh.setContactFavourite(e->pub_key, !e->fav)) refreshKeepingSelection();
|
||||
break;
|
||||
}
|
||||
case ACT_FAV: break; // value rows -- see cycleMenuValue()
|
||||
case ACT_PIN: {
|
||||
const Entry* e = selected();
|
||||
if (e && e->has_key) togglePinToDial(e->pub_key);
|
||||
@@ -662,7 +691,7 @@ class NearbyScreen : public UIScreen {
|
||||
_task->openAdminFor(ci, false); // direct from Nodes -- Cancel should return here, not to a pick-list
|
||||
break;
|
||||
}
|
||||
case ACT_SORT: break; // adjusted in-place via LEFT/RIGHT, not ENTER
|
||||
case ACT_SORT: break; // value rows -- see cycleMenuValue()
|
||||
case ACT_SCAN: enterScan(); break;
|
||||
}
|
||||
}
|
||||
@@ -951,23 +980,19 @@ public:
|
||||
}
|
||||
if (_ping_menu.active) { handlePingMenuInput(c); return true; }
|
||||
if (_menu.active) {
|
||||
// LEFT/RIGHT on the Sort row toggles the value in-place and rebuilds the
|
||||
// label; the popup stays open so the user can keep tapping. Other rows
|
||||
// swallow L/R. ENTER on Sort just closes (value changes via L/R only).
|
||||
// LEFT/RIGHT -- and Enter, which PopupMenu reports as VALUE_NEXT on a
|
||||
// value row -- cycle Sort and Fav in place; the popup stays open so the
|
||||
// user can keep tapping, and only Back closes it. Other rows swallow L/R.
|
||||
if (keyIsPrev(c) || keyIsNext(c)) {
|
||||
int i = _menu.selectedIndex();
|
||||
if (i >= 0 && i < _menu_action_count && _menu_actions[i] == ACT_SORT) {
|
||||
_sort = (_sort == SORT_DIST) ? SORT_TIME : SORT_DIST;
|
||||
buildSortLabel();
|
||||
refresh();
|
||||
}
|
||||
cycleMenuValue(_menu.selectedIndex());
|
||||
return true;
|
||||
}
|
||||
auto res = _menu.handleInput(c);
|
||||
if (res == PopupMenu::SELECTED) {
|
||||
if (res == PopupMenu::VALUE_NEXT) {
|
||||
cycleMenuValue(_menu.selectedIndex());
|
||||
} else if (res == PopupMenu::SELECTED) {
|
||||
int i = _menu.selectedIndex();
|
||||
if (i >= 0 && i < _menu_action_count && _menu_actions[i] != ACT_SORT)
|
||||
runAction(_menu_actions[i]);
|
||||
if (i >= 0 && i < _menu_action_count) runAction(_menu_actions[i]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -16,22 +16,39 @@ struct PopupMenu {
|
||||
int _cap; // actual visible cap, recomputed each render()
|
||||
bool active;
|
||||
const char* _title;
|
||||
// Rows added via addValueItem(): they carry a value the caller cycles rather
|
||||
// than an action to run, so Enter advances the value and leaves the menu open
|
||||
// (see handleInput). One bit per row; PM_MAX_ITEMS fits in a uint32_t.
|
||||
uint32_t _value_mask;
|
||||
|
||||
enum Result { NONE, SELECTED, CANCELLED };
|
||||
// VALUE_NEXT: Enter landed on a value row -- caller advances that row's value
|
||||
// (same as its RIGHT step) and the menu stays open.
|
||||
enum Result { NONE, SELECTED, CANCELLED, VALUE_NEXT };
|
||||
|
||||
PopupMenu() : _count(0), _sel(0), _scroll(0), _cap(3), active(false), _title(nullptr) {}
|
||||
PopupMenu() : _count(0), _sel(0), _scroll(0), _cap(3), active(false), _title(nullptr),
|
||||
_value_mask(0) {}
|
||||
|
||||
// `visible` is only a seed for the first frame: render() recomputes _cap from
|
||||
// the live display height, so it does not cap or pad the item list.
|
||||
void begin(const char* title, int visible = 3) {
|
||||
_count = 0; _sel = 0; _scroll = 0;
|
||||
_cap = visible; active = true; _title = title;
|
||||
_value_mask = 0;
|
||||
}
|
||||
|
||||
void addItem(const char* item) {
|
||||
if (_count < PM_MAX_ITEMS) _items[_count++] = item;
|
||||
}
|
||||
|
||||
// A row whose label shows a value ("Notif: ON", "Sort: Dist"). LEFT/RIGHT are
|
||||
// the caller's to handle as always; this only makes Enter behave like RIGHT
|
||||
// instead of picking the row and closing.
|
||||
void addValueItem(const char* item) {
|
||||
int i = _count;
|
||||
addItem(item);
|
||||
if (_count > i) _value_mask |= (1u << i);
|
||||
}
|
||||
|
||||
int render(DisplayDriver& display) {
|
||||
// Everything is derived from the live font metrics so the box fits its
|
||||
// content on every display — including landscape e-ink, where the font (and
|
||||
@@ -134,8 +151,13 @@ struct PopupMenu {
|
||||
// Selection only moves here; render() keeps it scrolled into view.
|
||||
if (c == KEY_UP) { _sel = (_sel > 0) ? _sel - 1 : _count - 1; return NONE; }
|
||||
if (c == KEY_DOWN) { _sel = (_sel < _count - 1) ? _sel + 1 : 0; return NONE; }
|
||||
if (c == KEY_ENTER) { active = false; return SELECTED; }
|
||||
if (c == KEY_CANCEL || c == KEY_CONTEXT_MENU) { active = false; return CANCELLED; }
|
||||
if (c == KEY_ENTER) {
|
||||
if (_value_mask & (1u << _sel)) return VALUE_NEXT; // value row -- stays open
|
||||
active = false; return SELECTED;
|
||||
}
|
||||
// Only Back closes a popup. Hold-Enter opens menus and cycles value rows;
|
||||
// it is deliberately not a second way to go back.
|
||||
if (c == KEY_CANCEL) { active = false; return CANCELLED; }
|
||||
return NONE;
|
||||
}
|
||||
|
||||
|
||||
@@ -242,7 +242,7 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
if (c == KEY_CANCEL || c == KEY_CONTEXT_MENU) {
|
||||
if (c == KEY_CANCEL) {
|
||||
_task->savePrefsIfDirty(_dirty);
|
||||
_task->gotoToolsScreen();
|
||||
return true;
|
||||
|
||||
@@ -81,8 +81,8 @@ public:
|
||||
_menu.begin("Options", 5);
|
||||
_menu.addItem(_menu_play_label);
|
||||
_menu.addItem(_menu_slot_label);
|
||||
_menu.addItem(_menu_dur_label);
|
||||
_menu.addItem(_menu_bpm_label);
|
||||
_menu.addValueItem(_menu_dur_label);
|
||||
_menu.addValueItem(_menu_bpm_label);
|
||||
_menu.addItem("Insert");
|
||||
_menu.addItem("Delete");
|
||||
_menu.addItem("Save & Exit");
|
||||
@@ -163,6 +163,21 @@ public:
|
||||
return 200;
|
||||
}
|
||||
|
||||
void cycleMenuValue(int sel, int dir) {
|
||||
if (sel == MI_DURATION && _cursor < _len) {
|
||||
uint8_t p = notePitch(_notes[_cursor]);
|
||||
uint8_t o = noteOctave(_notes[_cursor]);
|
||||
uint8_t di = noteDurIdx(_notes[_cursor]);
|
||||
di = (dir > 0) ? (di + 1) & 0x03 : (di + 3) & 0x03;
|
||||
_notes[_cursor] = packNote(p, o, di);
|
||||
snprintf(_menu_dur_label, sizeof(_menu_dur_label), "Duration: %s", DUR_LABELS[di]);
|
||||
} else if (sel == MI_BPM) {
|
||||
if (dir > 0) { if (_bpm_idx < 4) _bpm_idx++; }
|
||||
else { if (_bpm_idx > 0) _bpm_idx--; }
|
||||
snprintf(_menu_bpm_label, sizeof(_menu_bpm_label), "BPM: %u", BPM_OPTS[_bpm_idx]);
|
||||
}
|
||||
}
|
||||
|
||||
bool handleInput(char c) override {
|
||||
bool up = (c == KEY_UP);
|
||||
bool down = (c == KEY_DOWN);
|
||||
@@ -173,24 +188,11 @@ public:
|
||||
bool cancel = (c == KEY_CANCEL);
|
||||
|
||||
if (_menu.active) {
|
||||
// LEFT/RIGHT cycle Duration and BPM in-place, menu stays open.
|
||||
if (left || right) {
|
||||
int sel = _menu.selectedIndex();
|
||||
if (sel == MI_DURATION && _cursor < _len) {
|
||||
uint8_t p = notePitch(_notes[_cursor]);
|
||||
uint8_t o = noteOctave(_notes[_cursor]);
|
||||
uint8_t di = noteDurIdx(_notes[_cursor]);
|
||||
di = right ? (di + 1) & 0x03 : (di + 3) & 0x03;
|
||||
_notes[_cursor] = packNote(p, o, di);
|
||||
snprintf(_menu_dur_label, sizeof(_menu_dur_label), "Duration: %s", DUR_LABELS[di]);
|
||||
} else if (sel == MI_BPM) {
|
||||
if (right && _bpm_idx < 4) _bpm_idx++;
|
||||
else if (left && _bpm_idx > 0) _bpm_idx--;
|
||||
snprintf(_menu_bpm_label, sizeof(_menu_bpm_label), "BPM: %u", BPM_OPTS[_bpm_idx]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// LEFT/RIGHT -- and Enter, via VALUE_NEXT -- cycle Duration and BPM in
|
||||
// place; the menu stays open and only Back closes it.
|
||||
if (left || right) { cycleMenuValue(_menu.selectedIndex(), right ? 1 : -1); return true; }
|
||||
auto res = _menu.handleInput(c);
|
||||
if (res == PopupMenu::VALUE_NEXT) { cycleMenuValue(_menu.selectedIndex(), 1); return true; }
|
||||
if (res == PopupMenu::SELECTED) {
|
||||
switch ((MenuIdx)_menu.selectedIndex()) {
|
||||
case MI_PLAY:
|
||||
@@ -201,8 +203,8 @@ public:
|
||||
_task->stopMelody();
|
||||
this->selectSlot(1 - _slot);
|
||||
break;
|
||||
case MI_DURATION: break; // already handled by LEFT/RIGHT
|
||||
case MI_BPM: break; // already handled by LEFT/RIGHT
|
||||
case MI_DURATION: break; // value rows -- see cycleMenuValue()
|
||||
case MI_BPM: break;
|
||||
case MI_INSERT:
|
||||
if (_len < MAX_NOTES) {
|
||||
int ins = (_cursor < _len) ? _cursor + 1 : _cursor;
|
||||
|
||||
@@ -959,9 +959,9 @@ public:
|
||||
#if AUTO_OFF_MILLIS > 0
|
||||
if (_selected == AUTO_OFF && p) {
|
||||
int idx = autoOffIndex();
|
||||
if (right) idx = (idx + 1) % AUTO_OFF_COUNT;
|
||||
if (left) idx = (idx + AUTO_OFF_COUNT - 1) % AUTO_OFF_COUNT;
|
||||
if (left || right) { p->auto_off_secs = AUTO_OFF_OPTS[idx]; _dirty = true; return true; }
|
||||
if (right || enter) idx = (idx + 1) % AUTO_OFF_COUNT;
|
||||
else if (left) idx = (idx + AUTO_OFF_COUNT - 1) % AUTO_OFF_COUNT;
|
||||
if (left || right || enter) { p->auto_off_secs = AUTO_OFF_OPTS[idx]; _dirty = true; return true; }
|
||||
}
|
||||
#endif
|
||||
if (_selected == AUTO_LOCK && p && (left || right || enter)) {
|
||||
@@ -975,15 +975,15 @@ public:
|
||||
}
|
||||
if (_selected == LOW_BAT && p) {
|
||||
int idx = lowBatIndex();
|
||||
if (right) idx = (idx + 1) % LOW_BAT_COUNT;
|
||||
if (left) idx = (idx + LOW_BAT_COUNT - 1) % LOW_BAT_COUNT;
|
||||
if (left || right) { p->low_batt_mv = LOW_BAT_OPTS[idx]; _dirty = true; return true; }
|
||||
if (right || enter) idx = (idx + 1) % LOW_BAT_COUNT;
|
||||
else if (left) idx = (idx + LOW_BAT_COUNT - 1) % LOW_BAT_COUNT;
|
||||
if (left || right || enter) { p->low_batt_mv = LOW_BAT_OPTS[idx]; _dirty = true; return true; }
|
||||
}
|
||||
#if ENV_INCLUDE_GPS == 1
|
||||
if (_selected == GPS_DUTY_CYCLE && p && (left || right)) {
|
||||
if (_selected == GPS_DUTY_CYCLE && p && (left || right || enter)) {
|
||||
int idx = gpsDutyIndex();
|
||||
if (right) idx = (idx + 1) % GPS_DUTY_COUNT;
|
||||
if (left) idx = (idx + GPS_DUTY_COUNT - 1) % GPS_DUTY_COUNT;
|
||||
if (right || enter) idx = (idx + 1) % GPS_DUTY_COUNT;
|
||||
else if (left) idx = (idx + GPS_DUTY_COUNT - 1) % GPS_DUTY_COUNT;
|
||||
p->gps_interval = GPS_DUTY_OPTS[idx];
|
||||
_task->applyGpsInterval();
|
||||
_dirty = true;
|
||||
@@ -1049,9 +1049,9 @@ public:
|
||||
}
|
||||
if (_selected == BATT_DISPLAY && p) {
|
||||
int idx = p->batt_display_mode < BATT_DISPLAY_COUNT ? p->batt_display_mode : 0;
|
||||
if (right) idx = (idx + 1) % BATT_DISPLAY_COUNT;
|
||||
if (left) idx = (idx + BATT_DISPLAY_COUNT - 1) % BATT_DISPLAY_COUNT;
|
||||
if (left || right) { p->batt_display_mode = idx; _dirty = true; return true; }
|
||||
if (right || enter) idx = (idx + 1) % BATT_DISPLAY_COUNT;
|
||||
else if (left) idx = (idx + BATT_DISPLAY_COUNT - 1) % BATT_DISPLAY_COUNT;
|
||||
if (left || right || enter) { p->batt_display_mode = idx; _dirty = true; return true; }
|
||||
}
|
||||
#if FEAT_CLOCK_SECONDS_SETTING
|
||||
if (_selected == CLOCK_SECONDS && p && (left || right || enter)) {
|
||||
|
||||
@@ -108,7 +108,7 @@ public:
|
||||
}
|
||||
|
||||
bool handleInput(char c) override {
|
||||
if (c == KEY_CANCEL || c == KEY_CONTEXT_MENU) { _task->gotoHomeScreen(); return true; }
|
||||
if (c == KEY_CANCEL) { _task->gotoHomeScreen(); return true; }
|
||||
switch (_acc.handleInput(c)) {
|
||||
case AccordionList::ACTIVATED: {
|
||||
const AccordionList::Row& r = _acc.selected();
|
||||
|
||||
@@ -161,6 +161,11 @@ public:
|
||||
return true; // swallow elsewhere
|
||||
}
|
||||
auto res = _action_menu.handleInput(c);
|
||||
if (res == PopupMenu::VALUE_NEXT) {
|
||||
int idx = _action_menu.selectedIndex();
|
||||
if (idx >= 0 && idx < _act_count) cycleSetting((ActionId)_act_map[idx], 1);
|
||||
return true;
|
||||
}
|
||||
if (res == PopupMenu::SELECTED) {
|
||||
// GPS-off confirmation popup: rows aren't ActionIds, route by level.
|
||||
if (_menu_level == ML_CONFIRM_GPS) {
|
||||
@@ -177,13 +182,13 @@ public:
|
||||
switch (act) {
|
||||
case ACT_FILE: buildFileMenu(); return true; // descend into submenu
|
||||
case ACT_SETTINGS: buildSettingsMenu(); return true;
|
||||
// Settings rows: Enter advances/toggles the value and keeps focus.
|
||||
// Value rows -- Enter reaches them as VALUE_NEXT above, never here.
|
||||
case ACT_MIN_DIST:
|
||||
case ACT_UNITS:
|
||||
case ACT_GRID:
|
||||
case ACT_MARK_AVG:
|
||||
case ACT_AUTOSAVE:
|
||||
case ACT_AUTOPAUSE: cycleSetting(act, 1); reopenSettingsAt(sel); return true;
|
||||
case ACT_AUTOPAUSE: return true;
|
||||
case ACT_SHARE_NOW: shareMyLocationNow(); break;
|
||||
case ACT_TOGGLE:
|
||||
// Starting a trail with GPS switched off logs nothing and just
|
||||
@@ -346,6 +351,14 @@ private:
|
||||
_action_menu.addItem(label);
|
||||
}
|
||||
|
||||
// A settings row: its label carries a value, so Enter advances it and leaves
|
||||
// the popup open (PopupMenu reports VALUE_NEXT) instead of picking the row.
|
||||
void pushSetting(ActionId id, const char* label) {
|
||||
if (_act_count >= (int)sizeof(_act_map)) return;
|
||||
_act_map[_act_count++] = (uint8_t)id;
|
||||
_action_menu.addValueItem(label);
|
||||
}
|
||||
|
||||
bool fileMenuHasItems() const { return !_store->empty() || savedTrailExists(); }
|
||||
|
||||
// Hold-Enter entry point — always opens the short main menu.
|
||||
@@ -398,12 +411,12 @@ private:
|
||||
_menu_level = ML_SETTINGS;
|
||||
_act_count = 0;
|
||||
_action_menu.begin("Settings", 4);
|
||||
pushAction(ACT_MIN_DIST, _act_min_dist_label);
|
||||
pushAction(ACT_AUTOPAUSE, _act_autopause_label);
|
||||
pushAction(ACT_MARK_AVG, _act_mark_avg_label);
|
||||
pushAction(ACT_AUTOSAVE, _act_autosave_label);
|
||||
if (_view == V_SUMMARY) pushAction(ACT_UNITS, _act_units_label);
|
||||
if (_view == V_MAP) pushAction(ACT_GRID, _act_grid_label);
|
||||
pushSetting(ACT_MIN_DIST, _act_min_dist_label);
|
||||
pushSetting(ACT_AUTOPAUSE, _act_autopause_label);
|
||||
pushSetting(ACT_MARK_AVG, _act_mark_avg_label);
|
||||
pushSetting(ACT_AUTOSAVE, _act_autosave_label);
|
||||
if (_view == V_SUMMARY) pushSetting(ACT_UNITS, _act_units_label);
|
||||
if (_view == V_MAP) pushSetting(ACT_GRID, _act_grid_label);
|
||||
}
|
||||
|
||||
// Cycle a settings value. Returns true if `act` was a settings row.
|
||||
@@ -432,14 +445,6 @@ private:
|
||||
bool ownPos(int32_t& lat, int32_t& lon) const { return _task->currentLocation(lat, lon); }
|
||||
bool useImperial() const { return _task && _task->useImperial(); }
|
||||
|
||||
// After Enter on a settings row, the popup auto-closes per PopupMenu's
|
||||
// semantics. Re-open the Settings submenu with focus restored to that row so
|
||||
// the user can continue cycling.
|
||||
void reopenSettingsAt(int sel) {
|
||||
buildSettingsMenu();
|
||||
_action_menu.setSelected(sel);
|
||||
}
|
||||
|
||||
void cycleMinDelta(NodePrefs* p, int dir) {
|
||||
uint8_t idx = p->trail_min_delta_idx;
|
||||
if (idx >= TrailStore::MIN_DELTA_COUNT) idx = 0;
|
||||
|
||||
Reference in New Issue
Block a user