Introduce common helper functions

This commit is contained in:
jpirnay
2026-04-12 20:30:14 +02:00
parent df3fec574d
commit 54807911e7
16 changed files with 790 additions and 729 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
#include "KOReaderCredentialStore.h" #include "KOReaderCredentialStore.h"
#include "activities/settings/SettingsActivity.h" #include "activities/settings/SettingInfo.h"
// Shared settings list used by both the device settings UI and the web settings API. // Shared settings list used by both the device settings UI and the web settings API.
// //
+70
View File
@@ -0,0 +1,70 @@
#include "MenuListActivity.h"
#include <I18n.h>
#include "MappedInputManager.h"
#include "components/UITheme.h"
void MenuListActivity::initMenuList() {
const int count = static_cast<int>(menuItems.size());
const auto pred = UITheme::makeSelectablePredicate(count, [this](int i) { return menuItems[i].getTitle(); });
buttonNavigator.setSelectablePredicate(pred, count);
if (count > 0 && !pred(selectedIndex)) {
selectedIndex = buttonNavigator.nextIndex(selectedIndex);
}
}
void MenuListActivity::onEnter() {
Activity::onEnter();
initMenuList();
requestUpdate();
}
void MenuListActivity::handleNavigation() {
buttonNavigator.onNext([this] {
selectedIndex = buttonNavigator.nextIndex(selectedIndex);
requestUpdate();
});
buttonNavigator.onPrevious([this] {
selectedIndex = buttonNavigator.previousIndex(selectedIndex);
requestUpdate();
});
}
void MenuListActivity::toggleCurrentItem() {
if (selectedIndex < 0 || selectedIndex >= static_cast<int>(menuItems.size())) return;
auto& item = menuItems[selectedIndex];
if (item.isSeparator) return;
if (item.type == SettingType::ACTION) {
onActionSelected(selectedIndex);
return;
}
item.toggleValue();
onSettingToggled(selectedIndex);
requestUpdate();
}
std::string MenuListActivity::getItemValueString(int index) const {
return menuItems[index].getDisplayValue();
}
void MenuListActivity::drawMenuList(const Rect& rect) {
const int count = static_cast<int>(menuItems.size());
GUI.drawList(
renderer, rect, count, selectedIndex, [this](int index) { return menuItems[index].getTitle(); }, nullptr, nullptr,
[this](int index) { return getItemValueString(index); }, true);
}
void MenuListActivity::loop() {
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
onBackPressed();
return;
}
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
toggleCurrentItem();
return;
}
handleNavigation();
}
+107
View File
@@ -0,0 +1,107 @@
#pragma once
#include <vector>
#include "Activity.h"
#include "settings/SettingInfo.h"
#include "util/ButtonNavigator.h"
struct Rect;
// Base class for activities that display a scrollable list of SettingInfo items.
// Provides common navigation, toggle/cycle logic, and drawList rendering.
//
// Subclasses populate `menuItems` (typically in the constructor or onEnter()),
// then rely on the default loop()/onEnter() or override selectively.
//
// === Minimal example ===
//
// class MyMenuActivity final : public MenuListActivity {
// public:
// explicit MyMenuActivity(GfxRenderer& r, MappedInputManager& m)
// : MenuListActivity("MyMenu", r, m) {
// // Separator header
// menuItems.push_back(SettingInfo::Separator(StrId::STR_MY_SECTION));
//
// // Toggle bound to a CrossPointSettings field
// menuItems.push_back(SettingInfo::Toggle(StrId::STR_MY_TOGGLE, &CrossPointSettings::myFlag));
//
// // Enum bound to a CrossPointSettings field
// menuItems.push_back(SettingInfo::Enum(StrId::STR_MY_ENUM, &CrossPointSettings::myEnum,
// {StrId::STR_OPTION_A, StrId::STR_OPTION_B, StrId::STR_OPTION_C}));
//
// // DynamicEnum with local state (getter/setter lambdas)
// menuItems.push_back(SettingInfo::DynamicEnum(StrId::STR_MY_DYNAMIC,
// {StrId::STR_LOW, StrId::STR_HIGH},
// [this]() -> uint8_t { return localValue; },
// [this](uint8_t v) { localValue = v; }));
//
// // Action item (handled in onActionSelected)
// menuItems.push_back(SettingInfo::Action(StrId::STR_DO_SOMETHING, SettingAction::None));
// }
//
// // render(): draw header/footer around the list
// void render(RenderLock&&) override {
// renderer.clearScreen();
// const Rect r = UITheme::getContentRect(renderer, true, false);
// drawMenuList(r); // <-- draws the item list
// GUI.drawButtonHints(renderer, ...);
// renderer.displayBuffer();
// }
//
// private:
// uint8_t localValue = 0;
//
// // Called when an ACTION item is confirmed
// void onActionSelected(int index) override {
// if (menuItems[index].nameId == StrId::STR_DO_SOMETHING) { /* ... */ }
// }
//
// // Called after a TOGGLE/ENUM/VALUE is cycled
// void onSettingToggled(int) override { SETTINGS.saveToFile(); }
// };
//
// The base class provides:
// onEnter() — wires up the selectable-predicate (skips separators) and requestUpdate().
// loop() — handles Back (→ onBackPressed), Confirm (→ toggleCurrentItem), and nav.
// drawMenuList(rect) — calls GUI.drawList with SettingInfo-based title/value lambdas.
// getItemValueString(i) — override for custom per-item value display.
// onBackPressed() — override to customise Back behaviour (default: finish()).
//
class MenuListActivity : public Activity {
protected:
std::vector<SettingInfo> menuItems;
int selectedIndex = 0;
ButtonNavigator buttonNavigator;
// Call after building/rebuilding menuItems to wire up the selectable predicate.
void initMenuList();
// Handle up/down navigation via buttonNavigator. Call from loop() if overriding.
void handleNavigation();
// Toggle/cycle the currently selected item. For ACTION items, delegates to onActionSelected().
void toggleCurrentItem();
// Draw the list into the given rect using GUI.drawList().
void drawMenuList(const Rect& rect);
// Override to provide custom value display for specific items.
// Default returns SettingInfo::getDisplayValue().
[[nodiscard]] virtual std::string getItemValueString(int index) const;
// Called when an ACTION-type item is confirmed. Override to handle actions.
virtual void onActionSelected(int index) {}
// Called when Back is pressed. Default calls finish().
virtual void onBackPressed() { finish(); }
// Called after a TOGGLE/ENUM/VALUE item has been toggled.
// Override to persist changes or trigger side-effects.
virtual void onSettingToggled(int /*index*/) {}
public:
using Activity::Activity;
void onEnter() override;
void loop() override;
};
+151 -158
View File
@@ -14,149 +14,174 @@ EpubReaderMenuActivity::EpubReaderMenuActivity(GfxRenderer& renderer, MappedInpu
const bool hasFootnotes, const int8_t initialEmbeddedStyleOverride, const bool hasFootnotes, const int8_t initialEmbeddedStyleOverride,
const int8_t initialImageRenderingOverride, const int8_t initialImageRenderingOverride,
const uint8_t initialTextDarkness) const uint8_t initialTextDarkness)
: Activity("EpubReaderMenu", renderer, mappedInput), : MenuListActivity("EpubReaderMenu", renderer, mappedInput),
menuItems(buildMenuItems(hasFootnotes)),
title(title),
pendingOrientation(currentOrientation), pendingOrientation(currentOrientation),
pendingEmbeddedStyleOverride(initialEmbeddedStyleOverride), pendingEmbeddedStyleOverride(initialEmbeddedStyleOverride),
pendingImageRenderingOverride(initialImageRenderingOverride), pendingImageRenderingOverride(initialImageRenderingOverride),
pendingTextDarkness(initialTextDarkness), pendingTextDarkness(initialTextDarkness),
title(title),
currentPage(currentPage), currentPage(currentPage),
totalPages(totalPages), totalPages(totalPages),
bookProgressPercent(bookProgressPercent) {} bookProgressPercent(bookProgressPercent) {
buildMenuItems(hasFootnotes);
std::string EpubReaderMenuActivity::MenuItem::getTitle() const {
const auto t = I18N.get(labelId);
return isSeparator ? UITheme::makeSeparatorTitle(t) : t;
} }
std::vector<EpubReaderMenuActivity::MenuItem> EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes) { void EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes) {
std::vector<MenuItem> items; menuItems.reserve(18);
items.reserve(18);
// Navigation // --- Navigation ---
items.push_back(MenuItem::separator(StrId::STR_READER_NAVIGATION)); menuItems.push_back(SettingInfo::Separator(StrId::STR_READER_NAVIGATION));
items.push_back({MenuAction::SELECT_CHAPTER, StrId::STR_SELECT_CHAPTER}); menuItems.push_back(SettingInfo::Action(StrId::STR_SELECT_CHAPTER, SettingAction::None));
items.push_back({MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT}); menuItems.push_back(SettingInfo::Action(StrId::STR_GO_TO_PERCENT, SettingAction::None));
if (hasFootnotes) { if (hasFootnotes) {
items.push_back({MenuAction::FOOTNOTES, StrId::STR_FOOTNOTES}); menuItems.push_back(SettingInfo::Action(StrId::STR_FOOTNOTES, SettingAction::None));
} }
items.push_back({MenuAction::AUTO_PAGE_TURN, StrId::STR_AUTO_TURN_PAGES_PER_MIN}); // Auto page turn: ACTION type with custom cycling in onActionSelected
menuItems.push_back(SettingInfo::Action(StrId::STR_AUTO_TURN_PAGES_PER_MIN, SettingAction::None));
// Appearance // --- Appearance ---
items.push_back(MenuItem::separator(StrId::STR_READER_APPEARANCE)); menuItems.push_back(SettingInfo::Separator(StrId::STR_READER_APPEARANCE));
items.push_back({MenuAction::EMBEDDED_STYLE, StrId::STR_EMBEDDED_STYLE});
items.push_back({MenuAction::IMAGE_RENDERING, StrId::STR_IMAGES});
items.push_back({MenuAction::TEXT_DARKNESS, StrId::STR_TEXT_DARKNESS});
items.push_back({MenuAction::ROTATE_SCREEN, StrId::STR_ORIENTATION});
// Synchronisation (only if credentials are set, to avoid confusion) // Embedded style: cycles default(-1) -> ON(1) -> OFF(0) via DynamicEnum indices 0/1/2
menuItems.push_back(SettingInfo::DynamicEnum(
StrId::STR_EMBEDDED_STYLE, {StrId::STR_DEFAULT_VALUE, StrId::STR_STATE_ON, StrId::STR_STATE_OFF},
[this]() -> uint8_t {
if (pendingEmbeddedStyleOverride < 0) return 0;
if (pendingEmbeddedStyleOverride > 0) return 1;
return 2;
},
[this](uint8_t v) {
if (v == 0)
pendingEmbeddedStyleOverride = -1;
else if (v == 1)
pendingEmbeddedStyleOverride = 1;
else
pendingEmbeddedStyleOverride = 0;
}));
// Image rendering: cycles default(-1) -> display(0) -> placeholder(1) -> suppress(2)
menuItems.push_back(SettingInfo::DynamicEnum(
StrId::STR_IMAGES,
{StrId::STR_DEFAULT_VALUE, StrId::STR_IMAGES_DISPLAY, StrId::STR_IMAGES_PLACEHOLDER,
StrId::STR_IMAGES_SUPPRESS},
[this]() -> uint8_t { return (pendingImageRenderingOverride < 0) ? 0 : (pendingImageRenderingOverride + 1); },
[this](uint8_t v) { pendingImageRenderingOverride = (v == 0) ? -1 : static_cast<int8_t>(v - 1); }));
// Text darkness: straightforward 0-3 cycle
menuItems.push_back(SettingInfo::DynamicEnum(
StrId::STR_TEXT_DARKNESS,
{StrId::STR_NORMAL, StrId::STR_DARK, StrId::STR_EXTRA_DARK, StrId::STR_MAX_DARK},
[this]() -> uint8_t { return pendingTextDarkness; },
[this](uint8_t v) { pendingTextDarkness = v; }));
// Orientation: straightforward 0-3 cycle
menuItems.push_back(SettingInfo::DynamicEnum(
StrId::STR_ORIENTATION,
{StrId::STR_PORTRAIT, StrId::STR_LANDSCAPE_CW, StrId::STR_INVERTED, StrId::STR_LANDSCAPE_CCW},
[this]() -> uint8_t { return pendingOrientation; },
[this](uint8_t v) { pendingOrientation = v; }));
// --- Synchronisation (only if credentials are set) ---
if (KOREADER_STORE.hasCredentials()) { if (KOREADER_STORE.hasCredentials()) {
items.push_back(MenuItem::separator(StrId::STR_KOREADER_SYNC)); menuItems.push_back(SettingInfo::Separator(StrId::STR_KOREADER_SYNC));
items.push_back({MenuAction::PULL_REMOTE, StrId::STR_PULL_PROGRESS_FROM_OTHER_DEVICES}); menuItems.push_back(SettingInfo::Action(StrId::STR_PULL_PROGRESS_FROM_OTHER_DEVICES, SettingAction::None));
items.push_back({MenuAction::PUSH_LOCAL, StrId::STR_PUSH_PROGRESS_FROM_THIS_DEVICE}); menuItems.push_back(SettingInfo::Action(StrId::STR_PUSH_PROGRESS_FROM_THIS_DEVICE, SettingAction::None));
} }
// Tools // --- Tools ---
items.push_back(MenuItem::separator(StrId::STR_READER_TOOLS)); menuItems.push_back(SettingInfo::Separator(StrId::STR_READER_TOOLS));
items.push_back({MenuAction::SCREENSHOT, StrId::STR_SCREENSHOT_BUTTON}); menuItems.push_back(SettingInfo::Action(StrId::STR_SCREENSHOT_BUTTON, SettingAction::None));
items.push_back({MenuAction::DISPLAY_QR, StrId::STR_DISPLAY_QR}); menuItems.push_back(SettingInfo::Action(StrId::STR_DISPLAY_QR, SettingAction::None));
items.push_back({MenuAction::DELETE_CACHE, StrId::STR_DELETE_CACHE}); menuItems.push_back(SettingInfo::Action(StrId::STR_DELETE_CACHE, SettingAction::None));
items.push_back({MenuAction::GO_HOME, StrId::STR_GO_HOME_BUTTON}); menuItems.push_back(SettingInfo::Action(StrId::STR_GO_HOME_BUTTON, SettingAction::None));
return items; }
EpubReaderMenuActivity::MenuAction EpubReaderMenuActivity::actionForNameId(StrId nameId) {
switch (nameId) {
case StrId::STR_SELECT_CHAPTER:
return MenuAction::SELECT_CHAPTER;
case StrId::STR_GO_TO_PERCENT:
return MenuAction::GO_TO_PERCENT;
case StrId::STR_FOOTNOTES:
return MenuAction::FOOTNOTES;
case StrId::STR_AUTO_TURN_PAGES_PER_MIN:
return MenuAction::AUTO_PAGE_TURN;
case StrId::STR_EMBEDDED_STYLE:
return MenuAction::EMBEDDED_STYLE;
case StrId::STR_IMAGES:
return MenuAction::IMAGE_RENDERING;
case StrId::STR_TEXT_DARKNESS:
return MenuAction::TEXT_DARKNESS;
case StrId::STR_ORIENTATION:
return MenuAction::ROTATE_SCREEN;
case StrId::STR_PULL_PROGRESS_FROM_OTHER_DEVICES:
return MenuAction::PULL_REMOTE;
case StrId::STR_PUSH_PROGRESS_FROM_THIS_DEVICE:
return MenuAction::PUSH_LOCAL;
case StrId::STR_SCREENSHOT_BUTTON:
return MenuAction::SCREENSHOT;
case StrId::STR_DISPLAY_QR:
return MenuAction::DISPLAY_QR;
case StrId::STR_DELETE_CACHE:
return MenuAction::DELETE_CACHE;
case StrId::STR_GO_HOME_BUTTON:
return MenuAction::GO_HOME;
default:
return MenuAction::NONE;
}
}
void EpubReaderMenuActivity::finishWithAction(MenuAction action) {
setResult(MenuResult{static_cast<int>(action), pendingOrientation, selectedPageTurnOption,
pendingEmbeddedStyleOverride, pendingImageRenderingOverride, pendingTextDarkness});
finish();
}
void EpubReaderMenuActivity::onActionSelected(int index) {
const auto& item = menuItems[index];
// Auto page turn cycles locally (not a DynamicEnum because labels are raw strings)
if (item.nameId == StrId::STR_AUTO_TURN_PAGES_PER_MIN) {
selectedPageTurnOption = (selectedPageTurnOption + 1) % 5;
requestUpdate();
return;
}
// All other ACTION items finish with a result
finishWithAction(actionForNameId(item.nameId));
}
void EpubReaderMenuActivity::onSettingToggled(int /*index*/) {
// DynamicEnum items update pending state via their setters — no persistence needed.
}
void EpubReaderMenuActivity::onBackPressed() {
ActivityResult result;
result.isCancelled = true;
result.data = MenuResult{-1, pendingOrientation, selectedPageTurnOption, pendingEmbeddedStyleOverride,
pendingImageRenderingOverride, pendingTextDarkness};
setResult(std::move(result));
finish();
}
std::string EpubReaderMenuActivity::getItemValueString(int index) const {
const auto& item = menuItems[index];
// Auto page turn: custom labels
if (item.nameId == StrId::STR_AUTO_TURN_PAGES_PER_MIN) {
if (selectedPageTurnOption == 0) return std::string(tr(STR_STATE_OFF));
return std::string(pageTurnLabels[selectedPageTurnOption]);
}
// Plain ACTION items (select chapter, screenshot, etc.) show no value
if (item.type == SettingType::ACTION) return {};
// DynamicEnum items use the standard display
return MenuListActivity::getItemValueString(index);
} }
void EpubReaderMenuActivity::onEnter() { void EpubReaderMenuActivity::onEnter() {
Activity::onEnter(); MenuListActivity::onEnter();
const auto pred = UITheme::makeSelectablePredicate(static_cast<int>(menuItems.size()),
[this](int i) { return menuItems[i].getTitle(); });
buttonNavigator.setSelectablePredicate(pred, static_cast<int>(menuItems.size()));
if (!pred(selectedIndex)) {
selectedIndex = buttonNavigator.nextIndex(selectedIndex);
}
requestUpdate();
}
void EpubReaderMenuActivity::onExit() { Activity::onExit(); }
void EpubReaderMenuActivity::loop() {
// Handle navigation
buttonNavigator.onNext([this] {
selectedIndex = buttonNavigator.nextIndex(selectedIndex);
requestUpdate();
});
buttonNavigator.onPrevious([this] {
selectedIndex = buttonNavigator.previousIndex(selectedIndex);
requestUpdate();
});
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
const auto selectedAction = menuItems[selectedIndex].action;
if (selectedAction == MenuAction::NONE) {
return;
}
if (selectedAction == MenuAction::ROTATE_SCREEN) {
// Cycle orientation preview locally; actual rotation happens on menu exit.
pendingOrientation = (pendingOrientation + 1) % orientationLabels.size();
requestUpdate();
return;
}
if (selectedAction == MenuAction::AUTO_PAGE_TURN) {
selectedPageTurnOption = (selectedPageTurnOption + 1) % pageTurnLabels.size();
requestUpdate();
return;
}
if (selectedAction == MenuAction::EMBEDDED_STYLE) {
// Cycle per-book override: default -> ON -> OFF -> default.
if (pendingEmbeddedStyleOverride < 0) {
pendingEmbeddedStyleOverride = 1;
} else if (pendingEmbeddedStyleOverride > 0) {
pendingEmbeddedStyleOverride = 0;
} else {
pendingEmbeddedStyleOverride = -1;
}
requestUpdate();
return;
}
if (selectedAction == MenuAction::IMAGE_RENDERING) {
// Cycle per-book override: default -> display -> placeholder -> suppress -> default.
if (pendingImageRenderingOverride < 0) {
pendingImageRenderingOverride = 0;
} else if (pendingImageRenderingOverride >= 2) {
pendingImageRenderingOverride = -1;
} else {
pendingImageRenderingOverride++;
}
requestUpdate();
return;
}
if (selectedAction == MenuAction::TEXT_DARKNESS) {
pendingTextDarkness = (pendingTextDarkness + 1) % textDarknessLabels.size();
requestUpdate();
return;
}
setResult(MenuResult{static_cast<int>(selectedAction), pendingOrientation, selectedPageTurnOption,
pendingEmbeddedStyleOverride, pendingImageRenderingOverride, pendingTextDarkness});
finish();
return;
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
ActivityResult result;
result.isCancelled = true;
result.data = MenuResult{-1,
pendingOrientation,
selectedPageTurnOption,
pendingEmbeddedStyleOverride,
pendingImageRenderingOverride,
pendingTextDarkness};
setResult(std::move(result));
finish();
return;
}
} }
void EpubReaderMenuActivity::render(RenderLock&&) { void EpubReaderMenuActivity::render(RenderLock&&) {
@@ -166,7 +191,6 @@ void EpubReaderMenuActivity::render(RenderLock&&) {
// Title // Title
const std::string truncTitle = const std::string truncTitle =
renderer.truncatedText(UI_12_FONT_ID, title.c_str(), contentRect.width - 40, EpdFontFamily::BOLD); renderer.truncatedText(UI_12_FONT_ID, title.c_str(), contentRect.width - 40, EpdFontFamily::BOLD);
// Manual centering so we can respect the content gutter.
const int titleX = const int titleX =
contentRect.x + contentRect.x +
(contentRect.width - renderer.getTextWidth(UI_12_FONT_ID, truncTitle.c_str(), EpdFontFamily::BOLD)) / 2; (contentRect.width - renderer.getTextWidth(UI_12_FONT_ID, truncTitle.c_str(), EpdFontFamily::BOLD)) / 2;
@@ -184,38 +208,7 @@ void EpubReaderMenuActivity::render(RenderLock&&) {
// Menu Items // Menu Items
const int startY = 75 + contentRect.y; const int startY = 75 + contentRect.y;
const int listHeight = contentRect.height - (startY - contentRect.y); const int listHeight = contentRect.height - (startY - contentRect.y);
drawMenuList(Rect{contentRect.x, startY, contentRect.width, listHeight});
GUI.drawList(
renderer, Rect{contentRect.x, startY, contentRect.width, listHeight}, static_cast<int>(menuItems.size()),
selectedIndex, [this](int index) { return menuItems[index].getTitle(); }, nullptr, nullptr,
[this](int index) {
const auto& item = menuItems[index];
switch (item.action) {
case MenuAction::ROTATE_SCREEN:
return std::string(I18N.get(orientationLabels[pendingOrientation]));
case MenuAction::AUTO_PAGE_TURN:
return std::string(pageTurnLabels[selectedPageTurnOption]);
case MenuAction::EMBEDDED_STYLE:
if (pendingEmbeddedStyleOverride == 1) {
return std::string(tr(STR_STATE_ON));
} else if (pendingEmbeddedStyleOverride == 0) {
return std::string(tr(STR_STATE_OFF));
}
return std::string(tr(STR_DEFAULT_VALUE));
case MenuAction::IMAGE_RENDERING:
if (pendingImageRenderingOverride >= 0 && pendingImageRenderingOverride < imageRenderingLabels.size()) {
return std::string(I18N.get(imageRenderingLabels[pendingImageRenderingOverride]));
}
return std::string(tr(STR_DEFAULT_VALUE));
case MenuAction::TEXT_DARKNESS: {
const uint8_t idx = (pendingTextDarkness < textDarknessLabels.size()) ? pendingTextDarkness : 0;
return std::string(I18N.get(textDarknessLabels[idx]));
}
default:
return std::string();
}
},
true);
// Footer / Hints // Footer / Hints
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
+18 -27
View File
@@ -5,12 +5,12 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include "../Activity.h" #include "../MenuListActivity.h"
#include "util/ButtonNavigator.h"
class EpubReaderMenuActivity final : public Activity { class EpubReaderMenuActivity final : public MenuListActivity {
public: public:
// Menu actions available from the reader menu. // Menu actions identified by StrId of the menu item.
// Used by the parent activity to interpret the result.
enum class MenuAction { enum class MenuAction {
NONE, NONE,
SELECT_CHAPTER, SELECT_CHAPTER,
@@ -36,40 +36,31 @@ class EpubReaderMenuActivity final : public Activity {
const uint8_t initialTextDarkness); const uint8_t initialTextDarkness);
void onEnter() override; void onEnter() override;
void onExit() override;
void loop() override;
void render(RenderLock&&) override; void render(RenderLock&&) override;
private: private:
struct MenuItem { void buildMenuItems(bool hasFootnotes);
MenuAction action; void finishWithAction(MenuAction action);
StrId labelId;
bool isSeparator = false;
static MenuItem separator(StrId label) { return {MenuAction::NONE, label, true}; }
[[nodiscard]] std::string getTitle() const;
};
static std::vector<MenuItem> buildMenuItems(bool hasFootnotes); // MenuListActivity overrides
std::string getItemValueString(int index) const override;
void onActionSelected(int index) override;
void onBackPressed() override;
void onSettingToggled(int index) override;
// Fixed menu layout // Map from StrId to MenuAction for result passing
const std::vector<MenuItem> menuItems; static MenuAction actionForNameId(StrId nameId);
int selectedIndex = 0; // Pending state (mutated locally, returned to parent on finish)
ButtonNavigator buttonNavigator;
std::string title = "Reader Menu";
uint8_t pendingOrientation = 0; uint8_t pendingOrientation = 0;
uint8_t selectedPageTurnOption = 0; uint8_t selectedPageTurnOption = 0;
int8_t pendingEmbeddedStyleOverride = -1; int8_t pendingEmbeddedStyleOverride = -1;
int8_t pendingImageRenderingOverride = -1; int8_t pendingImageRenderingOverride = -1;
uint8_t pendingTextDarkness = 1; uint8_t pendingTextDarkness = 1;
const std::vector<StrId> orientationLabels = {StrId::STR_PORTRAIT, StrId::STR_LANDSCAPE_CW, StrId::STR_INVERTED,
StrId::STR_LANDSCAPE_CCW}; static constexpr const char* pageTurnLabels[] = {"", "1", "3", "6", "12"};
const std::vector<StrId> textDarknessLabels = {StrId::STR_NORMAL, StrId::STR_DARK, StrId::STR_EXTRA_DARK,
StrId::STR_MAX_DARK}; std::string title = "Reader Menu";
const std::vector<StrId> imageRenderingLabels = {StrId::STR_IMAGES_DISPLAY, StrId::STR_IMAGES_PLACEHOLDER,
StrId::STR_IMAGES_SUPPRESS};
const std::vector<const char*> pageTurnLabels = {I18N.get(StrId::STR_STATE_OFF), "1", "3", "6", "12"};
int currentPage = 0; int currentPage = 0;
int totalPages = 0; int totalPages = 0;
int bookProgressPercent = 0; int bookProgressPercent = 0;
@@ -3,8 +3,6 @@
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <I18n.h> #include <I18n.h>
#include <cstring>
#include "KOReaderAuthActivity.h" #include "KOReaderAuthActivity.h"
#include "KOReaderCredentialStore.h" #include "KOReaderCredentialStore.h"
#include "MappedInputManager.h" #include "MappedInputManager.h"
@@ -12,51 +10,58 @@
#include "components/UITheme.h" #include "components/UITheme.h"
#include "fontIds.h" #include "fontIds.h"
namespace { KOReaderSettingsActivity::KOReaderSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
constexpr int MENU_ITEMS = 6; : MenuListActivity("KOReaderSettings", renderer, mappedInput) {
const StrId menuNames[MENU_ITEMS] = {StrId::STR_USERNAME, StrId::STR_PASSWORD, StrId::STR_SYNC_SERVER_URL, buildMenuItems();
StrId::STR_DOCUMENT_MATCHING, StrId::STR_AUTHENTICATE, StrId::STR_REGISTER};
} // namespace
void KOReaderSettingsActivity::onEnter() {
Activity::onEnter();
selectedIndex = 0;
requestUpdate();
} }
void KOReaderSettingsActivity::onExit() { Activity::onExit(); } void KOReaderSettingsActivity::buildMenuItems() {
// Username, Password, Server URL: ACTION items with custom value display
menuItems.push_back(SettingInfo::Action(StrId::STR_USERNAME, SettingAction::None));
menuItems.push_back(SettingInfo::Action(StrId::STR_PASSWORD, SettingAction::None));
menuItems.push_back(SettingInfo::Action(StrId::STR_SYNC_SERVER_URL, SettingAction::None));
void KOReaderSettingsActivity::loop() { // Document matching: DynamicEnum toggling between Filename and Binary
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { menuItems.push_back(SettingInfo::DynamicEnum(
finish(); StrId::STR_DOCUMENT_MATCHING, {StrId::STR_FILENAME, StrId::STR_BINARY},
return; [] { return static_cast<uint8_t>(KOREADER_STORE.getMatchMethod()); },
} [](uint8_t v) {
KOREADER_STORE.setMatchMethod(static_cast<DocumentMatchMethod>(v));
KOREADER_STORE.saveToFile();
}));
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { // Authenticate and Register: ACTION items
handleSelection(); menuItems.push_back(SettingInfo::Action(StrId::STR_AUTHENTICATE, SettingAction::None));
return; menuItems.push_back(SettingInfo::Action(StrId::STR_REGISTER, SettingAction::None));
}
// Handle navigation
buttonNavigator.onNext([this] {
selectedIndex = (selectedIndex + 1) % MENU_ITEMS;
requestUpdate();
});
buttonNavigator.onPrevious([this] {
selectedIndex = (selectedIndex + MENU_ITEMS - 1) % MENU_ITEMS;
requestUpdate();
});
} }
void KOReaderSettingsActivity::handleSelection() { std::string KOReaderSettingsActivity::getItemValueString(int index) const {
if (selectedIndex == 0) { const auto& item = menuItems[index];
// Username
if (item.nameId == StrId::STR_USERNAME) {
auto username = KOREADER_STORE.getUsername();
return username.empty() ? std::string(tr(STR_NOT_SET)) : username;
}
if (item.nameId == StrId::STR_PASSWORD) {
return KOREADER_STORE.getPassword().empty() ? std::string(tr(STR_NOT_SET)) : std::string("******");
}
if (item.nameId == StrId::STR_SYNC_SERVER_URL) {
auto serverUrl = KOREADER_STORE.getServerUrl();
return serverUrl.empty() ? std::string(tr(STR_DEFAULT_VALUE)) : serverUrl;
}
if (item.nameId == StrId::STR_AUTHENTICATE || item.nameId == StrId::STR_REGISTER) {
return KOREADER_STORE.hasCredentials() ? "" : std::string("[") + tr(STR_SET_CREDENTIALS_FIRST) + "]";
}
return MenuListActivity::getItemValueString(index);
}
void KOReaderSettingsActivity::onActionSelected(int index) {
const auto& item = menuItems[index];
if (item.nameId == StrId::STR_USERNAME) {
startActivityForResult(std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_KOREADER_USERNAME), startActivityForResult(std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_KOREADER_USERNAME),
KOREADER_STORE.getUsername(), KOREADER_STORE.getUsername(), 64, false),
64, // maxLength
false), // not password
[this](const ActivityResult& result) { [this](const ActivityResult& result) {
if (!result.isCancelled) { if (!result.isCancelled) {
const auto& kb = std::get<KeyboardResult>(result.data); const auto& kb = std::get<KeyboardResult>(result.data);
@@ -64,12 +69,9 @@ void KOReaderSettingsActivity::handleSelection() {
KOREADER_STORE.saveToFile(); KOREADER_STORE.saveToFile();
} }
}); });
} else if (selectedIndex == 1) { } else if (item.nameId == StrId::STR_PASSWORD) {
// Password
startActivityForResult(std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_KOREADER_PASSWORD), startActivityForResult(std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_KOREADER_PASSWORD),
KOREADER_STORE.getPassword(), KOREADER_STORE.getPassword(), 64, false),
64, // maxLength
false), // show characters
[this](const ActivityResult& result) { [this](const ActivityResult& result) {
if (!result.isCancelled) { if (!result.isCancelled) {
const auto& kb = std::get<KeyboardResult>(result.data); const auto& kb = std::get<KeyboardResult>(result.data);
@@ -77,14 +79,11 @@ void KOReaderSettingsActivity::handleSelection() {
KOREADER_STORE.saveToFile(); KOREADER_STORE.saveToFile();
} }
}); });
} else if (selectedIndex == 2) { } else if (item.nameId == StrId::STR_SYNC_SERVER_URL) {
// Sync Server URL - prefill with https:// if empty to save typing
const std::string currentUrl = KOREADER_STORE.getServerUrl(); const std::string currentUrl = KOREADER_STORE.getServerUrl();
const std::string prefillUrl = currentUrl.empty() ? "https://" : currentUrl; const std::string prefillUrl = currentUrl.empty() ? "https://" : currentUrl;
startActivityForResult( startActivityForResult(
std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_SYNC_SERVER_URL), prefillUrl, std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_SYNC_SERVER_URL), prefillUrl, 128, false),
128, // maxLength - URLs can be long
false), // not password
[this](const ActivityResult& result) { [this](const ActivityResult& result) {
if (!result.isCancelled) { if (!result.isCancelled) {
const auto& kb = std::get<KeyboardResult>(result.data); const auto& kb = std::get<KeyboardResult>(result.data);
@@ -93,28 +92,13 @@ void KOReaderSettingsActivity::handleSelection() {
KOREADER_STORE.saveToFile(); KOREADER_STORE.saveToFile();
} }
}); });
} else if (selectedIndex == 3) { } else if (item.nameId == StrId::STR_AUTHENTICATE) {
// Document Matching - toggle between Filename and Binary if (!KOREADER_STORE.hasCredentials()) return;
const auto current = KOREADER_STORE.getMatchMethod();
const auto newMethod =
(current == DocumentMatchMethod::FILENAME) ? DocumentMatchMethod::BINARY : DocumentMatchMethod::FILENAME;
KOREADER_STORE.setMatchMethod(newMethod);
KOREADER_STORE.saveToFile();
requestUpdate();
} else if (selectedIndex == 4) {
// Authenticate
if (!KOREADER_STORE.hasCredentials()) {
// Can't authenticate without credentials - just show message briefly
return;
}
startActivityForResult( startActivityForResult(
std::make_unique<KOReaderAuthActivity>(renderer, mappedInput, KOReaderAuthActivity::Mode::LOGIN), std::make_unique<KOReaderAuthActivity>(renderer, mappedInput, KOReaderAuthActivity::Mode::LOGIN),
[](const ActivityResult&) {}); [](const ActivityResult&) {});
} else if (selectedIndex == 5) { } else if (item.nameId == StrId::STR_REGISTER) {
// Register if (!KOREADER_STORE.hasCredentials()) return;
if (!KOREADER_STORE.hasCredentials()) {
return;
}
startActivityForResult( startActivityForResult(
std::make_unique<KOReaderAuthActivity>(renderer, mappedInput, KOReaderAuthActivity::Mode::REGISTER), std::make_unique<KOReaderAuthActivity>(renderer, mappedInput, KOReaderAuthActivity::Mode::REGISTER),
[](const ActivityResult&) {}); [](const ActivityResult&) {});
@@ -132,33 +116,8 @@ void KOReaderSettingsActivity::render(RenderLock&&) {
const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing;
const int contentHeight = contentRect.height - contentTop - metrics.verticalSpacing * 2; const int contentHeight = contentRect.height - contentTop - metrics.verticalSpacing * 2;
GUI.drawList( drawMenuList(Rect{contentRect.x, contentTop, contentRect.width, contentHeight});
renderer, Rect{contentRect.x, contentTop, contentRect.width, contentHeight}, static_cast<int>(MENU_ITEMS),
static_cast<int>(selectedIndex), [](int index) { return std::string(I18N.get(menuNames[index])); }, nullptr,
nullptr,
[this](int index) {
// Draw status for each setting
if (index == 0) {
auto username = KOREADER_STORE.getUsername();
return username.empty() ? std::string(tr(STR_NOT_SET)) : username;
} else if (index == 1) {
return KOREADER_STORE.getPassword().empty() ? std::string(tr(STR_NOT_SET)) : std::string("******");
} else if (index == 2) {
auto serverUrl = KOREADER_STORE.getServerUrl();
return serverUrl.empty() ? std::string(tr(STR_DEFAULT_VALUE)) : serverUrl;
} else if (index == 3) {
return KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME ? std::string(tr(STR_FILENAME))
: std::string(tr(STR_BINARY));
} else if (index == 4) {
return KOREADER_STORE.hasCredentials() ? "" : std::string("[") + tr(STR_SET_CREDENTIALS_FIRST) + "]";
} else if (index == 5) {
return KOREADER_STORE.hasCredentials() ? "" : std::string("[") + tr(STR_SET_CREDENTIALS_FIRST) + "]";
}
return std::string(tr(STR_NOT_SET));
},
true);
// Draw help text at bottom
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); 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); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
@@ -1,26 +1,21 @@
#pragma once #pragma once
#include "activities/Activity.h" #include "activities/MenuListActivity.h"
#include "util/ButtonNavigator.h"
/** /**
* Submenu for KOReader Sync settings. * Submenu for KOReader Sync settings.
* Shows username, password, and authenticate options. * Shows username, password, server URL, document matching, authenticate, and register.
*/ */
class KOReaderSettingsActivity final : public Activity { class KOReaderSettingsActivity final : public MenuListActivity {
public: public:
explicit KOReaderSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) explicit KOReaderSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput);
: Activity("KOReaderSettings", renderer, mappedInput) {}
void onEnter() override;
void onExit() override;
void loop() override;
void render(RenderLock&&) override; void render(RenderLock&&) override;
private: private:
ButtonNavigator buttonNavigator; void buildMenuItems();
size_t selectedIndex = 0; // MenuListActivity overrides
std::string getItemValueString(int index) const override;
void handleSelection(); void onActionSelected(int index) override;
}; };
@@ -0,0 +1,51 @@
#include "SettingActionDispatch.h"
#include "ButtonRemapActivity.h"
#include "CalibreSettingsActivity.h"
#include "ClearCacheActivity.h"
#include "ClockSettingsActivity.h"
#include "DetectTimezoneActivity.h"
#include "KOReaderSettingsActivity.h"
#include "LanguageSelectActivity.h"
#include "OtaUpdateActivity.h"
#include "StatusBarSettingsActivity.h"
#include "SyncTimeActivity.h"
#include "SystemInformationActivity.h"
#include "activities/network/WifiSelectionActivity.h"
#include "activities/weather/WeatherSettingsActivity.h"
std::unique_ptr<Activity> createActivityForAction(SettingAction action, GfxRenderer& renderer,
MappedInputManager& mappedInput) {
switch (action) {
case SettingAction::RemapFrontButtons:
return std::make_unique<ButtonRemapActivity>(renderer, mappedInput);
case SettingAction::CustomiseStatusBar:
return std::make_unique<StatusBarSettingsActivity>(renderer, mappedInput);
case SettingAction::ClockSettings:
return std::make_unique<ClockSettingsActivity>(renderer, mappedInput);
case SettingAction::KOReaderSync:
return std::make_unique<KOReaderSettingsActivity>(renderer, mappedInput);
case SettingAction::OPDSBrowser:
return std::make_unique<CalibreSettingsActivity>(renderer, mappedInput);
case SettingAction::Network:
return std::make_unique<WifiSelectionActivity>(renderer, mappedInput, false);
case SettingAction::ClearCache:
return std::make_unique<ClearCacheActivity>(renderer, mappedInput);
case SettingAction::CheckForUpdates:
return std::make_unique<OtaUpdateActivity>(renderer, mappedInput);
case SettingAction::Language:
return std::make_unique<LanguageSelectActivity>(renderer, mappedInput);
case SettingAction::Weather:
return std::make_unique<WeatherSettingsActivity>(renderer, mappedInput);
case SettingAction::SystemInfo:
return std::make_unique<SystemInformationActivity>(renderer, mappedInput);
case SettingAction::SyncTime:
return std::make_unique<SyncTimeActivity>(renderer, mappedInput);
case SettingAction::DetectTimezone:
return std::make_unique<DetectTimezoneActivity>(renderer, mappedInput);
case SettingAction::Submenu:
case SettingAction::None:
return nullptr;
}
return nullptr;
}
@@ -0,0 +1,13 @@
#pragma once
#include <memory>
#include "SettingInfo.h"
class Activity;
class GfxRenderer;
class MappedInputManager;
// Creates the sub-activity corresponding to the given SettingAction.
// Returns nullptr for None, Submenu, or unknown actions (caller handles those).
std::unique_ptr<Activity> createActivityForAction(SettingAction action, GfxRenderer& renderer,
MappedInputManager& mappedInput);
+85
View File
@@ -0,0 +1,85 @@
#include "SettingInfo.h"
#include <I18n.h>
#include "CrossPointSettings.h"
#include "components/UITheme.h"
std::string SettingInfo::getTitle() const {
const auto t = I18N.get(nameId);
return isSeparator ? UITheme::makeSeparatorTitle(t) : t;
}
std::string SettingInfo::getDisplayValue() const {
if (isSeparator) return {};
switch (type) {
case SettingType::TOGGLE: {
bool value;
if (valuePtr)
value = SETTINGS.*(valuePtr);
else if (valueGetter)
value = valueGetter();
else
return {};
return std::string(value ? tr(STR_STATE_ON) : tr(STR_STATE_OFF));
}
case SettingType::ENUM: {
uint8_t value;
if (valuePtr)
value = SETTINGS.*(valuePtr);
else if (valueGetter)
value = valueGetter();
else
return {};
if (value < enumValues.size()) return std::string(I18N.get(enumValues[value]));
return {};
}
case SettingType::VALUE: {
if (valuePtr) return std::to_string(SETTINGS.*(valuePtr));
if (valueGetter) return std::to_string(valueGetter());
return {};
}
case SettingType::ACTION:
return std::string(">>");
case SettingType::STRING:
return {};
}
return {};
}
void SettingInfo::toggleValue() const {
if (isSeparator) return;
switch (type) {
case SettingType::TOGGLE:
if (valuePtr) {
SETTINGS.*(valuePtr) = !(SETTINGS.*(valuePtr));
} else if (valueGetter && valueSetter) {
valueSetter(!valueGetter());
}
break;
case SettingType::ENUM: {
const auto count = static_cast<uint8_t>(enumValues.size());
if (count == 0) break;
if (valuePtr) {
SETTINGS.*(valuePtr) = (SETTINGS.*(valuePtr) + 1) % count;
} else if (valueGetter && valueSetter) {
valueSetter((valueGetter() + 1) % count);
}
break;
}
case SettingType::VALUE:
if (valuePtr) {
const auto current = static_cast<int8_t>(SETTINGS.*(valuePtr));
SETTINGS.*(valuePtr) =
(current + valueRange.step > valueRange.max) ? valueRange.min : current + valueRange.step;
}
break;
default:
break;
}
}
+190
View File
@@ -0,0 +1,190 @@
#pragma once
#include <I18n.h>
#include <functional>
#include <string>
#include <vector>
#include "CrossPointSettings.h"
enum class SettingType { TOGGLE, ENUM, ACTION, VALUE, STRING };
enum class SettingAction {
None,
RemapFrontButtons,
CustomiseStatusBar,
ClockSettings,
KOReaderSync,
OPDSBrowser,
Network,
ClearCache,
CheckForUpdates,
Language,
SystemInfo,
DetectTimezone,
SyncTime,
Weather,
Submenu,
};
struct SettingInfo {
StrId nameId;
SettingType type;
uint8_t CrossPointSettings::* valuePtr = nullptr;
std::vector<StrId> enumValues;
SettingAction action = SettingAction::None;
struct ValueRange {
uint8_t min;
uint8_t max;
uint8_t step;
};
ValueRange valueRange = {};
const char* key = nullptr; // JSON API key (nullptr for ACTION types)
StrId category = StrId::STR_NONE_OPT; // Category for web UI grouping
bool obfuscated = false; // Save/load via base64 obfuscation (passwords)
// Direct char[] string fields (for settings stored in CrossPointSettings)
size_t stringOffset = 0;
size_t stringMaxLen = 0;
// Dynamic accessors (for settings stored outside CrossPointSettings, e.g. KOReaderCredentialStore)
std::function<uint8_t()> valueGetter;
std::function<void(uint8_t)> valueSetter;
std::function<std::string()> stringGetter;
std::function<void(const std::string&)> stringSetter;
SettingInfo& withObfuscated() {
obfuscated = true;
return *this;
}
static SettingInfo Toggle(StrId nameId, uint8_t CrossPointSettings::* ptr, const char* key = nullptr,
StrId category = StrId::STR_NONE_OPT) {
SettingInfo s;
s.nameId = nameId;
s.type = SettingType::TOGGLE;
s.valuePtr = ptr;
s.key = key;
s.category = category;
return s;
}
static SettingInfo Enum(StrId nameId, uint8_t CrossPointSettings::* ptr, std::vector<StrId> values,
const char* key = nullptr, StrId category = StrId::STR_NONE_OPT) {
SettingInfo s;
s.nameId = nameId;
s.type = SettingType::ENUM;
s.valuePtr = ptr;
s.enumValues = std::move(values);
s.key = key;
s.category = category;
return s;
}
static SettingInfo Action(StrId nameId, SettingAction action) {
SettingInfo s;
s.nameId = nameId;
s.type = SettingType::ACTION;
s.action = action;
return s;
}
static SettingInfo Value(StrId nameId, uint8_t CrossPointSettings::* ptr, const ValueRange valueRange,
const char* key = nullptr, StrId category = StrId::STR_NONE_OPT) {
SettingInfo s;
s.nameId = nameId;
s.type = SettingType::VALUE;
s.valuePtr = ptr;
s.valueRange = valueRange;
s.key = key;
s.category = category;
return s;
}
static SettingInfo String(StrId nameId, char* ptr, size_t maxLen, const char* key = nullptr,
StrId category = StrId::STR_NONE_OPT) {
SettingInfo s;
s.nameId = nameId;
s.type = SettingType::STRING;
s.stringOffset = (size_t)ptr - (size_t)&SETTINGS;
s.stringMaxLen = maxLen;
s.key = key;
s.category = category;
return s;
}
static SettingInfo DynamicEnum(StrId nameId, std::vector<StrId> values, std::function<uint8_t()> getter,
std::function<void(uint8_t)> setter, const char* key = nullptr,
StrId category = StrId::STR_NONE_OPT) {
SettingInfo s;
s.nameId = nameId;
s.type = SettingType::ENUM;
s.enumValues = std::move(values);
s.valueGetter = std::move(getter);
s.valueSetter = std::move(setter);
s.key = key;
s.category = category;
return s;
}
static SettingInfo DynamicString(StrId nameId, std::function<std::string()> getter,
std::function<void(const std::string&)> setter, const char* key = nullptr,
StrId category = StrId::STR_NONE_OPT) {
SettingInfo s;
s.nameId = nameId;
s.type = SettingType::STRING;
s.stringGetter = std::move(getter);
s.stringSetter = std::move(setter);
s.key = key;
s.category = category;
return s;
}
static SettingInfo Separator(StrId nameId) {
SettingInfo s;
s.nameId = nameId;
s.type = SettingType::ACTION;
s.isSeparator = true;
return s;
}
bool isSeparator = false;
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
// 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;
}
// Returns the localised title; separators are decorated automatically.
[[nodiscard]] std::string getTitle() const;
// Returns the current display value string (ON/OFF for toggles, enum label, numeric value, >>).
[[nodiscard]] std::string getDisplayValue() const;
// Toggles/cycles the underlying value for TOGGLE, ENUM, and VALUE types.
// Does nothing for ACTION and STRING types (callers handle those separately).
// Marked const because it mutates the external SETTINGS global (via valuePtr),
// not the SettingInfo itself.
void toggleValue() const;
};
+13 -105
View File
@@ -4,34 +4,17 @@
#include <HalClock.h> #include <HalClock.h>
#include <Logging.h> #include <Logging.h>
#include "ButtonRemapActivity.h"
#include "CalibreSettingsActivity.h"
#include "ClearCacheActivity.h"
#include "ClockSettingsActivity.h"
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
#include "DetectTimezoneActivity.h"
#include "KOReaderSettingsActivity.h"
#include "LanguageSelectActivity.h"
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include "OtaUpdateActivity.h" #include "SettingActionDispatch.h"
#include "SettingsList.h" #include "SettingsList.h"
#include "SettingsSubmenuActivity.h" #include "SettingsSubmenuActivity.h"
#include "StatusBarSettingsActivity.h"
#include "SyncTimeActivity.h"
#include "SystemInformationActivity.h"
#include "activities/network/WifiSelectionActivity.h"
#include "activities/weather/WeatherSettingsActivity.h"
#include "components/UITheme.h" #include "components/UITheme.h"
#include "fontIds.h" #include "fontIds.h"
const StrId SettingsActivity::categoryNames[categoryCount] = {StrId::STR_CAT_DISPLAY, StrId::STR_CAT_READER, const StrId SettingsActivity::categoryNames[categoryCount] = {StrId::STR_CAT_DISPLAY, StrId::STR_CAT_READER,
StrId::STR_CAT_CONTROLS, StrId::STR_CAT_SYSTEM}; StrId::STR_CAT_CONTROLS, StrId::STR_CAT_SYSTEM};
std::string SettingInfo::getTitle() const {
const auto t = I18N.get(nameId);
return isSeparator ? UITheme::makeSeparatorTitle(t) : t;
}
bool SettingsActivity::isListItemSelectable(int settingIdx) const { bool SettingsActivity::isListItemSelectable(int settingIdx) const {
return settingIdx >= 0 && settingIdx < settingsCount && !(*currentSettings)[settingIdx].isSeparator; return settingIdx >= 0 && settingIdx < settingsCount && !(*currentSettings)[settingIdx].isSeparator;
} }
@@ -225,82 +208,24 @@ void SettingsActivity::toggleCurrentSetting() {
const auto& setting = (*currentSettings)[selectedSetting]; const auto& setting = (*currentSettings)[selectedSetting];
if (setting.isSeparator) return; if (setting.isSeparator) return;
if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) { if (setting.type == SettingType::ACTION) {
// Toggle the boolean value using the member pointer
const bool currentValue = SETTINGS.*(setting.valuePtr);
SETTINGS.*(setting.valuePtr) = !currentValue;
} else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) {
const uint8_t currentValue = SETTINGS.*(setting.valuePtr);
SETTINGS.*(setting.valuePtr) = (currentValue + 1) % static_cast<uint8_t>(setting.enumValues.size());
} else if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) {
const int8_t currentValue = SETTINGS.*(setting.valuePtr);
if (currentValue + setting.valueRange.step > setting.valueRange.max) {
SETTINGS.*(setting.valuePtr) = setting.valueRange.min;
} else {
SETTINGS.*(setting.valuePtr) = currentValue + setting.valueRange.step;
}
} else if (setting.type == SettingType::ACTION) {
auto resultHandler = [this](const ActivityResult&) { SETTINGS.saveToFile(); }; auto resultHandler = [this](const ActivityResult&) { SETTINGS.saveToFile(); };
switch (setting.action) { if (setting.action == SettingAction::Submenu) {
case SettingAction::RemapFrontButtons: const auto it = std::find_if(submenuData.cbegin(), submenuData.cend(),
startActivityForResult(std::make_unique<ButtonRemapActivity>(renderer, mappedInput), resultHandler); [&setting](const SubmenuData& d) { return d.id == setting.nameId; });
break; if (it != submenuData.cend()) {
case SettingAction::CustomiseStatusBar: startActivityForResult(
startActivityForResult(std::make_unique<StatusBarSettingsActivity>(renderer, mappedInput), resultHandler); std::make_unique<SettingsSubmenuActivity>(renderer, mappedInput, setting.nameId, it->items), resultHandler);
break;
case SettingAction::ClockSettings:
startActivityForResult(std::make_unique<ClockSettingsActivity>(renderer, mappedInput), resultHandler);
break;
case SettingAction::KOReaderSync:
startActivityForResult(std::make_unique<KOReaderSettingsActivity>(renderer, mappedInput), resultHandler);
break;
case SettingAction::OPDSBrowser:
startActivityForResult(std::make_unique<CalibreSettingsActivity>(renderer, mappedInput), resultHandler);
break;
case SettingAction::Network:
startActivityForResult(std::make_unique<WifiSelectionActivity>(renderer, mappedInput, false), resultHandler);
break;
case SettingAction::ClearCache:
startActivityForResult(std::make_unique<ClearCacheActivity>(renderer, mappedInput), resultHandler);
break;
case SettingAction::CheckForUpdates:
startActivityForResult(std::make_unique<OtaUpdateActivity>(renderer, mappedInput), resultHandler);
break;
case SettingAction::Language:
startActivityForResult(std::make_unique<LanguageSelectActivity>(renderer, mappedInput), resultHandler);
break;
case SettingAction::Weather:
startActivityForResult(std::make_unique<WeatherSettingsActivity>(renderer, mappedInput), resultHandler);
break;
case SettingAction::SystemInfo:
startActivityForResult(std::make_unique<SystemInformationActivity>(renderer, mappedInput), resultHandler);
break;
case SettingAction::SyncTime:
startActivityForResult(std::make_unique<SyncTimeActivity>(renderer, mappedInput), resultHandler);
break;
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: } else {
// Do nothing auto activity = createActivityForAction(setting.action, renderer, mappedInput);
break; if (activity) startActivityForResult(std::move(activity), resultHandler);
} }
return; // Results will be handled in the result handler, so we can return early here
} else {
return; return;
} }
setting.toggleValue();
SETTINGS.saveToFile(); SETTINGS.saveToFile();
} }
@@ -331,24 +256,7 @@ void SettingsActivity::render(RenderLock&&) {
(metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing * 2)}, (metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing * 2)},
settingsCount, selectedSettingIndex - 1, [&settings](int index) { return settings[index].getTitle(); }, nullptr, settingsCount, selectedSettingIndex - 1, [&settings](int index) { return settings[index].getTitle(); }, nullptr,
nullptr, nullptr,
[&settings](int i) { [&settings](int i) { return settings[i].getDisplayValue(); },
const auto& setting = settings[i];
if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) {
const bool value = SETTINGS.*(setting.valuePtr);
return std::string(value ? tr(STR_STATE_ON) : tr(STR_STATE_OFF));
}
if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) {
const uint8_t value = SETTINGS.*(setting.valuePtr);
return std::string(I18N.get(setting.enumValues[value]));
}
if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) {
return std::to_string(SETTINGS.*(setting.valuePtr));
}
if (setting.type == SettingType::ACTION && !setting.isSeparator) {
return std::string(">>");
}
return std::string();
},
true); true);
// Draw help text // Draw help text
+1 -174
View File
@@ -1,185 +1,12 @@
#pragma once #pragma once
#include <I18n.h> #include <I18n.h>
#include <functional>
#include <string>
#include <vector> #include <vector>
#include "CrossPointSettings.h" #include "SettingInfo.h"
#include "activities/Activity.h" #include "activities/Activity.h"
#include "util/ButtonNavigator.h" #include "util/ButtonNavigator.h"
enum class SettingType { TOGGLE, ENUM, ACTION, VALUE, STRING };
enum class SettingAction {
None,
RemapFrontButtons,
CustomiseStatusBar,
ClockSettings,
KOReaderSync,
OPDSBrowser,
Network,
ClearCache,
CheckForUpdates,
Language,
SystemInfo,
DetectTimezone,
SyncTime,
Weather,
Submenu,
};
struct SettingInfo {
StrId nameId;
SettingType type;
uint8_t CrossPointSettings::* valuePtr = nullptr;
std::vector<StrId> enumValues;
SettingAction action = SettingAction::None;
struct ValueRange {
uint8_t min;
uint8_t max;
uint8_t step;
};
ValueRange valueRange = {};
const char* key = nullptr; // JSON API key (nullptr for ACTION types)
StrId category = StrId::STR_NONE_OPT; // Category for web UI grouping
bool obfuscated = false; // Save/load via base64 obfuscation (passwords)
// Direct char[] string fields (for settings stored in CrossPointSettings)
size_t stringOffset = 0;
size_t stringMaxLen = 0;
// Dynamic accessors (for settings stored outside CrossPointSettings, e.g. KOReaderCredentialStore)
std::function<uint8_t()> valueGetter;
std::function<void(uint8_t)> valueSetter;
std::function<std::string()> stringGetter;
std::function<void(const std::string&)> stringSetter;
SettingInfo& withObfuscated() {
obfuscated = true;
return *this;
}
static SettingInfo Toggle(StrId nameId, uint8_t CrossPointSettings::* ptr, const char* key = nullptr,
StrId category = StrId::STR_NONE_OPT) {
SettingInfo s;
s.nameId = nameId;
s.type = SettingType::TOGGLE;
s.valuePtr = ptr;
s.key = key;
s.category = category;
return s;
}
static SettingInfo Enum(StrId nameId, uint8_t CrossPointSettings::* ptr, std::vector<StrId> values,
const char* key = nullptr, StrId category = StrId::STR_NONE_OPT) {
SettingInfo s;
s.nameId = nameId;
s.type = SettingType::ENUM;
s.valuePtr = ptr;
s.enumValues = std::move(values);
s.key = key;
s.category = category;
return s;
}
static SettingInfo Action(StrId nameId, SettingAction action) {
SettingInfo s;
s.nameId = nameId;
s.type = SettingType::ACTION;
s.action = action;
return s;
}
static SettingInfo Value(StrId nameId, uint8_t CrossPointSettings::* ptr, const ValueRange valueRange,
const char* key = nullptr, StrId category = StrId::STR_NONE_OPT) {
SettingInfo s;
s.nameId = nameId;
s.type = SettingType::VALUE;
s.valuePtr = ptr;
s.valueRange = valueRange;
s.key = key;
s.category = category;
return s;
}
static SettingInfo String(StrId nameId, char* ptr, size_t maxLen, const char* key = nullptr,
StrId category = StrId::STR_NONE_OPT) {
SettingInfo s;
s.nameId = nameId;
s.type = SettingType::STRING;
s.stringOffset = (size_t)ptr - (size_t)&SETTINGS;
s.stringMaxLen = maxLen;
s.key = key;
s.category = category;
return s;
}
static SettingInfo DynamicEnum(StrId nameId, std::vector<StrId> values, std::function<uint8_t()> getter,
std::function<void(uint8_t)> setter, const char* key = nullptr,
StrId category = StrId::STR_NONE_OPT) {
SettingInfo s;
s.nameId = nameId;
s.type = SettingType::ENUM;
s.enumValues = std::move(values);
s.valueGetter = std::move(getter);
s.valueSetter = std::move(setter);
s.key = key;
s.category = category;
return s;
}
static SettingInfo DynamicString(StrId nameId, std::function<std::string()> getter,
std::function<void(const std::string&)> setter, const char* key = nullptr,
StrId category = StrId::STR_NONE_OPT) {
SettingInfo s;
s.nameId = nameId;
s.type = SettingType::STRING;
s.stringGetter = std::move(getter);
s.stringSetter = std::move(setter);
s.key = key;
s.category = category;
return s;
}
static SettingInfo Separator(StrId nameId) {
SettingInfo s;
s.nameId = nameId;
s.type = SettingType::ACTION;
s.isSeparator = true;
return s;
}
bool isSeparator = false;
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 { class SettingsActivity final : public Activity {
ButtonNavigator buttonNavigator; ButtonNavigator buttonNavigator;
@@ -3,123 +3,21 @@
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <I18n.h> #include <I18n.h>
#include "ButtonRemapActivity.h"
#include "CalibreSettingsActivity.h"
#include "ClearCacheActivity.h"
#include "ClockSettingsActivity.h"
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
#include "KOReaderSettingsActivity.h"
#include "LanguageSelectActivity.h"
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include "OtaUpdateActivity.h" #include "SettingActionDispatch.h"
#include "StatusBarSettingsActivity.h"
#include "SystemInformationActivity.h"
#include "activities/network/WifiSelectionActivity.h"
#include "activities/weather/WeatherSettingsActivity.h"
#include "components/UITheme.h" #include "components/UITheme.h"
#include "fontIds.h" #include "fontIds.h"
void SettingsSubmenuActivity::onEnter() { void SettingsSubmenuActivity::onActionSelected(int index) {
Activity::onEnter(); const auto& setting = menuItems[index];
itemCount = static_cast<int>(items.size()); auto resultHandler = [this](const ActivityResult&) { SETTINGS.saveToFile(); };
const auto pred = UITheme::makeSelectablePredicate(itemCount, [this](int i) { return items[i].getTitle(); });
buttonNavigator.setSelectablePredicate(pred, itemCount); auto activity = createActivityForAction(setting.action, renderer, mappedInput);
if (!pred(selectedIndex)) { if (activity) startActivityForResult(std::move(activity), resultHandler);
selectedIndex = buttonNavigator.nextIndex(selectedIndex);
}
requestUpdate();
} }
void SettingsSubmenuActivity::onExit() { Activity::onExit(); } void SettingsSubmenuActivity::onSettingToggled(int /*index*/) { SETTINGS.saveToFile(); }
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&&) { void SettingsSubmenuActivity::render(RenderLock&&) {
renderer.clearScreen(); renderer.clearScreen();
@@ -132,30 +30,7 @@ void SettingsSubmenuActivity::render(RenderLock&&) {
const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing;
const int contentHeight = contentRect.height - contentTop - metrics.verticalSpacing; const int contentHeight = contentRect.height - contentTop - metrics.verticalSpacing;
drawMenuList(Rect{contentRect.x, contentTop, contentRect.width, contentHeight});
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));
}
if (setting.type == SettingType::ACTION && !setting.isSeparator) {
return std::string(">>");
}
return std::string();
},
true);
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); 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); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
@@ -3,28 +3,24 @@
#include <vector> #include <vector>
#include "activities/Activity.h" #include "SettingInfo.h"
#include "activities/settings/SettingsActivity.h" #include "activities/MenuListActivity.h"
#include "util/ButtonNavigator.h"
// Displays a flat list of SettingInfo items launched from a SettingsActivity submenu entry. // Displays a flat list of SettingInfo items launched from a SettingsActivity submenu entry.
// Supports subcategory separators (withSubcategory) exactly as the parent settings tabs do. // Supports subcategory separators (withSubcategory) exactly as the parent settings tabs do.
class SettingsSubmenuActivity final : public Activity { class SettingsSubmenuActivity final : public MenuListActivity {
StrId titleId; StrId titleId;
std::vector<SettingInfo> items;
int selectedIndex = 0;
int itemCount = 0;
ButtonNavigator buttonNavigator;
void toggleItem(); // MenuListActivity overrides
void onActionSelected(int index) override;
void onSettingToggled(int index) override;
public: public:
explicit SettingsSubmenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, StrId titleId, explicit SettingsSubmenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, StrId titleId,
std::vector<SettingInfo> items) std::vector<SettingInfo> items)
: Activity("SettingsSubmenu", renderer, mappedInput), titleId(titleId), items(std::move(items)) {} : MenuListActivity("SettingsSubmenu", renderer, mappedInput), titleId(titleId) {
menuItems = std::move(items);
}
void onEnter() override;
void onExit() override;
void loop() override;
void render(RenderLock&&) override; void render(RenderLock&&) override;
}; };
+10 -9
View File
@@ -31,15 +31,14 @@ class UITheme {
// navigation. Pass the same title getter you pass to drawList so rendering and navigation // navigation. Pass the same title getter you pass to drawList so rendering and navigation
// always agree on which items are separators. // always agree on which items are separators.
// //
// Typical usage pattern for a menu with section headers: // Preferred approach: derive from MenuListActivity (see MenuListActivity.h) which handles
// separator skipping, navigation, and drawList automatically via SettingInfo items.
// //
// struct MenuItem { // Manual usage (for activities that don't derive from MenuListActivity):
// Action action; StrId labelId; bool isSeparator = false; //
// static MenuItem separator(StrId label) { return {Action::NONE, label, true}; } // std::vector<SettingInfo> items;
// std::string getTitle() const; // implemented in .cpp: makeSeparatorTitle when isSeparator // items.push_back(SettingInfo::Separator(StrId::STR_MY_SECTION));
// }; // items.push_back(SettingInfo::Toggle(StrId::STR_MY_TOGGLE, &CrossPointSettings::myFlag));
// const std::vector<MenuItem> items = buildMenuItems();
// // In buildMenuItems, use MenuItem::separator(StrId) for section headers.
// //
// // onEnter: wire navigation so separators are skipped: // // onEnter: wire navigation so separators are skipped:
// const auto pred = UITheme::makeSelectablePredicate(items.size(), // const auto pred = UITheme::makeSelectablePredicate(items.size(),
@@ -48,7 +47,9 @@ class UITheme {
// //
// // render: pass the same getter to drawList; separator rows are drawn automatically: // // render: pass the same getter to drawList; separator rows are drawn automatically:
// GUI.drawList(renderer, rect, items.size(), selectedIndex, // GUI.drawList(renderer, rect, items.size(), selectedIndex,
// [this](int i) { return items[i].getTitle(); }, ...); // [this](int i) { return items[i].getTitle(); },
// nullptr, nullptr,
// [this](int i) { return items[i].getDisplayValue(); }, true);
static std::function<bool(int)> makeSelectablePredicate(int total, std::function<std::string(int)> titleGetter); static std::function<bool(int)> makeSelectablePredicate(int total, std::function<std::string(int)> titleGetter);
// Returns the drawable content Rect accounting for screen orientation and visible button hints. // Returns the drawable content Rect accounting for screen orientation and visible button hints.