diff --git a/src/SettingsList.h b/src/SettingsList.h index ef6e99cb..cd6a07b2 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -18,6 +18,11 @@ // previous item's, SettingsActivity::onEnter() automatically inserts a separator // row before it. Add with .withSubcategory(StrId::STR_MY_SECTION). // Items without a subcategory (STR_NONE_OPT) never trigger a separator. +// submenu — optional submenu grouping. Items with the same submenu StrId are hidden from +// the main list and collected behind a single placeholder entry. Selecting that +// entry launches SettingsSubmenuActivity with those items. withSubcategory() +// works inside a submenu exactly as it does in the parent tab. +// Add with .withSubmenu(StrId::STR_MY_SUBMENU). // key — JSON property name used by the web settings API (nullptr = device-only). // // ACTION-type entries and entries without a key are device-only and are added directly diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 34f2cdca..d6f186d3 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -15,6 +15,7 @@ #include "MappedInputManager.h" #include "OtaUpdateActivity.h" #include "SettingsList.h" +#include "SettingsSubmenuActivity.h" #include "StatusBarSettingsActivity.h" #include "SyncTimeActivity.h" #include "SystemInformationActivity.h" @@ -45,13 +46,27 @@ void SettingsActivity::onEnter() { readerSettings.clear(); controlsSettings.clear(); systemSettings.clear(); + submenuData.clear(); StrId lastDisplaySub = StrId::STR_NONE_OPT; StrId lastReaderSub = StrId::STR_NONE_OPT; StrId lastControlsSub = StrId::STR_NONE_OPT; StrId lastSystemSub = StrId::STR_NONE_OPT; - auto addTo = [](std::vector& vec, StrId& lastSub, SettingInfo s) { + auto addTo = [this](std::vector& vec, StrId& lastSub, SettingInfo s) { + if (s.submenu != StrId::STR_NONE_OPT) { + // Item belongs to a submenu — collect it and insert a placeholder in the main + // list the first time this submenu ID is encountered. + auto it = std::find_if(submenuData.begin(), submenuData.end(), + [&s](const SubmenuData& d) { return d.id == s.submenu; }); + if (it == submenuData.end()) { + vec.push_back(SettingInfo::SubmenuEntry(s.submenu)); + submenuData.push_back({s.submenu, {}}); + it = submenuData.end() - 1; + } + it->items.push_back(std::move(s)); + return; + } if (s.subcategory != StrId::STR_NONE_OPT && s.subcategory != lastSub) { vec.push_back(SettingInfo::Separator(s.subcategory)); lastSub = s.subcategory; @@ -267,6 +282,16 @@ void SettingsActivity::toggleCurrentSetting() { case SettingAction::DetectTimezone: startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); break; + case SettingAction::Submenu: { + const auto it = std::find_if(submenuData.cbegin(), submenuData.cend(), + [&setting](const SubmenuData& d) { return d.id == setting.nameId; }); + if (it != submenuData.cend()) { + startActivityForResult( + std::make_unique(renderer, mappedInput, setting.nameId, it->items), + resultHandler); + } + break; + } case SettingAction::None: // Do nothing break; diff --git a/src/activities/settings/SettingsActivity.h b/src/activities/settings/SettingsActivity.h index 161f7ddf..459084c2 100644 --- a/src/activities/settings/SettingsActivity.h +++ b/src/activities/settings/SettingsActivity.h @@ -26,6 +26,7 @@ enum class SettingAction { DetectTimezone, SyncTime, Weather, + Submenu, }; struct SettingInfo { @@ -152,13 +153,31 @@ struct SettingInfo { } bool isSeparator = false; - StrId subcategory = StrId::STR_NONE_OPT; + StrId subcategory = StrId::STR_NONE_OPT; // Triggers a separator row on first use and on change + StrId submenu = StrId::STR_NONE_OPT; // Routes item into a submenu; hidden from main list [[nodiscard]] std::string getTitle() const; + // Inserts a separator row in the parent tab when this item's subcategory first appears or changes. SettingInfo& withSubcategory(StrId sub) { subcategory = sub; return *this; } + + // Hides this item from the parent tab and places it inside a SettingsSubmenuActivity instead. + // All items sharing the same submenu StrId are grouped under one placeholder entry. + SettingInfo& withSubmenu(StrId sub) { + submenu = sub; + return *this; + } + + // For internal use by SettingsActivity: placeholder entry that launches the submenu. + static SettingInfo SubmenuEntry(StrId titleId) { + SettingInfo s; + s.nameId = titleId; + s.type = SettingType::ACTION; + s.action = SettingAction::Submenu; + return s; + } }; class SettingsActivity final : public Activity { @@ -178,6 +197,12 @@ class SettingsActivity final : public Activity { static constexpr int categoryCount = 4; static const StrId categoryNames[categoryCount]; + struct SubmenuData { + StrId id; + std::vector items; + }; + std::vector submenuData; + void enterCategory(int categoryIndex); void toggleCurrentSetting(); [[nodiscard]] bool isListItemSelectable(int settingIdx) const; diff --git a/src/activities/settings/SettingsSubmenuActivity.cpp b/src/activities/settings/SettingsSubmenuActivity.cpp new file mode 100644 index 00000000..5e87a504 --- /dev/null +++ b/src/activities/settings/SettingsSubmenuActivity.cpp @@ -0,0 +1,162 @@ +#include "SettingsSubmenuActivity.h" + +#include +#include + +#include "ButtonRemapActivity.h" +#include "CalibreSettingsActivity.h" +#include "ClearCacheActivity.h" +#include "ClockSettingsActivity.h" +#include "CrossPointSettings.h" +#include "KOReaderSettingsActivity.h" +#include "LanguageSelectActivity.h" +#include "MappedInputManager.h" +#include "OtaUpdateActivity.h" +#include "StatusBarSettingsActivity.h" +#include "SystemInformationActivity.h" +#include "activities/network/WifiSelectionActivity.h" +#include "activities/weather/WeatherSettingsActivity.h" +#include "components/UITheme.h" +#include "fontIds.h" + +void SettingsSubmenuActivity::onEnter() { + Activity::onEnter(); + itemCount = static_cast(items.size()); + const auto pred = UITheme::makeSelectablePredicate(itemCount, + [this](int i) { return items[i].getTitle(); }); + buttonNavigator.setSelectablePredicate(pred, itemCount); + if (!pred(selectedIndex)) { + selectedIndex = buttonNavigator.nextIndex(selectedIndex); + } + requestUpdate(); +} + +void SettingsSubmenuActivity::onExit() { Activity::onExit(); } + +void SettingsSubmenuActivity::loop() { + if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { + finish(); + return; + } + + if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { + toggleItem(); + requestUpdate(); + return; + } + + buttonNavigator.onNextRelease([this] { + selectedIndex = buttonNavigator.nextIndex(selectedIndex); + requestUpdate(); + }); + buttonNavigator.onPreviousRelease([this] { + selectedIndex = buttonNavigator.previousIndex(selectedIndex); + requestUpdate(); + }); + buttonNavigator.onNextContinuous([this] { + selectedIndex = buttonNavigator.nextIndex(selectedIndex); + requestUpdate(); + }); + buttonNavigator.onPreviousContinuous([this] { + selectedIndex = buttonNavigator.previousIndex(selectedIndex); + requestUpdate(); + }); +} + +void SettingsSubmenuActivity::toggleItem() { + if (selectedIndex < 0 || selectedIndex >= itemCount) return; + const auto& setting = items[selectedIndex]; + if (setting.isSeparator) return; + + if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) { + SETTINGS.*(setting.valuePtr) = !SETTINGS.*(setting.valuePtr); + } else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) { + const uint8_t cur = SETTINGS.*(setting.valuePtr); + SETTINGS.*(setting.valuePtr) = (cur + 1) % static_cast(setting.enumValues.size()); + } else if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) { + const int8_t cur = SETTINGS.*(setting.valuePtr); + SETTINGS.*(setting.valuePtr) = (cur + setting.valueRange.step > setting.valueRange.max) + ? setting.valueRange.min + : cur + setting.valueRange.step; + } else if (setting.type == SettingType::ACTION) { + auto resultHandler = [this](const ActivityResult&) { SETTINGS.saveToFile(); }; + switch (setting.action) { + case SettingAction::RemapFrontButtons: + startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); + return; + case SettingAction::CustomiseStatusBar: + startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); + return; + case SettingAction::ClockSettings: + startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); + return; + case SettingAction::KOReaderSync: + startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); + return; + case SettingAction::OPDSBrowser: + startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); + return; + case SettingAction::Network: + startActivityForResult(std::make_unique(renderer, mappedInput, false), resultHandler); + return; + case SettingAction::ClearCache: + startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); + return; + case SettingAction::CheckForUpdates: + startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); + return; + case SettingAction::Language: + startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); + return; + case SettingAction::Weather: + startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); + return; + case SettingAction::SystemInfo: + startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); + return; + default: + return; + } + } + + SETTINGS.saveToFile(); +} + +void SettingsSubmenuActivity::render(RenderLock&&) { + renderer.clearScreen(); + + const auto& metrics = UITheme::getInstance().getMetrics(); + const Rect contentRect = UITheme::getContentRect(renderer, true, false); + + GUI.drawHeader(renderer, Rect{contentRect.x, metrics.topPadding, contentRect.width, metrics.headerHeight}, + I18N.get(titleId)); + + const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; + const int contentHeight = contentRect.height - contentTop - metrics.verticalSpacing; + + GUI.drawList( + renderer, Rect{contentRect.x, contentTop, contentRect.width, contentHeight}, itemCount, selectedIndex, + [this](int index) { return items[index].getTitle(); }, nullptr, nullptr, + [this](int i) { + const auto& setting = items[i]; + if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) { + return std::string(SETTINGS.*(setting.valuePtr) ? tr(STR_STATE_ON) : tr(STR_STATE_OFF)); + } + if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) { + const uint8_t value = SETTINGS.*(setting.valuePtr); + if (value < setting.enumValues.size()) { + return std::string(I18N.get(setting.enumValues[value])); + } + } + if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) { + return std::to_string(SETTINGS.*(setting.valuePtr)); + } + return std::string(); + }, + true); + + const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + + renderer.displayBuffer(); +} diff --git a/src/activities/settings/SettingsSubmenuActivity.h b/src/activities/settings/SettingsSubmenuActivity.h new file mode 100644 index 00000000..10d53197 --- /dev/null +++ b/src/activities/settings/SettingsSubmenuActivity.h @@ -0,0 +1,30 @@ +#pragma once +#include + +#include + +#include "activities/Activity.h" +#include "activities/settings/SettingsActivity.h" +#include "util/ButtonNavigator.h" + +// Displays a flat list of SettingInfo items launched from a SettingsActivity submenu entry. +// Supports subcategory separators (withSubcategory) exactly as the parent settings tabs do. +class SettingsSubmenuActivity final : public Activity { + StrId titleId; + std::vector items; + int selectedIndex = 0; + int itemCount = 0; + ButtonNavigator buttonNavigator; + + void toggleItem(); + + public: + explicit SettingsSubmenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, StrId titleId, + std::vector items) + : Activity("SettingsSubmenu", renderer, mappedInput), titleId(titleId), items(std::move(items)) {} + + void onEnter() override; + void onExit() override; + void loop() override; + void render(RenderLock&&) override; +};