Refacrored the separator component to be more flexible and reusable across different activities. Updated the EpubReaderMenuActivity and ClockSettingsActivity to utilize the new separator component for better UI consistency and maintainability. This change also involved updating the UITheme component to support the new separator styles.

This commit is contained in:
jpirnay
2026-04-12 05:46:10 +02:00
parent e9e39e91ac
commit e72ad0d2a9
6 changed files with 57 additions and 26 deletions
@@ -29,7 +29,7 @@ std::vector<EpubReaderMenuActivity::MenuItem> EpubReaderMenuActivity::buildMenuI
std::vector<MenuItem> items; std::vector<MenuItem> items;
items.reserve(18); items.reserve(18);
// Navigation // Navigation
items.push_back({MenuAction::NONE, StrId::STR_READER_NAVIGATION, true}); items.push_back(MenuItem::separator(StrId::STR_READER_NAVIGATION));
items.push_back({MenuAction::SELECT_CHAPTER, StrId::STR_SELECT_CHAPTER}); items.push_back({MenuAction::SELECT_CHAPTER, StrId::STR_SELECT_CHAPTER});
items.push_back({MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT}); items.push_back({MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT});
if (hasFootnotes) { if (hasFootnotes) {
@@ -38,7 +38,7 @@ std::vector<EpubReaderMenuActivity::MenuItem> EpubReaderMenuActivity::buildMenuI
items.push_back({MenuAction::AUTO_PAGE_TURN, StrId::STR_AUTO_TURN_PAGES_PER_MIN}); items.push_back({MenuAction::AUTO_PAGE_TURN, StrId::STR_AUTO_TURN_PAGES_PER_MIN});
// Appearance // Appearance
items.push_back({MenuAction::NONE, StrId::STR_READER_APPEARANCE, true}); items.push_back(MenuItem::separator(StrId::STR_READER_APPEARANCE));
items.push_back({MenuAction::EMBEDDED_STYLE, StrId::STR_EMBEDDED_STYLE}); items.push_back({MenuAction::EMBEDDED_STYLE, StrId::STR_EMBEDDED_STYLE});
items.push_back({MenuAction::IMAGE_RENDERING, StrId::STR_IMAGES}); items.push_back({MenuAction::IMAGE_RENDERING, StrId::STR_IMAGES});
items.push_back({MenuAction::TEXT_DARKNESS, StrId::STR_TEXT_DARKNESS}); items.push_back({MenuAction::TEXT_DARKNESS, StrId::STR_TEXT_DARKNESS});
@@ -46,13 +46,13 @@ std::vector<EpubReaderMenuActivity::MenuItem> EpubReaderMenuActivity::buildMenuI
// Synchronisation (only if credentials are set, to avoid confusion) // Synchronisation (only if credentials are set, to avoid confusion)
if (KOREADER_STORE.hasCredentials()) { if (KOREADER_STORE.hasCredentials()) {
items.push_back({MenuAction::NONE, StrId::STR_KOREADER_SYNC, true}); items.push_back(MenuItem::separator(StrId::STR_KOREADER_SYNC));
items.push_back({MenuAction::PULL_REMOTE, StrId::STR_PULL_PROGRESS_FROM_OTHER_DEVICES}); items.push_back({MenuAction::PULL_REMOTE, StrId::STR_PULL_PROGRESS_FROM_OTHER_DEVICES});
items.push_back({MenuAction::PUSH_LOCAL, StrId::STR_PUSH_PROGRESS_FROM_THIS_DEVICE}); items.push_back({MenuAction::PUSH_LOCAL, StrId::STR_PUSH_PROGRESS_FROM_THIS_DEVICE});
} }
// Tools // Tools
items.push_back({MenuAction::NONE, StrId::STR_READER_TOOLS, true}); items.push_back(MenuItem::separator(StrId::STR_READER_TOOLS));
items.push_back({MenuAction::SCREENSHOT, StrId::STR_SCREENSHOT_BUTTON}); items.push_back({MenuAction::SCREENSHOT, StrId::STR_SCREENSHOT_BUTTON});
items.push_back({MenuAction::DISPLAY_QR, StrId::STR_DISPLAY_QR}); items.push_back({MenuAction::DISPLAY_QR, StrId::STR_DISPLAY_QR});
items.push_back({MenuAction::DELETE_CACHE, StrId::STR_DELETE_CACHE}); items.push_back({MenuAction::DELETE_CACHE, StrId::STR_DELETE_CACHE});
@@ -60,17 +60,16 @@ std::vector<EpubReaderMenuActivity::MenuItem> EpubReaderMenuActivity::buildMenuI
return items; return items;
} }
std::function<bool(int)> EpubReaderMenuActivity::buildSelectablePredicate() const {
return [this](int index) {
return index >= 0 && index < static_cast<int>(menuItems.size()) && !menuItems[index].isSeparator;
};
}
void EpubReaderMenuActivity::onEnter() { void EpubReaderMenuActivity::onEnter() {
Activity::onEnter(); Activity::onEnter();
const auto selectablePredicate = buildSelectablePredicate(); const auto pred = UITheme::makeSelectablePredicate(
buttonNavigator.setSelectablePredicate(selectablePredicate, static_cast<int>(menuItems.size())); static_cast<int>(menuItems.size()), [this](int i) {
if (!selectablePredicate(selectedIndex)) { const auto& item = menuItems[i];
const auto t = I18N.get(item.labelId);
return item.isSeparator ? UITheme::makeSeparatorTitle(t) : t;
});
buttonNavigator.setSelectablePredicate(pred, static_cast<int>(menuItems.size()));
if (!pred(selectedIndex)) {
selectedIndex = buttonNavigator.nextIndex(selectedIndex); selectedIndex = buttonNavigator.nextIndex(selectedIndex);
} }
requestUpdate(); requestUpdate();
@@ -45,12 +45,11 @@ class EpubReaderMenuActivity final : public Activity {
MenuAction action; MenuAction action;
StrId labelId; StrId labelId;
bool isSeparator = false; bool isSeparator = false;
static MenuItem separator(StrId label) { return {MenuAction::NONE, label, true}; }
}; };
static std::vector<MenuItem> buildMenuItems(bool hasFootnotes); static std::vector<MenuItem> buildMenuItems(bool hasFootnotes);
std::function<bool(int)> buildSelectablePredicate() const;
// Fixed menu layout // Fixed menu layout
const std::vector<MenuItem> menuItems; const std::vector<MenuItem> menuItems;
@@ -23,28 +23,27 @@ std::vector<ClockSettingsActivity::MenuItem> ClockSettingsActivity::buildMenuIte
std::vector<MenuItem> items; std::vector<MenuItem> items;
items.reserve(7); items.reserve(7);
// Settings // Settings
items.push_back({Action::NONE, StrId::STR_SETTINGS_TITLE, true}); items.push_back(MenuItem::separator(StrId::STR_SETTINGS_TITLE));
items.push_back({Action::USE_CLOCK, StrId::STR_USE_CLOCK}); items.push_back({Action::USE_CLOCK, StrId::STR_USE_CLOCK});
items.push_back({Action::CLOCK_FORMAT, StrId::STR_CLOCK_FORMAT}); items.push_back({Action::CLOCK_FORMAT, StrId::STR_CLOCK_FORMAT});
items.push_back({Action::TIMEZONE, StrId::STR_TIMEZONE}); items.push_back({Action::TIMEZONE, StrId::STR_TIMEZONE});
// Tools // Tools
items.push_back({Action::NONE, StrId::STR_READER_TOOLS, true}); items.push_back(MenuItem::separator(StrId::STR_READER_TOOLS));
items.push_back({Action::DETECT_TIMEZONE, StrId::STR_DETECT_TIMEZONE}); items.push_back({Action::DETECT_TIMEZONE, StrId::STR_DETECT_TIMEZONE});
items.push_back({Action::SYNC_TIME, StrId::STR_SYNC_TIME}); items.push_back({Action::SYNC_TIME, StrId::STR_SYNC_TIME});
return items; return items;
} }
std::function<bool(int)> ClockSettingsActivity::buildSelectablePredicate() const {
return [this](int index) {
return index >= 0 && index < static_cast<int>(menuItems.size()) && !menuItems[index].isSeparator;
};
}
void ClockSettingsActivity::onEnter() { void ClockSettingsActivity::onEnter() {
Activity::onEnter(); Activity::onEnter();
buttonNavigator.setSelectablePredicate(buildSelectablePredicate(), static_cast<int>(menuItems.size())); const auto pred = UITheme::makeSelectablePredicate(
if (!buildSelectablePredicate()(selectedIndex)) { static_cast<int>(menuItems.size()), [this](int i) {
const auto t = I18N.get(menuItems[i].labelId);
return menuItems[i].isSeparator ? UITheme::makeSeparatorTitle(t) : t;
});
buttonNavigator.setSelectablePredicate(pred, static_cast<int>(menuItems.size()));
if (!pred(selectedIndex)) {
selectedIndex = buttonNavigator.nextIndex(selectedIndex); selectedIndex = buttonNavigator.nextIndex(selectedIndex);
} }
requestUpdate(); requestUpdate();
@@ -14,6 +14,7 @@ class ClockSettingsActivity final : public Activity {
Action action; Action action;
StrId labelId; StrId labelId;
bool isSeparator = false; bool isSeparator = false;
static MenuItem separator(StrId label) { return {Action::NONE, label, true}; }
}; };
ButtonNavigator buttonNavigator; ButtonNavigator buttonNavigator;
@@ -22,7 +23,6 @@ class ClockSettingsActivity final : public Activity {
static std::vector<MenuItem> buildMenuItems(); static std::vector<MenuItem> buildMenuItems();
void handleSelection(); void handleSelection();
std::function<bool(int)> buildSelectablePredicate() const;
public: public:
explicit ClockSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) explicit ClockSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
+7
View File
@@ -108,6 +108,13 @@ std::string UITheme::stripSeparatorTitle(const std::string& title) {
return isSeparatorTitle(title) ? title.substr(2) : title; return isSeparatorTitle(title) ? title.substr(2) : title;
} }
std::function<bool(int)> UITheme::makeSelectablePredicate(int total,
std::function<std::string(int)> titleGetter) {
return [total, titleGetter](int index) {
return index >= 0 && index < total && !isSeparatorTitle(titleGetter(index));
};
}
std::string UITheme::getCoverThumbPath(std::string coverBmpPath, int coverHeight) { std::string UITheme::getCoverThumbPath(std::string coverBmpPath, int coverHeight) {
size_t pos = coverBmpPath.find("[HEIGHT]", 0); size_t pos = coverBmpPath.find("[HEIGHT]", 0);
if (pos != std::string::npos) { if (pos != std::string::npos) {
+27
View File
@@ -26,6 +26,33 @@ class UITheme {
static std::string makeSeparatorTitle(StrId labelId); static std::string makeSeparatorTitle(StrId labelId);
static bool isSeparatorTitle(const std::string& title); static bool isSeparatorTitle(const std::string& title);
static std::string stripSeparatorTitle(const std::string& title); static std::string stripSeparatorTitle(const std::string& title);
// Returns a selectable predicate for use with ButtonNavigator::setSelectablePredicate().
// Items whose title is marked as a separator (via makeSeparatorTitle) are skipped during
// navigation. Pass the same title getter you pass to drawList so rendering and navigation
// always agree on which items are separators.
//
// Typical usage pattern for a menu with section headers:
//
// struct MenuItem {
// Action action; StrId labelId; bool isSeparator = false;
// static MenuItem separator(StrId label) { return {Action::NONE, label, true}; }
// };
// const std::vector<MenuItem> items = buildMenuItems();
// // In buildMenuItems, use MenuItem::separator(StrId) for section headers.
//
// // Title getter — used by both drawList and makeSelectablePredicate:
// auto titleGetter = [&](int i) {
// const auto t = I18N.get(items[i].labelId);
// return items[i].isSeparator ? UITheme::makeSeparatorTitle(t) : t;
// };
//
// // onEnter: wire navigation so separators are skipped:
// const auto pred = UITheme::makeSelectablePredicate(items.size(), titleGetter);
// buttonNavigator.setSelectablePredicate(pred, items.size());
//
// // render: pass the same getter to drawList; separator rows are drawn automatically:
// GUI.drawList(renderer, rect, items.size(), selectedIndex, 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.
// Bottom hints occupy the physical bottom edge; side hints occupy the physical right edge. // Bottom hints occupy the physical bottom edge; side hints occupy the physical right edge.