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.