Public Access
M0 step 7: Settings, About, Storage, first-boot setup
- lib/apps_model (host-tested): SettingsMenu (rows, readable values, choices, validation messages) and SetupWizard (names, Region confirmation, timezone; saves only when finished) - AppManager: modal Apps that Home/Back can't leave (setup wizard) - SettingsApp: all settings plus Storage (usage, erase SD behind a dialog) and About (version, node id, battery, memory, uptime) pages, replacing the temporary Diagnostics App - SetupApp: first-boot wizard, opened modally until SetupDone - Node id and default names derived from the MAC like Meshtastic - Widget demo is now hidden (About, then w) - lib_ldf_mode = deep+ so libraries see each other's headers Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
@@ -1,84 +0,0 @@
|
||||
#include "diagnostics_app.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#include "ui/fonts.h"
|
||||
#include "ui/widgets.h"
|
||||
#include "version.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
bool DiagnosticsApp::onKey(const KeyEvent& e) {
|
||||
requestRedraw();
|
||||
if (formatDialog_) {
|
||||
formatDialog_->onKey(e);
|
||||
if (formatDialog_->result() == 1) storage_.requestFormat();
|
||||
if (formatDialog_->result() != DialogModel::kPending) formatDialog_.reset();
|
||||
return true;
|
||||
}
|
||||
if (e.key != Key::Char) return false;
|
||||
int brightness = settings_.getInt(Setting::Brightness);
|
||||
switch (e.ch) {
|
||||
case '+':
|
||||
case '=': settings_.setInt(Setting::Brightness, brightness + 10); return true;
|
||||
case '-': settings_.setInt(Setting::Brightness, brightness - 10); return true;
|
||||
case 'd':
|
||||
settings_.setInt(Setting::DimTimeoutS, 5);
|
||||
settings_.setInt(Setting::OffTimeoutS, 10);
|
||||
return true;
|
||||
case 'r': // restore default timeouts
|
||||
settings_.setInt(Setting::OffTimeoutS, 60);
|
||||
settings_.setInt(Setting::DimTimeoutS, 30);
|
||||
return true;
|
||||
case 'f':
|
||||
if (storage_.state().present) formatDialog_.reset(new DialogModel({"Cancel", "Erase"}));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void DiagnosticsApp::update(uint32_t nowMs) {
|
||||
if (nowMs - lastRefreshMs_ >= 1000) {
|
||||
lastRefreshMs_ = nowMs;
|
||||
requestRedraw();
|
||||
}
|
||||
}
|
||||
|
||||
void DiagnosticsApp::draw(Canvas& c) {
|
||||
const auto& area = theme::kContent;
|
||||
const auto& b = battery_.estimator();
|
||||
const auto s = storage_.state();
|
||||
std::vector<std::string> lines;
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "%s %s", kProductName, versionString());
|
||||
lines.push_back(buf);
|
||||
snprintf(buf, sizeof(buf), "Battery %d%% %d mV (raw %d)", b.percent(), b.millivolts(), battery_.lastRawMillivolts());
|
||||
lines.push_back(buf);
|
||||
if (storage_.formatting())
|
||||
lines.push_back("SD: formatting...");
|
||||
else if (s.present) {
|
||||
snprintf(buf, sizeof(buf), "SD %d%% of %u MB logs:%s capt:%s", s.usedPercent,
|
||||
static_cast<unsigned>(s.totalBytes >> 20), s.logsAllowed ? "y" : "n", s.capturesAllowed ? "y" : "n");
|
||||
lines.push_back(buf);
|
||||
} else
|
||||
lines.push_back("SD: no card");
|
||||
snprintf(buf, sizeof(buf), "Uptime %lus clock %s", millis() / 1000, clock_.displayTime().c_str());
|
||||
lines.push_back(buf);
|
||||
snprintf(buf, sizeof(buf), "Heap %u KB free, min %u KB", ESP.getFreeHeap() / 1024, ESP.getMinFreeHeap() / 1024);
|
||||
lines.push_back(buf);
|
||||
snprintf(buf, sizeof(buf), "Bright %d%% dim %ds off %ds", static_cast<int>(settings_.getInt(Setting::Brightness)),
|
||||
static_cast<int>(settings_.getInt(Setting::DimTimeoutS)),
|
||||
static_cast<int>(settings_.getInt(Setting::OffTimeoutS)));
|
||||
lines.push_back(buf);
|
||||
widgets::textLines(c, lines, 0, area);
|
||||
|
||||
c.setFont(&fonts::small);
|
||||
c.setTextColor(theme::kMuted);
|
||||
c.drawString("+/- bright d: 5s/10s r: 30s/60s f: erase SD", 4, area.y + area.h - 10);
|
||||
|
||||
if (formatDialog_)
|
||||
widgets::dialog(c, "Erase SD card?", "Everything on the card is deleted. It becomes one FAT32 partition.",
|
||||
*formatDialog_);
|
||||
}
|
||||
|
||||
} // namespace roro
|
||||
@@ -1,33 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "app.h"
|
||||
#include "dialog_model.h"
|
||||
#include "services/battery_service.h"
|
||||
#include "services/clock_service.h"
|
||||
#include "services/power_service.h"
|
||||
#include "services/storage_service.h"
|
||||
#include "settings.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
// Live hardware values for bring-up; becomes the About screen in step 7.
|
||||
class DiagnosticsApp : public App {
|
||||
public:
|
||||
DiagnosticsApp(Settings& settings, BatteryService& battery, StorageService& storage, ClockService& clock)
|
||||
: settings_(settings), battery_(battery), storage_(storage), clock_(clock) {}
|
||||
bool onKey(const KeyEvent& e) override;
|
||||
void update(uint32_t nowMs) override;
|
||||
void draw(Canvas& c) override;
|
||||
|
||||
private:
|
||||
Settings& settings_;
|
||||
BatteryService& battery_;
|
||||
StorageService& storage_;
|
||||
ClockService& clock_;
|
||||
std::unique_ptr<DialogModel> formatDialog_;
|
||||
uint32_t lastRefreshMs_ = 0;
|
||||
};
|
||||
|
||||
} // namespace roro
|
||||
@@ -0,0 +1,210 @@
|
||||
#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;
|
||||
eraseDialog_.reset();
|
||||
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::Storage: return onStorageKey(e);
|
||||
case Page::About: return onAboutKey(e);
|
||||
}
|
||||
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: page_ = menu_.row(i) == Row::Storage ? Page::Storage : Page::About; 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::onStorageKey(const KeyEvent& e) {
|
||||
if (eraseDialog_) {
|
||||
eraseDialog_->onKey(e);
|
||||
if (eraseDialog_->result() == 1 && !d_.storage.requestFormat()) warn("A format is already running");
|
||||
if (eraseDialog_->result() != DialogModel::kPending) eraseDialog_.reset();
|
||||
return true;
|
||||
}
|
||||
if (e.key == Key::Select) {
|
||||
if (d_.storage.state().present) eraseDialog_.reset(new DialogModel({"Cancel", "Erase"}));
|
||||
else warn("No SD card");
|
||||
}
|
||||
if (e.key == Key::Back) page_ = Page::Menu;
|
||||
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::update(uint32_t nowMs) {
|
||||
// Live values on Storage and About.
|
||||
if ((page_ == Page::Storage || page_ == Page::About) && nowMs - lastRefreshMs_ >= 1000) {
|
||||
lastRefreshMs_ = nowMs;
|
||||
requestRedraw();
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> SettingsApp::storageLines() const {
|
||||
auto s = d_.storage.state();
|
||||
std::vector<std::string> lines;
|
||||
char buf[64];
|
||||
if (d_.storage.formatting()) {
|
||||
lines.push_back("Erasing the SD card...");
|
||||
} else if (!s.present) {
|
||||
lines.push_back("No SD card inserted.");
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), "SD card: %u MB, %d%% used", static_cast<unsigned>(s.totalBytes >> 20), s.usedPercent);
|
||||
lines.push_back(buf);
|
||||
lines.push_back(s.logsAllowed ? "Logs: recording" : "Logs: paused (over 90%)");
|
||||
lines.push_back(s.capturesAllowed ? "Captures: allowed" : "Captures: stopped (card full)");
|
||||
}
|
||||
lines.push_back("");
|
||||
lines.push_back("Enter: erase SD card");
|
||||
return lines;
|
||||
}
|
||||
|
||||
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(("Enter: save `: cancel " + std::to_string(editor_.text().size()) + "/" +
|
||||
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::Storage:
|
||||
widgets::textLines(c, storageLines(), 0, area);
|
||||
if (eraseDialog_)
|
||||
widgets::dialog(c, "Erase SD card?", "Everything on the card is deleted. It becomes one FAT32 partition.",
|
||||
*eraseDialog_);
|
||||
break;
|
||||
case Page::About: widgets::textLines(c, aboutLines(), 0, area); break;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace roro
|
||||
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "app.h"
|
||||
#include "app_manager.h"
|
||||
#include "dialog_model.h"
|
||||
#include "event_bus.h"
|
||||
#include "line_editor.h"
|
||||
#include "list_model.h"
|
||||
#include "services/battery_service.h"
|
||||
#include "services/clock_service.h"
|
||||
#include "services/storage_service.h"
|
||||
#include "settings_menu.h"
|
||||
#include "ui/theme.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
struct SettingsAppDeps {
|
||||
Settings& settings;
|
||||
EventBus& bus;
|
||||
AppManager& apps;
|
||||
BatteryService& battery;
|
||||
StorageService& storage;
|
||||
ClockService& clock;
|
||||
};
|
||||
|
||||
// Settings: every user-facing setting, plus the Storage and About pages.
|
||||
class SettingsApp : public App {
|
||||
public:
|
||||
explicit SettingsApp(const SettingsAppDeps& deps) : d_(deps), menu_(deps.settings) {}
|
||||
void onEnter() override;
|
||||
bool onKey(const KeyEvent& e) override;
|
||||
void update(uint32_t nowMs) override;
|
||||
void draw(Canvas& c) override;
|
||||
|
||||
private:
|
||||
enum class Page { Menu, Text, Choice, Storage, About };
|
||||
|
||||
bool onMenuKey(const KeyEvent& e);
|
||||
bool onTextKey(const KeyEvent& e);
|
||||
bool onChoiceKey(const KeyEvent& e);
|
||||
bool onStorageKey(const KeyEvent& e);
|
||||
bool onAboutKey(const KeyEvent& e);
|
||||
void warn(const std::string& text);
|
||||
std::vector<std::string> storageLines() const;
|
||||
std::vector<std::string> aboutLines() const;
|
||||
|
||||
SettingsAppDeps d_;
|
||||
SettingsMenu menu_;
|
||||
Page page_ = Page::Menu;
|
||||
ListModel list_{theme::kContent.h / theme::kLineHeight};
|
||||
ListModel choices_{theme::kContent.h / theme::kLineHeight};
|
||||
LineEditor editor_{39};
|
||||
std::unique_ptr<DialogModel> eraseDialog_;
|
||||
int editingRow_ = 0;
|
||||
uint32_t lastRefreshMs_ = 0;
|
||||
};
|
||||
|
||||
} // namespace roro
|
||||
@@ -0,0 +1,99 @@
|
||||
#include "setup_app.h"
|
||||
|
||||
#include "platform/identity.h"
|
||||
#include "ui/fonts.h"
|
||||
#include "ui/widgets.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
using Step = SetupWizard::Step;
|
||||
|
||||
SetupApp::SetupApp(Settings& settings, AppManager& apps)
|
||||
: apps_(apps), wizard_(settings, identity::defaultLongName(), identity::defaultShortName()) {}
|
||||
|
||||
bool SetupApp::onKey(const KeyEvent& e) {
|
||||
requestRedraw();
|
||||
bool choosing = wizard_.choiceCount() > 0;
|
||||
bool typing = wizard_.step() == Step::LongName || wizard_.step() == Step::ShortName;
|
||||
switch (e.key) {
|
||||
case Key::Select:
|
||||
if (choosing) wizard_.selectChoice(choices_.selected());
|
||||
if (wizard_.next()) {
|
||||
if (wizard_.finished()) {
|
||||
apps_.endModal();
|
||||
return true;
|
||||
}
|
||||
choices_.setCount(wizard_.choiceCount());
|
||||
if (wizard_.choiceCount()) choices_.select(wizard_.selectedChoice());
|
||||
}
|
||||
break;
|
||||
case Key::Back:
|
||||
case Key::Home:
|
||||
wizard_.back();
|
||||
choices_.setCount(wizard_.choiceCount());
|
||||
if (wizard_.choiceCount()) choices_.select(wizard_.selectedChoice());
|
||||
break;
|
||||
case Key::Up: if (choosing) choices_.up(); break;
|
||||
case Key::Down: if (choosing) choices_.down(); break;
|
||||
case Key::Left: if (typing) wizard_.editor().left(); break;
|
||||
case Key::Right: if (typing) wizard_.editor().right(); break;
|
||||
case Key::Delete: if (typing) wizard_.editor().backspace(); break;
|
||||
case Key::Char: if (typing) wizard_.editor().insert(e.ch); break;
|
||||
default: break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void SetupApp::draw(Canvas& c) {
|
||||
const auto& area = theme::kContent;
|
||||
auto heading = [&](const char* text) {
|
||||
c.setFont(&fonts::bold);
|
||||
c.setTextColor(theme::kAccent);
|
||||
c.drawString(text, 4, area.y + 3);
|
||||
c.setFont(&fonts::body);
|
||||
};
|
||||
auto note = [&](const std::string& text, int y, uint16_t color = theme::kMuted) {
|
||||
auto lines = wrapText(text, area.w - 8, widgets::bodyMeasure(c));
|
||||
c.setTextColor(color);
|
||||
for (size_t i = 0; i < lines.size(); i++) c.drawString(lines[i].c_str(), 4, y + i * theme::kLineHeight);
|
||||
};
|
||||
|
||||
switch (wizard_.step()) {
|
||||
case Step::Welcome:
|
||||
heading("Welcome to roro9stack");
|
||||
note("Let's set up this device: your names on the mesh, your radio region and your timezone.",
|
||||
area.y + 22, theme::kText);
|
||||
note("Enter: continue", area.y + area.h - 14);
|
||||
break;
|
||||
case Step::LongName:
|
||||
heading("Long name (1/4)");
|
||||
note("Shown to other mesh users. Up to 39 bytes.", area.y + 20);
|
||||
widgets::lineEditor(c, wizard_.editor(), {4, area.y + 50, area.w - 8, 0});
|
||||
break;
|
||||
case Step::ShortName:
|
||||
heading("Short name (2/4)");
|
||||
note("Up to 4 characters, shown where space is tight.", area.y + 20);
|
||||
widgets::lineEditor(c, wizard_.editor(), {4, area.y + 50, area.w - 8, 0});
|
||||
break;
|
||||
case Step::Region:
|
||||
heading("Radio region (3/4)");
|
||||
note("The radio only transmits once you confirm your region.", area.y + 18);
|
||||
widgets::list(c, choices_, {0, area.y + 48, area.w, 4 * theme::kLineHeight},
|
||||
[this](int i) { return wizard_.choiceLabel(i); });
|
||||
note("Enter: I confirm I'm in this region", area.y + area.h - 14);
|
||||
break;
|
||||
case Step::Timezone:
|
||||
heading("Timezone (4/4)");
|
||||
widgets::list(c, choices_, {0, area.y + 20, area.w, 4 * theme::kLineHeight},
|
||||
[this](int i) { return wizard_.choiceLabel(i); });
|
||||
break;
|
||||
case Step::Done:
|
||||
heading("All set");
|
||||
note("You can change all of this later in Settings.", area.y + 22, theme::kText);
|
||||
note("Enter: start `: go back", area.y + area.h - 14);
|
||||
break;
|
||||
}
|
||||
if (!wizard_.error().empty()) note(wizard_.error(), area.y + area.h - 14, theme::kWarning);
|
||||
}
|
||||
|
||||
} // namespace roro
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include "app.h"
|
||||
#include "app_manager.h"
|
||||
#include "list_model.h"
|
||||
#include "setup_wizard.h"
|
||||
#include "ui/theme.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
// First-boot setup, run as a modal App until finished.
|
||||
class SetupApp : public App {
|
||||
public:
|
||||
SetupApp(Settings& settings, AppManager& apps);
|
||||
bool onKey(const KeyEvent& e) override;
|
||||
void draw(Canvas& c) override;
|
||||
|
||||
private:
|
||||
AppManager& apps_;
|
||||
SetupWizard wizard_;
|
||||
ListModel choices_{4};
|
||||
};
|
||||
|
||||
} // namespace roro
|
||||
Reference in New Issue
Block a user