From 64b6818eebfc51979990e87e8a0437d80c523f69 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Thu, 9 Apr 2026 14:51:39 +0200 Subject: [PATCH 1/9] Make Reader menu theme aware --- .../reader/EpubReaderMenuActivity.cpp | 85 +++++++------------ src/components/themes/BaseTheme.cpp | 2 +- 2 files changed, 33 insertions(+), 54 deletions(-) diff --git a/src/activities/reader/EpubReaderMenuActivity.cpp b/src/activities/reader/EpubReaderMenuActivity.cpp index c1bf98b2..3d054bfc 100644 --- a/src/activities/reader/EpubReaderMenuActivity.cpp +++ b/src/activities/reader/EpubReaderMenuActivity.cpp @@ -158,60 +158,39 @@ void EpubReaderMenuActivity::render(RenderLock&&) { // Menu Items const int startY = 75 + contentRect.y; - constexpr int lineHeight = 30; + const int listHeight = contentRect.height - (startY - contentRect.y); - for (size_t i = 0; i < menuItems.size(); ++i) { - const int displayY = startY + (i * lineHeight); - const bool isSelected = (static_cast(i) == selectedIndex); - - if (isSelected) { - // Highlight only the content area so we don't paint over hint gutters. - renderer.fillRect(contentRect.x, displayY, contentRect.width - 1, lineHeight, true); - } - - renderer.drawText(UI_10_FONT_ID, contentRect.x + 20, displayY, I18N.get(menuItems[i].labelId), !isSelected); - - if (menuItems[i].action == MenuAction::ROTATE_SCREEN) { - // Render current orientation value on the right edge of the content area. - const char* value = I18N.get(orientationLabels[pendingOrientation]); - const auto width = renderer.getTextWidth(UI_10_FONT_ID, value); - renderer.drawText(UI_10_FONT_ID, contentRect.x + contentRect.width - 20 - width, displayY, value, !isSelected); - } - - if (menuItems[i].action == MenuAction::AUTO_PAGE_TURN) { - // Render current page turn value on the right edge of the content area. - const auto value = pageTurnLabels[selectedPageTurnOption]; - const auto width = renderer.getTextWidth(UI_10_FONT_ID, value); - renderer.drawText(UI_10_FONT_ID, contentRect.x + contentRect.width - 20 - width, displayY, value, !isSelected); - } - - if (menuItems[i].action == MenuAction::EMBEDDED_STYLE) { - const char* value = tr(STR_DEFAULT_VALUE); - if (pendingEmbeddedStyleOverride == 1) { - value = tr(STR_STATE_ON); - } else if (pendingEmbeddedStyleOverride == 0) { - value = tr(STR_STATE_OFF); - } - const auto width = renderer.getTextWidth(UI_10_FONT_ID, value); - renderer.drawText(UI_10_FONT_ID, contentRect.x + contentRect.width - 20 - width, displayY, value, !isSelected); - } - - if (menuItems[i].action == MenuAction::IMAGE_RENDERING) { - const char* value = tr(STR_DEFAULT_VALUE); - if (pendingImageRenderingOverride >= 0 && pendingImageRenderingOverride < imageRenderingLabels.size()) { - value = I18N.get(imageRenderingLabels[pendingImageRenderingOverride]); - } - const auto width = renderer.getTextWidth(UI_10_FONT_ID, value); - renderer.drawText(UI_10_FONT_ID, contentRect.x + contentRect.width - 20 - width, displayY, value, !isSelected); - } - - if (menuItems[i].action == MenuAction::TEXT_DARKNESS) { - const uint8_t idx = (pendingTextDarkness < textDarknessLabels.size()) ? pendingTextDarkness : 0; - const char* value = I18N.get(textDarknessLabels[idx]); - const auto width = renderer.getTextWidth(UI_10_FONT_ID, value); - renderer.drawText(UI_10_FONT_ID, contentRect.x + contentRect.width - 20 - width, displayY, value, !isSelected); - } - } + GUI.drawList( + renderer, Rect{contentRect.x, startY, contentRect.width, listHeight}, static_cast(menuItems.size()), + selectedIndex, [this](int index) { return I18N.get(menuItems[index].labelId); }, 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 const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index c982982b..d183a20d 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -273,7 +273,7 @@ void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, // Draw selection int contentWidth = rect.width - 5; if (selectedIndex >= 0) { - renderer.fillRect(0, rect.y + selectedIndex % pageItems * rowHeight - 2, rect.width, rowHeight); + renderer.fillRect(rect.x, rect.y + selectedIndex % pageItems * rowHeight - 2, rect.width, rowHeight); } // Draw all items const auto pageStartIndex = selectedIndex / pageItems * pageItems; From c4424dfe1c51e05dc0efa385454be136111355b8 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Thu, 9 Apr 2026 16:49:28 +0200 Subject: [PATCH 2/9] Introduce separators in drawList --- src/components/UITheme.cpp | 17 +++++ src/components/UITheme.h | 7 ++ src/components/themes/BaseTheme.cpp | 31 ++++++++- src/components/themes/BaseTheme.h | 2 + src/components/themes/lyra/LyraTheme.cpp | 20 +++++- src/util/ButtonNavigator.cpp | 82 ++++++++++++++++++++++++ src/util/ButtonNavigator.h | 13 ++++ 7 files changed, 169 insertions(+), 3 deletions(-) diff --git a/src/components/UITheme.cpp b/src/components/UITheme.cpp index 4a61f00d..7b76f617 100644 --- a/src/components/UITheme.cpp +++ b/src/components/UITheme.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -97,6 +98,22 @@ Rect UITheme::getContentRect(const GfxRenderer& renderer, bool hasBottomHints, b return Rect{left, top, w - left - right, h - top - bottom}; } +std::string UITheme::makeSeparatorTitle(const std::string& title) { + return std::string("__") + title; +} + +std::string UITheme::makeSeparatorTitle(StrId labelId) { + return std::string("__") + I18N.get(labelId); +} + +bool UITheme::isSeparatorTitle(const std::string& title) { + return title.rfind("__", 0) == 0; +} + +std::string UITheme::stripSeparatorTitle(const std::string& title) { + return isSeparatorTitle(title) ? title.substr(2) : title; +} + 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 e45c743d..17d5c997 100644 --- a/src/components/UITheme.h +++ b/src/components/UITheme.h @@ -3,6 +3,8 @@ #include #include +#include + #include "CrossPointSettings.h" #include "components/themes/BaseTheme.h" @@ -20,6 +22,11 @@ class UITheme { void setTheme(CrossPointSettings::UI_THEME type); static int getNumberOfItemsPerPage(const GfxRenderer& renderer, bool hasHeader, bool hasTabBar, bool hasButtonHints, bool hasSubtitle); + static std::string makeSeparatorTitle(const std::string& title); + static std::string makeSeparatorTitle(StrId labelId); + static bool isSeparatorTitle(const std::string& title); + static std::string stripSeparatorTitle(const std::string& title); + // 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. // The mapping to logical edges is orientation-dependent. diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index d183a20d..e38a63e2 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -270,9 +270,14 @@ void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, } } + bool selectedIsSeparator = false; + if (selectedIndex >= 0 && selectedIndex < itemCount) { + selectedIsSeparator = UITheme::isSeparatorTitle(rowTitle(selectedIndex)); + } + // Draw selection int contentWidth = rect.width - 5; - if (selectedIndex >= 0) { + if (selectedIndex >= 0 && !selectedIsSeparator) { renderer.fillRect(rect.x, rect.y + selectedIndex % pageItems * rowHeight - 2, rect.width, rowHeight); } // Draw all items @@ -283,9 +288,23 @@ void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, // Draw name auto itemName = rowTitle(i); + const bool isSeparator = UITheme::isSeparatorTitle(itemName); + if (isSeparator) { + itemName = UITheme::stripSeparatorTitle(itemName); + } + + if (isSeparator) { + drawListSeparator(renderer, + Rect{rect.x + BaseMetrics::values.contentSidePadding, itemY, + contentWidth - BaseMetrics::values.contentSidePadding * 2, rowHeight}, + rect.x + BaseMetrics::values.contentSidePadding, textWidth, itemName); + continue; + } + auto font = (rowSubtitle != nullptr) ? UI_12_FONT_ID : UI_10_FONT_ID; auto item = renderer.truncatedText(font, itemName.c_str(), textWidth); - renderer.drawText(font, rect.x + BaseMetrics::values.contentSidePadding, itemY, item.c_str(), i != selectedIndex); + renderer.drawText(font, rect.x + BaseMetrics::values.contentSidePadding, itemY, item.c_str(), true); + renderer.drawText(font, rect.x + BaseMetrics::values.contentSidePadding, itemY, item.c_str(), true); if (rowSubtitle != nullptr) { // Draw subtitle; if the text is newline-separated (author\nseries), join with • for single-line display @@ -309,6 +328,14 @@ void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, } } +void BaseTheme::drawListSeparator(const GfxRenderer& renderer, Rect rowRect, int textX, int textWidth, + const std::string& title) const { + const std::string item = renderer.truncatedText(SMALL_FONT_ID, title.c_str(), textWidth); + const int lineY = rowRect.y + rowRect.height - 2; + renderer.drawLine(rowRect.x, lineY, rowRect.x + rowRect.width - 1, lineY, true); + renderer.drawText(SMALL_FONT_ID, textX, rowRect.y + 7, item.c_str(), true, EpdFontFamily::BOLD); +} + void BaseTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* title, const char* subtitle) const { // Hide last battery draw constexpr int maxBatteryWidth = 80; diff --git a/src/components/themes/BaseTheme.h b/src/components/themes/BaseTheme.h index daa79c0c..e5ddc5d6 100644 --- a/src/components/themes/BaseTheme.h +++ b/src/components/themes/BaseTheme.h @@ -121,6 +121,8 @@ class BaseTheme { const std::function& rowIcon = nullptr, const std::function& rowValue = nullptr, bool highlightValue = false) const; + virtual void drawListSeparator(const GfxRenderer& renderer, Rect rowRect, int textX, int textWidth, + const std::string& title) const; virtual void drawHeader(const GfxRenderer& renderer, Rect rect, const char* title, const char* subtitle = nullptr) const; virtual void drawSubHeader(const GfxRenderer& renderer, Rect rect, const char* label, diff --git a/src/components/themes/lyra/LyraTheme.cpp b/src/components/themes/lyra/LyraTheme.cpp index d8f454b2..a7ff264d 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -401,11 +401,16 @@ void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, scrollBarHeight, true); } + bool selectedIsSeparator = false; + if (selectedIndex >= 0 && selectedIndex < itemCount && rowTitle != nullptr) { + selectedIsSeparator = UITheme::isSeparatorTitle(rowTitle(selectedIndex)); + } + // Draw selection int contentWidth = rect.width - (totalPages > 1 ? (LyraMetrics::values.scrollBarWidth + LyraMetrics::values.scrollBarRightOffset) : 1); - if (selectedIndex >= 0) { + if (selectedIndex >= 0 && !selectedIsSeparator) { renderer.fillRoundedRect( rect.x + LyraMetrics::values.contentSidePadding, rect.y + selectedIndex % pageItems * rowHeight, contentWidth - LyraMetrics::values.contentSidePadding * 2, rowHeight, cornerRadius, Color::LightGray); @@ -438,6 +443,19 @@ void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, } auto itemName = rowTitle(i); + const bool isSeparator = UITheme::isSeparatorTitle(itemName); + if (isSeparator) { + itemName = UITheme::stripSeparatorTitle(itemName); + } + + if (isSeparator) { + drawListSeparator(renderer, + Rect{rect.x + LyraMetrics::values.contentSidePadding, itemY, + contentWidth - LyraMetrics::values.contentSidePadding * 2, rowHeight}, + textX, rowTextWidth, itemName); + continue; + } + auto item = renderer.truncatedText(UI_10_FONT_ID, itemName.c_str(), rowTextWidth); renderer.drawText(UI_10_FONT_ID, textX, itemY + 7, item.c_str(), true); diff --git a/src/util/ButtonNavigator.cpp b/src/util/ButtonNavigator.cpp index d9844138..e8e8b499 100644 --- a/src/util/ButtonNavigator.cpp +++ b/src/util/ButtonNavigator.cpp @@ -73,6 +73,26 @@ bool ButtonNavigator::shouldNavigateContinuously() const { return buttonHeldLongEnough && navigationIntervalElapsed; } +void ButtonNavigator::setSelectablePredicate(std::function selectablePredicate, int totalItems) { + this->selectablePredicate = std::move(selectablePredicate); + this->selectableTotalItems = totalItems; +} + +void ButtonNavigator::clearSelectablePredicate() { + selectablePredicate = nullptr; + selectableTotalItems = 0; +} + +int ButtonNavigator::nextIndex(int currentIndex) const { + if (!selectablePredicate || selectableTotalItems <= 0) return currentIndex; + return nextIndex(currentIndex, selectableTotalItems, selectablePredicate); +} + +int ButtonNavigator::previousIndex(int currentIndex) const { + if (!selectablePredicate || selectableTotalItems <= 0) return currentIndex; + return previousIndex(currentIndex, selectableTotalItems, selectablePredicate); +} + int ButtonNavigator::nextIndex(const int currentIndex, const int totalItems) { if (totalItems <= 0) return 0; @@ -87,6 +107,68 @@ int ButtonNavigator::previousIndex(const int currentIndex, const int totalItems) return (currentIndex + totalItems - 1) % totalItems; } +int ButtonNavigator::nextIndex(const int currentIndex, const std::vector& selectable) { + const int totalItems = static_cast(selectable.size()); + if (totalItems <= 0) return 0; + + int index = nextIndex(currentIndex, totalItems); + for (int i = 0; i < totalItems; ++i) { + if (selectable[index]) { + return index; + } + index = nextIndex(index, totalItems); + } + + return currentIndex; +} + +int ButtonNavigator::previousIndex(const int currentIndex, const std::vector& selectable) { + const int totalItems = static_cast(selectable.size()); + if (totalItems <= 0) return 0; + + int index = previousIndex(currentIndex, totalItems); + for (int i = 0; i < totalItems; ++i) { + if (selectable[index]) { + return index; + } + index = previousIndex(index, totalItems); + } + + return currentIndex; +} + +int ButtonNavigator::nextIndex(const int currentIndex, const int totalItems, + const std::function& isSelectable) { + if (totalItems <= 0) return 0; + if (!isSelectable) return nextIndex(currentIndex, totalItems); + + int index = nextIndex(currentIndex, totalItems); + for (int i = 0; i < totalItems; ++i) { + if (isSelectable(index)) { + return index; + } + index = nextIndex(index, totalItems); + } + + return currentIndex; +} + +int ButtonNavigator::previousIndex(const int currentIndex, const int totalItems, + const std::function& isSelectable) { + if (totalItems <= 0) return 0; + if (!isSelectable) return previousIndex(currentIndex, totalItems); + + int index = previousIndex(currentIndex, totalItems); + for (int i = 0; i < totalItems; ++i) { + if (isSelectable(index)) { + return index; + } + index = previousIndex(index, totalItems); + } + + return currentIndex; +} + int ButtonNavigator::nextPageIndex(const int currentIndex, const int totalItems, const int itemsPerPage) { if (totalItems <= 0 || itemsPerPage <= 0) return 0; diff --git a/src/util/ButtonNavigator.h b/src/util/ButtonNavigator.h index 2f9afbc1..11a17449 100644 --- a/src/util/ButtonNavigator.h +++ b/src/util/ButtonNavigator.h @@ -13,6 +13,8 @@ class ButtonNavigator final { const uint16_t continuousIntervalMs; uint32_t lastContinuousNavTime = 0; static const MappedInputManager* mappedInput; + std::function selectablePredicate; + int selectableTotalItems = 0; [[nodiscard]] bool shouldNavigateContinuously() const; @@ -40,6 +42,17 @@ class ButtonNavigator final { [[nodiscard]] static int nextIndex(int currentIndex, int totalItems); [[nodiscard]] static int previousIndex(int currentIndex, int totalItems); + [[nodiscard]] static int nextIndex(int currentIndex, const std::vector& selectable); + [[nodiscard]] static int previousIndex(int currentIndex, const std::vector& selectable); + [[nodiscard]] static int nextIndex(int currentIndex, int totalItems, + const std::function& isSelectable); + [[nodiscard]] static int previousIndex(int currentIndex, int totalItems, + const std::function& isSelectable); + + [[nodiscard]] int nextIndex(int currentIndex) const; + [[nodiscard]] int previousIndex(int currentIndex) const; + void setSelectablePredicate(std::function selectablePredicate, int totalItems); + void clearSelectablePredicate(); [[nodiscard]] static int nextPageIndex(int currentIndex, int totalItems, int itemsPerPage); [[nodiscard]] static int previousPageIndex(int currentIndex, int totalItems, int itemsPerPage); From 5f4c5550e8510a5543b1a618be2e604f72d32cd8 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Thu, 9 Apr 2026 16:49:55 +0200 Subject: [PATCH 3/9] Restructure reader menu --- lib/I18n/translations/english.yaml | 5 +- .../reader/EpubReaderMenuActivity.cpp | 49 +++++++++++++++---- .../reader/EpubReaderMenuActivity.h | 4 ++ 3 files changed, 48 insertions(+), 10 deletions(-) diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 3a6f780d..b453bae4 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -465,4 +465,7 @@ STR_SLEEP_SCREEN_SET: "Sleep screen updated!" STR_IMAGE_DISPLAY_BW: ">> B&W" STR_IMAGE_DISPLAY_GRAYSCALE: ">> Gray" STR_WEATHER_MOON_INFO: "Moon" -STR_WEATHER_SUN_INFO: "Sun" \ No newline at end of file +STR_WEATHER_SUN_INFO: "Sun" +STR_READER_TOOLS: "Tools" +STR_READER_NAVIGATION: "Navigation" +STR_READER_APPEARANCE: "Appearance" \ No newline at end of file diff --git a/src/activities/reader/EpubReaderMenuActivity.cpp b/src/activities/reader/EpubReaderMenuActivity.cpp index 3d054bfc..1a5bfef9 100644 --- a/src/activities/reader/EpubReaderMenuActivity.cpp +++ b/src/activities/reader/EpubReaderMenuActivity.cpp @@ -27,30 +27,52 @@ EpubReaderMenuActivity::EpubReaderMenuActivity(GfxRenderer& renderer, MappedInpu std::vector EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes) { std::vector items; - items.reserve(13); + items.reserve(18); + // Navigation + items.push_back({MenuAction::NONE, StrId::STR_READER_NAVIGATION, true}); items.push_back({MenuAction::SELECT_CHAPTER, StrId::STR_SELECT_CHAPTER}); + items.push_back({MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT}); if (hasFootnotes) { items.push_back({MenuAction::FOOTNOTES, StrId::STR_FOOTNOTES}); } + 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({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}); - items.push_back({MenuAction::AUTO_PAGE_TURN, StrId::STR_AUTO_TURN_PAGES_PER_MIN}); - items.push_back({MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT}); - items.push_back({MenuAction::SCREENSHOT, StrId::STR_SCREENSHOT_BUTTON}); - items.push_back({MenuAction::DISPLAY_QR, StrId::STR_DISPLAY_QR}); - items.push_back({MenuAction::GO_HOME, StrId::STR_GO_HOME_BUTTON}); + + // 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({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({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}); + items.push_back({MenuAction::GO_HOME, StrId::STR_GO_HOME_BUTTON}); 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(); + buttonNavigator.setSelectablePredicate(buildSelectablePredicate(), static_cast(menuItems.size())); + const int next = buttonNavigator.nextIndex(selectedIndex); + if (next != selectedIndex) { + selectedIndex = next; + } requestUpdate(); } @@ -59,17 +81,20 @@ void EpubReaderMenuActivity::onExit() { Activity::onExit(); } void EpubReaderMenuActivity::loop() { // Handle navigation buttonNavigator.onNext([this] { - selectedIndex = ButtonNavigator::nextIndex(selectedIndex, static_cast(menuItems.size())); + selectedIndex = buttonNavigator.nextIndex(selectedIndex); requestUpdate(); }); buttonNavigator.onPrevious([this] { - selectedIndex = ButtonNavigator::previousIndex(selectedIndex, static_cast(menuItems.size())); + 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(); @@ -162,7 +187,13 @@ void EpubReaderMenuActivity::render(RenderLock&&) { GUI.drawList( renderer, Rect{contentRect.x, startY, contentRect.width, listHeight}, static_cast(menuItems.size()), - selectedIndex, [this](int index) { return I18N.get(menuItems[index].labelId); }, nullptr, nullptr, + selectedIndex, + [this](int index) { + const auto& item = menuItems[index]; + const auto title = I18N.get(item.labelId); + return item.isSeparator ? UITheme::makeSeparatorTitle(title) : title; + }, + nullptr, nullptr, [this](int index) { const auto& item = menuItems[index]; switch (item.action) { diff --git a/src/activities/reader/EpubReaderMenuActivity.h b/src/activities/reader/EpubReaderMenuActivity.h index a268cf73..93bd66d8 100644 --- a/src/activities/reader/EpubReaderMenuActivity.h +++ b/src/activities/reader/EpubReaderMenuActivity.h @@ -12,6 +12,7 @@ class EpubReaderMenuActivity final : public Activity { public: // Menu actions available from the reader menu. enum class MenuAction { + NONE, SELECT_CHAPTER, FOOTNOTES, EMBEDDED_STYLE, @@ -43,10 +44,13 @@ class EpubReaderMenuActivity final : public Activity { struct MenuItem { MenuAction action; StrId labelId; + bool isSeparator = false; }; static std::vector buildMenuItems(bool hasFootnotes); + std::function buildSelectablePredicate() const; + // Fixed menu layout const std::vector menuItems; From 68f175ecedc495bc7ffc2318b13e780146758052 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Thu, 9 Apr 2026 16:50:13 +0200 Subject: [PATCH 4/9] Restructure ClockSettings --- .../settings/ClockSettingsActivity.cpp | 87 ++++++++++++------- .../settings/ClockSettingsActivity.h | 23 ++++- 2 files changed, 80 insertions(+), 30 deletions(-) diff --git a/src/activities/settings/ClockSettingsActivity.cpp b/src/activities/settings/ClockSettingsActivity.cpp index b79a7bc8..95b533cc 100644 --- a/src/activities/settings/ClockSettingsActivity.cpp +++ b/src/activities/settings/ClockSettingsActivity.cpp @@ -12,10 +12,6 @@ #include "fontIds.h" namespace { -constexpr int MENU_ITEMS = 5; -const StrId menuNames[MENU_ITEMS] = {StrId::STR_USE_CLOCK, StrId::STR_CLOCK_FORMAT, StrId::STR_TIMEZONE, - StrId::STR_SYNC_TIME, StrId::STR_DETECT_TIMEZONE}; - const StrId timeZoneNames[CrossPointSettings::TIMEZONE_COUNT] = { StrId::STR_TZ_UTC, StrId::STR_TZ_CET, StrId::STR_TZ_EET, StrId::STR_TZ_MSK, StrId::STR_TZ_UTC_PLUS4, StrId::STR_TZ_IST, StrId::STR_TZ_UTC_PLUS7, StrId::STR_TZ_UTC_PLUS8, @@ -23,9 +19,34 @@ const StrId timeZoneNames[CrossPointSettings::TIMEZONE_COUNT] = { StrId::STR_TZ_EST, StrId::STR_TZ_CST, StrId::STR_TZ_MST, StrId::STR_TZ_PST}; } // namespace +std::vector ClockSettingsActivity::buildMenuItems() { + std::vector items; + items.reserve(7); + // Settings + items.push_back({Action::NONE, StrId::STR_SETTINGS_TITLE, true}); + 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({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(); - selectedIndex = 0; + buttonNavigator.setSelectablePredicate(buildSelectablePredicate(), static_cast(menuItems.size())); + if (!buildSelectablePredicate()(selectedIndex)) { + selectedIndex = buttonNavigator.nextIndex(selectedIndex); + } requestUpdate(); } @@ -44,44 +65,45 @@ void ClockSettingsActivity::loop() { } buttonNavigator.onNextRelease([this] { - selectedIndex = ButtonNavigator::nextIndex(selectedIndex, MENU_ITEMS); + selectedIndex = buttonNavigator.nextIndex(selectedIndex); requestUpdate(); }); buttonNavigator.onPreviousRelease([this] { - selectedIndex = ButtonNavigator::previousIndex(selectedIndex, MENU_ITEMS); + selectedIndex = buttonNavigator.previousIndex(selectedIndex); requestUpdate(); }); buttonNavigator.onNextContinuous([this] { - selectedIndex = ButtonNavigator::nextIndex(selectedIndex, MENU_ITEMS); + selectedIndex = buttonNavigator.nextIndex(selectedIndex); requestUpdate(); }); buttonNavigator.onPreviousContinuous([this] { - selectedIndex = ButtonNavigator::previousIndex(selectedIndex, MENU_ITEMS); + selectedIndex = buttonNavigator.previousIndex(selectedIndex); requestUpdate(); }); } void ClockSettingsActivity::handleSelection() { - if (selectedIndex == 0) { + const auto action = menuItems[selectedIndex].action; + if (action == Action::USE_CLOCK) { SETTINGS.useClock = (SETTINGS.useClock + 1) % 2; if (!SETTINGS.useClock) { SETTINGS.statusBarClock = 0; } SETTINGS.saveToFile(); - } else if (selectedIndex == 1) { + } else if (action == Action::CLOCK_FORMAT) { SETTINGS.clockFormat12h = (SETTINGS.clockFormat12h + 1) % 2; SETTINGS.saveToFile(); - } else if (selectedIndex == 2) { + } else if (action == Action::TIMEZONE) { SETTINGS.timeZone = (SETTINGS.timeZone + 1) % CrossPointSettings::TIMEZONE_COUNT; HalClock::applyTimezone(SETTINGS.timeZone); SETTINGS.saveToFile(); - } else if (selectedIndex == 3) { + } else if (action == Action::SYNC_TIME) { auto resultHandler = [](const ActivityResult&) { SETTINGS.saveToFile(); }; startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); - } else if (selectedIndex == 4) { + } else if (action == Action::DETECT_TIMEZONE) { auto resultHandler = [](const ActivityResult&) { SETTINGS.saveToFile(); }; startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); } @@ -102,23 +124,30 @@ void ClockSettingsActivity::render(RenderLock&&) { const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2; GUI.drawList( - renderer, Rect{0, contentTop, pageWidth, contentHeight}, MENU_ITEMS, selectedIndex, - [](int index) { return std::string(I18N.get(menuNames[index])); }, nullptr, nullptr, - [](int index) { - if (index == 0) { - return std::string(SETTINGS.useClock ? tr(STR_STATE_ON) : tr(STR_STATE_OFF)); - } - if (index == 1) { - return std::string(SETTINGS.clockFormat12h ? tr(STR_12H) : tr(STR_24H)); - } - if (index == 2) { - const auto tzIndex = static_cast(SETTINGS.timeZone); - if (tzIndex < (sizeof(timeZoneNames) / sizeof(timeZoneNames[0]))) { - return std::string(I18N.get(timeZoneNames[tzIndex])); + renderer, Rect{0, contentTop, pageWidth, contentHeight}, static_cast(menuItems.size()), + selectedIndex, + [this](int index) { + const auto title = I18N.get(menuItems[index].labelId); + return menuItems[index].isSeparator ? UITheme::makeSeparatorTitle(title) : title; + }, + nullptr, nullptr, + [this](int index) { + const auto action = menuItems[index].action; + switch (action) { + case Action::USE_CLOCK: + return std::string(SETTINGS.useClock ? tr(STR_STATE_ON) : tr(STR_STATE_OFF)); + case Action::CLOCK_FORMAT: + return std::string(SETTINGS.clockFormat12h ? tr(STR_12H) : tr(STR_24H)); + case Action::TIMEZONE: { + const auto tzIndex = static_cast(SETTINGS.timeZone); + if (tzIndex < (sizeof(timeZoneNames) / sizeof(timeZoneNames[0]))) { + return std::string(I18N.get(timeZoneNames[tzIndex])); + } + return std::string(tr(STR_TZ_UTC)); } - return std::string(tr(STR_TZ_UTC)); + default: + return std::string(""); } - return std::string(""); }, true); diff --git a/src/activities/settings/ClockSettingsActivity.h b/src/activities/settings/ClockSettingsActivity.h index e1f491bc..974259e4 100644 --- a/src/activities/settings/ClockSettingsActivity.h +++ b/src/activities/settings/ClockSettingsActivity.h @@ -1,18 +1,39 @@ #pragma once +#include +#include + #include #include "activities/Activity.h" #include "util/ButtonNavigator.h" class ClockSettingsActivity final : public Activity { + enum class Action { + USE_CLOCK, + CLOCK_FORMAT, + TIMEZONE, + SYNC_TIME, + DETECT_TIMEZONE, + NONE + }; + + struct MenuItem { + Action action; + StrId labelId; + bool isSeparator = false; + }; + ButtonNavigator buttonNavigator; int selectedIndex = 0; + const std::vector menuItems; + static std::vector buildMenuItems(); void handleSelection(); + std::function buildSelectablePredicate() const; public: explicit ClockSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) - : Activity("ClockSettings", renderer, mappedInput) {} + : Activity("ClockSettings", renderer, mappedInput), menuItems(buildMenuItems()) {} void onEnter() override; void onExit() override; void loop() override; From 14dcd8be0f1485c751d05f7b6c136bf402af3a59 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Thu, 9 Apr 2026 21:50:32 +0200 Subject: [PATCH 5/9] More reshaping --- lib/I18n/translations/english.yaml | 9 +++- .../reader/EpubReaderMenuActivity.cpp | 13 +++-- .../reader/EpubReaderMenuActivity.h | 16 +++--- .../settings/ClockSettingsActivity.cpp | 16 +++--- .../settings/ClockSettingsActivity.h | 13 ++--- src/activities/settings/SettingsActivity.cpp | 51 +++++++++++++++---- src/activities/settings/SettingsActivity.h | 17 ++++++- src/components/UITheme.cpp | 12 ++--- src/components/UITheme.h | 4 +- src/util/ButtonNavigator.h | 4 +- src/util/MenuItemHelpers.h | 48 +++++++++++++++++ 11 files changed, 144 insertions(+), 59 deletions(-) create mode 100644 src/util/MenuItemHelpers.h diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index b453bae4..15f19f6d 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -60,6 +60,7 @@ STR_CALIBRE_INSTRUCTION_4: "\"Keep this screen open while sending\"" STR_CAT_DISPLAY: "Display" STR_CAT_READER: "Reader" STR_CAT_CONTROLS: "Controls" +STR_CAT_NETWORK: "Network" STR_CAT_SYSTEM: "System" STR_SLEEP_SCREEN: "Sleep Screen" STR_SLEEP_COVER_MODE: "Sleep Screen Cover Mode" @@ -468,4 +469,10 @@ STR_WEATHER_MOON_INFO: "Moon" STR_WEATHER_SUN_INFO: "Sun" STR_READER_TOOLS: "Tools" STR_READER_NAVIGATION: "Navigation" -STR_READER_APPEARANCE: "Appearance" \ No newline at end of file +STR_READER_APPEARANCE: "Appearance" +STR_SETTINGS_MENU_SYSTEM: "System" +STR_SETTINGS_MENU_TOOLS: "Tools" +STR_SETTINGS_MENU_NETWORK: "Network" +STR_SETTINGS_MENU_SETTINGS: "Behaviour" +STR_SETTINGS_MENU_SOURCES: "Online Sources" +STR_SETTINGS_MENU_SYNC: "Synchronisation" \ No newline at end of file diff --git a/src/activities/reader/EpubReaderMenuActivity.cpp b/src/activities/reader/EpubReaderMenuActivity.cpp index 1a5bfef9..3ac7bcf2 100644 --- a/src/activities/reader/EpubReaderMenuActivity.cpp +++ b/src/activities/reader/EpubReaderMenuActivity.cpp @@ -7,6 +7,7 @@ #include "MappedInputManager.h" #include "components/UITheme.h" #include "fontIds.h" +#include "util/MenuItemHelpers.h" EpubReaderMenuActivity::EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title, const int currentPage, const int totalPages, @@ -29,7 +30,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(makeSeparatorMenuItem(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 +39,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(makeSeparatorMenuItem(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 +47,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(makeSeparatorMenuItem(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(makeSeparatorMenuItem(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}); @@ -61,9 +62,7 @@ std::vector EpubReaderMenuActivity::buildMenuI } std::function EpubReaderMenuActivity::buildSelectablePredicate() const { - return [this](int index) { - return index >= 0 && index < static_cast(menuItems.size()) && !menuItems[index].isSeparator; - }; + return makeSelectablePredicate(menuItems); } void EpubReaderMenuActivity::onEnter() { diff --git a/src/activities/reader/EpubReaderMenuActivity.h b/src/activities/reader/EpubReaderMenuActivity.h index 93bd66d8..681e6746 100644 --- a/src/activities/reader/EpubReaderMenuActivity.h +++ b/src/activities/reader/EpubReaderMenuActivity.h @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -29,6 +30,12 @@ class EpubReaderMenuActivity final : public Activity { DELETE_CACHE }; + struct MenuItem { + MenuAction action; + StrId labelId; + bool isSeparator = false; + }; + explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title, const int currentPage, const int totalPages, const int bookProgressPercent, const uint8_t currentOrientation, const bool hasFootnotes, @@ -40,17 +47,10 @@ class EpubReaderMenuActivity final : public Activity { void loop() override; void render(RenderLock&&) override; - private: - struct MenuItem { - MenuAction action; - StrId labelId; - bool isSeparator = false; - }; - static std::vector buildMenuItems(bool hasFootnotes); - std::function buildSelectablePredicate() const; + private: // Fixed menu layout const std::vector menuItems; diff --git a/src/activities/settings/ClockSettingsActivity.cpp b/src/activities/settings/ClockSettingsActivity.cpp index 95b533cc..a09b76f2 100644 --- a/src/activities/settings/ClockSettingsActivity.cpp +++ b/src/activities/settings/ClockSettingsActivity.cpp @@ -10,6 +10,7 @@ #include "SyncTimeActivity.h" #include "components/UITheme.h" #include "fontIds.h" +#include "util/MenuItemHelpers.h" namespace { const StrId timeZoneNames[CrossPointSettings::TIMEZONE_COUNT] = { @@ -23,22 +24,20 @@ std::vector ClockSettingsActivity::buildMenuIte std::vector items; items.reserve(7); // Settings - items.push_back({Action::NONE, StrId::STR_SETTINGS_TITLE, true}); + items.push_back(makeSeparatorMenuItem(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::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(makeSeparatorMenuItem(StrId::STR_READER_TOOLS)); 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; } std::function ClockSettingsActivity::buildSelectablePredicate() const { - return [this](int index) { - return index >= 0 && index < static_cast(menuItems.size()) && !menuItems[index].isSeparator; - }; + return makeSelectablePredicate(menuItems); } void ClockSettingsActivity::onEnter() { @@ -124,8 +123,7 @@ void ClockSettingsActivity::render(RenderLock&&) { const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2; GUI.drawList( - renderer, Rect{0, contentTop, pageWidth, contentHeight}, static_cast(menuItems.size()), - selectedIndex, + renderer, Rect{0, contentTop, pageWidth, contentHeight}, static_cast(menuItems.size()), selectedIndex, [this](int index) { const auto title = I18N.get(menuItems[index].labelId); return menuItems[index].isSeparator ? UITheme::makeSeparatorTitle(title) : title; diff --git a/src/activities/settings/ClockSettingsActivity.h b/src/activities/settings/ClockSettingsActivity.h index 974259e4..ffeea5d9 100644 --- a/src/activities/settings/ClockSettingsActivity.h +++ b/src/activities/settings/ClockSettingsActivity.h @@ -1,21 +1,14 @@ #pragma once +#include + #include #include -#include - #include "activities/Activity.h" #include "util/ButtonNavigator.h" class ClockSettingsActivity final : public Activity { - enum class Action { - USE_CLOCK, - CLOCK_FORMAT, - TIMEZONE, - SYNC_TIME, - DETECT_TIMEZONE, - NONE - }; + enum class Action { USE_CLOCK, CLOCK_FORMAT, TIMEZONE, SYNC_TIME, DETECT_TIMEZONE, NONE }; struct MenuItem { Action action; diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index d64ed44f..4afeece0 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -22,9 +22,11 @@ #include "activities/weather/WeatherSettingsActivity.h" #include "components/UITheme.h" #include "fontIds.h" +#include "util/MenuItemHelpers.h" 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_NETWORK, + StrId::STR_CAT_SYSTEM}; void SettingsActivity::onEnter() { Activity::onEnter(); @@ -33,7 +35,9 @@ void SettingsActivity::onEnter() { displaySettings.clear(); readerSettings.clear(); controlsSettings.clear(); + networkSettings.clear(); systemSettings.clear(); + systemSettings.push_back(SettingInfo::Separator(StrId::STR_SETTINGS_MENU_SETTINGS)); for (const auto& setting : getSettingsList()) { if (setting.category == StrId::STR_NONE_OPT) continue; @@ -50,6 +54,8 @@ void SettingsActivity::onEnter() { controlsSettings.push_back(setting); } else if (setting.category == StrId::STR_CAT_SYSTEM) { systemSettings.push_back(setting); + } else if (setting.category == StrId::STR_CAT_NETWORK) { + networkSettings.push_back(setting); } // Web-only categories (KOReader Sync, OPDS Browser) are skipped for device UI } @@ -57,15 +63,26 @@ void SettingsActivity::onEnter() { // Append device-only ACTION items controlsSettings.insert(controlsSettings.begin(), SettingInfo::Action(StrId::STR_REMAP_FRONT_BUTTONS, SettingAction::RemapFrontButtons)); + + networkSettings.push_back(SettingInfo::Separator(StrId::STR_SETTINGS_MENU_NETWORK)); + networkSettings.push_back(SettingInfo::Action(StrId::STR_WIFI_NETWORKS, SettingAction::Network)); + networkSettings.push_back(SettingInfo::Separator(StrId::STR_SETTINGS_MENU_SOURCES)); + networkSettings.push_back(SettingInfo::Action(StrId::STR_OPDS_BROWSER, SettingAction::OPDSBrowser)); + networkSettings.push_back(SettingInfo::Separator(StrId::STR_SETTINGS_MENU_SYNC)); + networkSettings.push_back(SettingInfo::Action(StrId::STR_KOREADER_SYNC, SettingAction::KOReaderSync)); + + // System settings with actions + systemSettings.push_back(SettingInfo::Action(StrId::STR_LANGUAGE, SettingAction::Language)); + + systemSettings.push_back(SettingInfo::Separator(StrId::STR_SETTINGS_MENU_TOOLS)); systemSettings.push_back(SettingInfo::Action(StrId::STR_CLOCK_SETTINGS, SettingAction::ClockSettings)); - systemSettings.push_back(SettingInfo::Action(StrId::STR_WIFI_NETWORKS, SettingAction::Network)); - systemSettings.push_back(SettingInfo::Action(StrId::STR_KOREADER_SYNC, SettingAction::KOReaderSync)); - systemSettings.push_back(SettingInfo::Action(StrId::STR_OPDS_BROWSER, SettingAction::OPDSBrowser)); + systemSettings.push_back(SettingInfo::Action(StrId::STR_WEATHER_SETTINGS, SettingAction::Weather)); + + systemSettings.push_back(SettingInfo::Separator(StrId::STR_SETTINGS_MENU_SYSTEM)); systemSettings.push_back(SettingInfo::Action(StrId::STR_CLEAR_READING_CACHE, SettingAction::ClearCache)); systemSettings.push_back(SettingInfo::Action(StrId::STR_CHECK_UPDATES, SettingAction::CheckForUpdates)); - systemSettings.push_back(SettingInfo::Action(StrId::STR_LANGUAGE, SettingAction::Language)); systemSettings.push_back(SettingInfo::Action(StrId::STR_SYSTEM_INFO, SettingAction::SystemInfo)); - systemSettings.push_back(SettingInfo::Action(StrId::STR_WEATHER_SETTINGS, SettingAction::Weather)); + readerSettings.push_back(SettingInfo::Action(StrId::STR_CUSTOMISE_STATUS_BAR, SettingAction::CustomiseStatusBar)); // Reset selection to first category @@ -75,11 +92,16 @@ void SettingsActivity::onEnter() { // Initialize with first category (Display) currentSettings = &displaySettings; settingsCount = static_cast(displaySettings.size()); + buttonNavigator.setSelectablePredicate(makeSelectablePredicate(*currentSettings, 1, true), settingsCount + 1); // Trigger first update requestUpdate(); } +std::function SettingsActivity::buildSelectablePredicate() const { + return makeSelectablePredicate(*currentSettings, 1, true); +} + void SettingsActivity::onExit() { Activity::onExit(); @@ -115,12 +137,12 @@ void SettingsActivity::loop() { // Handle navigation buttonNavigator.onNextRelease([this] { - selectedSettingIndex = ButtonNavigator::nextIndex(selectedSettingIndex, settingsCount + 1); + selectedSettingIndex = buttonNavigator.nextIndex(selectedSettingIndex); requestUpdate(); }); buttonNavigator.onPreviousRelease([this] { - selectedSettingIndex = ButtonNavigator::previousIndex(selectedSettingIndex, settingsCount + 1); + selectedSettingIndex = buttonNavigator.previousIndex(selectedSettingIndex); requestUpdate(); }); @@ -149,10 +171,14 @@ void SettingsActivity::loop() { currentSettings = &controlsSettings; break; case 3: + currentSettings = &networkSettings; + break; + case 4: currentSettings = &systemSettings; break; } settingsCount = static_cast(currentSettings->size()); + buttonNavigator.setSelectablePredicate(makeSelectablePredicate(*currentSettings, 1, true), settingsCount + 1); } } @@ -163,6 +189,9 @@ void SettingsActivity::toggleCurrentSetting() { } const auto& setting = (*currentSettings)[selectedSetting]; + if (setting.isSeparator) { + return; + } if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) { // Toggle the boolean value using the member pointer @@ -259,7 +288,11 @@ void SettingsActivity::render(RenderLock&&) { contentRect.height - (metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing * 2)}, settingsCount, selectedSettingIndex - 1, - [&settings](int index) { return std::string(I18N.get(settings[index].nameId)); }, nullptr, nullptr, + [&settings](int index) { + const auto title = I18N.get(settings[index].nameId); + return settings[index].isSeparator ? UITheme::makeSeparatorTitle(title) : title; + }, + nullptr, nullptr, [&settings](int i) { const auto& setting = settings[i]; std::string valueText = ""; diff --git a/src/activities/settings/SettingsActivity.h b/src/activities/settings/SettingsActivity.h index 1a1bf5f6..4c65f8da 100644 --- a/src/activities/settings/SettingsActivity.h +++ b/src/activities/settings/SettingsActivity.h @@ -30,6 +30,7 @@ enum class SettingAction { struct SettingInfo { StrId nameId; + bool isSeparator = false; SettingType type; uint8_t CrossPointSettings::* valuePtr = nullptr; std::vector enumValues; @@ -84,11 +85,21 @@ struct SettingInfo { return s; } - static SettingInfo Action(StrId nameId, SettingAction action) { + static SettingInfo Action(StrId nameId, SettingAction action, bool isSeparator = false) { SettingInfo s; s.nameId = nameId; s.type = SettingType::ACTION; s.action = action; + s.isSeparator = isSeparator; + return s; + } + + static SettingInfo Separator(StrId nameId) { + SettingInfo s; + s.nameId = nameId; + s.type = SettingType::ACTION; + s.action = SettingAction::None; + s.isSeparator = true; return s; } @@ -155,14 +166,16 @@ class SettingsActivity final : public Activity { std::vector displaySettings; std::vector readerSettings; std::vector controlsSettings; + std::vector networkSettings; std::vector systemSettings; const std::vector* currentSettings = nullptr; - static constexpr int categoryCount = 4; + static constexpr int categoryCount = 5; static const StrId categoryNames[categoryCount]; void enterCategory(int categoryIndex); void toggleCurrentSetting(); + std::function buildSelectablePredicate() const; public: explicit SettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) diff --git a/src/components/UITheme.cpp b/src/components/UITheme.cpp index 7b76f617..67a09e16 100644 --- a/src/components/UITheme.cpp +++ b/src/components/UITheme.cpp @@ -98,17 +98,11 @@ Rect UITheme::getContentRect(const GfxRenderer& renderer, bool hasBottomHints, b return Rect{left, top, w - left - right, h - top - bottom}; } -std::string UITheme::makeSeparatorTitle(const std::string& title) { - return std::string("__") + title; -} +std::string UITheme::makeSeparatorTitle(const std::string& title) { return std::string("__") + title; } -std::string UITheme::makeSeparatorTitle(StrId labelId) { - return std::string("__") + I18N.get(labelId); -} +std::string UITheme::makeSeparatorTitle(StrId labelId) { return std::string("__") + I18N.get(labelId); } -bool UITheme::isSeparatorTitle(const std::string& title) { - return title.rfind("__", 0) == 0; -} +bool UITheme::isSeparatorTitle(const std::string& title) { return title.rfind("__", 0) == 0; } std::string UITheme::stripSeparatorTitle(const std::string& title) { return isSeparatorTitle(title) ? title.substr(2) : title; diff --git a/src/components/UITheme.h b/src/components/UITheme.h index 17d5c997..abdc1906 100644 --- a/src/components/UITheme.h +++ b/src/components/UITheme.h @@ -1,10 +1,10 @@ #pragma once +#include + #include #include -#include - #include "CrossPointSettings.h" #include "components/themes/BaseTheme.h" diff --git a/src/util/ButtonNavigator.h b/src/util/ButtonNavigator.h index 11a17449..b76df714 100644 --- a/src/util/ButtonNavigator.h +++ b/src/util/ButtonNavigator.h @@ -45,9 +45,9 @@ class ButtonNavigator final { [[nodiscard]] static int nextIndex(int currentIndex, const std::vector& selectable); [[nodiscard]] static int previousIndex(int currentIndex, const std::vector& selectable); [[nodiscard]] static int nextIndex(int currentIndex, int totalItems, - const std::function& isSelectable); + const std::function& isSelectable); [[nodiscard]] static int previousIndex(int currentIndex, int totalItems, - const std::function& isSelectable); + const std::function& isSelectable); [[nodiscard]] int nextIndex(int currentIndex) const; [[nodiscard]] int previousIndex(int currentIndex) const; diff --git a/src/util/MenuItemHelpers.h b/src/util/MenuItemHelpers.h new file mode 100644 index 00000000..5c89f7ba --- /dev/null +++ b/src/util/MenuItemHelpers.h @@ -0,0 +1,48 @@ +#pragma once + +#include + +#include +#include +#include + +// Generic helper for constructing separator rows in menu item vectors. +// The item type must have an `action`, an `isSeparator`, and either a `labelId` or `nameId` member. +template +inline ItemType makeSeparatorMenuItem(StrId labelId) { + ItemType item{}; + if constexpr (std::is_member_object_pointer_v) { + item.action = static_cast(0); + } + if constexpr (std::is_member_object_pointer_v) { + item.labelId = labelId; + } else if constexpr (std::is_member_object_pointer_v) { + item.nameId = labelId; + } else { + static_assert(sizeof(ItemType) == 0, + "makeSeparatorMenuItem requires ItemType with labelId or nameId member"); + } + item.isSeparator = true; + return item; +} + +// Generic helper for creating a selectable predicate for menu lists. +// The item type must have an `isSeparator` member. +template +inline std::function makeSelectablePredicate(const std::vector& items) { + return + [&items](int index) { return index >= 0 && index < static_cast(items.size()) && !items[index].isSeparator; }; +} + +template +inline std::function makeSelectablePredicate(const std::vector& items, int indexOffset, + bool firstIndexSelectable) { + return [&items, indexOffset, firstIndexSelectable](int index) { + if (firstIndexSelectable && index == 0) { + return true; + } + + const int itemIndex = index - indexOffset; + return itemIndex >= 0 && itemIndex < static_cast(items.size()) && !items[itemIndex].isSeparator; + }; +} From 83bc8b41a4caccbcf2995284ec56fd020e5c7b66 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Thu, 9 Apr 2026 22:49:02 +0200 Subject: [PATCH 6/9] Revert "More reshaping" This reverts commit 14dcd8be0f1485c751d05f7b6c136bf402af3a59. --- lib/I18n/translations/english.yaml | 9 +--- .../reader/EpubReaderMenuActivity.cpp | 13 ++--- .../reader/EpubReaderMenuActivity.h | 16 +++--- .../settings/ClockSettingsActivity.cpp | 16 +++--- .../settings/ClockSettingsActivity.h | 13 +++-- src/activities/settings/SettingsActivity.cpp | 51 ++++--------------- src/activities/settings/SettingsActivity.h | 17 +------ src/components/UITheme.cpp | 12 +++-- src/components/UITheme.h | 4 +- src/util/ButtonNavigator.h | 4 +- src/util/MenuItemHelpers.h | 48 ----------------- 11 files changed, 59 insertions(+), 144 deletions(-) delete mode 100644 src/util/MenuItemHelpers.h diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 15f19f6d..b453bae4 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -60,7 +60,6 @@ STR_CALIBRE_INSTRUCTION_4: "\"Keep this screen open while sending\"" STR_CAT_DISPLAY: "Display" STR_CAT_READER: "Reader" STR_CAT_CONTROLS: "Controls" -STR_CAT_NETWORK: "Network" STR_CAT_SYSTEM: "System" STR_SLEEP_SCREEN: "Sleep Screen" STR_SLEEP_COVER_MODE: "Sleep Screen Cover Mode" @@ -469,10 +468,4 @@ STR_WEATHER_MOON_INFO: "Moon" STR_WEATHER_SUN_INFO: "Sun" STR_READER_TOOLS: "Tools" STR_READER_NAVIGATION: "Navigation" -STR_READER_APPEARANCE: "Appearance" -STR_SETTINGS_MENU_SYSTEM: "System" -STR_SETTINGS_MENU_TOOLS: "Tools" -STR_SETTINGS_MENU_NETWORK: "Network" -STR_SETTINGS_MENU_SETTINGS: "Behaviour" -STR_SETTINGS_MENU_SOURCES: "Online Sources" -STR_SETTINGS_MENU_SYNC: "Synchronisation" \ No newline at end of file +STR_READER_APPEARANCE: "Appearance" \ No newline at end of file diff --git a/src/activities/reader/EpubReaderMenuActivity.cpp b/src/activities/reader/EpubReaderMenuActivity.cpp index 3ac7bcf2..1a5bfef9 100644 --- a/src/activities/reader/EpubReaderMenuActivity.cpp +++ b/src/activities/reader/EpubReaderMenuActivity.cpp @@ -7,7 +7,6 @@ #include "MappedInputManager.h" #include "components/UITheme.h" #include "fontIds.h" -#include "util/MenuItemHelpers.h" EpubReaderMenuActivity::EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title, const int currentPage, const int totalPages, @@ -30,7 +29,7 @@ std::vector EpubReaderMenuActivity::buildMenuI std::vector items; items.reserve(18); // Navigation - items.push_back(makeSeparatorMenuItem(StrId::STR_READER_NAVIGATION)); + items.push_back({MenuAction::NONE, StrId::STR_READER_NAVIGATION, true}); items.push_back({MenuAction::SELECT_CHAPTER, StrId::STR_SELECT_CHAPTER}); items.push_back({MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT}); if (hasFootnotes) { @@ -39,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(makeSeparatorMenuItem(StrId::STR_READER_APPEARANCE)); + items.push_back({MenuAction::NONE, StrId::STR_READER_APPEARANCE, true}); 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}); @@ -47,13 +46,13 @@ std::vector EpubReaderMenuActivity::buildMenuI // Synchronisation (only if credentials are set, to avoid confusion) if (KOREADER_STORE.hasCredentials()) { - items.push_back(makeSeparatorMenuItem(StrId::STR_KOREADER_SYNC)); + items.push_back({MenuAction::NONE, StrId::STR_KOREADER_SYNC, true}); 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(makeSeparatorMenuItem(StrId::STR_READER_TOOLS)); + items.push_back({MenuAction::NONE, StrId::STR_READER_TOOLS, true}); 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}); @@ -62,7 +61,9 @@ std::vector EpubReaderMenuActivity::buildMenuI } std::function EpubReaderMenuActivity::buildSelectablePredicate() const { - return makeSelectablePredicate(menuItems); + return [this](int index) { + return index >= 0 && index < static_cast(menuItems.size()) && !menuItems[index].isSeparator; + }; } void EpubReaderMenuActivity::onEnter() { diff --git a/src/activities/reader/EpubReaderMenuActivity.h b/src/activities/reader/EpubReaderMenuActivity.h index 681e6746..93bd66d8 100644 --- a/src/activities/reader/EpubReaderMenuActivity.h +++ b/src/activities/reader/EpubReaderMenuActivity.h @@ -2,7 +2,6 @@ #include #include -#include #include #include @@ -30,12 +29,6 @@ class EpubReaderMenuActivity final : public Activity { DELETE_CACHE }; - struct MenuItem { - MenuAction action; - StrId labelId; - bool isSeparator = false; - }; - explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title, const int currentPage, const int totalPages, const int bookProgressPercent, const uint8_t currentOrientation, const bool hasFootnotes, @@ -47,10 +40,17 @@ class EpubReaderMenuActivity final : public Activity { void loop() override; void render(RenderLock&&) override; + private: + struct MenuItem { + MenuAction action; + StrId labelId; + bool isSeparator = false; + }; + static std::vector buildMenuItems(bool hasFootnotes); + std::function buildSelectablePredicate() const; - private: // Fixed menu layout const std::vector menuItems; diff --git a/src/activities/settings/ClockSettingsActivity.cpp b/src/activities/settings/ClockSettingsActivity.cpp index a09b76f2..95b533cc 100644 --- a/src/activities/settings/ClockSettingsActivity.cpp +++ b/src/activities/settings/ClockSettingsActivity.cpp @@ -10,7 +10,6 @@ #include "SyncTimeActivity.h" #include "components/UITheme.h" #include "fontIds.h" -#include "util/MenuItemHelpers.h" namespace { const StrId timeZoneNames[CrossPointSettings::TIMEZONE_COUNT] = { @@ -24,20 +23,22 @@ std::vector ClockSettingsActivity::buildMenuIte std::vector items; items.reserve(7); // Settings - items.push_back(makeSeparatorMenuItem(StrId::STR_SETTINGS_TITLE)); + items.push_back({Action::NONE, StrId::STR_SETTINGS_TITLE, true}); 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}); // Tools - items.push_back(makeSeparatorMenuItem(StrId::STR_READER_TOOLS)); + items.push_back({Action::NONE, StrId::STR_READER_TOOLS, true}); 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; } std::function ClockSettingsActivity::buildSelectablePredicate() const { - return makeSelectablePredicate(menuItems); + return [this](int index) { + return index >= 0 && index < static_cast(menuItems.size()) && !menuItems[index].isSeparator; + }; } void ClockSettingsActivity::onEnter() { @@ -123,7 +124,8 @@ void ClockSettingsActivity::render(RenderLock&&) { const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2; GUI.drawList( - renderer, Rect{0, contentTop, pageWidth, contentHeight}, static_cast(menuItems.size()), selectedIndex, + renderer, Rect{0, contentTop, pageWidth, contentHeight}, static_cast(menuItems.size()), + selectedIndex, [this](int index) { const auto title = I18N.get(menuItems[index].labelId); return menuItems[index].isSeparator ? UITheme::makeSeparatorTitle(title) : title; diff --git a/src/activities/settings/ClockSettingsActivity.h b/src/activities/settings/ClockSettingsActivity.h index ffeea5d9..974259e4 100644 --- a/src/activities/settings/ClockSettingsActivity.h +++ b/src/activities/settings/ClockSettingsActivity.h @@ -1,14 +1,21 @@ #pragma once -#include - #include #include +#include + #include "activities/Activity.h" #include "util/ButtonNavigator.h" class ClockSettingsActivity final : public Activity { - enum class Action { USE_CLOCK, CLOCK_FORMAT, TIMEZONE, SYNC_TIME, DETECT_TIMEZONE, NONE }; + enum class Action { + USE_CLOCK, + CLOCK_FORMAT, + TIMEZONE, + SYNC_TIME, + DETECT_TIMEZONE, + NONE + }; struct MenuItem { Action action; diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 4afeece0..d64ed44f 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -22,11 +22,9 @@ #include "activities/weather/WeatherSettingsActivity.h" #include "components/UITheme.h" #include "fontIds.h" -#include "util/MenuItemHelpers.h" const StrId SettingsActivity::categoryNames[categoryCount] = {StrId::STR_CAT_DISPLAY, StrId::STR_CAT_READER, - StrId::STR_CAT_CONTROLS, StrId::STR_CAT_NETWORK, - StrId::STR_CAT_SYSTEM}; + StrId::STR_CAT_CONTROLS, StrId::STR_CAT_SYSTEM}; void SettingsActivity::onEnter() { Activity::onEnter(); @@ -35,9 +33,7 @@ void SettingsActivity::onEnter() { displaySettings.clear(); readerSettings.clear(); controlsSettings.clear(); - networkSettings.clear(); systemSettings.clear(); - systemSettings.push_back(SettingInfo::Separator(StrId::STR_SETTINGS_MENU_SETTINGS)); for (const auto& setting : getSettingsList()) { if (setting.category == StrId::STR_NONE_OPT) continue; @@ -54,8 +50,6 @@ void SettingsActivity::onEnter() { controlsSettings.push_back(setting); } else if (setting.category == StrId::STR_CAT_SYSTEM) { systemSettings.push_back(setting); - } else if (setting.category == StrId::STR_CAT_NETWORK) { - networkSettings.push_back(setting); } // Web-only categories (KOReader Sync, OPDS Browser) are skipped for device UI } @@ -63,26 +57,15 @@ void SettingsActivity::onEnter() { // Append device-only ACTION items controlsSettings.insert(controlsSettings.begin(), SettingInfo::Action(StrId::STR_REMAP_FRONT_BUTTONS, SettingAction::RemapFrontButtons)); - - networkSettings.push_back(SettingInfo::Separator(StrId::STR_SETTINGS_MENU_NETWORK)); - networkSettings.push_back(SettingInfo::Action(StrId::STR_WIFI_NETWORKS, SettingAction::Network)); - networkSettings.push_back(SettingInfo::Separator(StrId::STR_SETTINGS_MENU_SOURCES)); - networkSettings.push_back(SettingInfo::Action(StrId::STR_OPDS_BROWSER, SettingAction::OPDSBrowser)); - networkSettings.push_back(SettingInfo::Separator(StrId::STR_SETTINGS_MENU_SYNC)); - networkSettings.push_back(SettingInfo::Action(StrId::STR_KOREADER_SYNC, SettingAction::KOReaderSync)); - - // System settings with actions - systemSettings.push_back(SettingInfo::Action(StrId::STR_LANGUAGE, SettingAction::Language)); - - systemSettings.push_back(SettingInfo::Separator(StrId::STR_SETTINGS_MENU_TOOLS)); systemSettings.push_back(SettingInfo::Action(StrId::STR_CLOCK_SETTINGS, SettingAction::ClockSettings)); - systemSettings.push_back(SettingInfo::Action(StrId::STR_WEATHER_SETTINGS, SettingAction::Weather)); - - systemSettings.push_back(SettingInfo::Separator(StrId::STR_SETTINGS_MENU_SYSTEM)); + systemSettings.push_back(SettingInfo::Action(StrId::STR_WIFI_NETWORKS, SettingAction::Network)); + systemSettings.push_back(SettingInfo::Action(StrId::STR_KOREADER_SYNC, SettingAction::KOReaderSync)); + systemSettings.push_back(SettingInfo::Action(StrId::STR_OPDS_BROWSER, SettingAction::OPDSBrowser)); systemSettings.push_back(SettingInfo::Action(StrId::STR_CLEAR_READING_CACHE, SettingAction::ClearCache)); systemSettings.push_back(SettingInfo::Action(StrId::STR_CHECK_UPDATES, SettingAction::CheckForUpdates)); + systemSettings.push_back(SettingInfo::Action(StrId::STR_LANGUAGE, SettingAction::Language)); systemSettings.push_back(SettingInfo::Action(StrId::STR_SYSTEM_INFO, SettingAction::SystemInfo)); - + systemSettings.push_back(SettingInfo::Action(StrId::STR_WEATHER_SETTINGS, SettingAction::Weather)); readerSettings.push_back(SettingInfo::Action(StrId::STR_CUSTOMISE_STATUS_BAR, SettingAction::CustomiseStatusBar)); // Reset selection to first category @@ -92,16 +75,11 @@ void SettingsActivity::onEnter() { // Initialize with first category (Display) currentSettings = &displaySettings; settingsCount = static_cast(displaySettings.size()); - buttonNavigator.setSelectablePredicate(makeSelectablePredicate(*currentSettings, 1, true), settingsCount + 1); // Trigger first update requestUpdate(); } -std::function SettingsActivity::buildSelectablePredicate() const { - return makeSelectablePredicate(*currentSettings, 1, true); -} - void SettingsActivity::onExit() { Activity::onExit(); @@ -137,12 +115,12 @@ void SettingsActivity::loop() { // Handle navigation buttonNavigator.onNextRelease([this] { - selectedSettingIndex = buttonNavigator.nextIndex(selectedSettingIndex); + selectedSettingIndex = ButtonNavigator::nextIndex(selectedSettingIndex, settingsCount + 1); requestUpdate(); }); buttonNavigator.onPreviousRelease([this] { - selectedSettingIndex = buttonNavigator.previousIndex(selectedSettingIndex); + selectedSettingIndex = ButtonNavigator::previousIndex(selectedSettingIndex, settingsCount + 1); requestUpdate(); }); @@ -171,14 +149,10 @@ void SettingsActivity::loop() { currentSettings = &controlsSettings; break; case 3: - currentSettings = &networkSettings; - break; - case 4: currentSettings = &systemSettings; break; } settingsCount = static_cast(currentSettings->size()); - buttonNavigator.setSelectablePredicate(makeSelectablePredicate(*currentSettings, 1, true), settingsCount + 1); } } @@ -189,9 +163,6 @@ void SettingsActivity::toggleCurrentSetting() { } const auto& setting = (*currentSettings)[selectedSetting]; - if (setting.isSeparator) { - return; - } if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) { // Toggle the boolean value using the member pointer @@ -288,11 +259,7 @@ void SettingsActivity::render(RenderLock&&) { contentRect.height - (metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing * 2)}, settingsCount, selectedSettingIndex - 1, - [&settings](int index) { - const auto title = I18N.get(settings[index].nameId); - return settings[index].isSeparator ? UITheme::makeSeparatorTitle(title) : title; - }, - nullptr, nullptr, + [&settings](int index) { return std::string(I18N.get(settings[index].nameId)); }, nullptr, nullptr, [&settings](int i) { const auto& setting = settings[i]; std::string valueText = ""; diff --git a/src/activities/settings/SettingsActivity.h b/src/activities/settings/SettingsActivity.h index 4c65f8da..1a1bf5f6 100644 --- a/src/activities/settings/SettingsActivity.h +++ b/src/activities/settings/SettingsActivity.h @@ -30,7 +30,6 @@ enum class SettingAction { struct SettingInfo { StrId nameId; - bool isSeparator = false; SettingType type; uint8_t CrossPointSettings::* valuePtr = nullptr; std::vector enumValues; @@ -85,21 +84,11 @@ struct SettingInfo { return s; } - static SettingInfo Action(StrId nameId, SettingAction action, bool isSeparator = false) { + static SettingInfo Action(StrId nameId, SettingAction action) { SettingInfo s; s.nameId = nameId; s.type = SettingType::ACTION; s.action = action; - s.isSeparator = isSeparator; - return s; - } - - static SettingInfo Separator(StrId nameId) { - SettingInfo s; - s.nameId = nameId; - s.type = SettingType::ACTION; - s.action = SettingAction::None; - s.isSeparator = true; return s; } @@ -166,16 +155,14 @@ class SettingsActivity final : public Activity { std::vector displaySettings; std::vector readerSettings; std::vector controlsSettings; - std::vector networkSettings; std::vector systemSettings; const std::vector* currentSettings = nullptr; - static constexpr int categoryCount = 5; + static constexpr int categoryCount = 4; static const StrId categoryNames[categoryCount]; void enterCategory(int categoryIndex); void toggleCurrentSetting(); - std::function buildSelectablePredicate() const; public: explicit SettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) diff --git a/src/components/UITheme.cpp b/src/components/UITheme.cpp index 67a09e16..7b76f617 100644 --- a/src/components/UITheme.cpp +++ b/src/components/UITheme.cpp @@ -98,11 +98,17 @@ Rect UITheme::getContentRect(const GfxRenderer& renderer, bool hasBottomHints, b return Rect{left, top, w - left - right, h - top - bottom}; } -std::string UITheme::makeSeparatorTitle(const std::string& title) { return std::string("__") + title; } +std::string UITheme::makeSeparatorTitle(const std::string& title) { + return std::string("__") + title; +} -std::string UITheme::makeSeparatorTitle(StrId labelId) { return std::string("__") + I18N.get(labelId); } +std::string UITheme::makeSeparatorTitle(StrId labelId) { + return std::string("__") + I18N.get(labelId); +} -bool UITheme::isSeparatorTitle(const std::string& title) { return title.rfind("__", 0) == 0; } +bool UITheme::isSeparatorTitle(const std::string& title) { + return title.rfind("__", 0) == 0; +} std::string UITheme::stripSeparatorTitle(const std::string& title) { return isSeparatorTitle(title) ? title.substr(2) : title; diff --git a/src/components/UITheme.h b/src/components/UITheme.h index abdc1906..17d5c997 100644 --- a/src/components/UITheme.h +++ b/src/components/UITheme.h @@ -1,10 +1,10 @@ #pragma once -#include - #include #include +#include + #include "CrossPointSettings.h" #include "components/themes/BaseTheme.h" diff --git a/src/util/ButtonNavigator.h b/src/util/ButtonNavigator.h index b76df714..11a17449 100644 --- a/src/util/ButtonNavigator.h +++ b/src/util/ButtonNavigator.h @@ -45,9 +45,9 @@ class ButtonNavigator final { [[nodiscard]] static int nextIndex(int currentIndex, const std::vector& selectable); [[nodiscard]] static int previousIndex(int currentIndex, const std::vector& selectable); [[nodiscard]] static int nextIndex(int currentIndex, int totalItems, - const std::function& isSelectable); + const std::function& isSelectable); [[nodiscard]] static int previousIndex(int currentIndex, int totalItems, - const std::function& isSelectable); + const std::function& isSelectable); [[nodiscard]] int nextIndex(int currentIndex) const; [[nodiscard]] int previousIndex(int currentIndex) const; diff --git a/src/util/MenuItemHelpers.h b/src/util/MenuItemHelpers.h deleted file mode 100644 index 5c89f7ba..00000000 --- a/src/util/MenuItemHelpers.h +++ /dev/null @@ -1,48 +0,0 @@ -#pragma once - -#include - -#include -#include -#include - -// Generic helper for constructing separator rows in menu item vectors. -// The item type must have an `action`, an `isSeparator`, and either a `labelId` or `nameId` member. -template -inline ItemType makeSeparatorMenuItem(StrId labelId) { - ItemType item{}; - if constexpr (std::is_member_object_pointer_v) { - item.action = static_cast(0); - } - if constexpr (std::is_member_object_pointer_v) { - item.labelId = labelId; - } else if constexpr (std::is_member_object_pointer_v) { - item.nameId = labelId; - } else { - static_assert(sizeof(ItemType) == 0, - "makeSeparatorMenuItem requires ItemType with labelId or nameId member"); - } - item.isSeparator = true; - return item; -} - -// Generic helper for creating a selectable predicate for menu lists. -// The item type must have an `isSeparator` member. -template -inline std::function makeSelectablePredicate(const std::vector& items) { - return - [&items](int index) { return index >= 0 && index < static_cast(items.size()) && !items[index].isSeparator; }; -} - -template -inline std::function makeSelectablePredicate(const std::vector& items, int indexOffset, - bool firstIndexSelectable) { - return [&items, indexOffset, firstIndexSelectable](int index) { - if (firstIndexSelectable && index == 0) { - return true; - } - - const int itemIndex = index - indexOffset; - return itemIndex >= 0 && itemIndex < static_cast(items.size()) && !items[itemIndex].isSeparator; - }; -} From 6cae4d4b89d6d2424e26e38a9c6733846a38c4e3 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Thu, 9 Apr 2026 22:49:36 +0200 Subject: [PATCH 7/9] yaclf --- src/activities/settings/ClockSettingsActivity.cpp | 7 +++---- src/activities/settings/ClockSettingsActivity.h | 13 +++---------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/src/activities/settings/ClockSettingsActivity.cpp b/src/activities/settings/ClockSettingsActivity.cpp index 95b533cc..9b99f3a8 100644 --- a/src/activities/settings/ClockSettingsActivity.cpp +++ b/src/activities/settings/ClockSettingsActivity.cpp @@ -25,13 +25,13 @@ std::vector ClockSettingsActivity::buildMenuIte // Settings items.push_back({Action::NONE, StrId::STR_SETTINGS_TITLE, true}); 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}); // Tools items.push_back({Action::NONE, StrId::STR_READER_TOOLS, true}); 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; } @@ -124,8 +124,7 @@ void ClockSettingsActivity::render(RenderLock&&) { const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2; GUI.drawList( - renderer, Rect{0, contentTop, pageWidth, contentHeight}, static_cast(menuItems.size()), - selectedIndex, + renderer, Rect{0, contentTop, pageWidth, contentHeight}, static_cast(menuItems.size()), selectedIndex, [this](int index) { const auto title = I18N.get(menuItems[index].labelId); return menuItems[index].isSeparator ? UITheme::makeSeparatorTitle(title) : title; diff --git a/src/activities/settings/ClockSettingsActivity.h b/src/activities/settings/ClockSettingsActivity.h index 974259e4..ffeea5d9 100644 --- a/src/activities/settings/ClockSettingsActivity.h +++ b/src/activities/settings/ClockSettingsActivity.h @@ -1,21 +1,14 @@ #pragma once +#include + #include #include -#include - #include "activities/Activity.h" #include "util/ButtonNavigator.h" class ClockSettingsActivity final : public Activity { - enum class Action { - USE_CLOCK, - CLOCK_FORMAT, - TIMEZONE, - SYNC_TIME, - DETECT_TIMEZONE, - NONE - }; + enum class Action { USE_CLOCK, CLOCK_FORMAT, TIMEZONE, SYNC_TIME, DETECT_TIMEZONE, NONE }; struct MenuItem { Action action; From 6e7e86b9b6ed146afd1a83543bf4e3f23a7d6809 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Thu, 9 Apr 2026 22:51:07 +0200 Subject: [PATCH 8/9] Ayayaclf --- src/components/UITheme.cpp | 12 +++--------- src/components/UITheme.h | 4 ++-- src/util/ButtonNavigator.h | 4 ++-- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/components/UITheme.cpp b/src/components/UITheme.cpp index 7b76f617..67a09e16 100644 --- a/src/components/UITheme.cpp +++ b/src/components/UITheme.cpp @@ -98,17 +98,11 @@ Rect UITheme::getContentRect(const GfxRenderer& renderer, bool hasBottomHints, b return Rect{left, top, w - left - right, h - top - bottom}; } -std::string UITheme::makeSeparatorTitle(const std::string& title) { - return std::string("__") + title; -} +std::string UITheme::makeSeparatorTitle(const std::string& title) { return std::string("__") + title; } -std::string UITheme::makeSeparatorTitle(StrId labelId) { - return std::string("__") + I18N.get(labelId); -} +std::string UITheme::makeSeparatorTitle(StrId labelId) { return std::string("__") + I18N.get(labelId); } -bool UITheme::isSeparatorTitle(const std::string& title) { - return title.rfind("__", 0) == 0; -} +bool UITheme::isSeparatorTitle(const std::string& title) { return title.rfind("__", 0) == 0; } std::string UITheme::stripSeparatorTitle(const std::string& title) { return isSeparatorTitle(title) ? title.substr(2) : title; diff --git a/src/components/UITheme.h b/src/components/UITheme.h index 17d5c997..abdc1906 100644 --- a/src/components/UITheme.h +++ b/src/components/UITheme.h @@ -1,10 +1,10 @@ #pragma once +#include + #include #include -#include - #include "CrossPointSettings.h" #include "components/themes/BaseTheme.h" diff --git a/src/util/ButtonNavigator.h b/src/util/ButtonNavigator.h index 11a17449..b76df714 100644 --- a/src/util/ButtonNavigator.h +++ b/src/util/ButtonNavigator.h @@ -45,9 +45,9 @@ class ButtonNavigator final { [[nodiscard]] static int nextIndex(int currentIndex, const std::vector& selectable); [[nodiscard]] static int previousIndex(int currentIndex, const std::vector& selectable); [[nodiscard]] static int nextIndex(int currentIndex, int totalItems, - const std::function& isSelectable); + const std::function& isSelectable); [[nodiscard]] static int previousIndex(int currentIndex, int totalItems, - const std::function& isSelectable); + const std::function& isSelectable); [[nodiscard]] int nextIndex(int currentIndex) const; [[nodiscard]] int previousIndex(int currentIndex) const; From 9fbd3cc1f787c2a3618bfb7a4bd0d0118b4add6d Mon Sep 17 00:00:00 2001 From: jpirnay Date: Thu, 9 Apr 2026 23:16:32 +0200 Subject: [PATCH 9/9] Review comments --- src/activities/reader/EpubReaderMenuActivity.cpp | 8 ++++---- src/components/themes/BaseTheme.cpp | 4 ---- src/components/themes/lyra/LyraTheme.cpp | 3 --- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/activities/reader/EpubReaderMenuActivity.cpp b/src/activities/reader/EpubReaderMenuActivity.cpp index 1a5bfef9..313f4890 100644 --- a/src/activities/reader/EpubReaderMenuActivity.cpp +++ b/src/activities/reader/EpubReaderMenuActivity.cpp @@ -68,10 +68,10 @@ std::function EpubReaderMenuActivity::buildSelectablePredicate() cons void EpubReaderMenuActivity::onEnter() { Activity::onEnter(); - buttonNavigator.setSelectablePredicate(buildSelectablePredicate(), static_cast(menuItems.size())); - const int next = buttonNavigator.nextIndex(selectedIndex); - if (next != selectedIndex) { - selectedIndex = next; + const auto selectablePredicate = buildSelectablePredicate(); + buttonNavigator.setSelectablePredicate(selectablePredicate, static_cast(menuItems.size())); + if (!selectablePredicate(selectedIndex)) { + selectedIndex = buttonNavigator.nextIndex(selectedIndex); } requestUpdate(); } diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index e38a63e2..65608a75 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -291,9 +291,6 @@ void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, const bool isSeparator = UITheme::isSeparatorTitle(itemName); if (isSeparator) { itemName = UITheme::stripSeparatorTitle(itemName); - } - - if (isSeparator) { drawListSeparator(renderer, Rect{rect.x + BaseMetrics::values.contentSidePadding, itemY, contentWidth - BaseMetrics::values.contentSidePadding * 2, rowHeight}, @@ -304,7 +301,6 @@ void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, auto font = (rowSubtitle != nullptr) ? UI_12_FONT_ID : UI_10_FONT_ID; auto item = renderer.truncatedText(font, itemName.c_str(), textWidth); renderer.drawText(font, rect.x + BaseMetrics::values.contentSidePadding, itemY, item.c_str(), true); - renderer.drawText(font, rect.x + BaseMetrics::values.contentSidePadding, itemY, item.c_str(), true); if (rowSubtitle != nullptr) { // Draw subtitle; if the text is newline-separated (author\nseries), join with • for single-line display diff --git a/src/components/themes/lyra/LyraTheme.cpp b/src/components/themes/lyra/LyraTheme.cpp index a7ff264d..7658356f 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -446,9 +446,6 @@ void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, const bool isSeparator = UITheme::isSeparatorTitle(itemName); if (isSeparator) { itemName = UITheme::stripSeparatorTitle(itemName); - } - - if (isSeparator) { drawListSeparator(renderer, Rect{rect.x + LyraMetrics::values.contentSidePadding, itemY, contentWidth - LyraMetrics::values.contentSidePadding * 2, rowHeight},