mirror of
https://github.com/ratspeak/ratdeck.git
synced 2026-08-28 05:24:32 +00:00
Playing w/ LVGL
This commit is contained in:
@@ -54,6 +54,7 @@ extern const lv_font_t lv_font_ratdeck_14;
|
||||
#define LV_USE_BTNMATRIX 1
|
||||
#define LV_USE_TEXTAREA 1
|
||||
#define LV_USE_LIST 1
|
||||
#define LV_USE_MENU 1
|
||||
#define LV_USE_BAR 1
|
||||
#define LV_USE_SLIDER 1
|
||||
#define LV_USE_SWITCH 1
|
||||
|
||||
+13
-2
@@ -125,7 +125,17 @@ static bool s_keyReady = false;
|
||||
static unsigned long s_lastTouchMs = 0;
|
||||
static bool s_cursorVisible = false;
|
||||
|
||||
// Suppress touch for 300ms after trackball/keyboard use to prevent accidental taps
|
||||
static unsigned long s_lastKeyMs = 0;
|
||||
static constexpr unsigned long TOUCH_SUPPRESS_MS = 300;
|
||||
|
||||
static void touchpad_read_cb(lv_indev_drv_t *indev_driver, lv_indev_data_t *data) {
|
||||
// Ignore touch input briefly after trackball/keyboard use
|
||||
if (millis() - s_lastKeyMs < TOUCH_SUPPRESS_MS) {
|
||||
data->state = LV_INDEV_STATE_REL;
|
||||
return;
|
||||
}
|
||||
|
||||
if (s_touch->isTouched()) {
|
||||
lv_obj_clear_flag(s_cursor, LV_OBJ_FLAG_HIDDEN);
|
||||
s_cursorVisible = true;
|
||||
@@ -144,6 +154,7 @@ static void touchpad_read_cb(lv_indev_drv_t *indev_driver, lv_indev_data_t *data
|
||||
|
||||
static void keypad_read_cb(lv_indev_drv_t* drv, lv_indev_data_t* data) {
|
||||
if (s_keyReady) {
|
||||
s_lastKeyMs = millis();
|
||||
lv_obj_add_flag(s_cursor, LV_OBJ_FLAG_HIDDEN);
|
||||
data->key = s_lastKey;
|
||||
data->state = s_keyState;
|
||||
@@ -199,8 +210,8 @@ void feedKey(const KeyEvent& evt) {
|
||||
uint32_t key = 0;
|
||||
|
||||
if (evt.enter) key = LV_KEY_ENTER;
|
||||
else if (evt.up) key = LV_KEY_UP;
|
||||
else if (evt.down) key = LV_KEY_DOWN;
|
||||
else if (evt.up) key = LV_KEY_PREV;
|
||||
else if (evt.down) key = LV_KEY_NEXT;
|
||||
else if (evt.left) key = LV_KEY_LEFT;
|
||||
else if (evt.right) key = LV_KEY_RIGHT;
|
||||
else if (evt.del) key = LV_KEY_BACKSPACE;
|
||||
|
||||
@@ -104,8 +104,13 @@ void init(lv_disp_t* disp) {
|
||||
|
||||
lv_style_init(&s_listBtnFocused);
|
||||
lv_style_set_bg_color(&s_listBtnFocused, lv_color_hex(Theme::SELECTION_BG));
|
||||
lv_style_set_bg_opa(&s_listBtnFocused, LV_OPA_COVER);
|
||||
lv_style_set_text_color(&s_listBtnFocused, lv_color_hex(Theme::ACCENT));
|
||||
lv_style_set_border_color(&s_listBtnFocused, lv_color_hex(Theme::PRIMARY));
|
||||
lv_style_set_border_width(&s_listBtnFocused, 1);
|
||||
// Override LVGL's default blue focus outline
|
||||
lv_style_set_outline_width(&s_listBtnFocused, 0);
|
||||
lv_style_set_outline_opa(&s_listBtnFocused, LV_OPA_TRANSP);
|
||||
|
||||
// Dropdown
|
||||
lv_style_init(&s_dropdown);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "LvContactsScreen.h"
|
||||
#include "ui/Theme.h"
|
||||
#include "ui/LvTheme.h"
|
||||
#include "ui/LvInput.h"
|
||||
#include "ui/UIManager.h"
|
||||
#include "reticulum/AnnounceManager.h"
|
||||
#include <Arduino.h>
|
||||
@@ -18,63 +20,16 @@ void LvContactsScreen::createUI(lv_obj_t* parent) {
|
||||
|
||||
_list = lv_obj_create(parent);
|
||||
lv_obj_set_size(_list, lv_pct(100), lv_pct(100));
|
||||
lv_obj_set_pos(_list, 0, 0);
|
||||
lv_obj_set_flex_grow(_list, 1);
|
||||
lv_obj_set_style_bg_color(_list, lv_color_hex(Theme::BG), 0);
|
||||
lv_obj_set_style_bg_opa(_list, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_border_width(_list, 0, 0);
|
||||
lv_obj_set_style_pad_all(_list, 0, 0);
|
||||
lv_obj_set_style_pad_row(_list, 0, 0);
|
||||
lv_obj_set_style_radius(_list, 0, 0);
|
||||
lv_obj_add_style(_list, LvTheme::styleList(), 0);
|
||||
lv_obj_set_layout(_list, LV_LAYOUT_FLEX);
|
||||
lv_obj_set_flex_flow(_list, LV_FLEX_FLOW_COLUMN);
|
||||
|
||||
// Pre-allocate row pool
|
||||
const lv_font_t* font = &lv_font_ratdeck_14;
|
||||
for (int i = 0; i < ROW_POOL_SIZE; i++) {
|
||||
lv_obj_t* row = lv_obj_create(_list);
|
||||
lv_obj_set_size(row, Theme::CONTENT_W, 28);
|
||||
lv_obj_set_style_bg_color(row, lv_color_hex(Theme::BG), 0);
|
||||
lv_obj_set_style_bg_opa(row, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_border_color(row, lv_color_hex(Theme::BORDER), 0);
|
||||
lv_obj_set_style_border_width(row, 1, 0);
|
||||
lv_obj_set_style_border_side(row, LV_BORDER_SIDE_BOTTOM, 0);
|
||||
lv_obj_set_style_pad_all(row, 0, 0);
|
||||
lv_obj_set_style_radius(row, 0, 0);
|
||||
lv_obj_clear_flag(row, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_add_flag(row, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_add_flag(row, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_user_data(row, (void*)(intptr_t)i);
|
||||
lv_obj_add_event_cb(row, [](lv_event_t* e) {
|
||||
auto* self = (LvContactsScreen*)lv_event_get_user_data(e);
|
||||
int poolIdx = (int)(intptr_t)lv_obj_get_user_data(lv_event_get_target(e));
|
||||
int dataIdx = self->_viewportStart + poolIdx;
|
||||
if (dataIdx < (int)self->_contactIndices.size() && self->_onSelect) {
|
||||
self->_selectedIdx = dataIdx;
|
||||
self->syncVisibleRows();
|
||||
int nodeIdx = self->_contactIndices[dataIdx];
|
||||
self->_onSelect(self->_am->nodes()[nodeIdx].hash.toHex());
|
||||
}
|
||||
}, LV_EVENT_CLICKED, this);
|
||||
|
||||
lv_obj_t* lbl = lv_label_create(row);
|
||||
lv_obj_set_style_text_font(lbl, font, 0);
|
||||
lv_obj_set_style_text_color(lbl, lv_color_hex(Theme::ACCENT), 0);
|
||||
lv_label_set_text(lbl, "");
|
||||
lv_obj_align(lbl, LV_ALIGN_LEFT_MID, 8, 0);
|
||||
|
||||
_poolRows[i] = row;
|
||||
_poolNameLabels[i] = lbl;
|
||||
}
|
||||
|
||||
_lastContactCount = -1;
|
||||
rebuildList();
|
||||
}
|
||||
|
||||
void LvContactsScreen::onEnter() {
|
||||
_lastContactCount = -1;
|
||||
_selectedIdx = 0;
|
||||
_viewportStart = 0;
|
||||
rebuildList();
|
||||
}
|
||||
|
||||
@@ -87,15 +42,12 @@ void LvContactsScreen::refreshUI() {
|
||||
}
|
||||
}
|
||||
|
||||
void LvContactsScreen::updateSelection(int oldIdx, int newIdx) {
|
||||
syncVisibleRows();
|
||||
}
|
||||
|
||||
void LvContactsScreen::rebuildList() {
|
||||
if (!_am || !_list) return;
|
||||
_rows.clear();
|
||||
_contactIndices.clear();
|
||||
|
||||
lv_obj_clean(_list);
|
||||
|
||||
const auto& nodes = _am->nodes();
|
||||
for (int i = 0; i < (int)nodes.size(); i++) {
|
||||
if (nodes[i].saved) _contactIndices.push_back(i);
|
||||
@@ -106,63 +58,55 @@ void LvContactsScreen::rebuildList() {
|
||||
if (count == 0) {
|
||||
lv_obj_clear_flag(_lblEmpty, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag(_list, LV_OBJ_FLAG_HIDDEN);
|
||||
for (int i = 0; i < ROW_POOL_SIZE; i++) lv_obj_add_flag(_poolRows[i], LV_OBJ_FLAG_HIDDEN);
|
||||
return;
|
||||
}
|
||||
|
||||
lv_obj_add_flag(_lblEmpty, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_clear_flag(_list, LV_OBJ_FLAG_HIDDEN);
|
||||
|
||||
if (_selectedIdx >= count) _selectedIdx = count - 1;
|
||||
if (_selectedIdx < 0) _selectedIdx = 0;
|
||||
|
||||
syncVisibleRows();
|
||||
}
|
||||
|
||||
void LvContactsScreen::syncVisibleRows() {
|
||||
if (!_am || !_list) return;
|
||||
int count = (int)_contactIndices.size();
|
||||
const auto& nodes = _am->nodes();
|
||||
|
||||
if (count == 0) {
|
||||
for (int i = 0; i < ROW_POOL_SIZE; i++) lv_obj_add_flag(_poolRows[i], LV_OBJ_FLAG_HIDDEN);
|
||||
return;
|
||||
}
|
||||
|
||||
// Compute viewport centered on selection
|
||||
int halfPool = ROW_POOL_SIZE / 2;
|
||||
_viewportStart = _selectedIdx - halfPool;
|
||||
if (_viewportStart < 0) _viewportStart = 0;
|
||||
if (_viewportStart + ROW_POOL_SIZE > count) {
|
||||
_viewportStart = count - ROW_POOL_SIZE;
|
||||
if (_viewportStart < 0) _viewportStart = 0;
|
||||
}
|
||||
|
||||
for (int i = 0; i < ROW_POOL_SIZE; i++) {
|
||||
int contactIdx = _viewportStart + i;
|
||||
if (contactIdx >= count) {
|
||||
lv_obj_add_flag(_poolRows[i], LV_OBJ_FLAG_HIDDEN);
|
||||
continue;
|
||||
}
|
||||
|
||||
lv_obj_clear_flag(_poolRows[i], LV_OBJ_FLAG_HIDDEN);
|
||||
int nodeIdx = _contactIndices[contactIdx];
|
||||
if (nodeIdx < 0 || nodeIdx >= (int)nodes.size()) {
|
||||
lv_obj_add_flag(_poolRows[i], LV_OBJ_FLAG_HIDDEN);
|
||||
continue;
|
||||
}
|
||||
for (int i = 0; i < count; i++) {
|
||||
int nodeIdx = _contactIndices[i];
|
||||
const auto& node = nodes[nodeIdx];
|
||||
bool isSelected = (contactIdx == _selectedIdx);
|
||||
|
||||
lv_obj_set_style_bg_color(_poolRows[i], lv_color_hex(
|
||||
isSelected ? Theme::SELECTION_BG : Theme::BG), 0);
|
||||
lv_label_set_text(_poolNameLabels[i], node.name.c_str());
|
||||
lv_obj_t* row = lv_obj_create(_list);
|
||||
lv_obj_set_size(row, Theme::CONTENT_W, 32);
|
||||
lv_obj_add_style(row, LvTheme::styleListBtn(), 0);
|
||||
lv_obj_add_style(row, LvTheme::styleListBtnFocused(), LV_STATE_FOCUSED);
|
||||
lv_obj_set_style_border_side(row, LV_BORDER_SIDE_BOTTOM, 0);
|
||||
lv_obj_set_style_border_width(row, 1, 0);
|
||||
lv_obj_set_style_border_color(row, lv_color_hex(Theme::BORDER), 0);
|
||||
lv_obj_clear_flag(row, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_add_flag(row, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_set_user_data(row, (void*)(intptr_t)i);
|
||||
|
||||
lv_obj_add_event_cb(row, [](lv_event_t* e) {
|
||||
auto* self = (LvContactsScreen*)lv_event_get_user_data(e);
|
||||
int idx = (int)(intptr_t)lv_obj_get_user_data(lv_event_get_target(e));
|
||||
if (idx < (int)self->_contactIndices.size() && self->_onSelect) {
|
||||
int nodeIdx = self->_contactIndices[idx];
|
||||
self->_onSelect(self->_am->nodes()[nodeIdx].hash.toHex());
|
||||
}
|
||||
}, LV_EVENT_CLICKED, this);
|
||||
|
||||
lv_group_add_obj(LvInput::group(), row);
|
||||
lv_obj_add_event_cb(row, [](lv_event_t* e) {
|
||||
lv_obj_scroll_to_view(lv_event_get_target(e), LV_ANIM_ON);
|
||||
}, LV_EVENT_FOCUSED, nullptr);
|
||||
|
||||
lv_obj_t* lbl = lv_label_create(row);
|
||||
lv_obj_set_style_text_font(lbl, &lv_font_ratdeck_14, 0);
|
||||
lv_obj_set_style_text_color(lbl, lv_color_hex(Theme::ACCENT), 0);
|
||||
lv_label_set_text(lbl, node.name.c_str());
|
||||
lv_obj_align(lbl, LV_ALIGN_LEFT_MID, 8, 0);
|
||||
}
|
||||
}
|
||||
|
||||
bool LvContactsScreen::handleLongPress() {
|
||||
if (!_am || _contactIndices.empty()) return false;
|
||||
if (_selectedIdx < 0 || _selectedIdx >= (int)_contactIndices.size()) return false;
|
||||
lv_obj_t* focused = lv_group_get_focused(LvInput::group());
|
||||
if (!focused) return false;
|
||||
_deleteIdx = (int)(intptr_t)lv_obj_get_user_data(focused);
|
||||
if (_deleteIdx < 0 || _deleteIdx >= (int)_contactIndices.size()) return false;
|
||||
_confirmDelete = true;
|
||||
if (_ui) _ui->lvStatusBar().showToast("Delete contact? Enter=Yes Esc=No", 5000);
|
||||
return true;
|
||||
@@ -171,17 +115,15 @@ bool LvContactsScreen::handleLongPress() {
|
||||
bool LvContactsScreen::handleKey(const KeyEvent& event) {
|
||||
if (!_am || _contactIndices.empty()) return false;
|
||||
|
||||
// Confirm delete mode
|
||||
if (_confirmDelete) {
|
||||
if (event.enter || event.character == '\n' || event.character == '\r') {
|
||||
if (_selectedIdx >= 0 && _selectedIdx < (int)_contactIndices.size()) {
|
||||
int nodeIdx = _contactIndices[_selectedIdx];
|
||||
if (_deleteIdx >= 0 && _deleteIdx < (int)_contactIndices.size()) {
|
||||
int nodeIdx = _contactIndices[_deleteIdx];
|
||||
if (nodeIdx >= 0 && nodeIdx < (int)_am->nodes().size()) {
|
||||
auto& nodes = const_cast<std::vector<DiscoveredNode>&>(_am->nodes());
|
||||
nodes.erase(nodes.begin() + nodeIdx);
|
||||
_am->saveContacts();
|
||||
if (_ui) _ui->lvStatusBar().showToast("Contact deleted", 1200);
|
||||
_selectedIdx = 0;
|
||||
rebuildList();
|
||||
}
|
||||
}
|
||||
@@ -193,28 +135,5 @@ bool LvContactsScreen::handleKey(const KeyEvent& event) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int count = (int)_contactIndices.size();
|
||||
|
||||
if (event.up) {
|
||||
if (_selectedIdx > 0) {
|
||||
_selectedIdx--;
|
||||
syncVisibleRows();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (event.down) {
|
||||
if (_selectedIdx < count - 1) {
|
||||
_selectedIdx++;
|
||||
syncVisibleRows();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (event.enter || event.character == '\n' || event.character == '\r') {
|
||||
if (_selectedIdx >= 0 && _selectedIdx < count && _onSelect) {
|
||||
int nodeIdx = _contactIndices[_selectedIdx];
|
||||
_onSelect(_am->nodes()[nodeIdx].hash.toHex());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -25,24 +25,15 @@ public:
|
||||
|
||||
private:
|
||||
void rebuildList();
|
||||
void syncVisibleRows();
|
||||
void updateSelection(int oldIdx, int newIdx);
|
||||
|
||||
AnnounceManager* _am = nullptr;
|
||||
class UIManager* _ui = nullptr;
|
||||
NodeSelectedCallback _onSelect;
|
||||
bool _confirmDelete = false;
|
||||
int _deleteIdx = -1;
|
||||
int _lastContactCount = -1;
|
||||
int _selectedIdx = 0;
|
||||
int _viewportStart = 0;
|
||||
std::vector<int> _contactIndices; // Maps row -> node index in _am->nodes()
|
||||
std::vector<int> _contactIndices;
|
||||
|
||||
lv_obj_t* _list = nullptr;
|
||||
lv_obj_t* _lblEmpty = nullptr;
|
||||
std::vector<lv_obj_t*> _rows;
|
||||
|
||||
// Object pool
|
||||
static constexpr int ROW_POOL_SIZE = 10;
|
||||
lv_obj_t* _poolRows[ROW_POOL_SIZE] = {};
|
||||
lv_obj_t* _poolNameLabels[ROW_POOL_SIZE] = {};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "LvMessagesScreen.h"
|
||||
#include "ui/Theme.h"
|
||||
#include "ui/LvTheme.h"
|
||||
#include "ui/LvInput.h"
|
||||
#include "ui/UIManager.h"
|
||||
#include "reticulum/LXMFManager.h"
|
||||
#include "reticulum/AnnounceManager.h"
|
||||
@@ -22,94 +24,16 @@ void LvMessagesScreen::createUI(lv_obj_t* parent) {
|
||||
|
||||
_list = lv_obj_create(parent);
|
||||
lv_obj_set_size(_list, lv_pct(100), lv_pct(100));
|
||||
lv_obj_set_pos(_list, 0, 0);
|
||||
lv_obj_set_flex_grow(_list, 1);
|
||||
lv_obj_set_style_bg_color(_list, lv_color_hex(Theme::BG), 0);
|
||||
lv_obj_set_style_bg_opa(_list, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_border_width(_list, 0, 0);
|
||||
lv_obj_set_style_pad_all(_list, 0, 0);
|
||||
lv_obj_set_style_pad_row(_list, 0, 0);
|
||||
lv_obj_set_style_radius(_list, 0, 0);
|
||||
lv_obj_add_style(_list, LvTheme::styleList(), 0);
|
||||
lv_obj_set_layout(_list, LV_LAYOUT_FLEX);
|
||||
lv_obj_set_flex_flow(_list, LV_FLEX_FLOW_COLUMN);
|
||||
|
||||
// Pre-allocate row pool (following LvNodesScreen pattern)
|
||||
const lv_font_t* nameFont = &lv_font_ratdeck_14;
|
||||
const lv_font_t* smallFont = &lv_font_ratdeck_12;
|
||||
|
||||
for (int i = 0; i < ROW_POOL_SIZE; i++) {
|
||||
lv_obj_t* row = lv_obj_create(_list);
|
||||
lv_obj_set_size(row, Theme::CONTENT_W, 38);
|
||||
lv_obj_set_style_bg_color(row, lv_color_hex(Theme::BG), 0);
|
||||
lv_obj_set_style_bg_opa(row, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_border_color(row, lv_color_hex(Theme::BORDER), 0);
|
||||
lv_obj_set_style_border_width(row, 1, 0);
|
||||
lv_obj_set_style_border_side(row, LV_BORDER_SIDE_BOTTOM, 0);
|
||||
lv_obj_set_style_pad_all(row, 0, 0);
|
||||
lv_obj_set_style_radius(row, 0, 0);
|
||||
lv_obj_clear_flag(row, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_add_flag(row, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_add_flag(row, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_user_data(row, (void*)(intptr_t)i);
|
||||
lv_obj_add_event_cb(row, [](lv_event_t* e) {
|
||||
auto* self = (LvMessagesScreen*)lv_event_get_user_data(e);
|
||||
lv_obj_t* target = lv_event_get_target(e);
|
||||
int poolIdx = (int)(intptr_t)lv_obj_get_user_data(target);
|
||||
int dataIdx = self->_viewportStart + poolIdx;
|
||||
if (dataIdx < (int)self->_sortedPeers.size() && self->_onOpen) {
|
||||
self->_selectedIdx = dataIdx;
|
||||
self->syncVisibleRows();
|
||||
self->_onOpen(self->_sortedPeers[dataIdx]);
|
||||
}
|
||||
}, LV_EVENT_CLICKED, this);
|
||||
|
||||
// Unread dot
|
||||
lv_obj_t* dot = lv_obj_create(row);
|
||||
lv_obj_set_size(dot, 6, 6);
|
||||
lv_obj_set_style_radius(dot, 3, 0);
|
||||
lv_obj_set_style_bg_color(dot, lv_color_hex(Theme::PRIMARY), 0);
|
||||
lv_obj_set_style_bg_opa(dot, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_border_width(dot, 0, 0);
|
||||
lv_obj_set_style_pad_all(dot, 0, 0);
|
||||
lv_obj_set_pos(dot, 5, 8);
|
||||
lv_obj_add_flag(dot, LV_OBJ_FLAG_HIDDEN);
|
||||
|
||||
// Name label (line 1)
|
||||
lv_obj_t* nameLbl = lv_label_create(row);
|
||||
lv_obj_set_style_text_font(nameLbl, nameFont, 0);
|
||||
lv_obj_set_style_text_color(nameLbl, lv_color_hex(Theme::PRIMARY), 0);
|
||||
lv_label_set_text(nameLbl, "");
|
||||
lv_obj_align(nameLbl, LV_ALIGN_TOP_LEFT, 14, 2);
|
||||
|
||||
// Time label (line 1, right)
|
||||
lv_obj_t* timeLbl = lv_label_create(row);
|
||||
lv_obj_set_style_text_font(timeLbl, &lv_font_ratdeck_10, 0);
|
||||
lv_obj_set_style_text_color(timeLbl, lv_color_hex(Theme::MUTED), 0);
|
||||
lv_label_set_text(timeLbl, "");
|
||||
lv_obj_align(timeLbl, LV_ALIGN_TOP_RIGHT, -4, 4);
|
||||
|
||||
// Preview label (line 2)
|
||||
lv_obj_t* prevLbl = lv_label_create(row);
|
||||
lv_obj_set_style_text_font(prevLbl, smallFont, 0);
|
||||
lv_obj_set_style_text_color(prevLbl, lv_color_hex(Theme::MUTED), 0);
|
||||
lv_label_set_text(prevLbl, "");
|
||||
lv_obj_align(prevLbl, LV_ALIGN_BOTTOM_LEFT, 14, -4);
|
||||
|
||||
_poolRows[i] = row;
|
||||
_poolDots[i] = dot;
|
||||
_poolNameLabels[i] = nameLbl;
|
||||
_poolTimeLabels[i] = timeLbl;
|
||||
_poolPreviewLabels[i] = prevLbl;
|
||||
}
|
||||
|
||||
_lastConvCount = -1;
|
||||
rebuildList();
|
||||
}
|
||||
|
||||
void LvMessagesScreen::onEnter() {
|
||||
_lastConvCount = -1;
|
||||
_selectedIdx = 0;
|
||||
_viewportStart = 0;
|
||||
rebuildList();
|
||||
}
|
||||
|
||||
@@ -122,11 +46,6 @@ void LvMessagesScreen::refreshUI() {
|
||||
}
|
||||
}
|
||||
|
||||
// Update only the selection highlight via pool sync
|
||||
void LvMessagesScreen::updateSelection(int oldIdx, int newIdx) {
|
||||
syncVisibleRows();
|
||||
}
|
||||
|
||||
void LvMessagesScreen::rebuildList() {
|
||||
if (!_lxmf || !_list) return;
|
||||
|
||||
@@ -137,10 +56,11 @@ void LvMessagesScreen::rebuildList() {
|
||||
_sortedPeers.clear();
|
||||
_sortedConvs.clear();
|
||||
|
||||
lv_obj_clean(_list);
|
||||
|
||||
if (count == 0) {
|
||||
lv_obj_clear_flag(_lblEmpty, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag(_list, LV_OBJ_FLAG_HIDDEN);
|
||||
for (int i = 0; i < ROW_POOL_SIZE; i++) lv_obj_add_flag(_poolRows[i], LV_OBJ_FLAG_HIDDEN);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -158,7 +78,6 @@ void LvMessagesScreen::rebuildList() {
|
||||
ci.preview = s->lastPreview;
|
||||
ci.hasUnread = s->unreadCount > 0;
|
||||
}
|
||||
// Resolve display name
|
||||
std::string peerName;
|
||||
if (_am) peerName = _am->lookupName(ci.peerHex);
|
||||
ci.displayName = !peerName.empty() ? peerName.substr(0, 15) : ci.peerHex.substr(0, 12);
|
||||
@@ -171,79 +90,97 @@ void LvMessagesScreen::rebuildList() {
|
||||
|
||||
for (auto& ci : _sortedConvs) _sortedPeers.push_back(ci.peerHex);
|
||||
|
||||
if (_selectedIdx >= count) _selectedIdx = count - 1;
|
||||
if (_selectedIdx < 0) _selectedIdx = 0;
|
||||
// Build list items with focus group support
|
||||
const lv_font_t* nameFont = &lv_font_ratdeck_14;
|
||||
const lv_font_t* smallFont = &lv_font_ratdeck_12;
|
||||
|
||||
syncVisibleRows();
|
||||
}
|
||||
for (int i = 0; i < count; i++) {
|
||||
const auto& ci = _sortedConvs[i];
|
||||
|
||||
void LvMessagesScreen::syncVisibleRows() {
|
||||
if (!_list) return;
|
||||
int count = (int)_sortedConvs.size();
|
||||
lv_obj_t* row = lv_obj_create(_list);
|
||||
lv_obj_set_size(row, Theme::CONTENT_W, 46);
|
||||
lv_obj_add_style(row, LvTheme::styleListBtn(), 0);
|
||||
lv_obj_add_style(row, LvTheme::styleListBtnFocused(), LV_STATE_FOCUSED);
|
||||
lv_obj_set_style_border_side(row, LV_BORDER_SIDE_BOTTOM, 0);
|
||||
lv_obj_set_style_border_width(row, 1, 0);
|
||||
lv_obj_set_style_border_color(row, lv_color_hex(Theme::BORDER), 0);
|
||||
lv_obj_clear_flag(row, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_add_flag(row, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_set_user_data(row, (void*)(intptr_t)i);
|
||||
|
||||
if (count == 0) {
|
||||
for (int i = 0; i < ROW_POOL_SIZE; i++) lv_obj_add_flag(_poolRows[i], LV_OBJ_FLAG_HIDDEN);
|
||||
return;
|
||||
}
|
||||
lv_obj_add_event_cb(row, [](lv_event_t* e) {
|
||||
auto* self = (LvMessagesScreen*)lv_event_get_user_data(e);
|
||||
int idx = (int)(intptr_t)lv_obj_get_user_data(lv_event_get_target(e));
|
||||
if (idx < (int)self->_sortedPeers.size() && self->_onOpen) {
|
||||
self->_onOpen(self->_sortedPeers[idx]);
|
||||
}
|
||||
}, LV_EVENT_CLICKED, this);
|
||||
|
||||
// Compute viewport centered on selection
|
||||
int halfPool = ROW_POOL_SIZE / 2;
|
||||
_viewportStart = _selectedIdx - halfPool;
|
||||
if (_viewportStart < 0) _viewportStart = 0;
|
||||
if (_viewportStart + ROW_POOL_SIZE > count) {
|
||||
_viewportStart = count - ROW_POOL_SIZE;
|
||||
if (_viewportStart < 0) _viewportStart = 0;
|
||||
}
|
||||
lv_group_add_obj(LvInput::group(), row);
|
||||
lv_obj_add_event_cb(row, [](lv_event_t* e) {
|
||||
lv_obj_scroll_to_view(lv_event_get_target(e), LV_ANIM_ON);
|
||||
}, LV_EVENT_FOCUSED, nullptr);
|
||||
|
||||
for (int i = 0; i < ROW_POOL_SIZE; i++) {
|
||||
int convIdx = _viewportStart + i;
|
||||
if (convIdx >= count) {
|
||||
lv_obj_add_flag(_poolRows[i], LV_OBJ_FLAG_HIDDEN);
|
||||
continue;
|
||||
}
|
||||
|
||||
lv_obj_clear_flag(_poolRows[i], LV_OBJ_FLAG_HIDDEN);
|
||||
const auto& ci = _sortedConvs[convIdx];
|
||||
bool isSelected = (convIdx == _selectedIdx);
|
||||
|
||||
// Selection highlight
|
||||
lv_obj_set_style_bg_color(_poolRows[i], lv_color_hex(
|
||||
isSelected ? Theme::SELECTION_BG : Theme::BG), 0);
|
||||
int leftPad = 14;
|
||||
|
||||
// Unread dot
|
||||
if (ci.hasUnread) {
|
||||
lv_obj_clear_flag(_poolDots[i], LV_OBJ_FLAG_HIDDEN);
|
||||
} else {
|
||||
lv_obj_add_flag(_poolDots[i], LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_t* dot = lv_obj_create(row);
|
||||
lv_obj_set_size(dot, 6, 6);
|
||||
lv_obj_set_style_radius(dot, 3, 0);
|
||||
lv_obj_set_style_bg_color(dot, lv_color_hex(Theme::PRIMARY), 0);
|
||||
lv_obj_set_style_bg_opa(dot, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_border_width(dot, 0, 0);
|
||||
lv_obj_set_style_pad_all(dot, 0, 0);
|
||||
lv_obj_set_pos(dot, 4, 7);
|
||||
}
|
||||
|
||||
// Name
|
||||
lv_label_set_text(_poolNameLabels[i], ci.displayName.c_str());
|
||||
// Name (top-left, first line)
|
||||
lv_obj_t* nameLbl = lv_label_create(row);
|
||||
lv_obj_set_style_text_font(nameLbl, nameFont, 0);
|
||||
lv_obj_set_style_text_color(nameLbl, lv_color_hex(Theme::PRIMARY), 0);
|
||||
lv_label_set_text(nameLbl, ci.displayName.c_str());
|
||||
lv_obj_set_pos(nameLbl, leftPad, 1);
|
||||
|
||||
// Time
|
||||
// Time (top-right)
|
||||
if (ci.lastTs > 1700000000) {
|
||||
time_t t = (time_t)ci.lastTs;
|
||||
struct tm* tm = localtime(&t);
|
||||
if (tm) {
|
||||
char timeBuf[8];
|
||||
snprintf(timeBuf, sizeof(timeBuf), "%02d:%02d", tm->tm_hour, tm->tm_min);
|
||||
lv_label_set_text(_poolTimeLabels[i], timeBuf);
|
||||
} else {
|
||||
lv_label_set_text(_poolTimeLabels[i], "");
|
||||
lv_obj_t* timeLbl = lv_label_create(row);
|
||||
lv_obj_set_style_text_font(timeLbl, &lv_font_ratdeck_10, 0);
|
||||
lv_obj_set_style_text_color(timeLbl, lv_color_hex(Theme::MUTED), 0);
|
||||
lv_label_set_text(timeLbl, timeBuf);
|
||||
lv_obj_align(timeLbl, LV_ALIGN_TOP_RIGHT, -4, 3);
|
||||
}
|
||||
} else {
|
||||
lv_label_set_text(_poolTimeLabels[i], "");
|
||||
}
|
||||
|
||||
// Preview
|
||||
lv_label_set_text(_poolPreviewLabels[i], ci.preview.c_str());
|
||||
// Preview (second line, below name)
|
||||
if (!ci.preview.empty()) {
|
||||
lv_obj_t* prevLbl = lv_label_create(row);
|
||||
lv_obj_set_style_text_font(prevLbl, smallFont, 0);
|
||||
lv_obj_set_style_text_color(prevLbl, lv_color_hex(Theme::MUTED), 0);
|
||||
lv_label_set_long_mode(prevLbl, LV_LABEL_LONG_CLIP);
|
||||
lv_obj_set_width(prevLbl, Theme::CONTENT_W - leftPad - 8);
|
||||
lv_label_set_text(prevLbl, ci.preview.c_str());
|
||||
lv_obj_set_pos(prevLbl, leftPad, 20);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int LvMessagesScreen::getFocusedPeerIdx() const {
|
||||
lv_obj_t* focused = lv_group_get_focused(LvInput::group());
|
||||
if (!focused) return -1;
|
||||
return (int)(intptr_t)lv_obj_get_user_data(focused);
|
||||
}
|
||||
|
||||
bool LvMessagesScreen::handleLongPress() {
|
||||
if (!_lxmf) return false;
|
||||
int count = (int)_lxmf->conversations().size();
|
||||
if (count == 0 || _selectedIdx >= count) return false;
|
||||
int idx = getFocusedPeerIdx();
|
||||
if (idx < 0 || idx >= (int)_sortedPeers.size()) return false;
|
||||
_lpPeerIdx = idx;
|
||||
_lpState = LP_MENU;
|
||||
_menuIdx = 0;
|
||||
if (_ui) _ui->lvStatusBar().showToast("Up/Down: Add Friend | Delete | Cancel", 5000);
|
||||
@@ -262,10 +199,8 @@ bool LvMessagesScreen::handleKey(const KeyEvent& event) {
|
||||
return true;
|
||||
}
|
||||
if (event.enter || event.character == '\n' || event.character == '\r') {
|
||||
int count = (int)_lxmf->conversations().size();
|
||||
if (_menuIdx == 0 && _selectedIdx < (int)_sortedPeers.size()) {
|
||||
// Add friend
|
||||
const auto& peerHex = _sortedPeers[_selectedIdx];
|
||||
if (_menuIdx == 0 && _lpPeerIdx < (int)_sortedPeers.size()) {
|
||||
const auto& peerHex = _sortedPeers[_lpPeerIdx];
|
||||
if (_am) {
|
||||
const DiscoveredNode* existing = _am->findNodeByHex(peerHex);
|
||||
if (existing && !existing->saved) {
|
||||
@@ -280,13 +215,11 @@ bool LvMessagesScreen::handleKey(const KeyEvent& event) {
|
||||
if (_ui) _ui->lvStatusBar().showToast("Already a friend", 1200);
|
||||
}
|
||||
}
|
||||
} else if (_menuIdx == 1 && _selectedIdx < (int)_sortedPeers.size()) {
|
||||
// Confirm delete
|
||||
} else if (_menuIdx == 1 && _lpPeerIdx < (int)_sortedPeers.size()) {
|
||||
_lpState = LP_CONFIRM_DELETE;
|
||||
if (_ui) _ui->lvStatusBar().showToast("Delete chat? Enter=Yes Esc=No", 5000);
|
||||
return true;
|
||||
} else {
|
||||
// Cancel
|
||||
if (_ui) _ui->lvStatusBar().showToast("Cancelled", 800);
|
||||
}
|
||||
_lpState = LP_NONE;
|
||||
@@ -303,8 +236,8 @@ bool LvMessagesScreen::handleKey(const KeyEvent& event) {
|
||||
// Confirm delete mode
|
||||
if (_lpState == LP_CONFIRM_DELETE) {
|
||||
if (event.enter || event.character == '\n' || event.character == '\r') {
|
||||
if (_selectedIdx < (int)_sortedPeers.size()) {
|
||||
const auto& peerHex = _sortedPeers[_selectedIdx];
|
||||
if (_lpPeerIdx < (int)_sortedPeers.size()) {
|
||||
const auto& peerHex = _sortedPeers[_lpPeerIdx];
|
||||
_lxmf->markRead(peerHex);
|
||||
extern MessageStore messageStore;
|
||||
messageStore.deleteConversation(peerHex);
|
||||
@@ -313,7 +246,6 @@ bool LvMessagesScreen::handleKey(const KeyEvent& event) {
|
||||
_ui->lvStatusBar().showToast("Chat deleted", 1200);
|
||||
_ui->lvTabBar().setUnreadCount(LvTabBar::TAB_MSGS, _lxmf->unreadCount());
|
||||
}
|
||||
_selectedIdx = 0;
|
||||
_lastConvCount = -1;
|
||||
rebuildList();
|
||||
}
|
||||
@@ -325,30 +257,6 @@ bool LvMessagesScreen::handleKey(const KeyEvent& event) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int count = (int)_lxmf->conversations().size();
|
||||
if (count == 0) return false;
|
||||
|
||||
if (event.up) {
|
||||
if (_selectedIdx > 0) {
|
||||
int prev = _selectedIdx;
|
||||
_selectedIdx--;
|
||||
syncVisibleRows();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (event.down) {
|
||||
if (_selectedIdx < count - 1) {
|
||||
int prev = _selectedIdx;
|
||||
_selectedIdx++;
|
||||
syncVisibleRows();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (event.enter || event.character == '\n' || event.character == '\r') {
|
||||
if (_selectedIdx < (int)_sortedPeers.size() && _onOpen) {
|
||||
_onOpen(_sortedPeers[_selectedIdx]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// Let LVGL focus group handle up/down/enter navigation
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -27,8 +27,7 @@ public:
|
||||
|
||||
private:
|
||||
void rebuildList();
|
||||
void syncVisibleRows();
|
||||
void updateSelection(int oldIdx, int newIdx);
|
||||
int getFocusedPeerIdx() const;
|
||||
|
||||
LXMFManager* _lxmf = nullptr;
|
||||
AnnounceManager* _am = nullptr;
|
||||
@@ -36,24 +35,14 @@ private:
|
||||
OpenCallback _onOpen;
|
||||
int _lastConvCount = -1;
|
||||
int _lastUnreadTotal = 0;
|
||||
int _selectedIdx = 0;
|
||||
int _viewportStart = 0;
|
||||
std::vector<std::string> _sortedPeers;
|
||||
enum LongPressState { LP_NONE, LP_MENU, LP_CONFIRM_DELETE };
|
||||
LongPressState _lpState = LP_NONE;
|
||||
int _menuIdx = 0; // 0=Add Friend, 1=Delete Chat, 2=Cancel
|
||||
int _lpPeerIdx = -1;
|
||||
int _menuIdx = 0;
|
||||
|
||||
lv_obj_t* _list = nullptr;
|
||||
lv_obj_t* _lblEmpty = nullptr;
|
||||
std::vector<lv_obj_t*> _rows;
|
||||
|
||||
// Object pool for conversation rows
|
||||
static constexpr int ROW_POOL_SIZE = 10;
|
||||
lv_obj_t* _poolRows[ROW_POOL_SIZE] = {};
|
||||
lv_obj_t* _poolNameLabels[ROW_POOL_SIZE] = {};
|
||||
lv_obj_t* _poolPreviewLabels[ROW_POOL_SIZE] = {};
|
||||
lv_obj_t* _poolTimeLabels[ROW_POOL_SIZE] = {};
|
||||
lv_obj_t* _poolDots[ROW_POOL_SIZE] = {};
|
||||
|
||||
// Cached sorted conversation data
|
||||
struct ConvInfo {
|
||||
|
||||
+122
-275
@@ -1,6 +1,7 @@
|
||||
#include "LvNodesScreen.h"
|
||||
#include "ui/Theme.h"
|
||||
#include "ui/LvTheme.h"
|
||||
#include "ui/LvInput.h"
|
||||
#include "ui/UIManager.h"
|
||||
#include "reticulum/AnnounceManager.h"
|
||||
#include "config/UserConfig.h"
|
||||
@@ -13,83 +14,23 @@ void LvNodesScreen::createUI(lv_obj_t* parent) {
|
||||
lv_obj_set_style_bg_color(parent, lv_color_hex(Theme::BG), 0);
|
||||
lv_obj_set_style_pad_all(parent, 0, 0);
|
||||
|
||||
// Empty state label
|
||||
_lblEmpty = lv_label_create(parent);
|
||||
lv_obj_set_style_text_font(_lblEmpty, &lv_font_ratdeck_14, 0);
|
||||
lv_obj_set_style_text_color(_lblEmpty, lv_color_hex(Theme::MUTED), 0);
|
||||
lv_label_set_text(_lblEmpty, "No nodes discovered");
|
||||
lv_obj_center(_lblEmpty);
|
||||
|
||||
// Scrollable list container
|
||||
_list = lv_obj_create(parent);
|
||||
lv_obj_set_size(_list, lv_pct(100), lv_pct(100));
|
||||
lv_obj_set_pos(_list, 0, 0);
|
||||
lv_obj_set_flex_grow(_list, 1);
|
||||
lv_obj_set_style_bg_color(_list, lv_color_hex(Theme::BG), 0);
|
||||
lv_obj_set_style_bg_opa(_list, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_border_width(_list, 0, 0);
|
||||
lv_obj_set_style_pad_all(_list, 0, 0);
|
||||
lv_obj_set_style_pad_row(_list, 0, 0);
|
||||
lv_obj_set_style_radius(_list, 0, 0);
|
||||
lv_obj_add_style(_list, LvTheme::styleList(), 0);
|
||||
lv_obj_set_layout(_list, LV_LAYOUT_FLEX);
|
||||
lv_obj_set_flex_flow(_list, LV_FLEX_FLOW_COLUMN);
|
||||
|
||||
// Pre-allocate ROW_POOL_SIZE row widgets
|
||||
const lv_font_t* font = &lv_font_ratdeck_14;
|
||||
const lv_font_t* smallFont = &lv_font_ratdeck_10;
|
||||
rebuildList();
|
||||
|
||||
for (int i = 0; i < ROW_POOL_SIZE; i++) {
|
||||
lv_obj_t* row = lv_obj_create(_list);
|
||||
lv_obj_set_size(row, Theme::CONTENT_W, 24);
|
||||
lv_obj_set_style_bg_color(row, lv_color_hex(Theme::BG), 0);
|
||||
lv_obj_set_style_bg_opa(row, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_border_width(row, 0, 0);
|
||||
lv_obj_set_style_pad_all(row, 0, 0);
|
||||
lv_obj_set_style_radius(row, 0, 0);
|
||||
lv_obj_clear_flag(row, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_add_flag(row, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_add_flag(row, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_user_data(row, (void*)(intptr_t)i);
|
||||
lv_obj_add_event_cb(row, [](lv_event_t* e) {
|
||||
auto* self = (LvNodesScreen*)lv_event_get_user_data(e);
|
||||
int poolIdx = (int)(intptr_t)lv_obj_get_user_data(lv_event_get_target(e));
|
||||
int entryIdx = self->_viewportStart + poolIdx;
|
||||
if (entryIdx < self->_totalEntries) {
|
||||
self->_selectedIdx = entryIdx;
|
||||
self->syncVisibleRows();
|
||||
int nodeIdx = self->getNodeIdxForEntry(entryIdx);
|
||||
if (nodeIdx >= 0 && nodeIdx < (int)self->_am->nodes().size()) {
|
||||
self->showActionMenu(nodeIdx);
|
||||
}
|
||||
}
|
||||
}, LV_EVENT_CLICKED, this);
|
||||
|
||||
lv_obj_t* nameLbl = lv_label_create(row);
|
||||
lv_obj_set_style_text_font(nameLbl, font, 0);
|
||||
lv_obj_set_style_text_color(nameLbl, lv_color_hex(Theme::PRIMARY), 0);
|
||||
lv_label_set_text(nameLbl, "");
|
||||
lv_obj_align(nameLbl, LV_ALIGN_LEFT_MID, 8, 0);
|
||||
|
||||
lv_obj_t* infoLbl = lv_label_create(row);
|
||||
lv_obj_set_style_text_font(infoLbl, smallFont, 0);
|
||||
lv_obj_set_style_text_color(infoLbl, lv_color_hex(Theme::SECONDARY), 0);
|
||||
lv_label_set_text(infoLbl, "");
|
||||
lv_obj_align(infoLbl, LV_ALIGN_RIGHT_MID, -4, 0);
|
||||
|
||||
_poolRows[i] = row;
|
||||
_poolNameLabels[i] = nameLbl;
|
||||
_poolInfoLabels[i] = infoLbl;
|
||||
}
|
||||
|
||||
_lastNodeCount = -1;
|
||||
_lastContactCount = -1;
|
||||
updateSortOrder();
|
||||
syncVisibleRows();
|
||||
|
||||
// --- Action modal overlay (hidden initially, on top layer so it floats above scroll) ---
|
||||
// --- Action modal overlay (on top layer, centered) ---
|
||||
_overlay = lv_obj_create(lv_layer_top());
|
||||
lv_obj_set_size(_overlay, 180, 100);
|
||||
// Center on screen (accounting for status bar offset)
|
||||
lv_obj_set_pos(_overlay, (320 - 180) / 2, 20 + (Theme::CONTENT_H - 100) / 2);
|
||||
lv_obj_set_style_bg_color(_overlay, lv_color_hex(0x001100), 0);
|
||||
lv_obj_set_style_bg_opa(_overlay, LV_OPA_COVER, 0);
|
||||
@@ -106,7 +47,6 @@ void LvNodesScreen::createUI(lv_obj_t* parent) {
|
||||
|
||||
const char* menuText[] = {"Add Contact", "Message", "Back"};
|
||||
for (int i = 0; i < 3; i++) {
|
||||
// Use a container for each menu item so it's tappable with a larger hit area
|
||||
lv_obj_t* btn = lv_obj_create(_overlay);
|
||||
lv_obj_set_size(btn, 166, 26);
|
||||
lv_obj_set_style_bg_color(btn, lv_color_hex(Theme::BG), 0);
|
||||
@@ -121,7 +61,6 @@ void LvNodesScreen::createUI(lv_obj_t* parent) {
|
||||
auto* self = (LvNodesScreen*)lv_event_get_user_data(e);
|
||||
int idx = (int)(intptr_t)lv_obj_get_user_data(lv_event_get_target(e));
|
||||
self->_menuIdx = idx;
|
||||
// Simulate enter key to execute the menu action
|
||||
KeyEvent tap = {};
|
||||
tap.enter = true;
|
||||
self->handleKey(tap);
|
||||
@@ -136,7 +75,7 @@ void LvNodesScreen::createUI(lv_obj_t* parent) {
|
||||
_menuBtns[i] = btn;
|
||||
}
|
||||
|
||||
// Nickname input widgets (hidden in menu mode)
|
||||
// Nickname input widgets
|
||||
_nicknameBox = lv_obj_create(_overlay);
|
||||
lv_obj_set_size(_nicknameBox, 166, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_bg_opa(_nicknameBox, LV_OPA_TRANSP, 0);
|
||||
@@ -168,10 +107,7 @@ void LvNodesScreen::createUI(lv_obj_t* parent) {
|
||||
void LvNodesScreen::onEnter() {
|
||||
_lastNodeCount = -1;
|
||||
_lastContactCount = -1;
|
||||
_selectedIdx = 0;
|
||||
_viewportStart = 0;
|
||||
updateSortOrder();
|
||||
syncVisibleRows();
|
||||
rebuildList();
|
||||
}
|
||||
|
||||
void LvNodesScreen::refreshUI() {
|
||||
@@ -185,193 +121,139 @@ void LvNodesScreen::refreshUI() {
|
||||
if (countDelta > 0 || contactDelta > 0) {
|
||||
_lastRebuild = now;
|
||||
if (countDelta > 3 || contactDelta > 0) {
|
||||
updateSortOrder();
|
||||
syncVisibleRows();
|
||||
rebuildList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LvNodesScreen::updateSortOrder() {
|
||||
if (!_am) return;
|
||||
void LvNodesScreen::rebuildList() {
|
||||
if (!_am || !_list) return;
|
||||
lv_obj_clean(_list);
|
||||
_sortedContactIndices.clear();
|
||||
_sortedOnlineIndices.clear();
|
||||
|
||||
const auto& nodes = _am->nodes();
|
||||
int count = (int)nodes.size();
|
||||
_lastNodeCount = count;
|
||||
|
||||
_sortedContactIndices.clear();
|
||||
_sortedOnlineIndices.clear();
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (nodes[i].saved) _sortedContactIndices.push_back(i);
|
||||
else _sortedOnlineIndices.push_back(i);
|
||||
}
|
||||
_lastContactCount = (int)_sortedContactIndices.size();
|
||||
|
||||
// Sort online nodes: most recently seen first
|
||||
std::sort(_sortedOnlineIndices.begin(), _sortedOnlineIndices.end(), [&nodes](int a, int b) {
|
||||
return nodes[a].lastSeen > nodes[b].lastSeen;
|
||||
});
|
||||
|
||||
// Total entries: contacts header (if any) + contacts + online header + online nodes
|
||||
_totalEntries = 0;
|
||||
if (!_sortedContactIndices.empty()) _totalEntries += 1 + (int)_sortedContactIndices.size();
|
||||
if (count > 0) _totalEntries += 1 + (int)_sortedOnlineIndices.size();
|
||||
|
||||
// Clamp selection
|
||||
if (_selectedIdx >= _totalEntries) _selectedIdx = _totalEntries - 1;
|
||||
if (_selectedIdx < 0) _selectedIdx = 0;
|
||||
// Skip headers
|
||||
while (_selectedIdx < _totalEntries && getNodeIdxForEntry(_selectedIdx) == -1) _selectedIdx++;
|
||||
if (_selectedIdx >= _totalEntries) _selectedIdx = _totalEntries - 1;
|
||||
|
||||
_dataChanged = true;
|
||||
}
|
||||
|
||||
// Map a logical entry index to a node index, or -1 for section headers
|
||||
static int mapEntryToNodeIdx(int entry, const std::vector<int>& contacts, const std::vector<int>& online) {
|
||||
int pos = 0;
|
||||
if (!contacts.empty()) {
|
||||
if (entry == pos) return -1; // Contacts header
|
||||
pos++;
|
||||
if (entry < pos + (int)contacts.size()) return contacts[entry - pos];
|
||||
pos += (int)contacts.size();
|
||||
}
|
||||
// Online header
|
||||
if (entry == pos) return -1;
|
||||
pos++;
|
||||
if (entry < pos + (int)online.size()) return online[entry - pos];
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Determine if entry is a header, and which section
|
||||
// Returns: 0 = contacts header, 1 = online header, -1 = not a header
|
||||
static int headerType(int entry, const std::vector<int>& contacts, const std::vector<int>& online) {
|
||||
int pos = 0;
|
||||
if (!contacts.empty()) {
|
||||
if (entry == pos) return 0;
|
||||
pos += 1 + (int)contacts.size();
|
||||
}
|
||||
if (entry == pos) return 1;
|
||||
return -1;
|
||||
}
|
||||
|
||||
void LvNodesScreen::syncVisibleRows() {
|
||||
if (!_am || !_list) return;
|
||||
|
||||
if (_totalEntries == 0) {
|
||||
if (_sortedContactIndices.empty() && _sortedOnlineIndices.empty()) {
|
||||
lv_obj_clear_flag(_lblEmpty, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag(_list, LV_OBJ_FLAG_HIDDEN);
|
||||
for (int i = 0; i < ROW_POOL_SIZE; i++) lv_obj_add_flag(_poolRows[i], LV_OBJ_FLAG_HIDDEN);
|
||||
return;
|
||||
}
|
||||
|
||||
lv_obj_add_flag(_lblEmpty, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_clear_flag(_list, LV_OBJ_FLAG_HIDDEN);
|
||||
|
||||
const auto& nodes = _am->nodes();
|
||||
bool devMode = _cfg && _cfg->settings().devMode;
|
||||
|
||||
// Compute viewport: center selected item with buffer
|
||||
int halfPool = ROW_POOL_SIZE / 2;
|
||||
_viewportStart = _selectedIdx - halfPool;
|
||||
if (_viewportStart < 0) _viewportStart = 0;
|
||||
if (_viewportStart + ROW_POOL_SIZE > _totalEntries) {
|
||||
_viewportStart = _totalEntries - ROW_POOL_SIZE;
|
||||
if (_viewportStart < 0) _viewportStart = 0;
|
||||
}
|
||||
auto addHeader = [&](const char* text) {
|
||||
lv_obj_t* hdr = lv_obj_create(_list);
|
||||
lv_obj_set_size(hdr, Theme::CONTENT_W, 22);
|
||||
lv_obj_set_style_bg_opa(hdr, LV_OPA_TRANSP, 0);
|
||||
lv_obj_set_style_border_color(hdr, lv_color_hex(Theme::BORDER), 0);
|
||||
lv_obj_set_style_border_width(hdr, 1, 0);
|
||||
lv_obj_set_style_border_side(hdr, LV_BORDER_SIDE_BOTTOM, 0);
|
||||
lv_obj_set_style_pad_all(hdr, 0, 0);
|
||||
lv_obj_set_style_radius(hdr, 0, 0);
|
||||
lv_obj_clear_flag(hdr, LV_OBJ_FLAG_SCROLLABLE | LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_t* lbl = lv_label_create(hdr);
|
||||
lv_obj_set_style_text_font(lbl, &lv_font_ratdeck_12, 0);
|
||||
lv_obj_set_style_text_color(lbl, lv_color_hex(Theme::ACCENT), 0);
|
||||
lv_label_set_text(lbl, text);
|
||||
lv_obj_align(lbl, LV_ALIGN_LEFT_MID, 4, 0);
|
||||
};
|
||||
|
||||
for (int i = 0; i < ROW_POOL_SIZE; i++) {
|
||||
int entryIdx = _viewportStart + i;
|
||||
if (entryIdx >= _totalEntries) {
|
||||
lv_obj_add_flag(_poolRows[i], LV_OBJ_FLAG_HIDDEN);
|
||||
continue;
|
||||
}
|
||||
auto addNodeRow = [&](int nodeIdx) {
|
||||
const auto& node = nodes[nodeIdx];
|
||||
|
||||
lv_obj_clear_flag(_poolRows[i], LV_OBJ_FLAG_HIDDEN);
|
||||
bool isSelected = (entryIdx == _selectedIdx);
|
||||
lv_obj_t* row = lv_obj_create(_list);
|
||||
lv_obj_set_size(row, Theme::CONTENT_W, 26);
|
||||
lv_obj_add_style(row, LvTheme::styleListBtn(), 0);
|
||||
lv_obj_add_style(row, LvTheme::styleListBtnFocused(), LV_STATE_FOCUSED);
|
||||
lv_obj_clear_flag(row, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_add_flag(row, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_set_user_data(row, (void*)(intptr_t)nodeIdx);
|
||||
|
||||
int hdr = headerType(entryIdx, _sortedContactIndices, _sortedOnlineIndices);
|
||||
if (hdr >= 0) {
|
||||
// Section header row
|
||||
char buf[32];
|
||||
if (hdr == 0) {
|
||||
snprintf(buf, sizeof(buf), "Contacts (%d)", (int)_sortedContactIndices.size());
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), "Online (%d)", (int)_sortedOnlineIndices.size());
|
||||
lv_obj_add_event_cb(row, [](lv_event_t* e) {
|
||||
auto* self = (LvNodesScreen*)lv_event_get_user_data(e);
|
||||
int idx = (int)(intptr_t)lv_obj_get_user_data(lv_event_get_target(e));
|
||||
if (idx >= 0 && idx < (int)self->_am->nodes().size()) {
|
||||
self->showActionMenu(idx);
|
||||
}
|
||||
lv_obj_set_size(_poolRows[i], Theme::CONTENT_W, 22);
|
||||
lv_obj_set_style_bg_color(_poolRows[i], lv_color_hex(Theme::BG), 0);
|
||||
lv_obj_set_style_border_width(_poolRows[i], 1, 0);
|
||||
lv_obj_set_style_border_color(_poolRows[i], lv_color_hex(Theme::BORDER), 0);
|
||||
lv_obj_set_style_border_side(_poolRows[i], LV_BORDER_SIDE_BOTTOM, 0);
|
||||
lv_obj_set_style_text_font(_poolNameLabels[i], &lv_font_ratdeck_12, 0);
|
||||
lv_obj_set_style_text_color(_poolNameLabels[i], lv_color_hex(Theme::ACCENT), 0);
|
||||
lv_label_set_text(_poolNameLabels[i], buf);
|
||||
lv_obj_align(_poolNameLabels[i], LV_ALIGN_LEFT_MID, 4, 0);
|
||||
lv_label_set_text(_poolInfoLabels[i], "");
|
||||
}, LV_EVENT_CLICKED, this);
|
||||
|
||||
lv_group_add_obj(LvInput::group(), row);
|
||||
lv_obj_add_event_cb(row, [](lv_event_t* e) {
|
||||
lv_obj_scroll_to_view(lv_event_get_target(e), LV_ANIM_ON);
|
||||
}, LV_EVENT_FOCUSED, nullptr);
|
||||
|
||||
// Name + hash
|
||||
std::string truncName = node.name.substr(0, devMode ? 12 : 15);
|
||||
std::string displayHash = node.hash.toHex().substr(0, 12);
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "%s [%s]", truncName.c_str(), displayHash.c_str());
|
||||
lv_obj_t* nameLbl = lv_label_create(row);
|
||||
lv_obj_set_style_text_font(nameLbl, &lv_font_ratdeck_12, 0);
|
||||
lv_obj_set_style_text_color(nameLbl, lv_color_hex(
|
||||
node.saved ? Theme::ACCENT : Theme::PRIMARY), 0);
|
||||
lv_label_set_text(nameLbl, buf);
|
||||
lv_obj_align(nameLbl, LV_ALIGN_LEFT_MID, 8, 0);
|
||||
|
||||
// Info (hops + age)
|
||||
unsigned long ageSec = (millis() - node.lastSeen) / 1000;
|
||||
char infoBuf[32];
|
||||
if (node.hops > 0 && node.hops < 128) {
|
||||
if (ageSec < 60) snprintf(infoBuf, sizeof(infoBuf), "%dhop %lus", node.hops, ageSec);
|
||||
else snprintf(infoBuf, sizeof(infoBuf), "%dhop %lum", node.hops, ageSec / 60);
|
||||
} else {
|
||||
// Node row
|
||||
int nodeIdx = mapEntryToNodeIdx(entryIdx, _sortedContactIndices, _sortedOnlineIndices);
|
||||
if (nodeIdx < 0 || nodeIdx >= (int)nodes.size()) {
|
||||
lv_obj_add_flag(_poolRows[i], LV_OBJ_FLAG_HIDDEN);
|
||||
continue;
|
||||
}
|
||||
const auto& node = nodes[nodeIdx];
|
||||
|
||||
lv_obj_set_size(_poolRows[i], Theme::CONTENT_W, 24);
|
||||
lv_obj_set_style_bg_color(_poolRows[i], lv_color_hex(
|
||||
isSelected ? Theme::SELECTION_BG : Theme::BG), 0);
|
||||
lv_obj_set_style_border_width(_poolRows[i], 0, 0);
|
||||
lv_obj_set_style_border_side(_poolRows[i], LV_BORDER_SIDE_NONE, 0);
|
||||
|
||||
// Name + hash (use smaller font to reduce truncation)
|
||||
bool devMode = _cfg && _cfg->settings().devMode;
|
||||
std::string truncName = node.name.substr(0, devMode ? 12 : 15);
|
||||
std::string displayHash = node.hash.toHex().substr(0, 12);
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "%s [%s]", truncName.c_str(), displayHash.c_str());
|
||||
lv_obj_set_style_text_font(_poolNameLabels[i], &lv_font_ratdeck_12, 0);
|
||||
lv_obj_set_style_text_color(_poolNameLabels[i], lv_color_hex(
|
||||
node.saved ? Theme::ACCENT : Theme::PRIMARY), 0);
|
||||
lv_label_set_text(_poolNameLabels[i], buf);
|
||||
lv_obj_align(_poolNameLabels[i], LV_ALIGN_LEFT_MID, 8, 0);
|
||||
|
||||
// Hops + age + optional RSSI (dev mode)
|
||||
unsigned long ageSec = (millis() - node.lastSeen) / 1000;
|
||||
char infoBuf[32];
|
||||
if (node.hops > 0 && node.hops < 128) {
|
||||
if (ageSec < 60) snprintf(infoBuf, sizeof(infoBuf), "%dhop %lus", node.hops, ageSec);
|
||||
else snprintf(infoBuf, sizeof(infoBuf), "%dhop %lum", node.hops, ageSec / 60);
|
||||
} else {
|
||||
if (ageSec < 60) snprintf(infoBuf, sizeof(infoBuf), "%lus", ageSec);
|
||||
else snprintf(infoBuf, sizeof(infoBuf), "%lum", ageSec / 60);
|
||||
}
|
||||
if (devMode && node.rssi != 0) {
|
||||
char rssiBuf[16];
|
||||
snprintf(rssiBuf, sizeof(rssiBuf), " %ddB", node.rssi);
|
||||
strncat(infoBuf, rssiBuf, sizeof(infoBuf) - strlen(infoBuf) - 1);
|
||||
}
|
||||
lv_label_set_text(_poolInfoLabels[i], infoBuf);
|
||||
lv_obj_align(_poolInfoLabels[i], LV_ALIGN_RIGHT_MID, -4, 0);
|
||||
if (ageSec < 60) snprintf(infoBuf, sizeof(infoBuf), "%lus", ageSec);
|
||||
else snprintf(infoBuf, sizeof(infoBuf), "%lum", ageSec / 60);
|
||||
}
|
||||
if (devMode && node.rssi != 0) {
|
||||
char rssiBuf[16];
|
||||
snprintf(rssiBuf, sizeof(rssiBuf), " %ddB", node.rssi);
|
||||
strncat(infoBuf, rssiBuf, sizeof(infoBuf) - strlen(infoBuf) - 1);
|
||||
}
|
||||
lv_obj_t* infoLbl = lv_label_create(row);
|
||||
lv_obj_set_style_text_font(infoLbl, &lv_font_ratdeck_10, 0);
|
||||
lv_obj_set_style_text_color(infoLbl, lv_color_hex(Theme::SECONDARY), 0);
|
||||
lv_label_set_text(infoLbl, infoBuf);
|
||||
lv_obj_align(infoLbl, LV_ALIGN_RIGHT_MID, -4, 0);
|
||||
};
|
||||
|
||||
// Build list: Contacts section, then Online section
|
||||
if (!_sortedContactIndices.empty()) {
|
||||
char hdrBuf[32];
|
||||
snprintf(hdrBuf, sizeof(hdrBuf), "Contacts (%d)", (int)_sortedContactIndices.size());
|
||||
addHeader(hdrBuf);
|
||||
for (int idx : _sortedContactIndices) addNodeRow(idx);
|
||||
}
|
||||
|
||||
_dataChanged = false;
|
||||
scrollToSelected();
|
||||
}
|
||||
|
||||
// Helper: get node index for a given logical entry, -1 for headers
|
||||
int LvNodesScreen::getNodeIdxForEntry(int entry) const {
|
||||
return mapEntryToNodeIdx(entry, _sortedContactIndices, _sortedOnlineIndices);
|
||||
}
|
||||
|
||||
void LvNodesScreen::scrollToSelected() {
|
||||
// With widget pool, scrolling is handled by viewport recomputation in syncVisibleRows
|
||||
// Find which pool row corresponds to selected entry and scroll to it
|
||||
int poolIdx = _selectedIdx - _viewportStart;
|
||||
if (poolIdx >= 0 && poolIdx < ROW_POOL_SIZE && _poolRows[poolIdx]) {
|
||||
lv_obj_scroll_to_view(_poolRows[poolIdx], LV_ANIM_OFF);
|
||||
{
|
||||
char hdrBuf[32];
|
||||
snprintf(hdrBuf, sizeof(hdrBuf), "Online (%d)", (int)_sortedOnlineIndices.size());
|
||||
addHeader(hdrBuf);
|
||||
for (int idx : _sortedOnlineIndices) addNodeRow(idx);
|
||||
}
|
||||
}
|
||||
|
||||
int LvNodesScreen::getFocusedNodeIdx() const {
|
||||
lv_obj_t* focused = lv_group_get_focused(LvInput::group());
|
||||
if (!focused) return -1;
|
||||
return (int)(intptr_t)lv_obj_get_user_data(focused);
|
||||
}
|
||||
|
||||
// --- Action modal helpers ---
|
||||
|
||||
void LvNodesScreen::showActionMenu(int nodeIdx) {
|
||||
@@ -396,7 +278,6 @@ void LvNodesScreen::hideOverlay() {
|
||||
|
||||
void LvNodesScreen::showNicknameInput() {
|
||||
_actionState = NodeAction::NICKNAME_INPUT;
|
||||
// Pre-fill with announce name
|
||||
if (_am && _actionNodeIdx >= 0 && _actionNodeIdx < (int)_am->nodes().size()) {
|
||||
_nicknameText = String(_am->nodes()[_actionNodeIdx].name.c_str());
|
||||
}
|
||||
@@ -413,8 +294,6 @@ void LvNodesScreen::updateMenuSelection() {
|
||||
lv_obj_set_style_bg_color(_menuBtns[i], lv_color_hex(
|
||||
sel ? Theme::PRIMARY : 0x001100), 0);
|
||||
lv_obj_set_style_bg_opa(_menuBtns[i], LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_pad_bottom(_menuLabels[i], 2, 0);
|
||||
lv_obj_set_style_radius(_menuLabels[i], 3, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,13 +305,13 @@ void LvNodesScreen::updateNicknameDisplay() {
|
||||
}
|
||||
|
||||
bool LvNodesScreen::handleLongPress() {
|
||||
if (!_am || _totalEntries == 0) return false;
|
||||
if (_selectedIdx < 0 || _selectedIdx >= _totalEntries) return false;
|
||||
int nodeIdx = getNodeIdxForEntry(_selectedIdx);
|
||||
if (!_am) return false;
|
||||
int nodeIdx = getFocusedNodeIdx();
|
||||
if (nodeIdx < 0 || nodeIdx >= (int)_am->nodes().size()) return false;
|
||||
const auto& node = _am->nodes()[nodeIdx];
|
||||
if (node.saved) {
|
||||
_confirmDelete = true;
|
||||
_actionNodeIdx = nodeIdx;
|
||||
if (_ui) _ui->lvStatusBar().showToast("Remove friend? Enter=Yes Esc=No", 5000);
|
||||
} else {
|
||||
showActionMenu(nodeIdx);
|
||||
@@ -441,18 +320,16 @@ bool LvNodesScreen::handleLongPress() {
|
||||
}
|
||||
|
||||
bool LvNodesScreen::handleKey(const KeyEvent& event) {
|
||||
if (!_am || _totalEntries == 0) return false;
|
||||
if (!_am) return false;
|
||||
|
||||
// --- Nickname input mode ---
|
||||
if (_actionState == NodeAction::NICKNAME_INPUT) {
|
||||
if (event.enter || event.character == '\n' || event.character == '\r') {
|
||||
// Save contact with nickname
|
||||
if (_actionNodeIdx >= 0 && _actionNodeIdx < (int)_am->nodes().size()) {
|
||||
auto& node = const_cast<DiscoveredNode&>(_am->nodes()[_actionNodeIdx]);
|
||||
String finalName = _nicknameText;
|
||||
finalName.trim();
|
||||
if (finalName.isEmpty()) {
|
||||
// Fallback: announce name → hex hash
|
||||
if (!node.name.empty()) finalName = String(node.name.c_str());
|
||||
else finalName = String(node.hash.toHex().substr(0, 12).c_str());
|
||||
}
|
||||
@@ -461,14 +338,13 @@ bool LvNodesScreen::handleKey(const KeyEvent& event) {
|
||||
_am->saveContacts();
|
||||
if (_ui) _ui->lvStatusBar().showToast("Contact saved!", 1200);
|
||||
hideOverlay();
|
||||
updateSortOrder();
|
||||
syncVisibleRows();
|
||||
rebuildList();
|
||||
} else {
|
||||
hideOverlay();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (event.character == 0x1B) { hideOverlay(); return true; } // Esc
|
||||
if (event.character == 0x1B) { hideOverlay(); return true; }
|
||||
if (event.character == '\b' || event.character == 0x7F) {
|
||||
if (_nicknameText.length() > 0) _nicknameText.remove(_nicknameText.length() - 1);
|
||||
updateNicknameDisplay();
|
||||
@@ -479,7 +355,7 @@ bool LvNodesScreen::handleKey(const KeyEvent& event) {
|
||||
updateNicknameDisplay();
|
||||
return true;
|
||||
}
|
||||
return true; // Consume all keys in input mode
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Action menu mode ---
|
||||
@@ -494,10 +370,10 @@ bool LvNodesScreen::handleKey(const KeyEvent& event) {
|
||||
}
|
||||
if (event.enter || event.character == '\n' || event.character == '\r') {
|
||||
switch (_menuIdx) {
|
||||
case 0: // Add Contact
|
||||
case 0:
|
||||
showNicknameInput();
|
||||
break;
|
||||
case 1: // Message
|
||||
case 1:
|
||||
if (_actionNodeIdx >= 0 && _actionNodeIdx < (int)_am->nodes().size() && _onSelect) {
|
||||
std::string hex = _am->nodes()[_actionNodeIdx].hash.toHex();
|
||||
hideOverlay();
|
||||
@@ -506,30 +382,25 @@ bool LvNodesScreen::handleKey(const KeyEvent& event) {
|
||||
hideOverlay();
|
||||
}
|
||||
break;
|
||||
case 2: // Back
|
||||
case 2:
|
||||
hideOverlay();
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (event.character == 0x1B) { hideOverlay(); return true; } // Esc
|
||||
return true; // Consume all keys in menu mode
|
||||
if (event.character == 0x1B) { hideOverlay(); return true; }
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Browse mode (normal) ---
|
||||
|
||||
// Confirm delete mode
|
||||
// --- Confirm delete mode ---
|
||||
if (_confirmDelete) {
|
||||
if (event.enter || event.character == '\n' || event.character == '\r') {
|
||||
int nodeIdx = getNodeIdxForEntry(_selectedIdx);
|
||||
if (nodeIdx >= 0 && nodeIdx < (int)_am->nodes().size()) {
|
||||
if (_actionNodeIdx >= 0 && _actionNodeIdx < (int)_am->nodes().size()) {
|
||||
auto& nodes = const_cast<std::vector<DiscoveredNode>&>(_am->nodes());
|
||||
nodes.erase(nodes.begin() + nodeIdx);
|
||||
nodes.erase(nodes.begin() + _actionNodeIdx);
|
||||
_am->saveContacts();
|
||||
if (_ui) _ui->lvStatusBar().showToast("Contact deleted", 1200);
|
||||
_selectedIdx = 0;
|
||||
updateSortOrder();
|
||||
syncVisibleRows();
|
||||
rebuildList();
|
||||
}
|
||||
_confirmDelete = false;
|
||||
return true;
|
||||
@@ -539,42 +410,18 @@ bool LvNodesScreen::handleKey(const KeyEvent& event) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.up) {
|
||||
int prev = _selectedIdx;
|
||||
_selectedIdx--;
|
||||
while (_selectedIdx >= 0 && getNodeIdxForEntry(_selectedIdx) == -1) _selectedIdx--;
|
||||
if (_selectedIdx < 0) _selectedIdx = prev;
|
||||
if (_selectedIdx != prev) syncVisibleRows();
|
||||
return true;
|
||||
}
|
||||
if (event.down) {
|
||||
int prev = _selectedIdx;
|
||||
_selectedIdx++;
|
||||
while (_selectedIdx < _totalEntries && getNodeIdxForEntry(_selectedIdx) == -1) _selectedIdx++;
|
||||
if (_selectedIdx >= _totalEntries) _selectedIdx = prev;
|
||||
if (_selectedIdx != prev) syncVisibleRows();
|
||||
return true;
|
||||
}
|
||||
if (event.enter || event.character == '\n' || event.character == '\r') {
|
||||
int nodeIdx = getNodeIdxForEntry(_selectedIdx);
|
||||
if (nodeIdx >= 0 && nodeIdx < (int)_am->nodes().size()) {
|
||||
showActionMenu(nodeIdx);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// 's' or 'S' to save/unsave contact
|
||||
if (event.character == 's' || event.character == 'S') {
|
||||
int nodeIdx = getNodeIdxForEntry(_selectedIdx);
|
||||
int nodeIdx = getFocusedNodeIdx();
|
||||
if (nodeIdx >= 0 && nodeIdx < (int)_am->nodes().size()) {
|
||||
auto& node = const_cast<DiscoveredNode&>(_am->nodes()[nodeIdx]);
|
||||
node.saved = !node.saved;
|
||||
if (node.saved) {
|
||||
_am->saveContacts();
|
||||
}
|
||||
updateSortOrder();
|
||||
syncVisibleRows();
|
||||
if (node.saved) _am->saveContacts();
|
||||
rebuildList();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Let LVGL focus group handle up/down/enter navigation
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -26,10 +26,8 @@ public:
|
||||
const char* title() const override { return "Nodes"; }
|
||||
|
||||
private:
|
||||
void updateSortOrder();
|
||||
void syncVisibleRows();
|
||||
void scrollToSelected();
|
||||
int getNodeIdxForEntry(int entry) const;
|
||||
void rebuildList();
|
||||
int getFocusedNodeIdx() const;
|
||||
|
||||
// Action modal helpers
|
||||
enum class NodeAction { BROWSE, ACTION_MENU, NICKNAME_INPUT };
|
||||
@@ -60,24 +58,14 @@ private:
|
||||
lv_obj_t* _nicknameHint = nullptr;
|
||||
int _lastNodeCount = -1;
|
||||
int _lastContactCount = -1;
|
||||
int _selectedIdx = 0;
|
||||
int _totalEntries = 0; // Total displayable entries (contacts + headers + online)
|
||||
|
||||
// Sorted index vectors (into _am->nodes())
|
||||
std::vector<int> _sortedContactIndices;
|
||||
std::vector<int> _sortedOnlineIndices;
|
||||
bool _dataChanged = false;
|
||||
|
||||
unsigned long _lastRebuild = 0;
|
||||
static constexpr unsigned long REBUILD_INTERVAL_MS = 5000;
|
||||
|
||||
// Widget pool — fixed set of pre-allocated row widgets
|
||||
static constexpr int ROW_POOL_SIZE = 14;
|
||||
lv_obj_t* _poolRows[ROW_POOL_SIZE] = {};
|
||||
lv_obj_t* _poolNameLabels[ROW_POOL_SIZE] = {};
|
||||
lv_obj_t* _poolInfoLabels[ROW_POOL_SIZE] = {};
|
||||
int _viewportStart = 0; // First visible sorted index
|
||||
|
||||
lv_obj_t* _list = nullptr;
|
||||
lv_obj_t* _lblEmpty = nullptr;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "LvSettingsScreen.h"
|
||||
#include "ui/Theme.h"
|
||||
#include "ui/LvTheme.h"
|
||||
#include "ui/LvInput.h"
|
||||
#include "config/Config.h"
|
||||
#include "config/UserConfig.h"
|
||||
#include "ui/screens/LvTimezoneScreen.h" // For TIMEZONE_TABLE
|
||||
@@ -734,11 +735,9 @@ void LvSettingsScreen::rebuildCategoryList() {
|
||||
|
||||
lv_obj_t* row = lv_obj_create(_scrollContainer);
|
||||
lv_obj_set_size(row, Theme::CONTENT_W, 34);
|
||||
lv_obj_set_style_bg_color(row, lv_color_hex(selected ? Theme::SELECTION_BG : Theme::BG), 0);
|
||||
lv_obj_set_style_bg_opa(row, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_border_width(row, 0, 0);
|
||||
lv_obj_add_style(row, LvTheme::styleListBtn(), 0);
|
||||
lv_obj_add_style(row, LvTheme::styleListBtnFocused(), LV_STATE_FOCUSED);
|
||||
lv_obj_set_style_pad_all(row, 0, 0);
|
||||
lv_obj_set_style_radius(row, 0, 0);
|
||||
lv_obj_clear_flag(row, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_add_flag(row, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_set_user_data(row, (void*)(intptr_t)i);
|
||||
@@ -748,6 +747,10 @@ void LvSettingsScreen::rebuildCategoryList() {
|
||||
self->_categoryIdx = idx;
|
||||
self->enterCategory(idx);
|
||||
}, LV_EVENT_CLICKED, this);
|
||||
lv_group_add_obj(LvInput::group(), row);
|
||||
lv_obj_add_event_cb(row, [](lv_event_t* e) {
|
||||
lv_obj_scroll_to_view(lv_event_get_target(e), LV_ANIM_ON);
|
||||
}, LV_EVENT_FOCUSED, nullptr);
|
||||
|
||||
// Category name + count
|
||||
char buf[48];
|
||||
@@ -776,6 +779,11 @@ void LvSettingsScreen::rebuildCategoryList() {
|
||||
|
||||
_rowObjs.push_back(row);
|
||||
}
|
||||
|
||||
// Restore focus to current category
|
||||
if (_categoryIdx >= 0 && _categoryIdx < (int)_rowObjs.size()) {
|
||||
lv_group_focus_obj(_rowObjs[_categoryIdx]);
|
||||
}
|
||||
}
|
||||
|
||||
void LvSettingsScreen::rebuildItemList() {
|
||||
@@ -816,12 +824,11 @@ void LvSettingsScreen::rebuildItemList() {
|
||||
|
||||
lv_obj_t* row = lv_obj_create(_scrollContainer);
|
||||
lv_obj_set_size(row, Theme::CONTENT_W, 22);
|
||||
lv_obj_set_style_bg_color(row, lv_color_hex(
|
||||
(selected && editable) ? Theme::SELECTION_BG : Theme::BG), 0);
|
||||
lv_obj_set_style_bg_opa(row, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_border_width(row, 0, 0);
|
||||
lv_obj_add_style(row, LvTheme::styleListBtn(), 0);
|
||||
if (editable) {
|
||||
lv_obj_add_style(row, LvTheme::styleListBtnFocused(), LV_STATE_FOCUSED);
|
||||
}
|
||||
lv_obj_set_style_pad_all(row, 0, 0);
|
||||
lv_obj_set_style_radius(row, 0, 0);
|
||||
lv_obj_clear_flag(row, LV_OBJ_FLAG_SCROLLABLE);
|
||||
if (editable) {
|
||||
lv_obj_add_flag(row, LV_OBJ_FLAG_CLICKABLE);
|
||||
@@ -829,7 +836,6 @@ void LvSettingsScreen::rebuildItemList() {
|
||||
lv_obj_add_event_cb(row, [](lv_event_t* e) {
|
||||
auto* self = (LvSettingsScreen*)lv_event_get_user_data(e);
|
||||
int idx = (int)(intptr_t)lv_obj_get_user_data(lv_event_get_target(e));
|
||||
// Cancel any in-progress edit before switching items
|
||||
if (self->_editing || self->_textEditing || self->_freqEditing) {
|
||||
self->_editing = false;
|
||||
self->_textEditing = false;
|
||||
@@ -841,6 +847,10 @@ void LvSettingsScreen::rebuildItemList() {
|
||||
tap.enter = true;
|
||||
self->handleKey(tap);
|
||||
}, LV_EVENT_CLICKED, this);
|
||||
lv_group_add_obj(LvInput::group(), row);
|
||||
lv_obj_add_event_cb(row, [](lv_event_t* e) {
|
||||
lv_obj_scroll_to_view(lv_event_get_target(e), LV_ANIM_ON);
|
||||
}, LV_EVENT_FOCUSED, nullptr);
|
||||
}
|
||||
|
||||
// Label
|
||||
@@ -911,6 +921,12 @@ void LvSettingsScreen::rebuildItemList() {
|
||||
|
||||
_rowObjs.push_back(row);
|
||||
}
|
||||
|
||||
// Restore focus to the currently selected item after rebuild
|
||||
int focusOffset = _selectedIdx - _catRangeStart;
|
||||
if (focusOffset >= 0 && focusOffset < (int)_rowObjs.size()) {
|
||||
lv_group_focus_obj(_rowObjs[focusOffset]);
|
||||
}
|
||||
}
|
||||
|
||||
void LvSettingsScreen::rebuildWifiList() {
|
||||
@@ -944,25 +960,48 @@ void LvSettingsScreen::rebuildWifiList() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make header tappable to go back
|
||||
lv_obj_add_flag(headerRow, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_add_event_cb(headerRow, [](lv_event_t* e) {
|
||||
auto* self = (LvSettingsScreen*)lv_event_get_user_data(e);
|
||||
self->_view = SettingsView::ITEM_LIST;
|
||||
self->rebuildItemList();
|
||||
}, LV_EVENT_CLICKED, this);
|
||||
|
||||
for (int i = 0; i < (int)_wifiResults.size(); i++) {
|
||||
auto& net = _wifiResults[i];
|
||||
bool selected = (i == _wifiPickerIdx);
|
||||
|
||||
lv_obj_t* row = lv_obj_create(_scrollContainer);
|
||||
lv_obj_set_size(row, Theme::CONTENT_W, 22);
|
||||
lv_obj_set_style_bg_color(row, lv_color_hex(selected ? Theme::SELECTION_BG : Theme::BG), 0);
|
||||
lv_obj_add_style(row, LvTheme::styleListBtn(), 0);
|
||||
lv_obj_add_style(row, LvTheme::styleListBtnFocused(), LV_STATE_FOCUSED);
|
||||
lv_obj_set_style_bg_opa(row, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_border_width(row, 0, 0);
|
||||
lv_obj_set_style_pad_all(row, 0, 0);
|
||||
lv_obj_set_style_radius(row, 0, 0);
|
||||
lv_obj_clear_flag(row, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_add_flag(row, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_set_user_data(row, (void*)(intptr_t)i);
|
||||
lv_obj_add_event_cb(row, [](lv_event_t* e) {
|
||||
auto* self = (LvSettingsScreen*)lv_event_get_user_data(e);
|
||||
int idx = (int)(intptr_t)lv_obj_get_user_data(lv_event_get_target(e));
|
||||
if (idx < (int)self->_wifiResults.size()) {
|
||||
auto& net = self->_wifiResults[idx];
|
||||
if (self->_cfg) { self->_cfg->settings().wifiSTASSID = net.ssid; self->applyAndSave(); }
|
||||
}
|
||||
self->_view = SettingsView::ITEM_LIST;
|
||||
self->rebuildItemList();
|
||||
}, LV_EVENT_CLICKED, this);
|
||||
lv_group_add_obj(LvInput::group(), row);
|
||||
lv_obj_add_event_cb(row, [](lv_event_t* e) {
|
||||
lv_obj_scroll_to_view(lv_event_get_target(e), LV_ANIM_ON);
|
||||
}, LV_EVENT_FOCUSED, nullptr);
|
||||
|
||||
// Lock + SSID
|
||||
char buf[48];
|
||||
snprintf(buf, sizeof(buf), "%s %s", net.encrypted ? "*" : " ", net.ssid.c_str());
|
||||
lv_obj_t* lbl = lv_label_create(row);
|
||||
lv_obj_set_style_text_font(lbl, font, 0);
|
||||
lv_obj_set_style_text_color(lbl, lv_color_hex(selected ? Theme::ACCENT : Theme::PRIMARY), 0);
|
||||
lv_obj_set_style_text_color(lbl, lv_color_hex(Theme::PRIMARY), 0);
|
||||
lv_label_set_text(lbl, buf);
|
||||
lv_obj_align(lbl, LV_ALIGN_LEFT_MID, 4, 0);
|
||||
|
||||
@@ -1056,23 +1095,11 @@ void LvSettingsScreen::exitToCategories() {
|
||||
bool LvSettingsScreen::handleKey(const KeyEvent& event) {
|
||||
switch (_view) {
|
||||
case SettingsView::CATEGORY_LIST: {
|
||||
if (event.up) {
|
||||
if (_categoryIdx > 0) {
|
||||
int prev = _categoryIdx;
|
||||
_categoryIdx--;
|
||||
updateCategorySelection(prev, _categoryIdx);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (event.down) {
|
||||
if (_categoryIdx < (int)_categories.size() - 1) {
|
||||
int prev = _categoryIdx;
|
||||
_categoryIdx++;
|
||||
updateCategorySelection(prev, _categoryIdx);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// LVGL focus group handles up/down navigation
|
||||
if (event.enter || event.character == '\n' || event.character == '\r') {
|
||||
// Get focused category from LVGL group
|
||||
lv_obj_t* focused = lv_group_get_focused(LvInput::group());
|
||||
if (focused) _categoryIdx = (int)(intptr_t)lv_obj_get_user_data(focused);
|
||||
enterCategory(_categoryIdx);
|
||||
return true;
|
||||
}
|
||||
@@ -1188,23 +1215,14 @@ bool LvSettingsScreen::handleKey(const KeyEvent& event) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Browse mode
|
||||
if (event.up) {
|
||||
int prev = _selectedIdx;
|
||||
skipToNextEditable(-1);
|
||||
if (_selectedIdx != prev) updateItemSelection(prev, _selectedIdx);
|
||||
return true;
|
||||
}
|
||||
if (event.down) {
|
||||
int prev = _selectedIdx;
|
||||
skipToNextEditable(1);
|
||||
if (_selectedIdx != prev) updateItemSelection(prev, _selectedIdx);
|
||||
return true;
|
||||
}
|
||||
// Browse mode — LVGL focus group handles up/down
|
||||
if (event.del || event.character == 8 || event.character == 0x1B) {
|
||||
exitToCategories(); return true;
|
||||
}
|
||||
if (event.enter || event.character == '\n' || event.character == '\r') {
|
||||
// Sync _selectedIdx from LVGL focus
|
||||
lv_obj_t* focused = lv_group_get_focused(LvInput::group());
|
||||
if (focused) _selectedIdx = (int)(intptr_t)lv_obj_get_user_data(focused);
|
||||
if (!isEditable(_selectedIdx)) return true;
|
||||
auto& item = _items[_selectedIdx];
|
||||
if (item.type == SettingType::ACTION) {
|
||||
@@ -1252,26 +1270,15 @@ bool LvSettingsScreen::handleKey(const KeyEvent& event) {
|
||||
}
|
||||
|
||||
case SettingsView::WIFI_PICKER: {
|
||||
if (event.up) {
|
||||
if (_wifiPickerIdx > 0) {
|
||||
int prev = _wifiPickerIdx;
|
||||
_wifiPickerIdx--;
|
||||
updateWifiSelection(prev, _wifiPickerIdx);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (event.down) {
|
||||
if (_wifiPickerIdx < (int)_wifiResults.size() - 1) {
|
||||
int prev = _wifiPickerIdx;
|
||||
_wifiPickerIdx++;
|
||||
updateWifiSelection(prev, _wifiPickerIdx);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// LVGL handles up/down navigation, click handler handles selection
|
||||
if (event.enter || event.character == '\n' || event.character == '\r') {
|
||||
if (_wifiPickerIdx < (int)_wifiResults.size()) {
|
||||
auto& net = _wifiResults[_wifiPickerIdx];
|
||||
if (_cfg) { _cfg->settings().wifiSTASSID = net.ssid; applyAndSave(); }
|
||||
lv_obj_t* focused = lv_group_get_focused(LvInput::group());
|
||||
if (focused) {
|
||||
int idx = (int)(intptr_t)lv_obj_get_user_data(focused);
|
||||
if (idx < (int)_wifiResults.size()) {
|
||||
auto& net = _wifiResults[idx];
|
||||
if (_cfg) { _cfg->settings().wifiSTASSID = net.ssid; applyAndSave(); }
|
||||
}
|
||||
}
|
||||
_view = SettingsView::ITEM_LIST;
|
||||
rebuildItemList();
|
||||
|
||||
Reference in New Issue
Block a user