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; + }; +}