From e72ad0d2a9c63e19f5687617352fbdc60d2358af Mon Sep 17 00:00:00 2001 From: jpirnay Date: Sun, 12 Apr 2026 05:46:10 +0200 Subject: [PATCH] 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. --- .../reader/EpubReaderMenuActivity.cpp | 25 +++++++++-------- .../reader/EpubReaderMenuActivity.h | 3 +-- .../settings/ClockSettingsActivity.cpp | 19 +++++++------ .../settings/ClockSettingsActivity.h | 2 +- src/components/UITheme.cpp | 7 +++++ src/components/UITheme.h | 27 +++++++++++++++++++ 6 files changed, 57 insertions(+), 26 deletions(-) diff --git a/src/activities/reader/EpubReaderMenuActivity.cpp b/src/activities/reader/EpubReaderMenuActivity.cpp index 313f4890..c82f6510 100644 --- a/src/activities/reader/EpubReaderMenuActivity.cpp +++ b/src/activities/reader/EpubReaderMenuActivity.cpp @@ -29,7 +29,7 @@ std::vector EpubReaderMenuActivity::buildMenuI std::vector items; items.reserve(18); // 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::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT}); if (hasFootnotes) { @@ -38,7 +38,7 @@ std::vector EpubReaderMenuActivity::buildMenuI items.push_back({MenuAction::AUTO_PAGE_TURN, StrId::STR_AUTO_TURN_PAGES_PER_MIN}); // 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::IMAGE_RENDERING, StrId::STR_IMAGES}); items.push_back({MenuAction::TEXT_DARKNESS, StrId::STR_TEXT_DARKNESS}); @@ -46,13 +46,13 @@ std::vector EpubReaderMenuActivity::buildMenuI // Synchronisation (only if credentials are set, to avoid confusion) 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::PUSH_LOCAL, StrId::STR_PUSH_PROGRESS_FROM_THIS_DEVICE}); } // 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::DISPLAY_QR, StrId::STR_DISPLAY_QR}); items.push_back({MenuAction::DELETE_CACHE, StrId::STR_DELETE_CACHE}); @@ -60,17 +60,16 @@ std::vector EpubReaderMenuActivity::buildMenuI return items; } -std::function EpubReaderMenuActivity::buildSelectablePredicate() const { - return [this](int index) { - return index >= 0 && index < static_cast(menuItems.size()) && !menuItems[index].isSeparator; - }; -} - void EpubReaderMenuActivity::onEnter() { Activity::onEnter(); - const auto selectablePredicate = buildSelectablePredicate(); - buttonNavigator.setSelectablePredicate(selectablePredicate, static_cast(menuItems.size())); - if (!selectablePredicate(selectedIndex)) { + const auto pred = UITheme::makeSelectablePredicate( + static_cast(menuItems.size()), [this](int i) { + const auto& item = menuItems[i]; + const auto t = I18N.get(item.labelId); + return item.isSeparator ? UITheme::makeSeparatorTitle(t) : t; + }); + buttonNavigator.setSelectablePredicate(pred, static_cast(menuItems.size())); + if (!pred(selectedIndex)) { selectedIndex = buttonNavigator.nextIndex(selectedIndex); } requestUpdate(); diff --git a/src/activities/reader/EpubReaderMenuActivity.h b/src/activities/reader/EpubReaderMenuActivity.h index 93bd66d8..216caae2 100644 --- a/src/activities/reader/EpubReaderMenuActivity.h +++ b/src/activities/reader/EpubReaderMenuActivity.h @@ -45,12 +45,11 @@ class EpubReaderMenuActivity final : public Activity { MenuAction action; StrId labelId; bool isSeparator = false; + static MenuItem separator(StrId label) { return {MenuAction::NONE, label, true}; } }; static std::vector buildMenuItems(bool hasFootnotes); - std::function buildSelectablePredicate() const; - // Fixed menu layout const std::vector menuItems; diff --git a/src/activities/settings/ClockSettingsActivity.cpp b/src/activities/settings/ClockSettingsActivity.cpp index 9b99f3a8..1e6a9b1c 100644 --- a/src/activities/settings/ClockSettingsActivity.cpp +++ b/src/activities/settings/ClockSettingsActivity.cpp @@ -23,28 +23,27 @@ std::vector ClockSettingsActivity::buildMenuIte std::vector items; items.reserve(7); // 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::CLOCK_FORMAT, StrId::STR_CLOCK_FORMAT}); items.push_back({Action::TIMEZONE, StrId::STR_TIMEZONE}); // 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::SYNC_TIME, StrId::STR_SYNC_TIME}); return items; } -std::function ClockSettingsActivity::buildSelectablePredicate() const { - return [this](int index) { - return index >= 0 && index < static_cast(menuItems.size()) && !menuItems[index].isSeparator; - }; -} - void ClockSettingsActivity::onEnter() { Activity::onEnter(); - buttonNavigator.setSelectablePredicate(buildSelectablePredicate(), static_cast(menuItems.size())); - if (!buildSelectablePredicate()(selectedIndex)) { + const auto pred = UITheme::makeSelectablePredicate( + static_cast(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(menuItems.size())); + if (!pred(selectedIndex)) { selectedIndex = buttonNavigator.nextIndex(selectedIndex); } requestUpdate(); diff --git a/src/activities/settings/ClockSettingsActivity.h b/src/activities/settings/ClockSettingsActivity.h index ffeea5d9..a5c02136 100644 --- a/src/activities/settings/ClockSettingsActivity.h +++ b/src/activities/settings/ClockSettingsActivity.h @@ -14,6 +14,7 @@ class ClockSettingsActivity final : public Activity { Action action; StrId labelId; bool isSeparator = false; + static MenuItem separator(StrId label) { return {Action::NONE, label, true}; } }; ButtonNavigator buttonNavigator; @@ -22,7 +23,6 @@ class ClockSettingsActivity final : public Activity { static std::vector buildMenuItems(); void handleSelection(); - std::function buildSelectablePredicate() const; public: explicit ClockSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) diff --git a/src/components/UITheme.cpp b/src/components/UITheme.cpp index 67a09e16..6b92df15 100644 --- a/src/components/UITheme.cpp +++ b/src/components/UITheme.cpp @@ -108,6 +108,13 @@ std::string UITheme::stripSeparatorTitle(const std::string& title) { return isSeparatorTitle(title) ? title.substr(2) : title; } +std::function UITheme::makeSelectablePredicate(int total, + std::function titleGetter) { + return [total, titleGetter](int index) { + return index >= 0 && index < total && !isSeparatorTitle(titleGetter(index)); + }; +} + std::string UITheme::getCoverThumbPath(std::string coverBmpPath, int coverHeight) { size_t pos = coverBmpPath.find("[HEIGHT]", 0); if (pos != std::string::npos) { diff --git a/src/components/UITheme.h b/src/components/UITheme.h index abdc1906..4cacca79 100644 --- a/src/components/UITheme.h +++ b/src/components/UITheme.h @@ -26,6 +26,33 @@ class UITheme { static std::string makeSeparatorTitle(StrId labelId); static bool isSeparatorTitle(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 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 makeSelectablePredicate(int total, std::function titleGetter); // 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.