diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 948ec599..6635e158 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -518,6 +518,7 @@ STR_IMAGE_DISPLAY_GRAYSCALE: ">> Gray" STR_WEATHER_MOON_INFO: "Moon" STR_WEATHER_SUN_INFO: "Sun" STR_READER_BOOKMARKS: "Bookmarks & Footnotes" +STR_READER_OVERRIDES: "Book-specific overrides" STR_READER_UTILS: "Helper" STR_READER_TOOLS: "Tools" STR_READER_NAVIGATION: "Navigation" diff --git a/src/JsonSettingsIO.cpp b/src/JsonSettingsIO.cpp index 1b818a12..d095010b 100644 --- a/src/JsonSettingsIO.cpp +++ b/src/JsonSettingsIO.cpp @@ -394,6 +394,8 @@ bool JsonSettingsIO::saveRecentBooks(const RecentBooksStore& store, const char* obj["coverBmpPath"] = book.coverBmpPath; obj["embeddedStyleOverride"] = book.embeddedStyleOverride; obj["imageRenderingOverride"] = book.imageRenderingOverride; + obj["fontFamilyOverride"] = book.fontFamilyOverride; + obj["fontSizeOverride"] = book.fontSizeOverride; } String json; @@ -428,6 +430,9 @@ bool JsonSettingsIO::loadRecentBooks(RecentBooksStore& store, const char* json) book.coverBmpPath = obj["coverBmpPath"] | std::string(""); book.embeddedStyleOverride = clampInt8(obj["embeddedStyleOverride"] | -1, -1, 1, -1); book.imageRenderingOverride = clampInt8(obj["imageRenderingOverride"] | -1, -1, 2, -1); + book.fontFamilyOverride = + clampInt8(obj["fontFamilyOverride"] | -1, -1, CrossPointSettings::FONT_FAMILY_COUNT - 1, -1); + book.fontSizeOverride = clampInt8(obj["fontSizeOverride"] | -1, -1, CrossPointSettings::FONT_SIZE_COUNT - 1, -1); store.recentBooks.push_back(book); } diff --git a/src/RecentBooksStore.cpp b/src/RecentBooksStore.cpp index 08e43168..8fbadd06 100644 --- a/src/RecentBooksStore.cpp +++ b/src/RecentBooksStore.cpp @@ -24,6 +24,8 @@ void RecentBooksStore::addBook(const std::string& path, const std::string& title const std::string& series, const std::string& coverBmpPath) { int8_t embeddedStyleOverride = -1; int8_t imageRenderingOverride = -1; + int8_t fontFamilyOverride = -1; + int8_t fontSizeOverride = -1; // Remove existing entry if present auto it = @@ -31,12 +33,14 @@ void RecentBooksStore::addBook(const std::string& path, const std::string& title if (it != recentBooks.end()) { embeddedStyleOverride = it->embeddedStyleOverride; imageRenderingOverride = it->imageRenderingOverride; + fontFamilyOverride = it->fontFamilyOverride; + fontSizeOverride = it->fontSizeOverride; recentBooks.erase(it); } // Add to front - recentBooks.insert(recentBooks.begin(), - {path, title, author, series, coverBmpPath, embeddedStyleOverride, imageRenderingOverride}); + recentBooks.insert(recentBooks.begin(), {path, title, author, series, coverBmpPath, embeddedStyleOverride, + imageRenderingOverride, fontFamilyOverride, fontSizeOverride}); // Trim to max size if (recentBooks.size() > MAX_RECENT_BOOKS) { @@ -85,9 +89,23 @@ bool RecentBooksStore::setReaderOverrides(const std::string& path, const int8_t if (it == recentBooks.end()) { return false; } + return setReaderOverrides(path, embeddedStyleOverride, imageRenderingOverride, it->fontFamilyOverride, + it->fontSizeOverride); +} + +bool RecentBooksStore::setReaderOverrides(const std::string& path, const int8_t embeddedStyleOverride, + const int8_t imageRenderingOverride, const int8_t fontFamilyOverride, + const int8_t fontSizeOverride) { + auto it = + std::find_if(recentBooks.begin(), recentBooks.end(), [&](const RecentBook& book) { return book.path == path; }); + if (it == recentBooks.end()) { + return false; + } it->embeddedStyleOverride = embeddedStyleOverride; it->imageRenderingOverride = imageRenderingOverride; + it->fontFamilyOverride = fontFamilyOverride; + it->fontSizeOverride = fontSizeOverride; return saveToFile(); } diff --git a/src/RecentBooksStore.h b/src/RecentBooksStore.h index afc6c3de..6ca8f7a6 100644 --- a/src/RecentBooksStore.h +++ b/src/RecentBooksStore.h @@ -13,6 +13,10 @@ struct RecentBook { int8_t embeddedStyleOverride = -1; // -1 = use global setting, otherwise CrossPointSettings::IMAGE_RENDERING value. int8_t imageRenderingOverride = -1; + // -1 = use global setting, otherwise CrossPointSettings::FONT_FAMILY value. + int8_t fontFamilyOverride = -1; + // -1 = use global setting, otherwise CrossPointSettings::FONT_SIZE value. + int8_t fontSizeOverride = -1; bool operator==(const RecentBook& other) const { return path == other.path; } }; @@ -58,6 +62,8 @@ class RecentBooksStore { RecentBook getDataFromBook(std::string path) const; RecentBook getBookByPath(const std::string& path) const; bool setReaderOverrides(const std::string& path, int8_t embeddedStyleOverride, int8_t imageRenderingOverride); + bool setReaderOverrides(const std::string& path, int8_t embeddedStyleOverride, int8_t imageRenderingOverride, + int8_t fontFamilyOverride, int8_t fontSizeOverride); private: bool loadFromBinaryFile(); diff --git a/src/activities/ActivityResult.h b/src/activities/ActivityResult.h index c7c716fe..151137b6 100644 --- a/src/activities/ActivityResult.h +++ b/src/activities/ActivityResult.h @@ -20,10 +20,13 @@ struct KeyboardResult { struct MenuResult { int action = -1; + int nameId = -1; uint8_t orientation = 0; uint8_t pageTurnOption = 0; int8_t embeddedStyleOverride = -1; int8_t imageRenderingOverride = -1; + int8_t fontFamilyOverride = -1; + int8_t fontSizeOverride = -1; uint8_t textDarkness = 1; }; diff --git a/src/activities/MenuListActivity.cpp b/src/activities/MenuListActivity.cpp index 4fd19076..d3b919b5 100644 --- a/src/activities/MenuListActivity.cpp +++ b/src/activities/MenuListActivity.cpp @@ -4,6 +4,7 @@ #include "MappedInputManager.h" #include "components/UITheme.h" +#include "settings/SettingsSubmenuActivity.h" void MenuListActivity::initMenuList() { const int count = static_cast(menuItems.size()); @@ -16,6 +17,10 @@ void MenuListActivity::initMenuList() { void MenuListActivity::onEnter() { Activity::onEnter(); + if (!submenusPrepared) { + prepareSubmenus(); + submenusPrepared = true; + } initMenuList(); requestUpdate(); } @@ -37,6 +42,10 @@ void MenuListActivity::toggleCurrentItem() { if (item.isSeparator) return; if (item.type == SettingType::ACTION) { + if (item.action == SettingAction::Submenu) { + openSubmenu(item); + return; + } onActionSelected(selectedIndex); return; } @@ -55,6 +64,18 @@ void MenuListActivity::drawMenuList(const Rect& rect) { [this](int index) { return getItemValueString(index); }, true); } +void MenuListActivity::prepareSubmenus() { SettingInfo::prepareSubmenus(menuItems, submenuData); } + +void MenuListActivity::openSubmenu(const SettingInfo& submenuEntry) { + auto it = std::find_if(submenuData.begin(), submenuData.end(), + [&submenuEntry](const SettingInfo::SubmenuData& d) { return d.id == submenuEntry.nameId; }); + if (it == submenuData.end()) return; + + startActivityForResult( + std::make_unique(renderer, mappedInput, submenuEntry.nameId, it->items), + [this](const ActivityResult&) { requestUpdate(); }); +} + void MenuListActivity::loop() { if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { onBackPressed(); diff --git a/src/activities/MenuListActivity.h b/src/activities/MenuListActivity.h index f8a8fdda..9d501fed 100644 --- a/src/activities/MenuListActivity.h +++ b/src/activities/MenuListActivity.h @@ -70,17 +70,23 @@ struct Rect; class MenuListActivity : public Activity { protected: std::vector menuItems; + std::vector submenuData; int selectedIndex = 0; ButtonNavigator buttonNavigator; + bool submenusPrepared = false; // Call after building/rebuilding menuItems to wire up the selectable predicate. void initMenuList(); + // Process SettingInfo items marked with withSubmenu() into submenu placeholders. + void prepareSubmenus(); + void openSubmenu(const SettingInfo& submenuEntry); + // Handle up/down navigation via buttonNavigator. Call from loop() if overriding. void handleNavigation(); // Toggle/cycle the currently selected item. For ACTION items, delegates to onActionSelected(). - void toggleCurrentItem(); + virtual void toggleCurrentItem(); // Draw the list into the given rect using GUI.drawList(). void drawMenuList(const Rect& rect); diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index e2711c7b..8293f805 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -148,6 +148,8 @@ void EpubReaderActivity::onEnter() { const RecentBook currentBook = RECENT_BOOKS.getBookByPath(epub->getPath()); bookEmbeddedStyleOverride = currentBook.embeddedStyleOverride; bookImageRenderingOverride = currentBook.imageRenderingOverride; + bookFontFamilyOverride = currentBook.fontFamilyOverride; + bookFontSizeOverride = currentBook.fontSizeOverride; logReaderMemSnapshot("onEnter_after_recent_books"); // Trigger first update @@ -239,22 +241,23 @@ void EpubReaderActivity::loop() { const bool isCurrentPageStarred = section && bookmarkStore.has(static_cast(currentSpineIndex), static_cast(section->currentPage)); ReaderUtils::enforceExitFullRefresh(renderer); - startActivityForResult( - std::make_unique( - renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent, SETTINGS.orientation, - !currentPageFootnotes.empty(), bookEmbeddedStyleOverride, bookImageRenderingOverride, SETTINGS.textDarkness, - !bookmarkStore.isEmpty(), isCurrentPageStarred), - [this](const ActivityResult& result) { - // Always apply orientation/darkness change even if the menu was cancelled - const auto& menu = std::get(result.data); - applyOrientation(menu.orientation); - applyTextDarkness(menu.textDarkness); - toggleAutoPageTurn(menu.pageTurnOption); - applyBookReaderOverrides(menu.embeddedStyleOverride, menu.imageRenderingOverride); - if (!result.isCancelled) { - onReaderMenuConfirm(static_cast(menu.action)); - } - }); + startActivityForResult(std::make_unique( + renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent, + SETTINGS.orientation, !currentPageFootnotes.empty(), bookEmbeddedStyleOverride, + bookImageRenderingOverride, bookFontFamilyOverride, bookFontSizeOverride, + SETTINGS.textDarkness, !bookmarkStore.isEmpty(), isCurrentPageStarred), + [this](const ActivityResult& result) { + // Always apply orientation/darkness change even if the menu was cancelled + const auto& menu = std::get(result.data); + applyOrientation(menu.orientation); + applyTextDarkness(menu.textDarkness); + toggleAutoPageTurn(menu.pageTurnOption); + applyBookReaderOverrides(menu.embeddedStyleOverride, menu.imageRenderingOverride, + menu.fontFamilyOverride, menu.fontSizeOverride); + if (!result.isCancelled) { + onReaderMenuConfirm(static_cast(menu.action)); + } + }); } // Long press BACK (1s+) goes to home screen @@ -814,18 +817,23 @@ void EpubReaderActivity::toggleAutoPageTurn(const uint8_t selectedPageTurnOption } void EpubReaderActivity::applyBookReaderOverrides(const int8_t embeddedStyleOverride, - const int8_t imageRenderingOverride) { + const int8_t imageRenderingOverride, const int8_t fontFamilyOverride, + const int8_t fontSizeOverride) { if (!epub) { return; } - if (bookEmbeddedStyleOverride == embeddedStyleOverride && bookImageRenderingOverride == imageRenderingOverride) { + if (bookEmbeddedStyleOverride == embeddedStyleOverride && bookImageRenderingOverride == imageRenderingOverride && + bookFontFamilyOverride == fontFamilyOverride && bookFontSizeOverride == fontSizeOverride) { return; } bookEmbeddedStyleOverride = embeddedStyleOverride; bookImageRenderingOverride = imageRenderingOverride; - RECENT_BOOKS.setReaderOverrides(epub->getPath(), bookEmbeddedStyleOverride, bookImageRenderingOverride); + bookFontFamilyOverride = fontFamilyOverride; + bookFontSizeOverride = fontSizeOverride; + RECENT_BOOKS.setReaderOverrides(epub->getPath(), bookEmbeddedStyleOverride, bookImageRenderingOverride, + bookFontFamilyOverride, bookFontSizeOverride); RenderLock lock(*this); if (section) { @@ -850,6 +858,51 @@ uint8_t EpubReaderActivity::getEffectiveImageRendering() const { return SETTINGS.imageRendering; } +int EpubReaderActivity::getEffectiveReaderFontId() const { + const uint8_t fontFamily = + (bookFontFamilyOverride >= 0) ? static_cast(bookFontFamilyOverride) : SETTINGS.fontFamily; + const uint8_t fontSize = (bookFontSizeOverride >= 0) ? static_cast(bookFontSizeOverride) : SETTINGS.fontSize; + switch (fontFamily) { + case CrossPointSettings::NOTOSANS: + switch (fontSize) { + case CrossPointSettings::SMALL: + return NOTOSANS_12_FONT_ID; + case CrossPointSettings::MEDIUM: + default: + return NOTOSANS_14_FONT_ID; + case CrossPointSettings::LARGE: + return NOTOSANS_16_FONT_ID; + case CrossPointSettings::EXTRA_LARGE: + return NOTOSANS_18_FONT_ID; + } + case CrossPointSettings::OPENDYSLEXIC: + switch (fontSize) { + case CrossPointSettings::SMALL: + return OPENDYSLEXIC_8_FONT_ID; + case CrossPointSettings::MEDIUM: + default: + return OPENDYSLEXIC_10_FONT_ID; + case CrossPointSettings::LARGE: + return OPENDYSLEXIC_12_FONT_ID; + case CrossPointSettings::EXTRA_LARGE: + return OPENDYSLEXIC_14_FONT_ID; + } + case CrossPointSettings::BOOKERLY: + default: + switch (fontSize) { + case CrossPointSettings::SMALL: + return BOOKERLY_12_FONT_ID; + case CrossPointSettings::MEDIUM: + default: + return BOOKERLY_14_FONT_ID; + case CrossPointSettings::LARGE: + return BOOKERLY_16_FONT_ID; + case CrossPointSettings::EXTRA_LARGE: + return BOOKERLY_18_FONT_ID; + } + } +} + void EpubReaderActivity::pageTurn(bool isForwardTurn) { if (isForwardTurn) { if (section->currentPage < section->pageCount - 1) { @@ -934,7 +987,7 @@ void EpubReaderActivity::render(RenderLock&& lock) { LOG_DBG("ERS", "Loading file: %s, index: %d", filepath.c_str(), currentSpineIndex); section = std::make_unique
(epub, currentSpineIndex, renderer); - if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), + if (!section->loadSectionFile(getEffectiveReaderFontId(), SETTINGS.getReaderLineCompression(), SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle, imageRendering)) { LOG_DBG("ERS", "Cache not found, building..."); @@ -947,7 +1000,7 @@ void EpubReaderActivity::render(RenderLock&& lock) { GUI.fillPopupProgress(renderer, popupRect, progress); }; - if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), + if (!section->createSectionFile(getEffectiveReaderFontId(), SETTINGS.getReaderLineCompression(), SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle, imageRendering, progressFn)) { @@ -1090,14 +1143,14 @@ void EpubReaderActivity::silentIndexNextChapterIfNeeded(const uint16_t viewportW const uint8_t imageRendering = getEffectiveImageRendering(); Section nextSection(epub, nextSpineIndex, renderer); - if (nextSection.loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), + if (nextSection.loadSectionFile(getEffectiveReaderFontId(), SETTINGS.getReaderLineCompression(), SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle, imageRendering)) { return; } LOG_DBG("ERS", "Silently indexing next chapter: %d", nextSpineIndex); - if (!nextSection.createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), + if (!nextSection.createSectionFile(getEffectiveReaderFontId(), SETTINGS.getReaderLineCompression(), SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle, imageRendering)) { LOG_ERR("ERS", "Failed silent indexing for chapter: %d", nextSpineIndex); @@ -1133,7 +1186,7 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or // Font prewarm: scan pass accumulates text, then prewarm, then real render const uint32_t heapBefore = esp_get_free_heap_size(); auto scope = fcm->createPrewarmScope(); - page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); // scan pass + page->render(renderer, getEffectiveReaderFontId(), orientedMarginLeft, orientedMarginTop); // scan pass scope.endScanAndPrewarm(); const uint32_t heapAfter = esp_get_free_heap_size(); fcm->logStats("prewarm"); @@ -1149,7 +1202,7 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or pendingHalfRefreshAfterImagePage = false; logReaderMemSnapshot("before_bw_render"); - page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); + page->render(renderer, getEffectiveReaderFontId(), orientedMarginLeft, orientedMarginTop); renderStatusBar(); fcm->logStats("bw_render"); const auto tBwRender = millis(); @@ -1168,7 +1221,7 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or // Re-render page content to restore images into the blanked area // Status bar is not re-rendered here to avoid reading stale dynamic values (e.g. battery %) - page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); + page->render(renderer, getEffectiveReaderFontId(), orientedMarginLeft, orientedMarginTop); renderer.displayBuffer(HalDisplay::FAST_REFRESH); } else { renderer.displayBuffer(HalDisplay::HALF_REFRESH); @@ -1201,7 +1254,7 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or logReaderMemSnapshot("gray_lsb_begin"); renderer.clearScreen(0x00); renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB); - page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); + page->render(renderer, getEffectiveReaderFontId(), orientedMarginLeft, orientedMarginTop); renderer.copyGrayscaleLsbBuffers(); const auto tGrayLsb = millis(); logReaderMemSnapshot("gray_lsb_end"); @@ -1210,7 +1263,7 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or logReaderMemSnapshot("gray_msb_begin"); renderer.clearScreen(0x00); renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB); - page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); + page->render(renderer, getEffectiveReaderFontId(), orientedMarginLeft, orientedMarginTop); renderer.copyGrayscaleMsbBuffers(); const auto tGrayMsb = millis(); logReaderMemSnapshot("gray_msb_end"); @@ -1385,16 +1438,63 @@ bool EpubReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, Gf // Load or rebuild the section cache. Rebuilding is needed when the cache is missing or stale // (e.g. after a firmware update). A no-op popup callback avoids any UI during sleep preparation. + const RecentBook currentBook = RECENT_BOOKS.getBookByPath(filePath); + const uint8_t effectiveFontFamily = + currentBook.fontFamilyOverride >= 0 ? static_cast(currentBook.fontFamilyOverride) : SETTINGS.fontFamily; + const uint8_t effectiveFontSize = + currentBook.fontSizeOverride >= 0 ? static_cast(currentBook.fontSizeOverride) : SETTINGS.fontSize; + auto getEffectiveFontId = [&](uint8_t family, uint8_t size) { + switch (family) { + case CrossPointSettings::NOTOSANS: + switch (size) { + case CrossPointSettings::SMALL: + return NOTOSANS_12_FONT_ID; + case CrossPointSettings::LARGE: + return NOTOSANS_16_FONT_ID; + case CrossPointSettings::EXTRA_LARGE: + return NOTOSANS_18_FONT_ID; + case CrossPointSettings::MEDIUM: + default: + return NOTOSANS_14_FONT_ID; + } + case CrossPointSettings::OPENDYSLEXIC: + switch (size) { + case CrossPointSettings::SMALL: + return OPENDYSLEXIC_8_FONT_ID; + case CrossPointSettings::LARGE: + return OPENDYSLEXIC_12_FONT_ID; + case CrossPointSettings::EXTRA_LARGE: + return OPENDYSLEXIC_14_FONT_ID; + case CrossPointSettings::MEDIUM: + default: + return OPENDYSLEXIC_10_FONT_ID; + } + case CrossPointSettings::BOOKERLY: + default: + switch (size) { + case CrossPointSettings::SMALL: + return BOOKERLY_12_FONT_ID; + case CrossPointSettings::LARGE: + return BOOKERLY_16_FONT_ID; + case CrossPointSettings::EXTRA_LARGE: + return BOOKERLY_18_FONT_ID; + case CrossPointSettings::MEDIUM: + default: + return BOOKERLY_14_FONT_ID; + } + } + }; + auto section = std::make_unique
(epub, spineIndex, renderer); - if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), - SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, - viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, - SETTINGS.imageRendering)) { + if (!section->loadSectionFile(getEffectiveFontId(effectiveFontFamily, effectiveFontSize), + SETTINGS.getReaderLineCompression(), SETTINGS.extraParagraphSpacing, + SETTINGS.paragraphAlignment, viewportWidth, viewportHeight, SETTINGS.hyphenationEnabled, + SETTINGS.embeddedStyle, SETTINGS.imageRendering)) { LOG_DBG("SLP", "EPUB: section cache not found for spine %d, rebuilding", spineIndex); - if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), - SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, - viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, - SETTINGS.imageRendering)) { + if (!section->createSectionFile(getEffectiveFontId(effectiveFontFamily, effectiveFontSize), + SETTINGS.getReaderLineCompression(), SETTINGS.extraParagraphSpacing, + SETTINGS.paragraphAlignment, viewportWidth, viewportHeight, + SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, SETTINGS.imageRendering)) { LOG_ERR("SLP", "EPUB: failed to rebuild section cache for spine %d", spineIndex); return false; } @@ -1410,7 +1510,7 @@ bool EpubReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, Gf } renderer.clearScreen(); - page->render(renderer, SETTINGS.getReaderFontId(), marginLeft, marginTop); + page->render(renderer, getEffectiveFontId(effectiveFontFamily, effectiveFontSize), marginLeft, marginTop); // No displayBuffer call — caller (SleepActivity) handles that after compositing the overlay return true; } diff --git a/src/activities/reader/EpubReaderActivity.h b/src/activities/reader/EpubReaderActivity.h index 8fa2d167..8a792860 100644 --- a/src/activities/reader/EpubReaderActivity.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -53,6 +53,8 @@ class EpubReaderActivity final : public Activity { // -1 means use global SETTINGS value. int8_t bookEmbeddedStyleOverride = -1; int8_t bookImageRenderingOverride = -1; + int8_t bookFontFamilyOverride = -1; + int8_t bookFontSizeOverride = -1; // Bookmarks (starred pages) BookmarkStore bookmarkStore; @@ -88,9 +90,11 @@ class EpubReaderActivity final : public Activity { void applyOrientation(uint8_t orientation); void applyTextDarkness(uint8_t textDarkness); void toggleAutoPageTurn(uint8_t selectedPageTurnOption); - void applyBookReaderOverrides(int8_t embeddedStyleOverride, int8_t imageRenderingOverride); + void applyBookReaderOverrides(int8_t embeddedStyleOverride, int8_t imageRenderingOverride, int8_t fontFamilyOverride, + int8_t fontSizeOverride); bool getEffectiveEmbeddedStyle() const; uint8_t getEffectiveImageRendering() const; + int getEffectiveReaderFontId() const; void pageTurn(bool isForwardTurn); // Footnote navigation diff --git a/src/activities/reader/EpubReaderMenuActivity.cpp b/src/activities/reader/EpubReaderMenuActivity.cpp index 7c1d3091..6e68462b 100644 --- a/src/activities/reader/EpubReaderMenuActivity.cpp +++ b/src/activities/reader/EpubReaderMenuActivity.cpp @@ -5,6 +5,7 @@ #include "KOReaderCredentialStore.h" #include "MappedInputManager.h" +#include "activities/settings/SettingsSubmenuActivity.h" #include "components/UITheme.h" #include "fontIds.h" @@ -13,13 +14,16 @@ EpubReaderMenuActivity::EpubReaderMenuActivity(GfxRenderer& renderer, MappedInpu const int bookProgressPercent, const uint8_t currentOrientation, const bool hasFootnotes, const int8_t initialEmbeddedStyleOverride, const int8_t initialImageRenderingOverride, - const uint8_t initialTextDarkness, const bool hasStarredPages, - const bool isCurrentPageStarred) + const int8_t initialFontFamilyOverride, + const int8_t initialFontSizeOverride, const uint8_t initialTextDarkness, + const bool hasStarredPages, const bool isCurrentPageStarred) : MenuListActivity("EpubReaderMenu", renderer, mappedInput), currentPageStarred(isCurrentPageStarred), pendingOrientation(currentOrientation), pendingEmbeddedStyleOverride(initialEmbeddedStyleOverride), pendingImageRenderingOverride(initialImageRenderingOverride), + pendingFontFamilyOverride(initialFontFamilyOverride), + pendingFontSizeOverride(initialFontSizeOverride), pendingTextDarkness(initialTextDarkness), title(title), currentPage(currentPage), @@ -51,50 +55,6 @@ void EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes, bool hasStarredPa menuItems.push_back(SettingInfo::Separator(StrId::STR_READER_APPEARANCE)); auto* self = this; - - // Embedded style: cycles default(-1) -> ON(1) -> OFF(0) via DynamicEnum indices 0/1/2 - menuItems.push_back(SettingInfo::DynamicEnumCtx( - StrId::STR_EMBEDDED_STYLE, {StrId::STR_DEFAULT_VALUE, StrId::STR_STATE_ON, StrId::STR_STATE_OFF}, self, - [](const void* ctx) -> uint8_t { - const auto* s = static_cast(ctx); - if (s->pendingEmbeddedStyleOverride < 0) return 0; - if (s->pendingEmbeddedStyleOverride > 0) return 1; - return 2; - }, - [](void* ctx, uint8_t v) { - auto* s = static_cast(ctx); - if (v == 0) - s->pendingEmbeddedStyleOverride = -1; - else if (v == 1) - s->pendingEmbeddedStyleOverride = 1; - else - s->pendingEmbeddedStyleOverride = 0; - })); - - // Image rendering: cycles default(-1) -> display(0) -> placeholder(1) -> suppress(2) - menuItems.push_back(SettingInfo::DynamicEnumCtx( - StrId::STR_IMAGES, - {StrId::STR_DEFAULT_VALUE, StrId::STR_IMAGES_DISPLAY, StrId::STR_IMAGES_PLACEHOLDER, StrId::STR_IMAGES_SUPPRESS}, - self, - [](const void* ctx) -> uint8_t { - const auto* s = static_cast(ctx); - return (s->pendingImageRenderingOverride < 0) ? 0 : (s->pendingImageRenderingOverride + 1); - }, - [](void* ctx, uint8_t v) { - auto* s = static_cast(ctx); - s->pendingImageRenderingOverride = (v == 0) ? -1 : static_cast(v - 1); - })); - - // Text darkness: straightforward 0-3 cycle - menuItems.push_back(SettingInfo::DynamicEnumCtx( - StrId::STR_TEXT_DARKNESS, {StrId::STR_NORMAL, StrId::STR_DARK, StrId::STR_EXTRA_DARK, StrId::STR_MAX_DARK}, self, - [](const void* ctx) -> uint8_t { return static_cast(ctx)->pendingTextDarkness; }, - [](void* ctx, uint8_t v) { static_cast(ctx)->pendingTextDarkness = v; })); - - // Helper functions, reading ruler, auto page turn, orientation - menuItems.push_back(SettingInfo::Separator(StrId::STR_READER_UTILS)); - // Auto page turn: ACTION type with custom cycling in onActionSelected - menuItems.push_back(SettingInfo::Action(StrId::STR_AUTO_TURN_PAGES_PER_MIN, SettingAction::None)); // Orientation: straightforward 0-3 cycle menuItems.push_back(SettingInfo::DynamicEnumCtx( StrId::STR_ORIENTATION, @@ -102,6 +62,89 @@ void EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes, bool hasStarredPa [](const void* ctx) -> uint8_t { return static_cast(ctx)->pendingOrientation; }, [](void* ctx, uint8_t v) { static_cast(ctx)->pendingOrientation = v; })); + // Embedded style: cycles default(-1) -> ON(1) -> OFF(0) via DynamicEnum indices 0/1/2 + menuItems.push_back(SettingInfo::DynamicEnumCtx( + StrId::STR_EMBEDDED_STYLE, + {StrId::STR_DEFAULT_VALUE, StrId::STR_STATE_ON, StrId::STR_STATE_OFF}, self, + [](const void* ctx) -> uint8_t { + const auto* s = static_cast(ctx); + if (s->pendingEmbeddedStyleOverride < 0) return 0; + if (s->pendingEmbeddedStyleOverride > 0) return 1; + return 2; + }, + [](void* ctx, uint8_t v) { + auto* s = static_cast(ctx); + if (v == 0) + s->pendingEmbeddedStyleOverride = -1; + else if (v == 1) + s->pendingEmbeddedStyleOverride = 1; + else + s->pendingEmbeddedStyleOverride = 0; + }) + .withSubmenu(StrId::STR_READER_OVERRIDES)); + + // Image rendering: cycles default(-1) -> display(0) -> placeholder(1) -> suppress(2) + menuItems.push_back(SettingInfo::DynamicEnumCtx( + StrId::STR_IMAGES, + {StrId::STR_DEFAULT_VALUE, StrId::STR_IMAGES_DISPLAY, StrId::STR_IMAGES_PLACEHOLDER, + StrId::STR_IMAGES_SUPPRESS}, + self, + [](const void* ctx) -> uint8_t { + const auto* s = static_cast(ctx); + return (s->pendingImageRenderingOverride < 0) ? 0 : (s->pendingImageRenderingOverride + 1); + }, + [](void* ctx, uint8_t v) { + auto* s = static_cast(ctx); + s->pendingImageRenderingOverride = (v == 0) ? -1 : static_cast(v - 1); + }) + .withSubmenu(StrId::STR_READER_OVERRIDES)); + + // Reader font family: cycles default(-1) -> Bookerly(0) -> Noto Sans(1) -> Open Dyslexic(2) + menuItems.push_back( + SettingInfo::DynamicEnumCtx( + StrId::STR_FONT_FAMILY, + {StrId::STR_DEFAULT_VALUE, StrId::STR_BOOKERLY, StrId::STR_NOTO_SANS, StrId::STR_OPEN_DYSLEXIC}, self, + [](const void* ctx) -> uint8_t { + const auto* s = static_cast(ctx); + return (s->pendingFontFamilyOverride < 0) ? 0 : static_cast(s->pendingFontFamilyOverride + 1); + }, + [](void* ctx, uint8_t v) { + auto* s = static_cast(ctx); + s->pendingFontFamilyOverride = (v == 0) ? -1 : static_cast(v - 1); + }) + .withSubmenu(StrId::STR_READER_OVERRIDES)); + + // Reader font size: cycles default(-1) -> Small(0) -> Medium(1) -> Large(2) -> X Large(3) + menuItems.push_back( + SettingInfo::DynamicEnumCtx( + StrId::STR_FONT_SIZE, + {StrId::STR_DEFAULT_VALUE, StrId::STR_SMALL, StrId::STR_MEDIUM, StrId::STR_LARGE, StrId::STR_X_LARGE}, self, + [](const void* ctx) -> uint8_t { + const auto* s = static_cast(ctx); + return (s->pendingFontSizeOverride < 0) ? 0 : static_cast(s->pendingFontSizeOverride + 1); + }, + [](void* ctx, uint8_t v) { + auto* s = static_cast(ctx); + s->pendingFontSizeOverride = (v == 0) ? -1 : static_cast(v - 1); + }) + .withSubmenu(StrId::STR_READER_OVERRIDES)); + + // Text darkness: straightforward 0-3 cycle + menuItems.push_back( + SettingInfo::DynamicEnumCtx( + StrId::STR_TEXT_DARKNESS, {StrId::STR_NORMAL, StrId::STR_DARK, StrId::STR_EXTRA_DARK, StrId::STR_MAX_DARK}, + self, + [](const void* ctx) -> uint8_t { + return static_cast(ctx)->pendingTextDarkness; + }, + [](void* ctx, uint8_t v) { static_cast(ctx)->pendingTextDarkness = v; }) + .withSubmenu(StrId::STR_READER_OVERRIDES)); + + // Helper functions, reading ruler, auto page turn, orientation + menuItems.push_back(SettingInfo::Separator(StrId::STR_READER_UTILS)); + // Auto page turn: ACTION type with custom cycling in onActionSelected + menuItems.push_back(SettingInfo::Action(StrId::STR_AUTO_TURN_PAGES_PER_MIN, SettingAction::None)); + // --- Synchronisation (only if credentials are set) --- if (KOREADER_STORE.hasCredentials()) { menuItems.push_back(SettingInfo::Separator(StrId::STR_KOREADER_SYNC)); @@ -111,9 +154,12 @@ void EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes, bool hasStarredPa // --- Tools --- menuItems.push_back(SettingInfo::Separator(StrId::STR_READER_TOOLS)); - menuItems.push_back(SettingInfo::Action(StrId::STR_SCREENSHOT_BUTTON, SettingAction::None)); - menuItems.push_back(SettingInfo::Action(StrId::STR_DISPLAY_QR, SettingAction::None)); - menuItems.push_back(SettingInfo::Action(StrId::STR_DELETE_CACHE, SettingAction::None)); + menuItems.push_back( + SettingInfo::Action(StrId::STR_SCREENSHOT_BUTTON, SettingAction::None).withSubmenu(StrId::STR_READER_TOOLS)); + menuItems.push_back( + SettingInfo::Action(StrId::STR_DISPLAY_QR, SettingAction::None).withSubmenu(StrId::STR_READER_TOOLS)); + menuItems.push_back( + SettingInfo::Action(StrId::STR_DELETE_CACHE, SettingAction::None).withSubmenu(StrId::STR_READER_TOOLS)); menuItems.push_back(SettingInfo::Action(StrId::STR_GO_HOME_BUTTON, SettingAction::None)); } @@ -156,9 +202,20 @@ EpubReaderMenuActivity::MenuAction EpubReaderMenuActivity::actionForNameId(StrId } } +EpubReaderMenuActivity::MenuAction EpubReaderMenuActivity::actionForSettingAction(SettingAction action) { + switch (action) { + case SettingAction::None: + case SettingAction::Submenu: + return MenuAction::NONE; + default: + return MenuAction::NONE; + } +} + void EpubReaderMenuActivity::finishWithAction(MenuAction action) { - setResult(MenuResult{static_cast(action), pendingOrientation, selectedPageTurnOption, - pendingEmbeddedStyleOverride, pendingImageRenderingOverride, pendingTextDarkness}); + setResult(MenuResult{static_cast(action), -1, pendingOrientation, selectedPageTurnOption, + pendingEmbeddedStyleOverride, pendingImageRenderingOverride, pendingFontFamilyOverride, + pendingFontSizeOverride, pendingTextDarkness}); finish(); } @@ -184,10 +241,13 @@ void EpubReaderMenuActivity::onBackPressed() { ActivityResult result; result.isCancelled = true; result.data = MenuResult{-1, + -1, pendingOrientation, selectedPageTurnOption, pendingEmbeddedStyleOverride, pendingImageRenderingOverride, + pendingFontFamilyOverride, + pendingFontSizeOverride, pendingTextDarkness}; setResult(std::move(result)); finish(); @@ -207,13 +267,119 @@ std::string EpubReaderMenuActivity::getItemValueString(int index) const { return currentPageStarred ? std::string(tr(STR_STATE_ON)) : std::string(tr(STR_STATE_OFF)); } - // Plain ACTION items (select chapter, screenshot, etc.) show no value - if (item.type == SettingType::ACTION) return {}; + if (item.type == SettingType::ACTION) { + if (item.action == SettingAction::Submenu) { + return MenuListActivity::getItemValueString(index); + } + return {}; + } + + if (item.type == SettingType::ENUM) { + if (item.nameId == StrId::STR_EMBEDDED_STYLE && pendingEmbeddedStyleOverride < 0) { + const auto defaultEffective = (SETTINGS.embeddedStyle != 0) ? tr(STR_STATE_ON) : tr(STR_STATE_OFF); + return std::string(tr(STR_DEFAULT_VALUE)) + " (" + defaultEffective + ")"; + } + if (item.nameId == StrId::STR_IMAGES && pendingImageRenderingOverride < 0) { + const auto defaultIndex = static_cast(SETTINGS.imageRendering + 1); + if (defaultIndex < item.enumValues.size()) { + return std::string(tr(STR_DEFAULT_VALUE)) + " (" + I18N.get(item.enumValues[defaultIndex]) + ")"; + } + } + if (item.nameId == StrId::STR_FONT_FAMILY && pendingFontFamilyOverride < 0) { + const auto defaultIndex = static_cast(SETTINGS.fontFamily + 1); + if (defaultIndex < item.enumValues.size()) { + return std::string(tr(STR_DEFAULT_VALUE)) + " (" + I18N.get(item.enumValues[defaultIndex]) + ")"; + } + } + if (item.nameId == StrId::STR_FONT_SIZE && pendingFontSizeOverride < 0) { + const auto defaultIndex = static_cast(SETTINGS.fontSize + 1); + if (defaultIndex < item.enumValues.size()) { + return std::string(tr(STR_DEFAULT_VALUE)) + " (" + I18N.get(item.enumValues[defaultIndex]) + ")"; + } + } + } // DynamicEnum items use the standard display return MenuListActivity::getItemValueString(index); } +void EpubReaderMenuActivity::openSubmenu(const SettingInfo& submenuEntry) { + auto it = std::find_if(submenuData.begin(), submenuData.end(), + [&submenuEntry](const SettingInfo::SubmenuData& d) { return d.id == submenuEntry.nameId; }); + if (it == submenuData.end()) return; + + auto itemValueStringOverride = [this](const SettingInfo& item) -> std::string { + if (item.nameId == StrId::STR_EMBEDDED_STYLE && pendingEmbeddedStyleOverride < 0) { + const auto defaultEffective = (SETTINGS.embeddedStyle != 0) ? tr(STR_STATE_ON) : tr(STR_STATE_OFF); + return std::string(tr(STR_DEFAULT_VALUE)) + " (" + defaultEffective + ")"; + } + if (item.nameId == StrId::STR_IMAGES && pendingImageRenderingOverride < 0) { + const auto valueIndex = static_cast(SETTINGS.imageRendering + 1); + if (valueIndex < item.enumValues.size()) { + return std::string(tr(STR_DEFAULT_VALUE)) + " (" + I18N.get(item.enumValues[valueIndex]) + ")"; + } + } + if (item.nameId == StrId::STR_FONT_FAMILY && pendingFontFamilyOverride < 0) { + const auto valueIndex = static_cast(SETTINGS.fontFamily + 1); + if (valueIndex < item.enumValues.size()) { + return std::string(tr(STR_DEFAULT_VALUE)) + " (" + I18N.get(item.enumValues[valueIndex]) + ")"; + } + } + if (item.nameId == StrId::STR_FONT_SIZE && pendingFontSizeOverride < 0) { + const auto valueIndex = static_cast(SETTINGS.fontSize + 1); + if (valueIndex < item.enumValues.size()) { + return std::string(tr(STR_DEFAULT_VALUE)) + " (" + I18N.get(item.enumValues[valueIndex]) + ")"; + } + } + return item.getDisplayValue(); + }; + + startActivityForResult(std::make_unique(renderer, mappedInput, submenuEntry.nameId, + it->items, std::move(itemValueStringOverride)), + [this](const ActivityResult& result) { + if (!result.isCancelled) { + const auto* menuResult = std::get_if(&result.data); + if (menuResult) { + if (menuResult->action != -1) { + const auto action = + actionForSettingAction(static_cast(menuResult->action)); + if (action != MenuAction::NONE) { + finishWithAction(action); + return; + } + } + if (menuResult->nameId != -1) { + const auto action = actionForNameId(static_cast(menuResult->nameId)); + if (action != MenuAction::NONE) { + finishWithAction(action); + return; + } + } + } + } + requestUpdate(); + }); +} + +void EpubReaderMenuActivity::toggleCurrentItem() { + if (selectedIndex < 0 || selectedIndex >= static_cast(menuItems.size())) return; + const auto& item = menuItems[selectedIndex]; + if (item.isSeparator) return; + + if (item.type == SettingType::ACTION) { + if (item.action == SettingAction::Submenu) { + openSubmenu(item); + return; + } + onActionSelected(selectedIndex); + return; + } + + menuItems[selectedIndex].toggleValue(); + onSettingToggled(selectedIndex); + requestUpdate(); +} + void EpubReaderMenuActivity::onEnter() { MenuListActivity::onEnter(); } void EpubReaderMenuActivity::render(RenderLock&&) { diff --git a/src/activities/reader/EpubReaderMenuActivity.h b/src/activities/reader/EpubReaderMenuActivity.h index d5fc72c9..a380e99b 100644 --- a/src/activities/reader/EpubReaderMenuActivity.h +++ b/src/activities/reader/EpubReaderMenuActivity.h @@ -35,6 +35,7 @@ class EpubReaderMenuActivity final : public MenuListActivity { const int currentPage, const int totalPages, const int bookProgressPercent, const uint8_t currentOrientation, const bool hasFootnotes, const int8_t initialEmbeddedStyleOverride, const int8_t initialImageRenderingOverride, + const int8_t initialFontFamilyOverride, const int8_t initialFontSizeOverride, const uint8_t initialTextDarkness, const bool hasStarredPages, const bool isCurrentPageStarred); @@ -52,15 +53,20 @@ class EpubReaderMenuActivity final : public MenuListActivity { void onActionSelected(int index) override; void onBackPressed() override; void onSettingToggled(int index) override; + void toggleCurrentItem() override; + void openSubmenu(const SettingInfo& submenuEntry); // Map from StrId to MenuAction for result passing static MenuAction actionForNameId(StrId nameId); + static MenuAction actionForSettingAction(SettingAction action); // Pending state (mutated locally, returned to parent on finish) uint8_t pendingOrientation = 0; uint8_t selectedPageTurnOption = 0; int8_t pendingEmbeddedStyleOverride = -1; int8_t pendingImageRenderingOverride = -1; + int8_t pendingFontFamilyOverride = -1; + int8_t pendingFontSizeOverride = -1; uint8_t pendingTextDarkness = 1; static constexpr const char* pageTurnLabels[] = {"", "1", "3", "6", "12"}; diff --git a/src/activities/settings/SettingInfo.h b/src/activities/settings/SettingInfo.h index d0e684c0..77abbe13 100644 --- a/src/activities/settings/SettingInfo.h +++ b/src/activities/settings/SettingInfo.h @@ -1,7 +1,9 @@ #pragma once #include +#include #include +#include #include #include @@ -88,6 +90,13 @@ struct SettingInfo { stringSetter(accessorCtx, v); } + struct SubmenuData { + StrId id = StrId::STR_NONE_OPT; + std::vector items; + }; + + static void prepareSubmenus(std::vector& items, std::vector& submenuData); + SettingInfo& withObfuscated() { obfuscated = true; return *this; @@ -237,3 +246,41 @@ struct SettingInfo { // not the SettingInfo itself. void toggleValue() const; }; + +inline void SettingInfo::prepareSubmenus(std::vector& items, + std::vector& submenuData) { + if (items.empty()) return; + + std::vector preparedItems; + std::vector preparedSubmenus; + preparedItems.reserve(items.size()); + + for (auto& item : items) { + if (item.submenu == StrId::STR_NONE_OPT) { + preparedItems.push_back(std::move(item)); + continue; + } + + auto it = std::find_if(preparedSubmenus.begin(), preparedSubmenus.end(), + [&item](const SubmenuData& d) { return d.id == item.submenu; }); + if (it == preparedSubmenus.end()) { + preparedItems.push_back(SettingInfo::SubmenuEntry(item.submenu)); + preparedSubmenus.push_back({item.submenu, {}}); + it = preparedSubmenus.end() - 1; + } + it->items.push_back(std::move(item)); + } + + items.swap(preparedItems); + + for (auto& submenu : preparedSubmenus) { + auto it = std::find_if(submenuData.begin(), submenuData.end(), + [&submenu](const SubmenuData& d) { return d.id == submenu.id; }); + if (it == submenuData.end()) { + submenuData.push_back(std::move(submenu)); + } else { + it->items.insert(it->items.end(), std::make_move_iterator(submenu.items.begin()), + std::make_move_iterator(submenu.items.end())); + } + } +} diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 789b38e2..01020f65 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -41,33 +41,19 @@ void SettingsActivity::onEnter() { StrId lastControlsSub = StrId::STR_NONE_OPT; StrId lastSystemSub = StrId::STR_NONE_OPT; - // Shared placement logic — locates submenu target or inserts separator. - // Returns the vector the caller should push into (either `vec` or a submenu's items). - auto locateTarget = [this](std::vector& vec, StrId& lastSub, - const SettingInfo& s) -> std::vector* { - if (s.submenu != StrId::STR_NONE_OPT) { - 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; - } - return &it->items; - } + auto addTo = [](std::vector& vec, StrId& lastSub, const SettingInfo& s) { if (s.subcategory != StrId::STR_NONE_OPT && s.subcategory != lastSub) { vec.push_back(SettingInfo::Separator(s.subcategory)); lastSub = s.subcategory; } - return &vec; + vec.push_back(s); }; - - auto addTo = [&locateTarget](std::vector& vec, StrId& lastSub, const SettingInfo& s) { - locateTarget(vec, lastSub, s)->push_back(s); - }; - auto addToMoved = [&locateTarget](std::vector& vec, StrId& lastSub, SettingInfo&& s) { - auto* target = locateTarget(vec, lastSub, s); - target->push_back(std::move(s)); + auto addToMoved = [](std::vector& vec, StrId& lastSub, SettingInfo&& s) { + if (s.subcategory != StrId::STR_NONE_OPT && s.subcategory != lastSub) { + vec.push_back(SettingInfo::Separator(s.subcategory)); + lastSub = s.subcategory; + } + vec.push_back(std::move(s)); }; for (const auto& setting : getSettingsList()) { @@ -122,6 +108,11 @@ void SettingsActivity::onEnter() { std::move(SettingInfo::Action(StrId::STR_SYSTEM_INFO, SettingAction::SystemInfo) .withSubcategory(StrId::STR_MENU_SYS_SYSTEM))); + SettingInfo::prepareSubmenus(displaySettings, submenuData); + SettingInfo::prepareSubmenus(readerSettings, submenuData); + SettingInfo::prepareSubmenus(controlsSettings, submenuData); + SettingInfo::prepareSubmenus(systemSettings, submenuData); + // Reset selection to first category selectedCategoryIndex = 0; selectedSettingIndex = 0; @@ -222,11 +213,20 @@ void SettingsActivity::toggleCurrentSetting() { if (setting.isSeparator) return; if (setting.type == SettingType::ACTION) { - auto resultHandler = [this](const ActivityResult&) { SETTINGS.saveToFile(); }; + auto resultHandler = [this](const ActivityResult& result) { + SETTINGS.saveToFile(); + const auto* menuResult = std::get_if(&result.data); + if (menuResult && menuResult->action != -1) { + auto activity = createActivityForAction(static_cast(menuResult->action), renderer, mappedInput); + if (activity) { + startActivityForResult(std::move(activity), [this](const ActivityResult&) { SETTINGS.saveToFile(); }); + } + } + }; if (setting.action == SettingAction::Submenu) { auto it = std::find_if(submenuData.begin(), submenuData.end(), - [&setting](const SubmenuData& d) { return d.id == setting.nameId; }); + [&setting](const SettingInfo::SubmenuData& d) { return d.id == setting.nameId; }); if (it != submenuData.end()) { startActivityForResult( std::make_unique(renderer, mappedInput, setting.nameId, it->items), resultHandler); diff --git a/src/activities/settings/SettingsActivity.h b/src/activities/settings/SettingsActivity.h index 60cf75b6..a84114b9 100644 --- a/src/activities/settings/SettingsActivity.h +++ b/src/activities/settings/SettingsActivity.h @@ -24,11 +24,7 @@ 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; + std::vector submenuData; void enterCategory(int categoryIndex); void toggleCurrentSetting(); diff --git a/src/activities/settings/SettingsSubmenuActivity.cpp b/src/activities/settings/SettingsSubmenuActivity.cpp index 5ac7ffff..0fc68ff0 100644 --- a/src/activities/settings/SettingsSubmenuActivity.cpp +++ b/src/activities/settings/SettingsSubmenuActivity.cpp @@ -9,12 +9,40 @@ #include "components/UITheme.h" #include "fontIds.h" +void SettingsSubmenuActivity::onEnter() { + Activity::onEnter(); + initMenuList(); + requestUpdate(); +} + void SettingsSubmenuActivity::onActionSelected(int index) { const auto& setting = menuItems[index]; - auto resultHandler = [this](const ActivityResult&) { SETTINGS.saveToFile(); }; + if (setting.isSeparator) return; - auto activity = createActivityForAction(setting.action, renderer, mappedInput); - if (activity) startActivityForResult(std::move(activity), resultHandler); + if (setting.type == SettingType::ACTION) { + MenuResult menuResult; + if (setting.action != SettingAction::None) { + menuResult.action = static_cast(setting.action); + } else { + menuResult.nameId = static_cast(setting.nameId); + } + setResult(ActivityResult(menuResult)); + finish(); + return; + } + + onSettingToggled(index); +} + +std::string SettingsSubmenuActivity::getItemValueString(int index) const { + const auto& item = menuItems[index]; + if (item.type == SettingType::ACTION && item.action != SettingAction::Submenu) { + return {}; + } + if (itemValueStringOverride) { + return itemValueStringOverride(item); + } + return MenuListActivity::getItemValueString(index); } void SettingsSubmenuActivity::onSettingToggled(int /*index*/) { SETTINGS.saveToFile(); } diff --git a/src/activities/settings/SettingsSubmenuActivity.h b/src/activities/settings/SettingsSubmenuActivity.h index 6fff3691..32618941 100644 --- a/src/activities/settings/SettingsSubmenuActivity.h +++ b/src/activities/settings/SettingsSubmenuActivity.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include "SettingInfo.h" @@ -10,15 +11,21 @@ // Supports subcategory separators (withSubcategory) exactly as the parent settings tabs do. class SettingsSubmenuActivity final : public MenuListActivity { StrId titleId; + std::function itemValueStringOverride; // MenuListActivity overrides + void onEnter() override; void onActionSelected(int index) override; void onSettingToggled(int index) override; + std::string getItemValueString(int index) const override; public: explicit SettingsSubmenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, StrId titleId, - std::vector items) - : MenuListActivity("SettingsSubmenu", renderer, mappedInput), titleId(titleId) { + std::vector items, + std::function itemValueStringOverride = {}) + : MenuListActivity("SettingsSubmenu", renderer, mappedInput), + titleId(titleId), + itemValueStringOverride(std::move(itemValueStringOverride)) { menuItems = std::move(items); }