mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-10-09 11:16:39 +00:00
BotConfig (tabs, rows, the NodePrefs field behind each, reply placeholders) moves into ui-core; ui-new's Tools > Bot renders it. L2: Home > Bot with switches, list pickers, text popups with placeholder buttons, quiet hours. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
241 lines
9.1 KiB
C++
241 lines
9.1 KiB
C++
#pragma once
|
|
// Custom screen — not part of upstream UITask.cpp
|
|
// Included by UITask.cpp after KeyboardWidget.h is defined.
|
|
|
|
#include "TabBar.h"
|
|
#include "../ui-core/BotConfig.h"
|
|
|
|
class BotScreen : public UIScreen {
|
|
UITask* _task;
|
|
NodePrefs* _prefs;
|
|
|
|
// Categories are a circular tab bar in the header (shared geometry with
|
|
// NearbyScreen's filter tabs and AdminScreen's category tabs — see
|
|
// TabBar.h). LEFT/RIGHT switches tabs; UP/DOWN moves within the active
|
|
// tab's rows; every value is edited via Enter. The rows and their NodePrefs
|
|
// fields are the UI Core's table (ui-core/BotConfig.h, shared with ui-lvgl).
|
|
typedef botcfg::Kind Kind;
|
|
typedef botcfg::Row Row;
|
|
static const Row& tabRow(int tab, int idx) { return botcfg::row(tab, idx); }
|
|
|
|
uint8_t _tab; // persists across visits, like NearbyScreen's _filter
|
|
int _sel; // selected row index within the active tab
|
|
int _scroll; // index of first visible row (managed by drawList in render)
|
|
bool _dirty;
|
|
|
|
// keyboard state (reused for trigger and reply fields)
|
|
int _kb_row; // -1=off, else the row index (within the active tab) being edited
|
|
KeyboardWidget* _kb;
|
|
|
|
// Quiet-hours stepper: Enter on Quiet from/to enters this sub-mode (LEFT/
|
|
// RIGHT is unavailable for +/- now that it switches tabs), where UP/DOWN
|
|
// steps the hour and Enter/Cancel exits back to normal row browsing.
|
|
int _stepper_row; // -1=off, else the row index being stepped
|
|
|
|
// channel/room counts (refreshed on enter) — just gate whether Enter opens
|
|
// the (possibly empty) picker; the picker itself browses the real list, so
|
|
// no local index cache is needed now that there's no LEFT/RIGHT quick-cycle.
|
|
int _num_channels;
|
|
int _num_rooms;
|
|
|
|
void refreshChannels() {
|
|
_num_channels = 0;
|
|
ChannelDetails ch;
|
|
for (int i = 0; i < MAX_GROUP_CHANNELS; i++)
|
|
if (the_mesh.getChannel(i, ch) && ch.name[0] != '\0') _num_channels++;
|
|
}
|
|
|
|
void refreshRooms() {
|
|
_num_rooms = 0;
|
|
ContactInfo ci;
|
|
int total = the_mesh.getNumContacts();
|
|
// +MAX_ANON_CONTACTS: getContactByIdx() indexes the raw contacts[] table,
|
|
// whose first MAX_ANON_CONTACTS slots are reserved anon-request entries
|
|
// that getNumContacts() already excludes from its count (see
|
|
// NearbyScreen.h's own contact scan, and MessagesScreen.h's
|
|
// buildContactList(), for the same offset) -- without it this underrode
|
|
// real room-server contacts by up to MAX_ANON_CONTACTS.
|
|
for (int i = 0; i < total; i++)
|
|
if (the_mesh.getContactByIdx(MAX_ANON_CONTACTS + i, ci) && ci.type == ADV_TYPE_ROOM) _num_rooms++;
|
|
}
|
|
|
|
// Header as a circular tab bar (shared geometry — see TabBar.h). `right_reserve`
|
|
// keeps the reply counter (drawn after this call) clear of the rightmost tab.
|
|
void drawTabBar(DisplayDriver& display, int right_reserve) {
|
|
tabbar::draw(display, botcfg::TAB_LABELS, botcfg::TAB_COUNT, _tab, right_reserve);
|
|
}
|
|
|
|
public:
|
|
BotScreen(UITask* task, NodePrefs* prefs, KeyboardWidget* kb)
|
|
: _task(task), _prefs(prefs), _tab(botcfg::TAB_CHANNEL), _kb(kb) {}
|
|
|
|
void onShow() override {
|
|
// _tab persists across visits (like NearbyScreen's _filter) — only reset
|
|
// the in-tab cursor and any open sub-mode.
|
|
_sel = 0;
|
|
_scroll = 0;
|
|
_kb_row = -1;
|
|
_stepper_row = -1;
|
|
_dirty = false;
|
|
refreshChannels();
|
|
refreshRooms();
|
|
}
|
|
|
|
int render(DisplayDriver& display) override {
|
|
display.setTextSize(1);
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
|
|
if (_kb_row >= 0) {
|
|
return _kb->render(display);
|
|
}
|
|
|
|
int val_x = display.valCol();
|
|
|
|
// Reply counter, right-aligned in the header — reserve its width from the
|
|
// tab bar before drawing so a right-side tab can't run under it.
|
|
uint16_t sent = the_mesh.botReplyCount();
|
|
char cbuf[12];
|
|
int cw = 0;
|
|
if (sent > 0) {
|
|
snprintf(cbuf, sizeof(cbuf), "%u", (unsigned)sent);
|
|
cw = display.getTextWidth(cbuf);
|
|
}
|
|
drawTabBar(display, sent > 0 ? cw + 2 : 0);
|
|
if (sent > 0) {
|
|
display.setCursor(display.width() - cw - 1, 0);
|
|
display.print(cbuf);
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
}
|
|
|
|
int n = botcfg::rowCount(_tab);
|
|
int mq_delay = 0;
|
|
drawList(display, n, _sel, _scroll, [&](int i, int y, bool sel, int reserve) {
|
|
Row r = tabRow(_tab, i);
|
|
drawRowSelection(display, y, sel, reserve);
|
|
display.setCursor(2, y);
|
|
display.print(r.label);
|
|
display.setCursor(val_x, y);
|
|
|
|
int cap;
|
|
const char* txt = botcfg::text(_prefs, r.kind, cap);
|
|
const uint8_t* fl = botcfg::flag(_prefs, r.kind);
|
|
char name[32];
|
|
if (fl) {
|
|
display.print(r.kind == botcfg::DM_SCOPE ? (*fl ? "Fav" : "All") : (*fl ? "ON" : "OFF"));
|
|
} else if (txt) {
|
|
int mqr = display.drawTextEllipsized(val_x, y, display.width() - val_x - 1 - reserve,
|
|
botcfg::shownText(txt, botcfg::isTrigger(r.kind)), sel);
|
|
if (sel && mqr > 0) mq_delay = mqr;
|
|
} else if (r.kind == botcfg::CHANNEL || r.kind == botcfg::ROOM) {
|
|
bool any = r.kind == botcfg::CHANNEL ? _num_channels > 0 : _num_rooms > 0;
|
|
bool ok = r.kind == botcfg::CHANNEL ? botcfg::channelName(_prefs, name, sizeof(name))
|
|
: botcfg::roomName(_prefs, name, sizeof(name));
|
|
if (!any) display.print("(none)");
|
|
else if (!ok) display.print("?");
|
|
else {
|
|
int mqr = display.drawTextEllipsized(val_x, y, display.width() - val_x - 1 - reserve, name, sel);
|
|
if (sel && mqr > 0) mq_delay = mqr;
|
|
}
|
|
} else if (botcfg::isHour(r.kind)) {
|
|
char hb[10];
|
|
if (botcfg::quietOff(_prefs)) strcpy(hb, "OFF");
|
|
else snprintf(hb, sizeof(hb), "%02d:00", botcfg::hour(_prefs, r.kind));
|
|
// Bracket the value while the stepper sub-mode is open on this row,
|
|
// as a visual cue that UP/DOWN now steps it instead of moving rows.
|
|
if (_stepper_row == i) {
|
|
char bracketed[14];
|
|
snprintf(bracketed, sizeof(bracketed), "[%s]", hb);
|
|
display.print(bracketed);
|
|
} else {
|
|
display.print(hb);
|
|
}
|
|
}
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
});
|
|
return 2000;
|
|
}
|
|
|
|
bool handleInput(char c) override {
|
|
bool up = (c == KEY_UP);
|
|
bool down = (c == KEY_DOWN);
|
|
bool enter = (c == KEY_ENTER);
|
|
bool cancel = (c == KEY_CANCEL);
|
|
|
|
if (_kb_row >= 0) {
|
|
auto res = _kb->handleInput(c);
|
|
if (res == KeyboardWidget::DONE) {
|
|
Kind k = tabRow(_tab, _kb_row).kind;
|
|
int cap = 0;
|
|
char* dst = botcfg::text(_prefs, k, cap);
|
|
if (dst) { strncpy(dst, _kb->buf, cap - 1); dst[cap - 1] = '\0'; }
|
|
_dirty = true;
|
|
_kb_row = -1;
|
|
} else if (res == KeyboardWidget::CANCELLED) {
|
|
_kb_row = -1;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (_stepper_row >= 0) {
|
|
uint8_t& h = botcfg::hour(_prefs, tabRow(_tab, _stepper_row).kind);
|
|
if (up) { h = (h + 1) % 24; _dirty = true; return true; }
|
|
if (down) { h = (h + 23) % 24; _dirty = true; return true; }
|
|
if (enter || cancel) { _stepper_row = -1; return true; }
|
|
return true; // swallow LEFT/RIGHT etc. while stepping
|
|
}
|
|
|
|
if (cancel) {
|
|
_task->savePrefsIfDirty(_dirty);
|
|
_task->gotoToolsScreen();
|
|
return true;
|
|
}
|
|
if (keyIsPrev(c)) { _tab = (_tab + botcfg::TAB_COUNT - 1) % botcfg::TAB_COUNT; _sel = _scroll = 0; return true; }
|
|
if (keyIsNext(c)) { _tab = (_tab + 1) % botcfg::TAB_COUNT; _sel = _scroll = 0; return true; }
|
|
|
|
int n = botcfg::rowCount(_tab);
|
|
// drawList() reclamps _scroll from _sel every render.
|
|
if (up) { _sel = (_sel > 0) ? _sel - 1 : n - 1; return true; }
|
|
if (down) { _sel = (_sel < n - 1) ? _sel + 1 : 0; return true; }
|
|
|
|
if (!enter) return false;
|
|
Kind k = tabRow(_tab, _sel).kind;
|
|
|
|
if (uint8_t* fl = botcfg::flag(_prefs, k)) { *fl ^= 1; _dirty = true; return true; }
|
|
int cap = 0;
|
|
if (char* txt = botcfg::text(_prefs, k, cap)) {
|
|
_kb_row = _sel;
|
|
_kb->begin(txt, cap - 1);
|
|
if (botcfg::isTrigger(k)) {
|
|
_kb->clearPlaceholders(); // trigger is literal — placeholders never match
|
|
} else {
|
|
kbAddSensorPlaceholders(*_kb, &sensors);
|
|
// Only meaningful in a bot reply (there's an actual triggering
|
|
// sender/hop-count to fill them with) — not offered on the general
|
|
// compose keyboard's own placeholder list.
|
|
_kb->addPlaceholder("{name}");
|
|
_kb->addPlaceholder("{hops}");
|
|
}
|
|
return true;
|
|
}
|
|
switch (k) {
|
|
case botcfg::CHANNEL:
|
|
// Full browsable channel picker (same experience as Live Share's "To"
|
|
// row); which channel is *active* is the separate Enable row.
|
|
if (_num_channels == 0) return false;
|
|
_task->savePrefsIfDirty(_dirty);
|
|
_task->pickBotChannelTarget();
|
|
return true;
|
|
case botcfg::ROOM:
|
|
if (_num_rooms == 0) return false;
|
|
_task->savePrefsIfDirty(_dirty);
|
|
_task->pickBotRoomTarget();
|
|
return true;
|
|
case botcfg::QUIET_FROM:
|
|
case botcfg::QUIET_TO:
|
|
_stepper_row = _sel;
|
|
return true;
|
|
default: return false;
|
|
}
|
|
}
|
|
};
|