Files
roro9stack/src/apps/settings_app.cpp
T
twislaandClaude Opus 5.5 34e6714785
CI / build (pull_request) Successful in 7m14s
Site / build (pull_request) Successful in 8s
Keys: one table file for the device's help panel and the website's key tables (#72)
Every screen's keys are constant tables in lib/core/src/app_keys.h (52 of
them, each under an `// id: Title` comment). An App's help() picks the table
of the state it is in. site/tools/gen_dev_docs.py reads the same file and
writes site/data/keys.toml; the `keys` shortcode shows a screen's tables on
its guide page, and /guide/keys/ shows all of them.

The Site job fails when the data file is out of date or a page asks for a
table that doesn't exist, and now also runs when app_keys.h changes. A key
added to an App shows up on the website without anyone editing a page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
2026-10-07 01:42:48 +02:00

220 lines
7.6 KiB
C++

#include "app_keys.h"
#include "settings_app.h"
#include <Arduino.h>
#include "platform/identity.h"
#include "ui/fonts.h"
#include "ui/widgets.h"
#include "version.h"
namespace roro {
using Kind = SettingsMenu::Kind;
using Row = SettingsMenu::Row;
void SettingsApp::onEnter() {
page_ = Page::Menu;
list_.setCount(menu_.count());
}
void SettingsApp::warn(const std::string& text) {
d_.bus.publish(Event::withText(EventType::Notification, text.c_str(), static_cast<int32_t>(NotificationLevel::Warning)));
}
bool SettingsApp::onKey(const KeyEvent& e) {
requestRedraw();
switch (page_) {
case Page::Menu: return onMenuKey(e);
case Page::Text: return onTextKey(e);
case Page::Choice: return onChoiceKey(e);
case Page::About: return onAboutKey(e);
case Page::Wifi:
if (!wifiPage_.onKey(e)) page_ = Page::Menu;
return true;
case Page::Firmware:
if (!firmwarePage_.onKey(e)) page_ = Page::Menu;
return true;
case Page::Debug:
if (!debugPage_.onKey(e)) page_ = Page::Menu;
return true;
}
return false;
}
bool SettingsApp::onMenuKey(const KeyEvent& e) {
int i = list_.selected();
switch (e.key) {
case Key::Up: list_.up(); return true;
case Key::Down: list_.down(); return true;
case Key::Left:
case Key::Right:
if (menu_.kind(i) == Kind::Slider) menu_.adjust(i, e.key == Key::Right ? 1 : -1);
if (menu_.kind(i) == Kind::Toggle) menu_.toggle(i);
return true;
case Key::Select: break;
default: return false; // Back leaves Settings
}
editingRow_ = i;
switch (menu_.kind(i)) {
case Kind::Text:
editor_ = LineEditor(menu_.maxBytes(i));
editor_.setText(menu_.value(i));
page_ = Page::Text;
break;
case Kind::Choice:
choices_.setCount(static_cast<int>(menu_.choices(i).size()));
choices_.select(menu_.currentChoice(i) < 0 ? 0 : menu_.currentChoice(i));
page_ = Page::Choice;
break;
case Kind::Toggle: menu_.toggle(i); break;
case Kind::Slider: break;
case Kind::Page:
switch (menu_.row(i)) {
case Row::Wifi:
page_ = Page::Wifi;
wifiPage_.enter();
break;
case Row::Firmware:
page_ = Page::Firmware;
firmwarePage_.enter();
break;
case Row::DebugConsole:
page_ = Page::Debug;
debugPage_.enter();
break;
default: page_ = Page::About; break;
}
break;
}
return true;
}
bool SettingsApp::onTextKey(const KeyEvent& e) {
switch (e.key) {
case Key::Char: editor_.insert(e.ch); break;
case Key::Delete: editor_.backspace(); break;
case Key::Left: editor_.left(); break;
case Key::Right: editor_.right(); break;
case Key::Select: {
std::string error = menu_.setText(editingRow_, editor_.text());
if (error.empty()) page_ = Page::Menu;
else warn(error);
break;
}
case Key::Back: page_ = Page::Menu; break; // cancel
default: break;
}
return true;
}
bool SettingsApp::onChoiceKey(const KeyEvent& e) {
switch (e.key) {
case Key::Up: choices_.up(); break;
case Key::Down: choices_.down(); break;
case Key::Select: {
std::string error = menu_.choose(editingRow_, choices_.selected());
if (error.empty()) page_ = Page::Menu;
else warn(error);
break;
}
case Key::Back: page_ = Page::Menu; break;
default: break;
}
return true;
}
bool SettingsApp::onAboutKey(const KeyEvent& e) {
if (e.key == Key::Back) page_ = Page::Menu;
if (e.key == Key::Char && e.ch == 'w') d_.apps.open("demo"); // hidden widget demo
return true;
}
void SettingsApp::help(std::vector<KeyHelp>& out) const {
switch (page_) {
case Page::Menu: keys::add(out, keys::kSettings); break;
case Page::Text: keys::add(out, keys::kText); break;
case Page::Choice: keys::add(out, keys::kSettingsChoice); break;
case Page::About: break;
case Page::Wifi: wifiPage_.help(out); break;
case Page::Firmware: firmwarePage_.help(out); break;
case Page::Debug: debugPage_.help(out); break;
}
}
const char* SettingsApp::helpTitle() const {
switch (page_) {
case Page::Wifi: return wifiPage_.helpTitle();
case Page::Firmware: return firmwarePage_.helpTitle();
case Page::Debug: return debugPage_.helpTitle();
case Page::About: return "About";
default: return nullptr;
}
}
void SettingsApp::update(uint32_t nowMs) {
// Live values on About and Firmware.
bool live = page_ == Page::About || page_ == Page::Firmware || page_ == Page::Debug ||
(page_ == Page::Wifi && wifiPage_.live());
if (live && nowMs - lastRefreshMs_ >= 500) {
lastRefreshMs_ = nowMs;
requestRedraw();
}
}
std::vector<std::string> SettingsApp::aboutLines() const {
std::vector<std::string> lines;
char buf[64];
snprintf(buf, sizeof(buf), "%s %s", kProductName, versionString());
lines.push_back(buf);
lines.push_back("Node " + identity::nodeId());
const auto& b = d_.battery.estimator();
snprintf(buf, sizeof(buf), "Battery %d%% (%d mV)", b.percent(), b.millivolts());
lines.push_back(buf);
snprintf(buf, sizeof(buf), "Memory %u KB free (min %u KB)", ESP.getFreeHeap() / 1024, ESP.getMinFreeHeap() / 1024);
lines.push_back(buf);
uint32_t up = millis() / 1000;
snprintf(buf, sizeof(buf), "Uptime %luh %02lum %02lus", (unsigned long)(up / 3600), (unsigned long)(up / 60 % 60),
(unsigned long)(up % 60));
lines.push_back(buf);
lines.push_back("Clock " + d_.clock.displayTime());
lines.push_back("License GPL-3.0");
return lines;
}
void SettingsApp::draw(Canvas& c) {
const auto& area = theme::kContent;
switch (page_) {
case Page::Menu:
widgets::list(
c, list_, area, [this](int i) { return menu_.label(i); },
[this](int i) {
auto kind = menu_.kind(i);
if (kind == Kind::Page) return std::string(">");
if (kind == Kind::Slider) return "< " + menu_.value(i) + " >";
return menu_.value(i);
});
break;
case Page::Text:
c.setFont(&fonts::body);
c.setTextColor(theme::kMuted);
c.drawString(menu_.label(editingRow_).c_str(), 4, area.y + 4);
widgets::lineEditor(c, editor_, {4, area.y + 22, area.w - 8, 0});
c.drawString((std::to_string(editor_.text().size()) + " of " + std::to_string(editor_.maxBytes()) + " bytes").c_str(), 4,
area.y + 44);
break;
case Page::Choice: {
auto options = menu_.choices(editingRow_);
int current = menu_.currentChoice(editingRow_);
widgets::list(c, choices_, area, [&](int i) { return (i == current ? "* " : " ") + options[i]; });
break;
}
case Page::About: widgets::textLines(c, aboutLines(), 0, area); break;
case Page::Wifi: wifiPage_.draw(c); break;
case Page::Firmware: firmwarePage_.draw(c); break;
case Page::Debug: debugPage_.draw(c); break;
}
}
} // namespace roro