mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-26 14:58:12 +00:00
PopupMenu: - _cap now uses max_by_height as a hard ceiling (never draw outside screen). Previously max(_visible, max_by_height) let callers exceed the screen on OLED 64px. Now: OLED→4 items, landscape e-ink→10, portrait e-ink→22. QuickMsgScreen: - Contact context menu: Notif and Melody cycle with LEFT/RIGHT in-place; ENTER closes without re-cycling. - Channel context menu: Notif, Melody and Fav same pattern. RingtoneEditorScreen: - Migrated from bespoke menu to PopupMenu (same pattern as everywhere else). - Duration (1/4…1/32) and BPM (60…180) are now single rows that cycle with LEFT/RIGHT; separate BPM+/BPM- rows removed. - Fixed _menu_dur_label buffer too small for "Duration: 1/16" (14→16 bytes). NearbyScreen: - Removed redundant "Back" item from context menu (Cancel key navigates back). TrailScreen: - Grid toggle responds to LEFT/RIGHT in addition to ENTER. - Export labels: "Export GPX (live/saved)" → "Export (live/saved)" to fit PM_BW. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
98 lines
3.4 KiB
C
98 lines
3.4 KiB
C
#pragma once
|
|
#include <helpers/ui/DisplayDriver.h>
|
|
#include <Arduino.h>
|
|
|
|
// Generic scrollable popup menu overlay.
|
|
// Caller owns item string lifetimes — addItem() stores const char* pointers.
|
|
// Navigation wraps around: up from first item goes to last, and vice versa.
|
|
struct PopupMenu {
|
|
static const int PM_MAX_ITEMS = 16;
|
|
static const int PM_BX = 12;
|
|
static const int PM_BY = 10;
|
|
static const int PM_BW = 104;
|
|
static const int PM_ITEM_H = 10;
|
|
|
|
const char* _items[PM_MAX_ITEMS];
|
|
int _count;
|
|
int _sel;
|
|
int _scroll;
|
|
int _visible; // caller hint (min visible items)
|
|
int _cap; // actual visible cap, updated each render()
|
|
bool active;
|
|
const char* _title;
|
|
|
|
enum Result { NONE, SELECTED, CANCELLED };
|
|
|
|
PopupMenu() : _count(0), _sel(0), _scroll(0), _visible(3), _cap(3), active(false), _title(nullptr) {}
|
|
|
|
void begin(const char* title, int visible = 3) {
|
|
_count = 0; _sel = 0; _scroll = 0;
|
|
_visible = visible; _cap = visible; active = true; _title = title;
|
|
}
|
|
|
|
void addItem(const char* item) {
|
|
if (_count < PM_MAX_ITEMS) _items[_count++] = item;
|
|
}
|
|
|
|
int render(DisplayDriver& display) {
|
|
// Hard ceiling: never show more items than physically fit on screen.
|
|
// On tall displays (portrait e-ink) this expands beyond _visible;
|
|
// on small displays (OLED 64px) it clamps below _visible.
|
|
int max_by_height = (display.height() - PM_BY - 12) / PM_ITEM_H;
|
|
if (max_by_height < 1) max_by_height = 1;
|
|
_cap = max_by_height;
|
|
int vis = (_count < _cap) ? _count : _cap;
|
|
int bh = 12 + vis * PM_ITEM_H;
|
|
|
|
display.setColor(DisplayDriver::DARK);
|
|
display.fillRect(PM_BX, PM_BY, PM_BW, bh);
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
display.drawRect(PM_BX, PM_BY, PM_BW, bh);
|
|
display.setCursor(PM_BX + 4, PM_BY + 1);
|
|
display.print(_title);
|
|
display.fillRect(PM_BX, PM_BY + 10, PM_BW, 1);
|
|
|
|
if (_scroll > 0)
|
|
{ display.setCursor(PM_BX + PM_BW - 7, PM_BY + 1); display.print("^"); }
|
|
if (_scroll + vis < _count)
|
|
{ display.setCursor(PM_BX + PM_BW - 7, PM_BY + 2 + vis * PM_ITEM_H); display.print("v"); }
|
|
|
|
for (int i = 0; i < vis && (_scroll + i) < _count; i++) {
|
|
int idx = _scroll + i;
|
|
int py = PM_BY + 12 + i * PM_ITEM_H;
|
|
if (idx == _sel) {
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
display.fillRect(PM_BX + 1, py - 1, PM_BW - 2, PM_ITEM_H);
|
|
display.setColor(DisplayDriver::DARK);
|
|
} else {
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
}
|
|
display.setCursor(PM_BX + 4, py);
|
|
display.print(_items[idx]);
|
|
}
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
return 50;
|
|
}
|
|
|
|
Result handleInput(char c) {
|
|
if (_count == 0) { active = false; return CANCELLED; }
|
|
if (c == KEY_UP) {
|
|
_sel = (_sel > 0) ? _sel - 1 : _count - 1;
|
|
if (_sel < _scroll) _scroll = _sel;
|
|
else if (_sel == _count - 1 && _count > _cap) _scroll = _count - _cap;
|
|
return NONE;
|
|
}
|
|
if (c == KEY_DOWN) {
|
|
_sel = (_sel < _count - 1) ? _sel + 1 : 0;
|
|
if (_sel >= _scroll + _cap) _scroll = _sel - _cap + 1;
|
|
else if (_sel == 0) _scroll = 0;
|
|
return NONE;
|
|
}
|
|
if (c == KEY_ENTER) { active = false; return SELECTED; }
|
|
if (c == KEY_CANCEL || c == KEY_CONTEXT_MENU) { active = false; return CANCELLED; }
|
|
return NONE;
|
|
}
|
|
|
|
int selectedIndex() const { return _sel; }
|
|
};
|