A fork of https://github.com/crosspoint-reader/crosspoint-reader
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: adding categories to settings screen (#331)

## Summary

* **What is the goal of this PR?** (e.g., Fixes a bug in the user
authentication module, Implements the new feature for
file uploading.)

As we get more settings, I think it makes sense to do categories for
them. This just allows users to find the settings easier and navigate to
them.

* **What changes are included?**

## Additional Context

* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks, specific areas to
focus on).

Co-authored-by: dpoulter <daniel@yoco.com>
Co-authored-by: Dave Allie <dave@daveallie.com>

authored by

Daniel Poulter
dpoulter
Dave Allie
and committed by
GitHub
73c30748 6d684668

+331 -140
+184
src/activities/settings/CategorySettingsActivity.cpp
··· 1 + #include "CategorySettingsActivity.h" 2 + 3 + #include <GfxRenderer.h> 4 + #include <HardwareSerial.h> 5 + 6 + #include <cstring> 7 + 8 + #include "CalibreSettingsActivity.h" 9 + #include "CrossPointSettings.h" 10 + #include "KOReaderSettingsActivity.h" 11 + #include "MappedInputManager.h" 12 + #include "OtaUpdateActivity.h" 13 + #include "fontIds.h" 14 + 15 + void CategorySettingsActivity::taskTrampoline(void* param) { 16 + auto* self = static_cast<CategorySettingsActivity*>(param); 17 + self->displayTaskLoop(); 18 + } 19 + 20 + void CategorySettingsActivity::onEnter() { 21 + Activity::onEnter(); 22 + renderingMutex = xSemaphoreCreateMutex(); 23 + 24 + selectedSettingIndex = 0; 25 + updateRequired = true; 26 + 27 + xTaskCreate(&CategorySettingsActivity::taskTrampoline, "CategorySettingsActivityTask", 4096, this, 1, 28 + &displayTaskHandle); 29 + } 30 + 31 + void CategorySettingsActivity::onExit() { 32 + ActivityWithSubactivity::onExit(); 33 + 34 + // Wait until not rendering to delete task to avoid killing mid-instruction to EPD 35 + xSemaphoreTake(renderingMutex, portMAX_DELAY); 36 + if (displayTaskHandle) { 37 + vTaskDelete(displayTaskHandle); 38 + displayTaskHandle = nullptr; 39 + } 40 + vSemaphoreDelete(renderingMutex); 41 + renderingMutex = nullptr; 42 + } 43 + 44 + void CategorySettingsActivity::loop() { 45 + if (subActivity) { 46 + subActivity->loop(); 47 + return; 48 + } 49 + 50 + // Handle actions with early return 51 + if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { 52 + toggleCurrentSetting(); 53 + updateRequired = true; 54 + return; 55 + } 56 + 57 + if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { 58 + SETTINGS.saveToFile(); 59 + onGoBack(); 60 + return; 61 + } 62 + 63 + // Handle navigation 64 + if (mappedInput.wasPressed(MappedInputManager::Button::Up) || 65 + mappedInput.wasPressed(MappedInputManager::Button::Left)) { 66 + selectedSettingIndex = (selectedSettingIndex > 0) ? (selectedSettingIndex - 1) : (settingsCount - 1); 67 + updateRequired = true; 68 + } else if (mappedInput.wasPressed(MappedInputManager::Button::Down) || 69 + mappedInput.wasPressed(MappedInputManager::Button::Right)) { 70 + selectedSettingIndex = (selectedSettingIndex < settingsCount - 1) ? (selectedSettingIndex + 1) : 0; 71 + updateRequired = true; 72 + } 73 + } 74 + 75 + void CategorySettingsActivity::toggleCurrentSetting() { 76 + if (selectedSettingIndex < 0 || selectedSettingIndex >= settingsCount) { 77 + return; 78 + } 79 + 80 + const auto& setting = settingsList[selectedSettingIndex]; 81 + 82 + if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) { 83 + // Toggle the boolean value using the member pointer 84 + const bool currentValue = SETTINGS.*(setting.valuePtr); 85 + SETTINGS.*(setting.valuePtr) = !currentValue; 86 + } else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) { 87 + const uint8_t currentValue = SETTINGS.*(setting.valuePtr); 88 + SETTINGS.*(setting.valuePtr) = (currentValue + 1) % static_cast<uint8_t>(setting.enumValues.size()); 89 + } else if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) { 90 + const int8_t currentValue = SETTINGS.*(setting.valuePtr); 91 + if (currentValue + setting.valueRange.step > setting.valueRange.max) { 92 + SETTINGS.*(setting.valuePtr) = setting.valueRange.min; 93 + } else { 94 + SETTINGS.*(setting.valuePtr) = currentValue + setting.valueRange.step; 95 + } 96 + } else if (setting.type == SettingType::ACTION) { 97 + if (strcmp(setting.name, "KOReader Sync") == 0) { 98 + xSemaphoreTake(renderingMutex, portMAX_DELAY); 99 + exitActivity(); 100 + enterNewActivity(new KOReaderSettingsActivity(renderer, mappedInput, [this] { 101 + exitActivity(); 102 + updateRequired = true; 103 + })); 104 + xSemaphoreGive(renderingMutex); 105 + } else if (strcmp(setting.name, "Calibre Settings") == 0) { 106 + xSemaphoreTake(renderingMutex, portMAX_DELAY); 107 + exitActivity(); 108 + enterNewActivity(new CalibreSettingsActivity(renderer, mappedInput, [this] { 109 + exitActivity(); 110 + updateRequired = true; 111 + })); 112 + xSemaphoreGive(renderingMutex); 113 + } else if (strcmp(setting.name, "Check for updates") == 0) { 114 + xSemaphoreTake(renderingMutex, portMAX_DELAY); 115 + exitActivity(); 116 + enterNewActivity(new OtaUpdateActivity(renderer, mappedInput, [this] { 117 + exitActivity(); 118 + updateRequired = true; 119 + })); 120 + xSemaphoreGive(renderingMutex); 121 + } 122 + } else { 123 + return; 124 + } 125 + 126 + SETTINGS.saveToFile(); 127 + } 128 + 129 + void CategorySettingsActivity::displayTaskLoop() { 130 + while (true) { 131 + if (updateRequired && !subActivity) { 132 + updateRequired = false; 133 + xSemaphoreTake(renderingMutex, portMAX_DELAY); 134 + render(); 135 + xSemaphoreGive(renderingMutex); 136 + } 137 + vTaskDelay(10 / portTICK_PERIOD_MS); 138 + } 139 + } 140 + 141 + void CategorySettingsActivity::render() const { 142 + renderer.clearScreen(); 143 + 144 + const auto pageWidth = renderer.getScreenWidth(); 145 + const auto pageHeight = renderer.getScreenHeight(); 146 + 147 + renderer.drawCenteredText(UI_12_FONT_ID, 15, categoryName, true, EpdFontFamily::BOLD); 148 + 149 + // Draw selection highlight 150 + renderer.fillRect(0, 60 + selectedSettingIndex * 30 - 2, pageWidth - 1, 30); 151 + 152 + // Draw all settings 153 + for (int i = 0; i < settingsCount; i++) { 154 + const int settingY = 60 + i * 30; // 30 pixels between settings 155 + const bool isSelected = (i == selectedSettingIndex); 156 + 157 + // Draw setting name 158 + renderer.drawText(UI_10_FONT_ID, 20, settingY, settingsList[i].name, !isSelected); 159 + 160 + // Draw value based on setting type 161 + std::string valueText; 162 + if (settingsList[i].type == SettingType::TOGGLE && settingsList[i].valuePtr != nullptr) { 163 + const bool value = SETTINGS.*(settingsList[i].valuePtr); 164 + valueText = value ? "ON" : "OFF"; 165 + } else if (settingsList[i].type == SettingType::ENUM && settingsList[i].valuePtr != nullptr) { 166 + const uint8_t value = SETTINGS.*(settingsList[i].valuePtr); 167 + valueText = settingsList[i].enumValues[value]; 168 + } else if (settingsList[i].type == SettingType::VALUE && settingsList[i].valuePtr != nullptr) { 169 + valueText = std::to_string(SETTINGS.*(settingsList[i].valuePtr)); 170 + } 171 + if (!valueText.empty()) { 172 + const auto width = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str()); 173 + renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, valueText.c_str(), !isSelected); 174 + } 175 + } 176 + 177 + renderer.drawText(SMALL_FONT_ID, pageWidth - 20 - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION), 178 + pageHeight - 60, CROSSPOINT_VERSION); 179 + 180 + const auto labels = mappedInput.mapLabels("« Back", "Toggle", "", ""); 181 + renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); 182 + 183 + renderer.displayBuffer(); 184 + }
+70
src/activities/settings/CategorySettingsActivity.h
··· 1 + #pragma once 2 + #include <freertos/FreeRTOS.h> 3 + #include <freertos/semphr.h> 4 + #include <freertos/task.h> 5 + 6 + #include <functional> 7 + #include <string> 8 + #include <vector> 9 + 10 + #include "activities/ActivityWithSubactivity.h" 11 + 12 + class CrossPointSettings; 13 + 14 + enum class SettingType { TOGGLE, ENUM, ACTION, VALUE }; 15 + 16 + struct SettingInfo { 17 + const char* name; 18 + SettingType type; 19 + uint8_t CrossPointSettings::* valuePtr; 20 + std::vector<std::string> enumValues; 21 + 22 + struct ValueRange { 23 + uint8_t min; 24 + uint8_t max; 25 + uint8_t step; 26 + }; 27 + ValueRange valueRange; 28 + 29 + static SettingInfo Toggle(const char* name, uint8_t CrossPointSettings::* ptr) { 30 + return {name, SettingType::TOGGLE, ptr}; 31 + } 32 + 33 + static SettingInfo Enum(const char* name, uint8_t CrossPointSettings::* ptr, std::vector<std::string> values) { 34 + return {name, SettingType::ENUM, ptr, std::move(values)}; 35 + } 36 + 37 + static SettingInfo Action(const char* name) { return {name, SettingType::ACTION, nullptr}; } 38 + 39 + static SettingInfo Value(const char* name, uint8_t CrossPointSettings::* ptr, const ValueRange valueRange) { 40 + return {name, SettingType::VALUE, ptr, {}, valueRange}; 41 + } 42 + }; 43 + 44 + class CategorySettingsActivity final : public ActivityWithSubactivity { 45 + TaskHandle_t displayTaskHandle = nullptr; 46 + SemaphoreHandle_t renderingMutex = nullptr; 47 + bool updateRequired = false; 48 + int selectedSettingIndex = 0; 49 + const char* categoryName; 50 + const SettingInfo* settingsList; 51 + int settingsCount; 52 + const std::function<void()> onGoBack; 53 + 54 + static void taskTrampoline(void* param); 55 + [[noreturn]] void displayTaskLoop(); 56 + void render() const; 57 + void toggleCurrentSetting(); 58 + 59 + public: 60 + CategorySettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const char* categoryName, 61 + const SettingInfo* settingsList, int settingsCount, const std::function<void()>& onGoBack) 62 + : ActivityWithSubactivity("CategorySettings", renderer, mappedInput), 63 + categoryName(categoryName), 64 + settingsList(settingsList), 65 + settingsCount(settingsCount), 66 + onGoBack(onGoBack) {} 67 + void onEnter() override; 68 + void onExit() override; 69 + void loop() override; 70 + };
+71 -105
src/activities/settings/SettingsActivity.cpp
··· 3 3 #include <GfxRenderer.h> 4 4 #include <HardwareSerial.h> 5 5 6 - #include <cstring> 7 - 8 - #include "CalibreSettingsActivity.h" 6 + #include "CategorySettingsActivity.h" 9 7 #include "CrossPointSettings.h" 10 - #include "KOReaderSettingsActivity.h" 11 8 #include "MappedInputManager.h" 12 - #include "OtaUpdateActivity.h" 13 9 #include "fontIds.h" 14 10 15 - // Define the static settings list 11 + const char* SettingsActivity::categoryNames[categoryCount] = {"Display", "Reader", "Controls", "System"}; 12 + 16 13 namespace { 17 - constexpr int settingsCount = 22; 18 - const SettingInfo settingsList[settingsCount] = { 14 + constexpr int displaySettingsCount = 5; 15 + const SettingInfo displaySettings[displaySettingsCount] = { 19 16 // Should match with SLEEP_SCREEN_MODE 20 17 SettingInfo::Enum("Sleep Screen", &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover", "None"}), 21 18 SettingInfo::Enum("Sleep Screen Cover Mode", &CrossPointSettings::sleepScreenCoverMode, {"Fit", "Crop"}), 22 19 SettingInfo::Enum("Status Bar", &CrossPointSettings::statusBar, {"None", "No Progress", "Full"}), 23 20 SettingInfo::Enum("Hide Battery %", &CrossPointSettings::hideBatteryPercentage, {"Never", "In Reader", "Always"}), 24 - SettingInfo::Toggle("Extra Paragraph Spacing", &CrossPointSettings::extraParagraphSpacing), 25 - SettingInfo::Toggle("Text Anti-Aliasing", &CrossPointSettings::textAntiAliasing), 26 - SettingInfo::Enum("Short Power Button Click", &CrossPointSettings::shortPwrBtn, {"Ignore", "Sleep", "Page Turn"}), 21 + SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency, 22 + {"1 page", "5 pages", "10 pages", "15 pages", "30 pages"})}; 23 + 24 + constexpr int readerSettingsCount = 9; 25 + const SettingInfo readerSettings[readerSettingsCount] = { 26 + SettingInfo::Enum("Font Family", &CrossPointSettings::fontFamily, {"Bookerly", "Noto Sans", "Open Dyslexic"}), 27 + SettingInfo::Enum("Font Size", &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}), 28 + SettingInfo::Enum("Line Spacing", &CrossPointSettings::lineSpacing, {"Tight", "Normal", "Wide"}), 29 + SettingInfo::Value("Screen Margin", &CrossPointSettings::screenMargin, {5, 40, 5}), 30 + SettingInfo::Enum("Paragraph Alignment", &CrossPointSettings::paragraphAlignment, 31 + {"Justify", "Left", "Center", "Right"}), 32 + SettingInfo::Toggle("Hyphenation", &CrossPointSettings::hyphenationEnabled), 27 33 SettingInfo::Enum("Reading Orientation", &CrossPointSettings::orientation, 28 34 {"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}), 35 + SettingInfo::Toggle("Extra Paragraph Spacing", &CrossPointSettings::extraParagraphSpacing), 36 + SettingInfo::Toggle("Text Anti-Aliasing", &CrossPointSettings::textAntiAliasing)}; 37 + 38 + constexpr int controlsSettingsCount = 4; 39 + const SettingInfo controlsSettings[controlsSettingsCount] = { 29 40 SettingInfo::Enum("Front Button Layout", &CrossPointSettings::frontButtonLayout, 30 41 {"Bck, Cnfrm, Lft, Rght", "Lft, Rght, Bck, Cnfrm", "Lft, Bck, Cnfrm, Rght"}), 31 42 SettingInfo::Enum("Side Button Layout (reader)", &CrossPointSettings::sideButtonLayout, 32 43 {"Prev, Next", "Next, Prev"}), 33 44 SettingInfo::Toggle("Long-press Chapter Skip", &CrossPointSettings::longPressChapterSkip), 34 - SettingInfo::Enum("Reader Font Family", &CrossPointSettings::fontFamily, 35 - {"Bookerly", "Noto Sans", "Open Dyslexic"}), 36 - SettingInfo::Enum("Reader Font Size", &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}), 37 - SettingInfo::Enum("Reader Line Spacing", &CrossPointSettings::lineSpacing, {"Tight", "Normal", "Wide"}), 38 - SettingInfo::Value("Reader Screen Margin", &CrossPointSettings::screenMargin, {5, 40, 5}), 39 - SettingInfo::Enum("Reader Paragraph Alignment", &CrossPointSettings::paragraphAlignment, 40 - {"Justify", "Left", "Center", "Right"}), 41 - SettingInfo::Toggle("Hyphenation", &CrossPointSettings::hyphenationEnabled), 45 + SettingInfo::Enum("Short Power Button Click", &CrossPointSettings::shortPwrBtn, {"Ignore", "Sleep", "Page Turn"})}; 46 + 47 + constexpr int systemSettingsCount = 4; 48 + const SettingInfo systemSettings[systemSettingsCount] = { 42 49 SettingInfo::Enum("Time to Sleep", &CrossPointSettings::sleepTimeout, 43 50 {"1 min", "5 min", "10 min", "15 min", "30 min"}), 44 - SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency, 45 - {"1 page", "5 pages", "10 pages", "15 pages", "30 pages"}), 46 - SettingInfo::Action("KOReader Sync"), 47 - SettingInfo::Action("Calibre Settings"), 51 + SettingInfo::Action("KOReader Sync"), SettingInfo::Action("Calibre Settings"), 48 52 SettingInfo::Action("Check for updates")}; 49 53 } // namespace 50 54 ··· 57 61 Activity::onEnter(); 58 62 renderingMutex = xSemaphoreCreateMutex(); 59 63 60 - // Reset selection to first item 61 - selectedSettingIndex = 0; 64 + // Reset selection to first category 65 + selectedCategoryIndex = 0; 62 66 63 67 // Trigger first update 64 68 updateRequired = true; ··· 90 94 return; 91 95 } 92 96 93 - // Handle actions with early return 97 + // Handle category selection 94 98 if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { 95 - toggleCurrentSetting(); 96 - updateRequired = true; 99 + enterCategory(selectedCategoryIndex); 97 100 return; 98 101 } 99 102 ··· 107 110 if (mappedInput.wasPressed(MappedInputManager::Button::Up) || 108 111 mappedInput.wasPressed(MappedInputManager::Button::Left)) { 109 112 // Move selection up (with wrap-around) 110 - selectedSettingIndex = (selectedSettingIndex > 0) ? (selectedSettingIndex - 1) : (settingsCount - 1); 113 + selectedCategoryIndex = (selectedCategoryIndex > 0) ? (selectedCategoryIndex - 1) : (categoryCount - 1); 111 114 updateRequired = true; 112 115 } else if (mappedInput.wasPressed(MappedInputManager::Button::Down) || 113 116 mappedInput.wasPressed(MappedInputManager::Button::Right)) { 114 117 // Move selection down (with wrap around) 115 - selectedSettingIndex = (selectedSettingIndex < settingsCount - 1) ? (selectedSettingIndex + 1) : 0; 118 + selectedCategoryIndex = (selectedCategoryIndex < categoryCount - 1) ? (selectedCategoryIndex + 1) : 0; 116 119 updateRequired = true; 117 120 } 118 121 } 119 122 120 - void SettingsActivity::toggleCurrentSetting() { 121 - if (selectedSettingIndex < 0 || selectedSettingIndex >= settingsCount) { 123 + void SettingsActivity::enterCategory(int categoryIndex) { 124 + if (categoryIndex < 0 || categoryIndex >= categoryCount) { 122 125 return; 123 126 } 124 127 125 - const auto& setting = settingsList[selectedSettingIndex]; 128 + xSemaphoreTake(renderingMutex, portMAX_DELAY); 129 + exitActivity(); 126 130 127 - if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) { 128 - // Toggle the boolean value using the member pointer 129 - const bool currentValue = SETTINGS.*(setting.valuePtr); 130 - SETTINGS.*(setting.valuePtr) = !currentValue; 131 - } else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) { 132 - const uint8_t currentValue = SETTINGS.*(setting.valuePtr); 133 - SETTINGS.*(setting.valuePtr) = (currentValue + 1) % static_cast<uint8_t>(setting.enumValues.size()); 134 - } else if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) { 135 - // Decreasing would also be nice for large ranges I think but oh well can't have everything 136 - const int8_t currentValue = SETTINGS.*(setting.valuePtr); 137 - // Wrap to minValue if exceeding setting value boundary 138 - if (currentValue + setting.valueRange.step > setting.valueRange.max) { 139 - SETTINGS.*(setting.valuePtr) = setting.valueRange.min; 140 - } else { 141 - SETTINGS.*(setting.valuePtr) = currentValue + setting.valueRange.step; 142 - } 143 - } else if (setting.type == SettingType::ACTION) { 144 - if (strcmp(setting.name, "KOReader Sync") == 0) { 145 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 146 - exitActivity(); 147 - enterNewActivity(new KOReaderSettingsActivity(renderer, mappedInput, [this] { 148 - exitActivity(); 149 - updateRequired = true; 150 - })); 151 - xSemaphoreGive(renderingMutex); 152 - } else if (strcmp(setting.name, "Calibre Settings") == 0) { 153 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 154 - exitActivity(); 155 - enterNewActivity(new CalibreSettingsActivity(renderer, mappedInput, [this] { 156 - exitActivity(); 157 - updateRequired = true; 158 - })); 159 - xSemaphoreGive(renderingMutex); 160 - } else if (strcmp(setting.name, "Check for updates") == 0) { 161 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 162 - exitActivity(); 163 - enterNewActivity(new OtaUpdateActivity(renderer, mappedInput, [this] { 164 - exitActivity(); 165 - updateRequired = true; 166 - })); 167 - xSemaphoreGive(renderingMutex); 168 - } 169 - } else { 170 - // Only toggle if it's a toggle type and has a value pointer 171 - return; 131 + const SettingInfo* settingsList = nullptr; 132 + int settingsCount = 0; 133 + 134 + switch (categoryIndex) { 135 + case 0: // Display 136 + settingsList = displaySettings; 137 + settingsCount = displaySettingsCount; 138 + break; 139 + case 1: // Reader 140 + settingsList = readerSettings; 141 + settingsCount = readerSettingsCount; 142 + break; 143 + case 2: // Controls 144 + settingsList = controlsSettings; 145 + settingsCount = controlsSettingsCount; 146 + break; 147 + case 3: // System 148 + settingsList = systemSettings; 149 + settingsCount = systemSettingsCount; 150 + break; 172 151 } 173 152 174 - // Save settings when they change 175 - SETTINGS.saveToFile(); 153 + enterNewActivity(new CategorySettingsActivity(renderer, mappedInput, categoryNames[categoryIndex], settingsList, 154 + settingsCount, [this] { 155 + exitActivity(); 156 + updateRequired = true; 157 + })); 158 + xSemaphoreGive(renderingMutex); 176 159 } 177 160 178 161 void SettingsActivity::displayTaskLoop() { ··· 196 179 // Draw header 197 180 renderer.drawCenteredText(UI_12_FONT_ID, 15, "Settings", true, EpdFontFamily::BOLD); 198 181 199 - // Draw selection highlight 200 - renderer.fillRect(0, 60 + selectedSettingIndex * 30 - 2, pageWidth - 1, 30); 182 + // Draw selection 183 + renderer.fillRect(0, 60 + selectedCategoryIndex * 30 - 2, pageWidth - 1, 30); 201 184 202 - // Draw all settings 203 - for (int i = 0; i < settingsCount; i++) { 204 - const int settingY = 60 + i * 30; // 30 pixels between settings 205 - const bool isSelected = (i == selectedSettingIndex); 206 - 207 - // Draw setting name 208 - renderer.drawText(UI_10_FONT_ID, 20, settingY, settingsList[i].name, !isSelected); 185 + // Draw all categories 186 + for (int i = 0; i < categoryCount; i++) { 187 + const int categoryY = 60 + i * 30; // 30 pixels between categories 209 188 210 - // Draw value based on setting type 211 - std::string valueText; 212 - if (settingsList[i].type == SettingType::TOGGLE && settingsList[i].valuePtr != nullptr) { 213 - const bool value = SETTINGS.*(settingsList[i].valuePtr); 214 - valueText = value ? "ON" : "OFF"; 215 - } else if (settingsList[i].type == SettingType::ENUM && settingsList[i].valuePtr != nullptr) { 216 - const uint8_t value = SETTINGS.*(settingsList[i].valuePtr); 217 - valueText = settingsList[i].enumValues[value]; 218 - } else if (settingsList[i].type == SettingType::VALUE && settingsList[i].valuePtr != nullptr) { 219 - valueText = std::to_string(SETTINGS.*(settingsList[i].valuePtr)); 220 - } 221 - if (!valueText.empty()) { 222 - const auto width = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str()); 223 - renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, valueText.c_str(), !isSelected); 224 - } 189 + // Draw category name 190 + renderer.drawText(UI_10_FONT_ID, 20, categoryY, categoryNames[i], i != selectedCategoryIndex); 225 191 } 226 192 227 193 // Draw version text above button hints ··· 229 195 pageHeight - 60, CROSSPOINT_VERSION); 230 196 231 197 // Draw help text 232 - const auto labels = mappedInput.mapLabels("« Save", "Toggle", "", ""); 198 + const auto labels = mappedInput.mapLabels("« Back", "Select", "", ""); 233 199 renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); 234 200 235 201 // Always use standard refresh for settings screen
+6 -35
src/activities/settings/SettingsActivity.h
··· 10 10 #include "activities/ActivityWithSubactivity.h" 11 11 12 12 class CrossPointSettings; 13 - 14 - enum class SettingType { TOGGLE, ENUM, ACTION, VALUE }; 15 - 16 - // Structure to hold setting information 17 - struct SettingInfo { 18 - const char* name; // Display name of the setting 19 - SettingType type; // Type of setting 20 - uint8_t CrossPointSettings::* valuePtr; // Pointer to member in CrossPointSettings (for TOGGLE/ENUM/VALUE) 21 - std::vector<std::string> enumValues; 22 - 23 - struct ValueRange { 24 - uint8_t min; 25 - uint8_t max; 26 - uint8_t step; 27 - }; 28 - // Bounds/step for VALUE type settings 29 - ValueRange valueRange; 30 - 31 - // Static constructors 32 - static SettingInfo Toggle(const char* name, uint8_t CrossPointSettings::* ptr) { 33 - return {name, SettingType::TOGGLE, ptr}; 34 - } 35 - 36 - static SettingInfo Enum(const char* name, uint8_t CrossPointSettings::* ptr, std::vector<std::string> values) { 37 - return {name, SettingType::ENUM, ptr, std::move(values)}; 38 - } 39 - 40 - static SettingInfo Action(const char* name) { return {name, SettingType::ACTION, nullptr}; } 41 - 42 - static SettingInfo Value(const char* name, uint8_t CrossPointSettings::* ptr, const ValueRange valueRange) { 43 - return {name, SettingType::VALUE, ptr, {}, valueRange}; 44 - } 45 - }; 13 + struct SettingInfo; 46 14 47 15 class SettingsActivity final : public ActivityWithSubactivity { 48 16 TaskHandle_t displayTaskHandle = nullptr; 49 17 SemaphoreHandle_t renderingMutex = nullptr; 50 18 bool updateRequired = false; 51 - int selectedSettingIndex = 0; // Currently selected setting 19 + int selectedCategoryIndex = 0; // Currently selected category 52 20 const std::function<void()> onGoHome; 53 21 22 + static constexpr int categoryCount = 4; 23 + static const char* categoryNames[categoryCount]; 24 + 54 25 static void taskTrampoline(void* param); 55 26 [[noreturn]] void displayTaskLoop(); 56 27 void render() const; 57 - void toggleCurrentSetting(); 28 + void enterCategory(int categoryIndex); 58 29 59 30 public: 60 31 explicit SettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,