Submenu basis

This commit is contained in:
jpirnay
2026-04-12 07:21:42 +02:00
parent 7feff4e241
commit 9f16066bce
5 changed files with 249 additions and 2 deletions
+5
View File
@@ -18,6 +18,11 @@
// previous item's, SettingsActivity::onEnter() automatically inserts a separator
// row before it. Add with .withSubcategory(StrId::STR_MY_SECTION).
// Items without a subcategory (STR_NONE_OPT) never trigger a separator.
// submenu — optional submenu grouping. Items with the same submenu StrId are hidden from
// the main list and collected behind a single placeholder entry. Selecting that
// entry launches SettingsSubmenuActivity with those items. withSubcategory()
// works inside a submenu exactly as it does in the parent tab.
// Add with .withSubmenu(StrId::STR_MY_SUBMENU).
// key — JSON property name used by the web settings API (nullptr = device-only).
//
// ACTION-type entries and entries without a key are device-only and are added directly
+26 -1
View File
@@ -15,6 +15,7 @@
#include "MappedInputManager.h"
#include "OtaUpdateActivity.h"
#include "SettingsList.h"
#include "SettingsSubmenuActivity.h"
#include "StatusBarSettingsActivity.h"
#include "SyncTimeActivity.h"
#include "SystemInformationActivity.h"
@@ -45,13 +46,27 @@ void SettingsActivity::onEnter() {
readerSettings.clear();
controlsSettings.clear();
systemSettings.clear();
submenuData.clear();
StrId lastDisplaySub = StrId::STR_NONE_OPT;
StrId lastReaderSub = StrId::STR_NONE_OPT;
StrId lastControlsSub = StrId::STR_NONE_OPT;
StrId lastSystemSub = StrId::STR_NONE_OPT;
auto addTo = [](std::vector<SettingInfo>& vec, StrId& lastSub, SettingInfo s) {
auto addTo = [this](std::vector<SettingInfo>& vec, StrId& lastSub, SettingInfo s) {
if (s.submenu != StrId::STR_NONE_OPT) {
// Item belongs to a submenu — collect it and insert a placeholder in the main
// list the first time this submenu ID is encountered.
auto it = std::find_if(submenuData.begin(), submenuData.end(),
[&s](const SubmenuData& d) { return d.id == s.submenu; });
if (it == submenuData.end()) {
vec.push_back(SettingInfo::SubmenuEntry(s.submenu));
submenuData.push_back({s.submenu, {}});
it = submenuData.end() - 1;
}
it->items.push_back(std::move(s));
return;
}
if (s.subcategory != StrId::STR_NONE_OPT && s.subcategory != lastSub) {
vec.push_back(SettingInfo::Separator(s.subcategory));
lastSub = s.subcategory;
@@ -267,6 +282,16 @@ void SettingsActivity::toggleCurrentSetting() {
case SettingAction::DetectTimezone:
startActivityForResult(std::make_unique<DetectTimezoneActivity>(renderer, mappedInput), resultHandler);
break;
case SettingAction::Submenu: {
const auto it = std::find_if(submenuData.cbegin(), submenuData.cend(),
[&setting](const SubmenuData& d) { return d.id == setting.nameId; });
if (it != submenuData.cend()) {
startActivityForResult(
std::make_unique<SettingsSubmenuActivity>(renderer, mappedInput, setting.nameId, it->items),
resultHandler);
}
break;
}
case SettingAction::None:
// Do nothing
break;
+26 -1
View File
@@ -26,6 +26,7 @@ enum class SettingAction {
DetectTimezone,
SyncTime,
Weather,
Submenu,
};
struct SettingInfo {
@@ -152,13 +153,31 @@ struct SettingInfo {
}
bool isSeparator = false;
StrId subcategory = StrId::STR_NONE_OPT;
StrId subcategory = StrId::STR_NONE_OPT; // Triggers a separator row on first use and on change
StrId submenu = StrId::STR_NONE_OPT; // Routes item into a submenu; hidden from main list
[[nodiscard]] std::string getTitle() const;
// Inserts a separator row in the parent tab when this item's subcategory first appears or changes.
SettingInfo& withSubcategory(StrId sub) {
subcategory = sub;
return *this;
}
// Hides this item from the parent tab and places it inside a SettingsSubmenuActivity instead.
// All items sharing the same submenu StrId are grouped under one placeholder entry.
SettingInfo& withSubmenu(StrId sub) {
submenu = sub;
return *this;
}
// For internal use by SettingsActivity: placeholder entry that launches the submenu.
static SettingInfo SubmenuEntry(StrId titleId) {
SettingInfo s;
s.nameId = titleId;
s.type = SettingType::ACTION;
s.action = SettingAction::Submenu;
return s;
}
};
class SettingsActivity final : public Activity {
@@ -178,6 +197,12 @@ class SettingsActivity final : public Activity {
static constexpr int categoryCount = 4;
static const StrId categoryNames[categoryCount];
struct SubmenuData {
StrId id;
std::vector<SettingInfo> items;
};
std::vector<SubmenuData> submenuData;
void enterCategory(int categoryIndex);
void toggleCurrentSetting();
[[nodiscard]] bool isListItemSelectable(int settingIdx) const;
@@ -0,0 +1,162 @@
#include "SettingsSubmenuActivity.h"
#include <GfxRenderer.h>
#include <I18n.h>
#include "ButtonRemapActivity.h"
#include "CalibreSettingsActivity.h"
#include "ClearCacheActivity.h"
#include "ClockSettingsActivity.h"
#include "CrossPointSettings.h"
#include "KOReaderSettingsActivity.h"
#include "LanguageSelectActivity.h"
#include "MappedInputManager.h"
#include "OtaUpdateActivity.h"
#include "StatusBarSettingsActivity.h"
#include "SystemInformationActivity.h"
#include "activities/network/WifiSelectionActivity.h"
#include "activities/weather/WeatherSettingsActivity.h"
#include "components/UITheme.h"
#include "fontIds.h"
void SettingsSubmenuActivity::onEnter() {
Activity::onEnter();
itemCount = static_cast<int>(items.size());
const auto pred = UITheme::makeSelectablePredicate(itemCount,
[this](int i) { return items[i].getTitle(); });
buttonNavigator.setSelectablePredicate(pred, itemCount);
if (!pred(selectedIndex)) {
selectedIndex = buttonNavigator.nextIndex(selectedIndex);
}
requestUpdate();
}
void SettingsSubmenuActivity::onExit() { Activity::onExit(); }
void SettingsSubmenuActivity::loop() {
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
finish();
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
toggleItem();
requestUpdate();
return;
}
buttonNavigator.onNextRelease([this] {
selectedIndex = buttonNavigator.nextIndex(selectedIndex);
requestUpdate();
});
buttonNavigator.onPreviousRelease([this] {
selectedIndex = buttonNavigator.previousIndex(selectedIndex);
requestUpdate();
});
buttonNavigator.onNextContinuous([this] {
selectedIndex = buttonNavigator.nextIndex(selectedIndex);
requestUpdate();
});
buttonNavigator.onPreviousContinuous([this] {
selectedIndex = buttonNavigator.previousIndex(selectedIndex);
requestUpdate();
});
}
void SettingsSubmenuActivity::toggleItem() {
if (selectedIndex < 0 || selectedIndex >= itemCount) return;
const auto& setting = items[selectedIndex];
if (setting.isSeparator) return;
if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) {
SETTINGS.*(setting.valuePtr) = !SETTINGS.*(setting.valuePtr);
} else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) {
const uint8_t cur = SETTINGS.*(setting.valuePtr);
SETTINGS.*(setting.valuePtr) = (cur + 1) % static_cast<uint8_t>(setting.enumValues.size());
} else if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) {
const int8_t cur = SETTINGS.*(setting.valuePtr);
SETTINGS.*(setting.valuePtr) = (cur + setting.valueRange.step > setting.valueRange.max)
? setting.valueRange.min
: cur + setting.valueRange.step;
} else if (setting.type == SettingType::ACTION) {
auto resultHandler = [this](const ActivityResult&) { SETTINGS.saveToFile(); };
switch (setting.action) {
case SettingAction::RemapFrontButtons:
startActivityForResult(std::make_unique<ButtonRemapActivity>(renderer, mappedInput), resultHandler);
return;
case SettingAction::CustomiseStatusBar:
startActivityForResult(std::make_unique<StatusBarSettingsActivity>(renderer, mappedInput), resultHandler);
return;
case SettingAction::ClockSettings:
startActivityForResult(std::make_unique<ClockSettingsActivity>(renderer, mappedInput), resultHandler);
return;
case SettingAction::KOReaderSync:
startActivityForResult(std::make_unique<KOReaderSettingsActivity>(renderer, mappedInput), resultHandler);
return;
case SettingAction::OPDSBrowser:
startActivityForResult(std::make_unique<CalibreSettingsActivity>(renderer, mappedInput), resultHandler);
return;
case SettingAction::Network:
startActivityForResult(std::make_unique<WifiSelectionActivity>(renderer, mappedInput, false), resultHandler);
return;
case SettingAction::ClearCache:
startActivityForResult(std::make_unique<ClearCacheActivity>(renderer, mappedInput), resultHandler);
return;
case SettingAction::CheckForUpdates:
startActivityForResult(std::make_unique<OtaUpdateActivity>(renderer, mappedInput), resultHandler);
return;
case SettingAction::Language:
startActivityForResult(std::make_unique<LanguageSelectActivity>(renderer, mappedInput), resultHandler);
return;
case SettingAction::Weather:
startActivityForResult(std::make_unique<WeatherSettingsActivity>(renderer, mappedInput), resultHandler);
return;
case SettingAction::SystemInfo:
startActivityForResult(std::make_unique<SystemInformationActivity>(renderer, mappedInput), resultHandler);
return;
default:
return;
}
}
SETTINGS.saveToFile();
}
void SettingsSubmenuActivity::render(RenderLock&&) {
renderer.clearScreen();
const auto& metrics = UITheme::getInstance().getMetrics();
const Rect contentRect = UITheme::getContentRect(renderer, true, false);
GUI.drawHeader(renderer, Rect{contentRect.x, metrics.topPadding, contentRect.width, metrics.headerHeight},
I18N.get(titleId));
const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing;
const int contentHeight = contentRect.height - contentTop - metrics.verticalSpacing;
GUI.drawList(
renderer, Rect{contentRect.x, contentTop, contentRect.width, contentHeight}, itemCount, selectedIndex,
[this](int index) { return items[index].getTitle(); }, nullptr, nullptr,
[this](int i) {
const auto& setting = items[i];
if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) {
return std::string(SETTINGS.*(setting.valuePtr) ? tr(STR_STATE_ON) : tr(STR_STATE_OFF));
}
if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) {
const uint8_t value = SETTINGS.*(setting.valuePtr);
if (value < setting.enumValues.size()) {
return std::string(I18N.get(setting.enumValues[value]));
}
}
if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) {
return std::to_string(SETTINGS.*(setting.valuePtr));
}
return std::string();
},
true);
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();
}
@@ -0,0 +1,30 @@
#pragma once
#include <I18n.h>
#include <vector>
#include "activities/Activity.h"
#include "activities/settings/SettingsActivity.h"
#include "util/ButtonNavigator.h"
// Displays a flat list of SettingInfo items launched from a SettingsActivity submenu entry.
// Supports subcategory separators (withSubcategory) exactly as the parent settings tabs do.
class SettingsSubmenuActivity final : public Activity {
StrId titleId;
std::vector<SettingInfo> items;
int selectedIndex = 0;
int itemCount = 0;
ButtonNavigator buttonNavigator;
void toggleItem();
public:
explicit SettingsSubmenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, StrId titleId,
std::vector<SettingInfo> items)
: Activity("SettingsSubmenu", renderer, mappedInput), titleId(titleId), items(std::move(items)) {}
void onEnter() override;
void onExit() override;
void loop() override;
void render(RenderLock&&) override;
};