From 329c897601284db9170e6509475746cde84ae77b Mon Sep 17 00:00:00 2001 From: Boris Faure Date: Thu, 22 Jan 2026 23:25:34 +0100 Subject: [PATCH] feat: rework CrossPointSettings Multiple goals are achieved with this change: - make descriptions of settings close to their definitions - settings validation on loading with reset to default in case of bad value - more constexpr to reduce RAM usage - less hardcoded values - more maintainable RAM Usage: From RAM: [=== ] 32.4% (used 106084 bytes from 327680 bytes) Flash: [========= ] 92.8% (used 6082238 bytes from 6553600 bytes) To RAM: [=== ] 32.2% (used 105436 bytes from 327680 bytes) Flash: [========= ] 92.8% (used 6079222 bytes from 6553600 bytes) Boot config validation with a test where the status bar config is wrong: [1256] [SD] SD card detected [1256] [CPS] Loading settings from file [1265] [CPS] Invalid value (0x3) for Status Bar, resetting to default [1265] [CPS] Settings loaded from file --- src/CrossPointSettings.cpp | 206 ++++++++++++------ src/CrossPointSettings.h | 93 ++++++++ .../settings/CategorySettingsActivity.cpp | 152 +++++++------ .../settings/CategorySettingsActivity.h | 45 +--- src/activities/settings/SettingsActivity.cpp | 116 ++++------ src/activities/settings/SettingsActivity.h | 3 - 6 files changed, 380 insertions(+), 235 deletions(-) diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index f5e8ded5..ed302185 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -11,13 +11,105 @@ // Initialize the static instance CrossPointSettings CrossPointSettings::instance; +// SettingDescriptor implementations +bool SettingDescriptor::validate(const CrossPointSettings& settings) const { + if (!validator) { + return true; // No validator means always valid + } + const uint8_t value = settings.*(memberPtr); + return validator(value); +} + +uint8_t SettingDescriptor::getValue(const CrossPointSettings& settings) const { return settings.*(memberPtr); } + +void SettingDescriptor::setValue(CrossPointSettings& settings, uint8_t value) const { settings.*(memberPtr) = value; } + +void SettingDescriptor::resetToDefault(CrossPointSettings& settings) const { settings.*(memberPtr) = defaultValue; } + +void SettingDescriptor::save(FsFile& file, const CrossPointSettings& settings) const { + serialization::writePod(file, settings.*(memberPtr)); +} + +void SettingDescriptor::load(FsFile& file, CrossPointSettings& settings) const { + uint8_t value; + serialization::readPod(file, value); + settings.*(memberPtr) = value; +} + namespace { constexpr uint8_t SETTINGS_FILE_VERSION = 1; // Increment this when adding new persisted settings fields -constexpr uint8_t SETTINGS_COUNT = 20; +constexpr uint8_t SETTINGS_COUNT = CrossPointSettings::DESCRIPTOR_COUNT + 1; // descriptors + opdsServerUrl string constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin"; } // namespace +// Define enum value arrays +namespace { +constexpr const char* sleepScreenValues[] = {"Dark", "Light", "Custom", "Cover", "None"}; +constexpr const char* shortPwrBtnValues[] = {"Ignore", "Sleep", "Page Turn"}; +constexpr const char* statusBarValues[] = {"None", "No Progress", "Full"}; +constexpr const char* orientationValues[] = {"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}; +constexpr const char* frontButtonLayoutValues[] = {"Back/Confirm/Left/Right", "Left/Right/Back/Confirm", + "Left/Back/Confirm/Right"}; +constexpr const char* sideButtonLayoutValues[] = {"Prev/Next", "Next/Prev"}; +constexpr const char* fontFamilyValues[] = {"Bookerly", "Noto Sans", "Open Dyslexic"}; +constexpr const char* fontSizeValues[] = {"Small", "Medium", "Large", "X Large"}; +constexpr const char* lineSpacingValues[] = {"Tight", "Normal", "Wide"}; +constexpr const char* paragraphAlignmentValues[] = {"Justify", "Left", "Center", "Right"}; +constexpr const char* sleepTimeoutValues[] = {"1 min", "5 min", "10 min", "15 min", "30 min"}; +constexpr const char* refreshFrequencyValues[] = {"1 page", "5 pages", "10 pages", "15 pages", "30 pages"}; +constexpr const char* sleepScreenCoverModeValues[] = {"Fit", "Crop"}; +constexpr const char* hideBatteryPercentageValues[] = {"Never", "In Reader", "Always"}; + +// Helper function template to deduce array size automatically +template +constexpr SettingDescriptor makeEnumDescriptor(const char* name, uint8_t CrossPointSettings::* ptr, + uint8_t defaultValue, const char* const (&enumValues)[N]) { + return SettingDescriptor(name, SettingType::ENUM, ptr, defaultValue, validateEnum, enumValues, N); +} +} // namespace + +// Define static constexpr members (required in C++14 and earlier) +constexpr size_t CrossPointSettings::DESCRIPTOR_COUNT; + +// Define the static constexpr array of all setting descriptors +// Order must match current serialization order for file format compatibility! +const std::array CrossPointSettings::descriptors = { + {makeEnumDescriptor("Sleep Screen", &CrossPointSettings::sleepScreen, CrossPointSettings::DARK, sleepScreenValues), + {"Extra Paragraph Spacing", SettingType::TOGGLE, &CrossPointSettings::extraParagraphSpacing, 1, validateToggle, + nullptr, 0}, + makeEnumDescriptor("Short Power Button Click", &CrossPointSettings::shortPwrBtn, CrossPointSettings::IGNORE, + shortPwrBtnValues), + makeEnumDescriptor("Status Bar", &CrossPointSettings::statusBar, CrossPointSettings::FULL, statusBarValues), + makeEnumDescriptor("Reading Orientation", &CrossPointSettings::orientation, CrossPointSettings::PORTRAIT, + orientationValues), + makeEnumDescriptor("Front Button Layout", &CrossPointSettings::frontButtonLayout, + CrossPointSettings::BACK_CONFIRM_LEFT_RIGHT, frontButtonLayoutValues), + makeEnumDescriptor("Side Button Layout", &CrossPointSettings::sideButtonLayout, CrossPointSettings::PREV_NEXT, + sideButtonLayoutValues), + makeEnumDescriptor("Reader Font Family", &CrossPointSettings::fontFamily, CrossPointSettings::BOOKERLY, + fontFamilyValues), + makeEnumDescriptor("Reader Font Size", &CrossPointSettings::fontSize, CrossPointSettings::MEDIUM, fontSizeValues), + makeEnumDescriptor("Reader Line Spacing", &CrossPointSettings::lineSpacing, CrossPointSettings::NORMAL, + lineSpacingValues), + makeEnumDescriptor("Reader Paragraph Alignment", &CrossPointSettings::paragraphAlignment, + CrossPointSettings::JUSTIFIED, paragraphAlignmentValues), + makeEnumDescriptor("Time to Sleep", &CrossPointSettings::sleepTimeout, CrossPointSettings::SLEEP_10_MIN, + sleepTimeoutValues), + makeEnumDescriptor("Refresh Frequency", &CrossPointSettings::refreshFrequency, CrossPointSettings::REFRESH_15, + refreshFrequencyValues), + {"Reader Screen Margin", SettingType::VALUE, &CrossPointSettings::screenMargin, 5, validateRange<5, 40>, + ValueRange{5, 40, 5}}, + makeEnumDescriptor("Sleep Screen Cover Mode", &CrossPointSettings::sleepScreenCoverMode, CrossPointSettings::FIT, + sleepScreenCoverModeValues), + // Note: opdsServerUrl (string) at position 15 is handled separately in serialization + {"Text Anti-Aliasing", SettingType::TOGGLE, &CrossPointSettings::textAntiAliasing, 1, validateToggle, nullptr, 0}, + makeEnumDescriptor("Hide Battery %", &CrossPointSettings::hideBatteryPercentage, CrossPointSettings::HIDE_NEVER, + hideBatteryPercentageValues), + {"Long-press Chapter Skip", SettingType::TOGGLE, &CrossPointSettings::longPressChapterSkip, 1, validateToggle, + nullptr, 0}, + {"Hyphenation", SettingType::TOGGLE, &CrossPointSettings::hyphenationEnabled, 0, validateToggle, nullptr, 0}}}; + bool CrossPointSettings::saveToFile() const { // Make sure the directory exists SdMan.mkdir("/.crosspoint"); @@ -29,26 +121,20 @@ bool CrossPointSettings::saveToFile() const { serialization::writePod(outputFile, SETTINGS_FILE_VERSION); serialization::writePod(outputFile, SETTINGS_COUNT); - serialization::writePod(outputFile, sleepScreen); - serialization::writePod(outputFile, extraParagraphSpacing); - serialization::writePod(outputFile, shortPwrBtn); - serialization::writePod(outputFile, statusBar); - serialization::writePod(outputFile, orientation); - serialization::writePod(outputFile, frontButtonLayout); - serialization::writePod(outputFile, sideButtonLayout); - serialization::writePod(outputFile, fontFamily); - serialization::writePod(outputFile, fontSize); - serialization::writePod(outputFile, lineSpacing); - serialization::writePod(outputFile, paragraphAlignment); - serialization::writePod(outputFile, sleepTimeout); - serialization::writePod(outputFile, refreshFrequency); - serialization::writePod(outputFile, screenMargin); - serialization::writePod(outputFile, sleepScreenCoverMode); - serialization::writeString(outputFile, std::string(opdsServerUrl)); - serialization::writePod(outputFile, textAntiAliasing); - serialization::writePod(outputFile, hideBatteryPercentage); - serialization::writePod(outputFile, longPressChapterSkip); - serialization::writePod(outputFile, hyphenationEnabled); + + // Use descriptors to automatically serialize all uint8_t settings + // opdsServerUrl string is written at position 15 (between descriptors 14 and 15) + uint8_t descriptorIndex = 0; + for (const auto& desc : descriptors) { + // Write opdsServerUrl string before descriptor 15 (at position 15) + if (descriptorIndex == 15) { + serialization::writeString(outputFile, std::string(opdsServerUrl)); + } + + desc.save(outputFile, *this); + descriptorIndex++; + } + outputFile.close(); Serial.printf("[%lu] [CPS] Settings saved to file\n", millis()); @@ -56,8 +142,10 @@ bool CrossPointSettings::saveToFile() const { } bool CrossPointSettings::loadFromFile() { + Serial.printf("[%lu] [CPS] Loading settings from file\n", millis()); FsFile inputFile; if (!SdMan.openFileForRead("CPS", SETTINGS_FILE, inputFile)) { + Serial.printf("[%lu] [CPS] Deserialization failed: Could not open settings file\n", millis()); return false; } @@ -72,57 +160,45 @@ bool CrossPointSettings::loadFromFile() { uint8_t fileSettingsCount = 0; serialization::readPod(inputFile, fileSettingsCount); - // load settings that exist (support older files with fewer fields) - uint8_t settingsRead = 0; - do { - serialization::readPod(inputFile, sleepScreen); - if (++settingsRead >= fileSettingsCount) break; - serialization::readPod(inputFile, extraParagraphSpacing); - if (++settingsRead >= fileSettingsCount) break; - serialization::readPod(inputFile, shortPwrBtn); - if (++settingsRead >= fileSettingsCount) break; - serialization::readPod(inputFile, statusBar); - if (++settingsRead >= fileSettingsCount) break; - serialization::readPod(inputFile, orientation); - if (++settingsRead >= fileSettingsCount) break; - serialization::readPod(inputFile, frontButtonLayout); - if (++settingsRead >= fileSettingsCount) break; - serialization::readPod(inputFile, sideButtonLayout); - if (++settingsRead >= fileSettingsCount) break; - serialization::readPod(inputFile, fontFamily); - if (++settingsRead >= fileSettingsCount) break; - serialization::readPod(inputFile, fontSize); - if (++settingsRead >= fileSettingsCount) break; - serialization::readPod(inputFile, lineSpacing); - if (++settingsRead >= fileSettingsCount) break; - serialization::readPod(inputFile, paragraphAlignment); - if (++settingsRead >= fileSettingsCount) break; - serialization::readPod(inputFile, sleepTimeout); - if (++settingsRead >= fileSettingsCount) break; - serialization::readPod(inputFile, refreshFrequency); - if (++settingsRead >= fileSettingsCount) break; - serialization::readPod(inputFile, screenMargin); - if (++settingsRead >= fileSettingsCount) break; - serialization::readPod(inputFile, sleepScreenCoverMode); - if (++settingsRead >= fileSettingsCount) break; - { + // Use descriptors to automatically deserialize all uint8_t settings + // opdsServerUrl string is at position 15 (between descriptors 14 and 15) + uint8_t descriptorIndex = 0; + uint8_t filePosition = 0; + + for (const auto& desc : descriptors) { + if (filePosition >= fileSettingsCount) { + break; // File has fewer settings than current version + } + + // Read opdsServerUrl string at position 15 (before descriptor 15) + if (descriptorIndex == 15) { std::string urlStr; serialization::readString(inputFile, urlStr); strncpy(opdsServerUrl, urlStr.c_str(), sizeof(opdsServerUrl) - 1); opdsServerUrl[sizeof(opdsServerUrl) - 1] = '\0'; + filePosition++; + + if (filePosition >= fileSettingsCount) { + break; + } } - if (++settingsRead >= fileSettingsCount) break; - serialization::readPod(inputFile, textAntiAliasing); - if (++settingsRead >= fileSettingsCount) break; - serialization::readPod(inputFile, hideBatteryPercentage); - if (++settingsRead >= fileSettingsCount) break; - serialization::readPod(inputFile, longPressChapterSkip); - if (++settingsRead >= fileSettingsCount) break; - serialization::readPod(inputFile, hyphenationEnabled); - if (++settingsRead >= fileSettingsCount) break; - } while (false); + + desc.load(inputFile, *this); + descriptorIndex++; + filePosition++; + } inputFile.close(); + + // Validate each setting and reset invalid values to defaults + for (const auto& desc : descriptors) { + if (!desc.validate(*this)) { + Serial.printf("[%lu] [CPS] Invalid value (0x%X) for %s, resetting to default\n", millis(), desc.getValue(*this), + desc.name); + desc.resetToDefault(*this); + } + } + Serial.printf("[%lu] [CPS] Settings loaded from file\n", millis()); return true; } diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 8ce32a2c..e1f48389 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -1,6 +1,95 @@ #pragma once +#include #include #include +#include +#include + +// Setting descriptor infrastructure +enum class SettingType { TOGGLE, ENUM, VALUE }; + +// Validator function pointer (not std::function to save memory) +using SettingValidator = bool (*)(uint8_t); + +// Forward declare for descriptors +class CrossPointSettings; + +// Forward declare file type +class FsFile; + +// Base descriptor for all settings (non-virtual for constexpr) +struct SettingDescriptorBase { + const char* name; // Display name + SettingType type; +}; + +// Value range for VALUE type settings +struct ValueRange { + uint8_t min, max, step; +}; + +// Concrete descriptor for uint8_t settings (constexpr-compatible) +struct SettingDescriptor : public SettingDescriptorBase { + uint8_t CrossPointSettings::* memberPtr; // Member pointer + uint8_t defaultValue; + SettingValidator validator; // Optional validator function + + union { + // For ENUM types + struct { + const char* const* values; + uint8_t count; + } enumData; + + // For VALUE types + ValueRange valueRange; + }; + + // Constexpr constructors for different setting types + // TOGGLE/ENUM constructor + constexpr SettingDescriptor(const char* name_, SettingType type_, uint8_t CrossPointSettings::* ptr, uint8_t defVal, + SettingValidator val, const char* const* enumVals, uint8_t enumCnt) + : SettingDescriptorBase{name_, type_}, + memberPtr(ptr), + defaultValue(defVal), + validator(val), + enumData{enumVals, enumCnt} {} + + // VALUE constructor + constexpr SettingDescriptor(const char* name_, SettingType type_, uint8_t CrossPointSettings::* ptr, uint8_t defVal, + SettingValidator val, ValueRange valRange) + : SettingDescriptorBase{name_, type_}, + memberPtr(ptr), + defaultValue(defVal), + validator(val), + valueRange(valRange) {} + + bool validate(const CrossPointSettings& settings) const; + uint8_t getValue(const CrossPointSettings& settings) const; + void setValue(CrossPointSettings& settings, uint8_t value) const; + void resetToDefault(CrossPointSettings& settings) const; + void save(FsFile& file, const CrossPointSettings& settings) const; + void load(FsFile& file, CrossPointSettings& settings) const; + + // Helper to get enum value as string + const char* getEnumValueString(uint8_t index) const { + if (index < enumData.count && enumData.values) { + return enumData.values[index]; + } + return ""; + } +}; + +// Validator functions (constexpr for compile-time optimization) +constexpr bool validateToggle(uint8_t v) { return v <= 1; } +template +constexpr bool validateEnum(uint8_t v) { + return v < MAX; +} +template +constexpr bool validateRange(uint8_t v) { + return v >= MIN && v <= MAX; +} class CrossPointSettings { private: @@ -15,6 +104,10 @@ class CrossPointSettings { CrossPointSettings(const CrossPointSettings&) = delete; CrossPointSettings& operator=(const CrossPointSettings&) = delete; + // Static constexpr array of all setting descriptors + static constexpr size_t DESCRIPTOR_COUNT = 19; + static const std::array descriptors; + // Should match with SettingsActivity text enum SLEEP_SCREEN_MODE { DARK = 0, LIGHT = 1, CUSTOM = 2, COVER = 3, BLANK = 4 }; enum SLEEP_SCREEN_COVER_MODE { FIT = 0, CROP = 1 }; diff --git a/src/activities/settings/CategorySettingsActivity.cpp b/src/activities/settings/CategorySettingsActivity.cpp index a6182b5c..314511b6 100644 --- a/src/activities/settings/CategorySettingsActivity.cpp +++ b/src/activities/settings/CategorySettingsActivity.cpp @@ -62,77 +62,84 @@ void CategorySettingsActivity::loop() { } // Handle navigation + const int totalItemsCount = descriptors.size() + actionItems.size(); + if (mappedInput.wasPressed(MappedInputManager::Button::Up) || mappedInput.wasPressed(MappedInputManager::Button::Left)) { - selectedSettingIndex = (selectedSettingIndex > 0) ? (selectedSettingIndex - 1) : (settingsCount - 1); + selectedSettingIndex = (selectedSettingIndex > 0) ? (selectedSettingIndex - 1) : (totalItemsCount - 1); updateRequired = true; } else if (mappedInput.wasPressed(MappedInputManager::Button::Down) || mappedInput.wasPressed(MappedInputManager::Button::Right)) { - selectedSettingIndex = (selectedSettingIndex < settingsCount - 1) ? (selectedSettingIndex + 1) : 0; + selectedSettingIndex = (selectedSettingIndex < totalItemsCount - 1) ? (selectedSettingIndex + 1) : 0; updateRequired = true; } } void CategorySettingsActivity::toggleCurrentSetting() { - if (selectedSettingIndex < 0 || selectedSettingIndex >= settingsCount) { + const int totalItemsCount = descriptors.size() + actionItems.size(); + + if (selectedSettingIndex < 0 || selectedSettingIndex >= totalItemsCount) { return; } - const auto& setting = settingsList[selectedSettingIndex]; + // Check if it's a descriptor or an action item + if (selectedSettingIndex < static_cast(descriptors.size())) { + // Handle descriptor + const auto* desc = descriptors[selectedSettingIndex]; - if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) { - // Toggle the boolean value using the member pointer - const bool currentValue = SETTINGS.*(setting.valuePtr); - SETTINGS.*(setting.valuePtr) = !currentValue; - } else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) { - const uint8_t currentValue = SETTINGS.*(setting.valuePtr); - SETTINGS.*(setting.valuePtr) = (currentValue + 1) % static_cast(setting.enumValues.size()); - } else if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) { - const int8_t currentValue = SETTINGS.*(setting.valuePtr); - if (currentValue + setting.valueRange.step > setting.valueRange.max) { - SETTINGS.*(setting.valuePtr) = setting.valueRange.min; - } else { - SETTINGS.*(setting.valuePtr) = currentValue + setting.valueRange.step; - } - } else if (setting.type == SettingType::ACTION) { - if (strcmp(setting.name, "KOReader Sync") == 0) { - xSemaphoreTake(renderingMutex, portMAX_DELAY); - exitActivity(); - enterNewActivity(new KOReaderSettingsActivity(renderer, mappedInput, [this] { - exitActivity(); - updateRequired = true; - })); - xSemaphoreGive(renderingMutex); - } else if (strcmp(setting.name, "Calibre Settings") == 0) { - xSemaphoreTake(renderingMutex, portMAX_DELAY); - exitActivity(); - enterNewActivity(new CalibreSettingsActivity(renderer, mappedInput, [this] { - exitActivity(); - updateRequired = true; - })); - xSemaphoreGive(renderingMutex); - } else if (strcmp(setting.name, "Clear Cache") == 0) { - xSemaphoreTake(renderingMutex, portMAX_DELAY); - exitActivity(); - enterNewActivity(new ClearCacheActivity(renderer, mappedInput, [this] { - exitActivity(); - updateRequired = true; - })); - xSemaphoreGive(renderingMutex); - } else if (strcmp(setting.name, "Check for updates") == 0) { - xSemaphoreTake(renderingMutex, portMAX_DELAY); - exitActivity(); - enterNewActivity(new OtaUpdateActivity(renderer, mappedInput, [this] { - exitActivity(); - updateRequired = true; - })); - xSemaphoreGive(renderingMutex); + if (desc->type == SettingType::TOGGLE) { + uint8_t currentValue = desc->getValue(SETTINGS); + desc->setValue(SETTINGS, !currentValue); + } else if (desc->type == SettingType::ENUM) { + uint8_t currentValue = desc->getValue(SETTINGS); + desc->setValue(SETTINGS, (currentValue + 1) % desc->enumData.count); + } else if (desc->type == SettingType::VALUE) { + uint8_t currentValue = desc->getValue(SETTINGS); + if (currentValue + desc->valueRange.step > desc->valueRange.max) { + desc->setValue(SETTINGS, desc->valueRange.min); + } else { + desc->setValue(SETTINGS, currentValue + desc->valueRange.step); + } } + + SETTINGS.saveToFile(); } else { - return; - } + // Handle action item + const int actionIndex = selectedSettingIndex - descriptors.size(); + const auto& action = actionItems[actionIndex]; - SETTINGS.saveToFile(); + xSemaphoreTake(renderingMutex, portMAX_DELAY); + exitActivity(); + + switch (action.type) { + case ActionItem::Type::KOREADER_SYNC: + enterNewActivity(new KOReaderSettingsActivity(renderer, mappedInput, [this] { + exitActivity(); + updateRequired = true; + })); + break; + case ActionItem::Type::CALIBRE_SETTINGS: + enterNewActivity(new CalibreSettingsActivity(renderer, mappedInput, [this] { + exitActivity(); + updateRequired = true; + })); + break; + case ActionItem::Type::CLEAR_CACHE: + enterNewActivity(new ClearCacheActivity(renderer, mappedInput, [this] { + exitActivity(); + updateRequired = true; + })); + break; + case ActionItem::Type::CHECK_UPDATES: + enterNewActivity(new OtaUpdateActivity(renderer, mappedInput, [this] { + exitActivity(); + updateRequired = true; + })); + break; + } + + xSemaphoreGive(renderingMutex); + } } void CategorySettingsActivity::displayTaskLoop() { @@ -153,29 +160,31 @@ void CategorySettingsActivity::render() const { const auto pageWidth = renderer.getScreenWidth(); const auto pageHeight = renderer.getScreenHeight(); + // Draw header with category name renderer.drawCenteredText(UI_12_FONT_ID, 15, categoryName, true, EpdFontFamily::BOLD); // Draw selection highlight renderer.fillRect(0, 60 + selectedSettingIndex * 30 - 2, pageWidth - 1, 30); - // Draw all settings - for (int i = 0; i < settingsCount; i++) { - const int settingY = 60 + i * 30; // 30 pixels between settings + // Draw all descriptors + for (size_t i = 0; i < descriptors.size(); i++) { + const auto* desc = descriptors[i]; + const int settingY = 60 + i * 30; const bool isSelected = (i == selectedSettingIndex); // Draw setting name - renderer.drawText(UI_10_FONT_ID, 20, settingY, settingsList[i].name, !isSelected); + renderer.drawText(UI_10_FONT_ID, 20, settingY, desc->name, !isSelected); // Draw value based on setting type std::string valueText; - if (settingsList[i].type == SettingType::TOGGLE && settingsList[i].valuePtr != nullptr) { - const bool value = SETTINGS.*(settingsList[i].valuePtr); + if (desc->type == SettingType::TOGGLE) { + const bool value = desc->getValue(SETTINGS); valueText = value ? "ON" : "OFF"; - } else if (settingsList[i].type == SettingType::ENUM && settingsList[i].valuePtr != nullptr) { - const uint8_t value = SETTINGS.*(settingsList[i].valuePtr); - valueText = settingsList[i].enumValues[value]; - } else if (settingsList[i].type == SettingType::VALUE && settingsList[i].valuePtr != nullptr) { - valueText = std::to_string(SETTINGS.*(settingsList[i].valuePtr)); + } else if (desc->type == SettingType::ENUM) { + const uint8_t value = desc->getValue(SETTINGS); + valueText = desc->getEnumValueString(value); + } else if (desc->type == SettingType::VALUE) { + valueText = std::to_string(desc->getValue(SETTINGS)); } if (!valueText.empty()) { const auto width = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str()); @@ -183,9 +192,22 @@ void CategorySettingsActivity::render() const { } } + // Draw all action items + for (size_t i = 0; i < actionItems.size(); i++) { + const auto& action = actionItems[i]; + const int itemIndex = descriptors.size() + i; + const int settingY = 60 + itemIndex * 30; + const bool isSelected = (itemIndex == selectedSettingIndex); + + // Draw action name + renderer.drawText(UI_10_FONT_ID, 20, settingY, action.name, !isSelected); + } + + // Draw version text above button hints renderer.drawText(SMALL_FONT_ID, pageWidth - 20 - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION), pageHeight - 60, CROSSPOINT_VERSION); + // Draw help text const auto labels = mappedInput.mapLabels("« Back", "Toggle", "", ""); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); diff --git a/src/activities/settings/CategorySettingsActivity.h b/src/activities/settings/CategorySettingsActivity.h index a7d1f0ce..cf40655e 100644 --- a/src/activities/settings/CategorySettingsActivity.h +++ b/src/activities/settings/CategorySettingsActivity.h @@ -7,38 +7,14 @@ #include #include +#include "CrossPointSettings.h" #include "activities/ActivityWithSubactivity.h" -class CrossPointSettings; - -enum class SettingType { TOGGLE, ENUM, ACTION, VALUE }; - -struct SettingInfo { +// Action items for the System category +struct ActionItem { const char* name; - SettingType type; - uint8_t CrossPointSettings::* valuePtr; - std::vector enumValues; - - struct ValueRange { - uint8_t min; - uint8_t max; - uint8_t step; - }; - ValueRange valueRange; - - static SettingInfo Toggle(const char* name, uint8_t CrossPointSettings::* ptr) { - return {name, SettingType::TOGGLE, ptr}; - } - - static SettingInfo Enum(const char* name, uint8_t CrossPointSettings::* ptr, std::vector values) { - return {name, SettingType::ENUM, ptr, std::move(values)}; - } - - static SettingInfo Action(const char* name) { return {name, SettingType::ACTION, nullptr}; } - - static SettingInfo Value(const char* name, uint8_t CrossPointSettings::* ptr, const ValueRange valueRange) { - return {name, SettingType::VALUE, ptr, {}, valueRange}; - } + enum class Type { KOREADER_SYNC, CALIBRE_SETTINGS, CLEAR_CACHE, CHECK_UPDATES }; + Type type; }; class CategorySettingsActivity final : public ActivityWithSubactivity { @@ -47,8 +23,8 @@ class CategorySettingsActivity final : public ActivityWithSubactivity { bool updateRequired = false; int selectedSettingIndex = 0; const char* categoryName; - const SettingInfo* settingsList; - int settingsCount; + const std::vector descriptors; + const std::vector actionItems; const std::function onGoBack; static void taskTrampoline(void* param); @@ -58,11 +34,12 @@ class CategorySettingsActivity final : public ActivityWithSubactivity { public: CategorySettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const char* categoryName, - const SettingInfo* settingsList, int settingsCount, const std::function& onGoBack) + const std::vector& descriptors, + const std::vector& actionItems, const std::function& onGoBack) : ActivityWithSubactivity("CategorySettings", renderer, mappedInput), categoryName(categoryName), - settingsList(settingsList), - settingsCount(settingsCount), + descriptors(descriptors), + actionItems(actionItems), onGoBack(onGoBack) {} void onEnter() override; void onExit() override; diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 943fdb4c..b1b9ad6c 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -3,6 +3,8 @@ #include #include +#include + #include "CategorySettingsActivity.h" #include "CrossPointSettings.h" #include "MappedInputManager.h" @@ -10,47 +12,12 @@ const char* SettingsActivity::categoryNames[categoryCount] = {"Display", "Reader", "Controls", "System"}; -namespace { -constexpr int displaySettingsCount = 5; -const SettingInfo displaySettings[displaySettingsCount] = { - // Should match with SLEEP_SCREEN_MODE - SettingInfo::Enum("Sleep Screen", &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover", "None"}), - SettingInfo::Enum("Sleep Screen Cover Mode", &CrossPointSettings::sleepScreenCoverMode, {"Fit", "Crop"}), - SettingInfo::Enum("Status Bar", &CrossPointSettings::statusBar, {"None", "No Progress", "Full"}), - SettingInfo::Enum("Hide Battery %", &CrossPointSettings::hideBatteryPercentage, {"Never", "In Reader", "Always"}), - SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency, - {"1 page", "5 pages", "10 pages", "15 pages", "30 pages"})}; - -constexpr int readerSettingsCount = 9; -const SettingInfo readerSettings[readerSettingsCount] = { - SettingInfo::Enum("Font Family", &CrossPointSettings::fontFamily, {"Bookerly", "Noto Sans", "Open Dyslexic"}), - SettingInfo::Enum("Font Size", &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}), - SettingInfo::Enum("Line Spacing", &CrossPointSettings::lineSpacing, {"Tight", "Normal", "Wide"}), - SettingInfo::Value("Screen Margin", &CrossPointSettings::screenMargin, {5, 40, 5}), - SettingInfo::Enum("Paragraph Alignment", &CrossPointSettings::paragraphAlignment, - {"Justify", "Left", "Center", "Right"}), - SettingInfo::Toggle("Hyphenation", &CrossPointSettings::hyphenationEnabled), - SettingInfo::Enum("Reading Orientation", &CrossPointSettings::orientation, - {"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}), - SettingInfo::Toggle("Extra Paragraph Spacing", &CrossPointSettings::extraParagraphSpacing), - SettingInfo::Toggle("Text Anti-Aliasing", &CrossPointSettings::textAntiAliasing)}; - -constexpr int controlsSettingsCount = 4; -const SettingInfo controlsSettings[controlsSettingsCount] = { - SettingInfo::Enum("Front Button Layout", &CrossPointSettings::frontButtonLayout, - {"Bck, Cnfrm, Lft, Rght", "Lft, Rght, Bck, Cnfrm", "Lft, Bck, Cnfrm, Rght"}), - SettingInfo::Enum("Side Button Layout (reader)", &CrossPointSettings::sideButtonLayout, - {"Prev, Next", "Next, Prev"}), - SettingInfo::Toggle("Long-press Chapter Skip", &CrossPointSettings::longPressChapterSkip), - SettingInfo::Enum("Short Power Button Click", &CrossPointSettings::shortPwrBtn, {"Ignore", "Sleep", "Page Turn"})}; - -constexpr int systemSettingsCount = 5; -const SettingInfo systemSettings[systemSettingsCount] = { - SettingInfo::Enum("Time to Sleep", &CrossPointSettings::sleepTimeout, - {"1 min", "5 min", "10 min", "15 min", "30 min"}), - SettingInfo::Action("KOReader Sync"), SettingInfo::Action("Calibre Settings"), SettingInfo::Action("Clear Cache"), - SettingInfo::Action("Check for updates")}; -} // namespace +// Helper function to find descriptor by member pointer +static const SettingDescriptor* findDescriptor(uint8_t CrossPointSettings::* memberPtr) { + auto it = std::find_if(CrossPointSettings::descriptors.begin(), CrossPointSettings::descriptors.end(), + [memberPtr](const SettingDescriptor& desc) { return desc.memberPtr == memberPtr; }); + return (it != CrossPointSettings::descriptors.end()) ? &(*it) : nullptr; +} void SettingsActivity::taskTrampoline(void* param) { auto* self = static_cast(param); @@ -121,37 +88,50 @@ void SettingsActivity::loop() { } void SettingsActivity::enterCategory(int categoryIndex) { - if (categoryIndex < 0 || categoryIndex >= categoryCount) { - return; + std::vector descriptors; + std::vector actionItems; + + switch (categoryIndex) { + case 0: // Display + descriptors.push_back(findDescriptor(&CrossPointSettings::sleepScreen)); + descriptors.push_back(findDescriptor(&CrossPointSettings::sleepScreenCoverMode)); + descriptors.push_back(findDescriptor(&CrossPointSettings::statusBar)); + descriptors.push_back(findDescriptor(&CrossPointSettings::hideBatteryPercentage)); + descriptors.push_back(findDescriptor(&CrossPointSettings::refreshFrequency)); + break; + + case 1: // Reader + descriptors.push_back(findDescriptor(&CrossPointSettings::fontFamily)); + descriptors.push_back(findDescriptor(&CrossPointSettings::fontSize)); + descriptors.push_back(findDescriptor(&CrossPointSettings::lineSpacing)); + descriptors.push_back(findDescriptor(&CrossPointSettings::screenMargin)); + descriptors.push_back(findDescriptor(&CrossPointSettings::paragraphAlignment)); + descriptors.push_back(findDescriptor(&CrossPointSettings::hyphenationEnabled)); + descriptors.push_back(findDescriptor(&CrossPointSettings::orientation)); + descriptors.push_back(findDescriptor(&CrossPointSettings::extraParagraphSpacing)); + descriptors.push_back(findDescriptor(&CrossPointSettings::textAntiAliasing)); + break; + + case 2: // Controls + descriptors.push_back(findDescriptor(&CrossPointSettings::frontButtonLayout)); + descriptors.push_back(findDescriptor(&CrossPointSettings::sideButtonLayout)); + descriptors.push_back(findDescriptor(&CrossPointSettings::longPressChapterSkip)); + descriptors.push_back(findDescriptor(&CrossPointSettings::shortPwrBtn)); + break; + + case 3: // System + descriptors.push_back(findDescriptor(&CrossPointSettings::sleepTimeout)); + actionItems.push_back({"KOReader Sync", ActionItem::Type::KOREADER_SYNC}); + actionItems.push_back({"Calibre Settings", ActionItem::Type::CALIBRE_SETTINGS}); + actionItems.push_back({"Clear Cache", ActionItem::Type::CLEAR_CACHE}); + actionItems.push_back({"Check for updates", ActionItem::Type::CHECK_UPDATES}); + break; } xSemaphoreTake(renderingMutex, portMAX_DELAY); exitActivity(); - - const SettingInfo* settingsList = nullptr; - int settingsCount = 0; - - switch (categoryIndex) { - case 0: // Display - settingsList = displaySettings; - settingsCount = displaySettingsCount; - break; - case 1: // Reader - settingsList = readerSettings; - settingsCount = readerSettingsCount; - break; - case 2: // Controls - settingsList = controlsSettings; - settingsCount = controlsSettingsCount; - break; - case 3: // System - settingsList = systemSettings; - settingsCount = systemSettingsCount; - break; - } - - enterNewActivity(new CategorySettingsActivity(renderer, mappedInput, categoryNames[categoryIndex], settingsList, - settingsCount, [this] { + enterNewActivity(new CategorySettingsActivity(renderer, mappedInput, categoryNames[categoryIndex], descriptors, + actionItems, [this] { exitActivity(); updateRequired = true; })); diff --git a/src/activities/settings/SettingsActivity.h b/src/activities/settings/SettingsActivity.h index 821dda42..4a93d7fd 100644 --- a/src/activities/settings/SettingsActivity.h +++ b/src/activities/settings/SettingsActivity.h @@ -9,9 +9,6 @@ #include "activities/ActivityWithSubactivity.h" -class CrossPointSettings; -struct SettingInfo; - class SettingsActivity final : public ActivityWithSubactivity { TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr;