diff --git a/src/activities/settings/CategorySettingsActivity.cpp b/src/activities/settings/CategorySettingsActivity.cpp index a6182b5c..46e934eb 100644 --- a/src/activities/settings/CategorySettingsActivity.cpp +++ b/src/activities/settings/CategorySettingsActivity.cpp @@ -11,6 +11,7 @@ #include "KOReaderSettingsActivity.h" #include "MappedInputManager.h" #include "OtaUpdateActivity.h" +#include "SleepBmpSelectionActivity.h" #include "fontIds.h" void CategorySettingsActivity::taskTrampoline(void* param) { @@ -61,24 +62,40 @@ void CategorySettingsActivity::loop() { return; } - // Handle navigation + // Handle navigation (skip hidden settings) + const int visibleCount = getVisibleSettingsCount(); + if (visibleCount == 0) { + return; // No visible settings + } + if (mappedInput.wasPressed(MappedInputManager::Button::Up) || mappedInput.wasPressed(MappedInputManager::Button::Left)) { - selectedSettingIndex = (selectedSettingIndex > 0) ? (selectedSettingIndex - 1) : (settingsCount - 1); + // Move to previous visible setting + int currentActual = mapVisibleIndexToActualIndex(selectedSettingIndex); + do { + currentActual = (currentActual > 0) ? (currentActual - 1) : (settingsCount - 1); + } while (!shouldShowSetting(currentActual) && currentActual != mapVisibleIndexToActualIndex(selectedSettingIndex)); + selectedSettingIndex = mapActualIndexToVisibleIndex(currentActual); updateRequired = true; } else if (mappedInput.wasPressed(MappedInputManager::Button::Down) || mappedInput.wasPressed(MappedInputManager::Button::Right)) { - selectedSettingIndex = (selectedSettingIndex < settingsCount - 1) ? (selectedSettingIndex + 1) : 0; + // Move to next visible setting + int currentActual = mapVisibleIndexToActualIndex(selectedSettingIndex); + do { + currentActual = (currentActual < settingsCount - 1) ? (currentActual + 1) : 0; + } while (!shouldShowSetting(currentActual) && currentActual != mapVisibleIndexToActualIndex(selectedSettingIndex)); + selectedSettingIndex = mapActualIndexToVisibleIndex(currentActual); updateRequired = true; } } void CategorySettingsActivity::toggleCurrentSetting() { - if (selectedSettingIndex < 0 || selectedSettingIndex >= settingsCount) { + const int actualIndex = mapVisibleIndexToActualIndex(selectedSettingIndex); + if (actualIndex < 0 || actualIndex >= settingsCount) { return; } - const auto& setting = settingsList[selectedSettingIndex]; + const auto& setting = settingsList[actualIndex]; if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) { // Toggle the boolean value using the member pointer @@ -87,6 +104,16 @@ void CategorySettingsActivity::toggleCurrentSetting() { } 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()); + + // If sleep screen changed away from CUSTOM, adjust selection if needed + if (setting.valuePtr == &CrossPointSettings::sleepScreen) { + const int visibleCount = getVisibleSettingsCount(); + // If current selection is now hidden or out of bounds, adjust it + const int currentActual = mapVisibleIndexToActualIndex(selectedSettingIndex); + if (!shouldShowSetting(currentActual) || selectedSettingIndex >= visibleCount) { + selectedSettingIndex = visibleCount > 0 ? visibleCount - 1 : 0; + } + } } else if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) { const int8_t currentValue = SETTINGS.*(setting.valuePtr); if (currentValue + setting.valueRange.step > setting.valueRange.max) { @@ -127,6 +154,14 @@ void CategorySettingsActivity::toggleCurrentSetting() { updateRequired = true; })); xSemaphoreGive(renderingMutex); + } else if (strcmp(setting.name, "Select Sleep BMP") == 0) { + xSemaphoreTake(renderingMutex, portMAX_DELAY); + exitActivity(); + enterNewActivity(new SleepBmpSelectionActivity(renderer, mappedInput, [this] { + exitActivity(); + updateRequired = true; + })); + xSemaphoreGive(renderingMutex); } } else { return; @@ -147,6 +182,57 @@ void CategorySettingsActivity::displayTaskLoop() { } } +bool CategorySettingsActivity::shouldShowSetting(int index) const { + if (index < 0 || index >= settingsCount) { + return false; + } + // Hide "Select Sleep BMP" if sleep screen is not set to CUSTOM + if (settingsList[index].type == SettingType::ACTION && + strcmp(settingsList[index].name, "Select Sleep BMP") == 0) { + return SETTINGS.sleepScreen == CrossPointSettings::SLEEP_SCREEN_MODE::CUSTOM; + } + return true; +} + +int CategorySettingsActivity::getVisibleSettingsCount() const { + int count = 0; + for (int i = 0; i < settingsCount; i++) { + if (shouldShowSetting(i)) { + count++; + } + } + return count; +} + +int CategorySettingsActivity::mapVisibleIndexToActualIndex(int visibleIndex) const { + int visibleCount = 0; + for (int i = 0; i < settingsCount; i++) { + if (shouldShowSetting(i)) { + if (visibleCount == visibleIndex) { + return i; + } + visibleCount++; + } + } + // If visibleIndex is out of bounds, return first visible setting + for (int i = 0; i < settingsCount; i++) { + if (shouldShowSetting(i)) { + return i; + } + } + return 0; // Fallback +} + +int CategorySettingsActivity::mapActualIndexToVisibleIndex(int actualIndex) const { + int visibleIndex = 0; + for (int i = 0; i < actualIndex; i++) { + if (shouldShowSetting(i)) { + visibleIndex++; + } + } + return visibleIndex; +} + void CategorySettingsActivity::render() const { renderer.clearScreen(); @@ -155,13 +241,31 @@ void CategorySettingsActivity::render() const { renderer.drawCenteredText(UI_12_FONT_ID, 15, categoryName, true, EpdFontFamily::BOLD); + // Calculate visible settings count and map selection + const int visibleCount = getVisibleSettingsCount(); + const int actualSelectedIndex = mapVisibleIndexToActualIndex(selectedSettingIndex); + // Draw selection highlight - renderer.fillRect(0, 60 + selectedSettingIndex * 30 - 2, pageWidth - 1, 30); - - // Draw all settings + int visibleIndex = 0; for (int i = 0; i < settingsCount; i++) { - const int settingY = 60 + i * 30; // 30 pixels between settings - const bool isSelected = (i == selectedSettingIndex); + if (shouldShowSetting(i)) { + if (i == actualSelectedIndex) { + renderer.fillRect(0, 60 + visibleIndex * 30 - 2, pageWidth - 1, 30); + break; + } + visibleIndex++; + } + } + + // Draw all visible settings + visibleIndex = 0; + for (int i = 0; i < settingsCount; i++) { + if (!shouldShowSetting(i)) { + continue; + } + + const int settingY = 60 + visibleIndex * 30; // 30 pixels between settings + const bool isSelected = (i == actualSelectedIndex); // Draw setting name renderer.drawText(UI_10_FONT_ID, 20, settingY, settingsList[i].name, !isSelected); @@ -176,11 +280,22 @@ void CategorySettingsActivity::render() const { 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 (settingsList[i].type == SettingType::ACTION && strcmp(settingsList[i].name, "Select Sleep BMP") == 0) { + if (SETTINGS.selectedSleepBmp[0] != '\0') { + valueText = SETTINGS.selectedSleepBmp; + if (valueText.length() > 20) { + valueText = valueText.substr(0, 17) + "..."; + } + } else { + valueText = "Random"; + } } if (!valueText.empty()) { const auto width = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str()); renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, valueText.c_str(), !isSelected); } + + visibleIndex++; } renderer.drawText(SMALL_FONT_ID, pageWidth - 20 - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION), diff --git a/src/activities/settings/CategorySettingsActivity.h b/src/activities/settings/CategorySettingsActivity.h index a7d1f0ce..b31dc1e7 100644 --- a/src/activities/settings/CategorySettingsActivity.h +++ b/src/activities/settings/CategorySettingsActivity.h @@ -55,6 +55,10 @@ class CategorySettingsActivity final : public ActivityWithSubactivity { [[noreturn]] void displayTaskLoop(); void render() const; void toggleCurrentSetting(); + bool shouldShowSetting(int index) const; + int getVisibleSettingsCount() const; + int mapVisibleIndexToActualIndex(int visibleIndex) const; + int mapActualIndexToVisibleIndex(int actualIndex) const; public: CategorySettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const char* categoryName, diff --git a/src/activities/settings/SleepBmpSelectionActivity.cpp b/src/activities/settings/SleepBmpSelectionActivity.cpp index c6095f28..fd93976c 100644 --- a/src/activities/settings/SleepBmpSelectionActivity.cpp +++ b/src/activities/settings/SleepBmpSelectionActivity.cpp @@ -14,16 +14,18 @@ #include "../../../lib/GfxRenderer/Bitmap.h" namespace { -constexpr int PAGE_ITEMS = 23; constexpr int SKIP_PAGE_MS = 700; constexpr unsigned long IGNORE_INPUT_MS = 300; // Ignore input for 300ms after entering +constexpr int LINE_HEIGHT = 30; +constexpr int START_Y = 60; +constexpr int BOTTOM_BAR_HEIGHT = 60; // Space for button hints void sortFileList(std::vector& strs) { std::sort(begin(strs), end(strs), [](const std::string& str1, const std::string& str2) { - return lexicographical_compare(begin(str1), end(str1), begin(str2), end(str2), - [](const char& char1, const char& char2) { - return tolower(char1) < tolower(char2); - }); + return std::lexicographical_compare(begin(str1), end(str1), begin(str2), end(str2), + [](const char& char1, const char& char2) { + return std::tolower(char1) < std::tolower(char2); + }); }); } } // namespace @@ -35,44 +37,48 @@ void SleepBmpSelectionActivity::taskTrampoline(void* param) { void SleepBmpSelectionActivity::loadFiles() { files.clear(); - + + std::vector bmpFiles; + auto dir = SdMan.open("/sleep"); - if (!dir || !dir.isDirectory()) { - if (dir) dir.close(); - return; + if (dir && dir.isDirectory()) { + dir.rewindDirectory(); + char name[500]; + + for (auto file = dir.openNextFile(); file; file = dir.openNextFile()) { + if (file.isDirectory()) { + file.close(); + continue; + } + + file.getName(name, sizeof(name)); + auto filename = std::string(name); + + if (filename[0] == '.' || filename.length() < 4 || + filename.substr(filename.length() - 4) != ".bmp") { + file.close(); + continue; + } + + // Validate BMP + Bitmap bitmap(file); + if (bitmap.parseHeaders() != BmpReaderError::Ok) { + file.close(); + continue; + } + file.close(); + + bmpFiles.emplace_back(filename); + } + dir.close(); + + // Sort alphabetically (case-insensitive) + sortFileList(bmpFiles); } - - dir.rewindDirectory(); - - char name[500]; - for (auto file = dir.openNextFile(); file; file = dir.openNextFile()) { - if (file.isDirectory()) { - file.close(); - continue; - } - file.getName(name, sizeof(name)); - auto filename = std::string(name); - if (filename[0] == '.') { - file.close(); - continue; - } - - if (filename.substr(filename.length() - 4) != ".bmp") { - file.close(); - continue; - } - - Bitmap bitmap(file); - if (bitmap.parseHeaders() != BmpReaderError::Ok) { - file.close(); - continue; - } - file.close(); - - files.emplace_back(filename); - } - dir.close(); - sortFileList(files); + + // Add "Random" as first option, then sorted BMP files + files.emplace_back("Random"); + files.insert(files.end(), bmpFiles.begin(), bmpFiles.end()); } void SleepBmpSelectionActivity::onEnter() { @@ -81,7 +87,21 @@ void SleepBmpSelectionActivity::onEnter() { renderingMutex = xSemaphoreCreateMutex(); loadFiles(); - selectorIndex = 0; + + // Set initial selection: "Random" if no file selected, otherwise find the selected file + if (SETTINGS.selectedSleepBmp[0] == '\0') { + selectorIndex = 0; // "Random" is at index 0 + } else { + // Find the selected file in the sorted list + selectorIndex = 0; // Default to "Random" if not found + for (size_t i = 1; i < files.size(); i++) { + if (files[i] == SETTINGS.selectedSleepBmp) { + selectorIndex = i; + break; + } + } + } + enterTime = millis(); updateRequired = true; @@ -122,28 +142,51 @@ void SleepBmpSelectionActivity::loop() { const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS; if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - if (files.empty()) { + if (files.empty() || selectorIndex >= files.size()) { return; } const std::string selectedFile = files[selectorIndex]; - strncpy(SETTINGS.selectedSleepBmp, selectedFile.c_str(), sizeof(SETTINGS.selectedSleepBmp) - 1); - SETTINGS.selectedSleepBmp[sizeof(SETTINGS.selectedSleepBmp) - 1] = '\0'; + if (selectedFile == "Random") { + // Clear the selection to use random + SETTINGS.selectedSleepBmp[0] = '\0'; + } else { + strncpy(SETTINGS.selectedSleepBmp, selectedFile.c_str(), sizeof(SETTINGS.selectedSleepBmp) - 1); + SETTINGS.selectedSleepBmp[sizeof(SETTINGS.selectedSleepBmp) - 1] = '\0'; + } SETTINGS.saveToFile(); onBack(); } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { onBack(); } else if (prevReleased) { + if (files.empty()) { + return; + } + + // Calculate items per page dynamically + const int screenHeight = renderer.getScreenHeight(); + const int availableHeight = screenHeight - START_Y - BOTTOM_BAR_HEIGHT; + const int pageItems = (availableHeight / LINE_HEIGHT); + if (skipPage) { - selectorIndex = ((selectorIndex / PAGE_ITEMS - 1) * PAGE_ITEMS + files.size()) % files.size(); + selectorIndex = ((selectorIndex / pageItems - 1) * pageItems + files.size()) % files.size(); } else { selectorIndex = (selectorIndex + files.size() - 1) % files.size(); } updateRequired = true; } else if (nextReleased) { + if (files.empty()) { + return; + } + + // Calculate items per page dynamically + const int screenHeight = renderer.getScreenHeight(); + const int availableHeight = screenHeight - START_Y - BOTTOM_BAR_HEIGHT; + const int pageItems = (availableHeight / LINE_HEIGHT); + if (skipPage) { - selectorIndex = ((selectorIndex / PAGE_ITEMS + 1) * PAGE_ITEMS) % files.size(); + selectorIndex = ((selectorIndex / pageItems + 1) * pageItems) % files.size(); } else { selectorIndex = (selectorIndex + 1) % files.size(); } @@ -179,11 +222,27 @@ void SleepBmpSelectionActivity::render() const { return; } - const auto pageStartIndex = selectorIndex / PAGE_ITEMS * PAGE_ITEMS; - renderer.fillRect(0, 60 + (selectorIndex % PAGE_ITEMS) * 30 - 2, pageWidth - 1, 30); - for (size_t i = pageStartIndex; i < files.size() && i < pageStartIndex + PAGE_ITEMS; i++) { + // Calculate items per page based on screen height + const int screenHeight = renderer.getScreenHeight(); + const int availableHeight = screenHeight - START_Y - BOTTOM_BAR_HEIGHT; + const int pageItems = (availableHeight / LINE_HEIGHT); + + // Calculate page start index + const auto pageStartIndex = selectorIndex / pageItems * pageItems; + + // Draw selection highlight + const int visibleSelectedIndex = static_cast(selectorIndex - pageStartIndex); + if (visibleSelectedIndex >= 0 && visibleSelectedIndex < pageItems && selectorIndex < files.size()) { + renderer.fillRect(0, START_Y + visibleSelectedIndex * LINE_HEIGHT - 2, pageWidth - 1, LINE_HEIGHT); + } + + // Draw visible files + int visibleIndex = 0; + for (size_t i = pageStartIndex; i < files.size() && visibleIndex < pageItems; i++) { auto item = renderer.truncatedText(UI_10_FONT_ID, files[i].c_str(), renderer.getScreenWidth() - 40); - renderer.drawText(UI_10_FONT_ID, 20, 60 + (i % PAGE_ITEMS) * 30, item.c_str(), i != selectorIndex); + const bool isSelected = (i == selectorIndex); + renderer.drawText(UI_10_FONT_ID, 20, START_Y + visibleIndex * LINE_HEIGHT, item.c_str(), !isSelected); + visibleIndex++; } renderer.displayBuffer(); diff --git a/src/activities/settings/SleepBmpSelectionActivity.h b/src/activities/settings/SleepBmpSelectionActivity.h index f1737d05..f9906a39 100644 --- a/src/activities/settings/SleepBmpSelectionActivity.h +++ b/src/activities/settings/SleepBmpSelectionActivity.h @@ -12,7 +12,7 @@ class SleepBmpSelectionActivity final : public Activity { TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; - std::vector files; + std::vector files; // Sorted list of valid BMP filenames ("Random" at index 0) size_t selectorIndex = 0; bool updateRequired = false; unsigned long enterTime = 0; // Time when activity was entered @@ -21,7 +21,7 @@ class SleepBmpSelectionActivity final : public Activity { static void taskTrampoline(void* param); [[noreturn]] void displayTaskLoop(); void render() const; - void loadFiles(); + void loadFiles(); // Load and sort all valid BMP files public: explicit SleepBmpSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,