diff --git a/src/ButtonEventManager.cpp b/src/ButtonEventManager.cpp index af344e09..1741c851 100644 --- a/src/ButtonEventManager.cpp +++ b/src/ButtonEventManager.cpp @@ -33,6 +33,13 @@ void ButtonEventManager::pushEvent(const Button button, const PressType type) { eventTail = next; } +void ButtonEventManager::pushEventFront(const Button button, const PressType type) { + const int prev = (eventHead - 1 + EVENT_BUF) % EVENT_BUF; + if (prev == eventTail) return; // buffer full + eventHead = prev; + eventBuf[eventHead] = {button, type}; +} + bool ButtonEventManager::consumeEvent(ButtonEvent& out) { if (eventHead == eventTail) return false; out = eventBuf[eventHead]; diff --git a/src/ButtonEventManager.h b/src/ButtonEventManager.h index 59b7562d..530dc246 100644 --- a/src/ButtonEventManager.h +++ b/src/ButtonEventManager.h @@ -34,7 +34,7 @@ class ButtonEventManager { }; // Timing constants (milliseconds) - static constexpr unsigned long LONG_PRESS_MS = 600; + static constexpr unsigned long LONG_PRESS_MS = 1000; static constexpr unsigned long DOUBLE_WINDOW_MS = 300; explicit ButtonEventManager(MappedInputManager& input) : input(input) {} @@ -49,6 +49,10 @@ class ButtonEventManager { // Reset all per-button FSMs. Call on activity transitions to prevent bleed-through. void drain(); + // Preserve a default event for activity processing after main loop dispatch. + // This is used when the configured action is BTN_DEFAULT. + void pushEventFront(Button button, PressType type); + // Returns true if a double-click action is configured for this button. // ButtonEventManager queries CrossPointSettings internally. static bool hasDoubleAction(Button button); diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index e6719399..cd51a99f 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -58,6 +58,11 @@ void applyLegacyFrontButtonLayout(CrossPointSettings& settings) { } } +void enforceFixedShortActions(CrossPointSettings& settings) { + settings.btnShortBack = static_cast(CrossPointSettings::BUTTON_ACTION::BTN_DEFAULT); + settings.btnShortConfirm = static_cast(CrossPointSettings::BUTTON_ACTION::BTN_DEFAULT); +} + } // namespace void CrossPointSettings::validateFrontButtonMapping(CrossPointSettings& settings) { @@ -88,11 +93,14 @@ bool CrossPointSettings::loadFromFile() { if (!json.isEmpty()) { bool resave = false; bool result = JsonSettingsIO::loadSettings(*this, json.c_str(), &resave); - if (result && resave) { - if (saveToFile()) { - LOG_DBG("CPS", "Resaved settings to update format"); - } else { - LOG_ERR("CPS", "Failed to resave settings after format update"); + if (result) { + enforceFixedShortActions(*this); + if (resave) { + if (saveToFile()) { + LOG_DBG("CPS", "Resaved settings to update format"); + } else { + LOG_ERR("CPS", "Failed to resave settings after format update"); + } } } return result; @@ -102,6 +110,7 @@ bool CrossPointSettings::loadFromFile() { // Fall back to binary migration if (Storage.exists(SETTINGS_FILE_BIN)) { if (loadFromBinaryFile()) { + enforceFixedShortActions(*this); if (saveToFile()) { Storage.rename(SETTINGS_FILE_BIN, SETTINGS_FILE_BAK); LOG_DBG("CPS", "Migrated settings.bin to settings.json"); diff --git a/src/SettingsList.h b/src/SettingsList.h index f106470d..3d712a92 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -144,8 +144,8 @@ inline const std::vector list = { StrId::STR_BTN_ACT_KOREADER_SYNC // Back button: short=exit reader, double=ignore, long=go home - SettingInfo::Enum(StrId::STR_BTN_SHORT_PRESS, &CrossPointSettings::btnShortBack, - {StrId::STR_BTN_DEF_EXIT_READER, BTN_ACT_OPTIONS}, "btnShortBack", StrId::STR_CAT_CONTROLS) + SettingInfo::Enum(StrId::STR_BTN_SHORT_PRESS, &CrossPointSettings::btnShortBack, {StrId::STR_BTN_DEF_EXIT_READER}, + "btnShortBack", StrId::STR_CAT_CONTROLS) .withSubcategory(StrId::STR_MENU_BTN_ACTIONS) .withSubmenu(StrId::STR_BTN_BACK), SettingInfo::Enum(StrId::STR_BTN_DOUBLE_PRESS, &CrossPointSettings::btnDoubleBack, @@ -156,7 +156,7 @@ inline const std::vector list = { .withSubmenu(StrId::STR_BTN_BACK), // Confirm button: short=reader menu, double=ignore, long=KOReader sync SettingInfo::Enum(StrId::STR_BTN_SHORT_PRESS, &CrossPointSettings::btnShortConfirm, - {StrId::STR_BTN_DEF_READER_MENU, BTN_ACT_OPTIONS}, "btnShortConfirm", StrId::STR_CAT_CONTROLS) + {StrId::STR_BTN_DEF_READER_MENU}, "btnShortConfirm", StrId::STR_CAT_CONTROLS) .withSubmenu(StrId::STR_BTN_CONFIRM), SettingInfo::Enum(StrId::STR_BTN_DOUBLE_PRESS, &CrossPointSettings::btnDoubleConfirm, {StrId::STR_BTN_DEF_IGNORE, BTN_ACT_OPTIONS}, "btnDoubleConfirm", StrId::STR_CAT_CONTROLS) diff --git a/src/activities/home/FileBrowserActivity.cpp b/src/activities/home/FileBrowserActivity.cpp index feb43aea..59f15216 100644 --- a/src/activities/home/FileBrowserActivity.cpp +++ b/src/activities/home/FileBrowserActivity.cpp @@ -16,10 +16,6 @@ #include "components/UITheme.h" #include "fontIds.h" -namespace { -constexpr unsigned long GO_HOME_MS = 1000; -} // namespace - void sortFileList(std::vector& strs) { std::sort(begin(strs), end(strs), [](const std::string& str1, const std::string& str2) { // Directories first @@ -141,110 +137,109 @@ void FileBrowserActivity::clearFileMetadata(const std::string& fullPath) { void FileBrowserActivity::loop() { const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, false); - // Long press BACK always navigates to home - if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= GO_HOME_MS) { - onGoHome(); - return; - } - - // Short press BACK goes up one directory (if not root) or home (at root) - if (mappedInput.wasReleased(MappedInputManager::Button::Back) && mappedInput.getHeldTime() < GO_HOME_MS) { - if (basepath != "/") { - const std::string oldPath = basepath; - basepath.replace(basepath.find_last_of('/'), std::string::npos, ""); - if (basepath.empty()) basepath = "/"; - loadFiles(); - const auto pos = oldPath.find_last_of('/'); - const std::string dirName = oldPath.substr(pos + 1) + "/"; - const size_t idx = findEntry(dirName); - selectorIndex = (idx < files.size()) ? idx : 0; - requestUpdate(); - } else { - onGoHome(); - } - return; - } - - // Confirm short press opens selected entry; long press does nothing - if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) && mappedInput.getHeldTime() < GO_HOME_MS) { - if (files.empty()) return; - - const std::string& entry = files[selectorIndex]; - const bool isDirectory = (entry.back() == '/'); - - if (isDirectory) { - if (basepath.back() != '/') basepath += "/"; - basepath += entry.substr(0, entry.length() - 1); - loadFiles(); - selectorIndex = 0; - requestUpdate(); - } else { - std::string fullPath = basepath; - if (fullPath.back() != '/') fullPath += "/"; - fullPath += entry; - ReturnHint hint; - hint.target = ReturnTo::FileBrowser; - hint.path = basepath; - hint.selectName = entry; - activityManager.replaceWithReader(std::move(fullPath), std::move(hint)); - } - return; - } - - // Left short press does nothing; long press deletes selected file after confirmation - if (mappedInput.wasReleased(MappedInputManager::Button::Left)) { - if (mappedInput.getHeldTime() < GO_HOME_MS || files.empty()) { - return; - } - - const std::string& entry = files[selectorIndex]; - const bool isDirectory = (entry.back() == '/'); - if (isDirectory) { - return; - } - - std::string cleanBase = basepath; - if (cleanBase.back() != '/') cleanBase += "/"; - const std::string fullPath = cleanBase + entry; - - auto handler = [this, fullPath](const ActivityResult& res) { - if (!res.isCancelled) { - LOG_DBG("FileBrowser", "Attempting to delete: %s", fullPath.c_str()); - clearFileMetadata(fullPath); - if (Storage.remove(fullPath.c_str())) { - LOG_DBG("FileBrowser", "Deleted successfully"); - loadFiles(); - if (files.empty()) { - selectorIndex = 0; - } else if (selectorIndex >= files.size()) { - selectorIndex = files.size() - 1; - } - requestUpdate(true); - } else { - LOG_ERR("FileBrowser", "Failed to delete file: %s", fullPath.c_str()); - } - } else { - LOG_DBG("FileBrowser", "Delete cancelled by user"); + ButtonEventManager::ButtonEvent ev; + while (buttonEvents.consumeEvent(ev)) { + if (ev.button == MappedInputManager::Button::Back) { + if (ev.type == ButtonEventManager::PressType::Long) { + onGoHome(); + return; } - }; + if (ev.type == ButtonEventManager::PressType::Short) { + if (basepath != "/") { + const std::string oldPath = basepath; + basepath.replace(basepath.find_last_of('/'), std::string::npos, ""); + if (basepath.empty()) basepath = "/"; + loadFiles(); + const auto pos = oldPath.find_last_of('/'); + const std::string dirName = oldPath.substr(pos + 1) + "/"; + const size_t idx = findEntry(dirName); + selectorIndex = (idx < files.size()) ? idx : 0; + requestUpdate(); + } else { + onGoHome(); + } + return; + } + } - startActivityForResult( - std::make_unique(renderer, mappedInput, tr(STR_DELETE) + std::string("? "), entry), - handler); - return; - } + if (ev.button == MappedInputManager::Button::Confirm && ev.type == ButtonEventManager::PressType::Short) { + if (files.empty()) return; + + const std::string& entry = files[selectorIndex]; + const bool isDirectory = (entry.back() == '/'); + + if (isDirectory) { + if (basepath.back() != '/') basepath += "/"; + basepath += entry.substr(0, entry.length() - 1); + loadFiles(); + selectorIndex = 0; + requestUpdate(); + } else { + std::string fullPath = basepath; + if (fullPath.back() != '/') fullPath += "/"; + fullPath += entry; + ReturnHint hint; + hint.target = ReturnTo::FileBrowser; + hint.path = basepath; + hint.selectName = entry; + activityManager.replaceWithReader(std::move(fullPath), std::move(hint)); + } + return; + } + + if (ev.button == MappedInputManager::Button::Left && ev.type == ButtonEventManager::PressType::Long) { + if (files.empty()) { + return; + } + + const std::string& entry = files[selectorIndex]; + const bool isDirectory = (entry.back() == '/'); + if (isDirectory) { + return; + } - // Right opens the info page for epub files - if (mappedInput.wasReleased(MappedInputManager::Button::Right)) { - if (files.empty()) return; - const std::string& entry = files[selectorIndex]; - if (entry.back() != '/' && (FsHelpers::hasEpubExtension(entry) || FsHelpers::hasXtcExtension(entry))) { std::string cleanBase = basepath; if (cleanBase.back() != '/') cleanBase += "/"; - startActivityForResult(std::make_unique(renderer, mappedInput, cleanBase + entry), - [this](const ActivityResult&) { requestUpdate(); }); + const std::string fullPath = cleanBase + entry; + + auto handler = [this, fullPath](const ActivityResult& res) { + if (!res.isCancelled) { + LOG_DBG("FileBrowser", "Attempting to delete: %s", fullPath.c_str()); + clearFileMetadata(fullPath); + if (Storage.remove(fullPath.c_str())) { + LOG_DBG("FileBrowser", "Deleted successfully"); + loadFiles(); + if (files.empty()) { + selectorIndex = 0; + } else if (selectorIndex >= files.size()) { + selectorIndex = files.size() - 1; + } + requestUpdate(true); + } else { + LOG_ERR("FileBrowser", "Failed to delete file: %s", fullPath.c_str()); + } + } else { + LOG_DBG("FileBrowser", "Delete cancelled by user"); + } + }; + + startActivityForResult( + std::make_unique(renderer, mappedInput, tr(STR_DELETE) + std::string("? "), entry), + handler); + return; + } + + if (ev.button == MappedInputManager::Button::Right && ev.type == ButtonEventManager::PressType::Short) { + if (files.empty()) return; + const std::string& entry = files[selectorIndex]; + if (entry.back() != '/' && (FsHelpers::hasEpubExtension(entry) || FsHelpers::hasXtcExtension(entry))) { + std::string cleanBase = basepath; + if (cleanBase.back() != '/') cleanBase += "/"; + startActivityForResult(std::make_unique(renderer, mappedInput, cleanBase + entry), + [this](const ActivityResult&) { requestUpdate(); }); + } + return; } - return; } // Up/Down side buttons navigate the list diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 26621ec8..130c6e64 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -219,12 +219,14 @@ void EpubReaderActivity::loop() { } if (inputDrainGuard.shouldDrain(mappedInput)) { + buttonEvents.drain(); return; } if (automaticPageTurnActive) { if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) || mappedInput.wasReleased(MappedInputManager::Button::Back)) { + buttonEvents.drain(); automaticPageTurnActive = false; // updates chapter title space to indicate page turn disabled requestUpdate(); @@ -248,69 +250,66 @@ void EpubReaderActivity::loop() { } } - // Long press CONFIRM (1s+) goes directly to KOReader sync when credentials are configured. - // We intentionally keep long-press on the richer compare flow so advanced - // conflict-resolution behavior stays available even after simplifying menu UX. - // Without credentials, fall through to the regular menu on release. - if (mappedInput.isPressed(MappedInputManager::Button::Confirm) && - mappedInput.getHeldTime() >= ReaderUtils::GO_HOME_MS && KOREADER_STORE.hasCredentials()) { - launchKOReaderSync(SyncLaunchMode::COMPARE); - return; - } + bool delayedPrevTurn = false; + bool delayedNextTurn = false; + using BA = CrossPointSettings::BUTTON_ACTION; - // Short press CONFIRM enters reader menu activity. - if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) && - mappedInput.getHeldTime() < ReaderUtils::GO_HOME_MS) { - const int currentPage = section ? section->currentPage + 1 : 0; - const int totalPages = section ? section->pageCount : 0; - float bookProgress = 0.0f; - if (epub->getBookSize() > 0 && section && section->pageCount > 0) { - const float chapterProgress = static_cast(section->currentPage) / static_cast(section->pageCount); - bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f; + ButtonEventManager::ButtonEvent ev; + while (buttonEvents.consumeEvent(ev)) { + if (ev.button == MappedInputManager::Button::Confirm) { + if (ev.type == ButtonEventManager::PressType::Long && KOREADER_STORE.hasCredentials()) { + launchKOReaderSync(SyncLaunchMode::COMPARE); + return; + } + if (ev.type == ButtonEventManager::PressType::Short) { + openReaderMenu(); + return; + } } - const int bookProgressPercent = clampPercent(static_cast(bookProgress + 0.5f)); - 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, 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 - if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= ReaderUtils::GO_HOME_MS) { - ReaderUtils::enforceExitFullRefresh(renderer); - onGoHome(); - return; - } + if (ev.button == MappedInputManager::Button::Back) { + if (ev.type == ButtonEventManager::PressType::Long) { + ReaderUtils::enforceExitFullRefresh(renderer); + onGoHome(); + return; + } + if (ev.type == ButtonEventManager::PressType::Short) { + if (footnoteDepth > 0) { + restoreSavedPosition(); + return; + } + ReaderUtils::enforceExitFullRefresh(renderer); + finish(); + return; + } + } - // Short press BACK returns to the calling activity (or restores position if viewing footnote) - if (mappedInput.wasReleased(MappedInputManager::Button::Back) && - mappedInput.getHeldTime() < ReaderUtils::GO_HOME_MS) { - if (footnoteDepth > 0) { - restoreSavedPosition(); - return; + if (ev.type == ButtonEventManager::PressType::Short) { + if ((ev.button == MappedInputManager::Button::PageBack && SETTINGS.btnShortPageBack == BA::BTN_DEFAULT && + ButtonEventManager::hasDoubleAction(MappedInputManager::Button::PageBack)) || + (ev.button == MappedInputManager::Button::Left && SETTINGS.btnShortLeft == BA::BTN_DEFAULT && + ButtonEventManager::hasDoubleAction(MappedInputManager::Button::Left))) { + delayedPrevTurn = true; + continue; + } + if ((ev.button == MappedInputManager::Button::PageForward && SETTINGS.btnShortPageForward == BA::BTN_DEFAULT && + ButtonEventManager::hasDoubleAction(MappedInputManager::Button::PageForward)) || + (ev.button == MappedInputManager::Button::Right && SETTINGS.btnShortRight == BA::BTN_DEFAULT && + ButtonEventManager::hasDoubleAction(MappedInputManager::Button::Right))) { + delayedNextTurn = true; + continue; + } } - ReaderUtils::enforceExitFullRefresh(renderer); - finish(); - return; } auto [prevTriggered, nextTriggered] = ReaderUtils::detectPageTurn(mappedInput); + if (!prevTriggered && !nextTriggered) { + if (!delayedPrevTurn && !delayedNextTurn) { + return; + } + prevTriggered = delayedPrevTurn; + nextTriggered = delayedNextTurn; + } if (!prevTriggered && !nextTriggered) { return; } @@ -327,65 +326,6 @@ void EpubReaderActivity::loop() { return; } - const bool skipChapter = mappedInput.getHeldTime() > skipChapterMs; - - // Chapter skip navigates by TOC entries, not spine boundaries. - // Spine items without their own TOC entry inherit the previous spine's tocIndex - // (see BookMetadataCache), so they're treated as continuations of the last chapter. - // At the boundaries: skipping forward past the last TOC entry jumps to end-of-book - // (clamped in render()); skipping backward before the first TOC entry jumps to the - // spine before the current chapter's first spine (clamped to 0 in render()). - if (skipChapter) { - lastPageTurnTime = millis(); - { - RenderLock lock(*this); - - if (section && section->pageCount > 0) { - const int curTocIndex = section->getTocIndexForPage(section->currentPage); - const int nextTocIndex = nextTriggered ? curTocIndex + 1 : curTocIndex - 1; - - if (curTocIndex < 0) { - // No TOC entry for this spine, fall back to spine-level skip - nextPageNumber = 0; - currentSpineIndex = nextTriggered ? currentSpineIndex + 1 : currentSpineIndex - 1; - section.reset(); - } else if (nextTocIndex >= 0 && nextTocIndex < epub->getTocItemsCount()) { - const int newSpineIndex = epub->getSpineIndexForTocIndex(nextTocIndex); - - if (newSpineIndex == currentSpineIndex) { - if (const auto resolvedPage = section->getPageForTocIndex(nextTocIndex)) { - section->currentPage = *resolvedPage; - } else { - LOG_DBG("ERS", "No page boundary for TOC %d in spine %d, staying on current page", nextTocIndex, - currentSpineIndex); - } - } else { - pendingTocIndex = nextTocIndex; - nextPageNumber = 0; - currentSpineIndex = newSpineIndex; - section.reset(); - } - } else if (nextTriggered) { - // Beyond last TOC entry, go to end of book - nextPageNumber = 0; - currentSpineIndex = epub->getSpineItemsCount(); - section.reset(); - } else { - // Before first TOC entry, skip to spine before the current chapter - nextPageNumber = 0; - currentSpineIndex = epub->getTocItem(curTocIndex).spineIndex - 1; - section.reset(); - } - } else { - nextPageNumber = 0; - currentSpineIndex = nextTriggered ? currentSpineIndex + 1 : currentSpineIndex - 1; - section.reset(); - } - } - requestUpdate(); - return; - } - // No current section, attempt to rerender the book if (!section) { requestUpdate(); @@ -1774,6 +1714,36 @@ bool EpubReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, Gf return true; } +void EpubReaderActivity::openReaderMenu() { + const int currentPage = section ? section->currentPage + 1 : 0; + const int totalPages = section ? section->pageCount : 0; + float bookProgress = 0.0f; + if (epub->getBookSize() > 0 && section && section->pageCount > 0) { + const float chapterProgress = static_cast(section->currentPage) / static_cast(section->pageCount); + bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f; + } + const int bookProgressPercent = clampPercent(static_cast(bookProgress + 0.5f)); + 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, bookFontFamilyOverride, + bookFontSizeOverride, SETTINGS.textDarkness, !bookmarkStore.isEmpty(), isCurrentPageStarred), + [this](const ActivityResult& result) { + 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)); + } + }); +} + void EpubReaderActivity::onButtonAction(const CrossPointSettings::BUTTON_ACTION action) { using BA = CrossPointSettings::BUTTON_ACTION; switch (action) { @@ -1892,35 +1862,7 @@ void EpubReaderActivity::onButtonAction(const CrossPointSettings::BUTTON_ACTION break; case BA::BTN_READER_MENU: if (epub) { - const int currentPage = section ? section->currentPage + 1 : 0; - const int totalPages = section ? section->pageCount : 0; - float bookProgress = 0.0f; - if (epub->getBookSize() > 0 && section && section->pageCount > 0) { - const float chapterProgress = - static_cast(section->currentPage) / static_cast(section->pageCount); - bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f; - } - const int bookProgressPercent = clampPercent(static_cast(bookProgress + 0.5f)); - 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, bookFontFamilyOverride, bookFontSizeOverride, SETTINGS.textDarkness, - !bookmarkStore.isEmpty(), isCurrentPageStarred), - [this](const ActivityResult& result) { - 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)); - } - }); + openReaderMenu(); } break; case BA::BTN_KOREADER_SYNC: diff --git a/src/activities/reader/EpubReaderActivity.h b/src/activities/reader/EpubReaderActivity.h index 42c8e332..6f95fd0e 100644 --- a/src/activities/reader/EpubReaderActivity.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -166,6 +166,7 @@ class EpubReaderActivity final : public Activity { void toggleAutoPageTurn(uint8_t selectedPageTurnOption); void applyBookReaderOverrides(int8_t embeddedStyleOverride, int8_t imageRenderingOverride, int8_t fontFamilyOverride, int8_t fontSizeOverride); + void openReaderMenu(); bool getEffectiveEmbeddedStyle() const; uint8_t getEffectiveImageRendering() const; int getEffectiveReaderFontId() const; diff --git a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp index 39b31c4e..386e4a55 100644 --- a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp +++ b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp @@ -42,22 +42,40 @@ void EpubReaderChapterSelectionActivity::loop() { const int pageItems = getPageItems(); const int totalItems = getTotalItems(); - if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - const auto newSpineIndex = epub->getSpineIndexForTocIndex(selectorIndex); - if (newSpineIndex == -1) { + ButtonEventManager::ButtonEvent ev; + while (buttonEvents.consumeEvent(ev)) { + if (ev.button == MappedInputManager::Button::Confirm && ev.type == ButtonEventManager::PressType::Short) { + const auto newSpineIndex = epub->getSpineIndexForTocIndex(selectorIndex); + if (newSpineIndex == -1) { + ActivityResult result; + result.isCancelled = true; + setResult(std::move(result)); + finish(); + } else { + setResult(ChapterResult{newSpineIndex, selectorIndex}); + finish(); + } + return; + } + if (ev.button == MappedInputManager::Button::Back && ev.type == ButtonEventManager::PressType::Short) { ActivityResult result; result.isCancelled = true; setResult(std::move(result)); finish(); - } else { - setResult(ChapterResult{newSpineIndex, selectorIndex}); - finish(); + return; + } + if ((ev.button == MappedInputManager::Button::PageBack || ev.button == MappedInputManager::Button::Left) && + ev.type == ButtonEventManager::PressType::Short) { + selectorIndex = ButtonNavigator::previousIndex(selectorIndex, totalItems); + requestUpdate(); + return; + } + if ((ev.button == MappedInputManager::Button::PageForward || ev.button == MappedInputManager::Button::Right) && + ev.type == ButtonEventManager::PressType::Short) { + selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems); + requestUpdate(); + return; } - } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { - ActivityResult result; - result.isCancelled = true; - setResult(std::move(result)); - finish(); } buttonNavigator.onNextRelease([this, totalItems] { diff --git a/src/activities/reader/EpubReaderFootnotesActivity.cpp b/src/activities/reader/EpubReaderFootnotesActivity.cpp index 25ed5ca1..c042b0e2 100644 --- a/src/activities/reader/EpubReaderFootnotesActivity.cpp +++ b/src/activities/reader/EpubReaderFootnotesActivity.cpp @@ -18,35 +18,53 @@ void EpubReaderFootnotesActivity::onEnter() { void EpubReaderFootnotesActivity::onExit() { Activity::onExit(); } void EpubReaderFootnotesActivity::loop() { - if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { - ActivityResult result; - result.isCancelled = true; - setResult(std::move(result)); - finish(); - return; - } - - if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - if (selectedIndex >= 0 && selectedIndex < static_cast(footnotes.size())) { - setResult(FootnoteResult{footnotes[selectedIndex].href}); + ButtonEventManager::ButtonEvent ev; + while (buttonEvents.consumeEvent(ev)) { + if (ev.button == MappedInputManager::Button::Back && ev.type == ButtonEventManager::PressType::Short) { + ActivityResult result; + result.isCancelled = true; + setResult(std::move(result)); finish(); + return; + } + + if (ev.button == MappedInputManager::Button::Confirm && ev.type == ButtonEventManager::PressType::Short) { + if (selectedIndex >= 0 && selectedIndex < static_cast(footnotes.size())) { + setResult(FootnoteResult{footnotes[selectedIndex].href}); + finish(); + } + return; + } + + if ((ev.button == MappedInputManager::Button::PageBack || ev.button == MappedInputManager::Button::Left) && + ev.type == ButtonEventManager::PressType::Short) { + advanceSelection(-1); + return; + } + + if ((ev.button == MappedInputManager::Button::PageForward || ev.button == MappedInputManager::Button::Right) && + ev.type == ButtonEventManager::PressType::Short) { + advanceSelection(1); + return; } - return; } - buttonNavigator.onNext([this] { - if (!footnotes.empty()) { - selectedIndex = (selectedIndex + 1) % footnotes.size(); - requestUpdate(); - } - }); + buttonNavigator.onNextRelease([this] { advanceSelection(1); }); - buttonNavigator.onPrevious([this] { - if (!footnotes.empty()) { - selectedIndex = (selectedIndex - 1 + footnotes.size()) % footnotes.size(); - requestUpdate(); - } - }); + buttonNavigator.onPreviousRelease([this] { advanceSelection(-1); }); + + buttonNavigator.onNextContinuous([this] { advanceSelection(1); }); + + buttonNavigator.onPreviousContinuous([this] { advanceSelection(-1); }); +} + +void EpubReaderFootnotesActivity::advanceSelection(int delta) { + if (footnotes.empty()) { + return; + } + const int n = static_cast(footnotes.size()); + selectedIndex = ((selectedIndex + delta) % n + n) % n; + requestUpdate(); } void EpubReaderFootnotesActivity::render(RenderLock&&) { diff --git a/src/activities/reader/EpubReaderFootnotesActivity.h b/src/activities/reader/EpubReaderFootnotesActivity.h index 7336d038..19a9ee8e 100644 --- a/src/activities/reader/EpubReaderFootnotesActivity.h +++ b/src/activities/reader/EpubReaderFootnotesActivity.h @@ -21,6 +21,7 @@ class EpubReaderFootnotesActivity final : public Activity { void render(RenderLock&&) override; private: + void advanceSelection(int delta); const std::vector& footnotes; int selectedIndex = 0; int scrollOffset = 0; diff --git a/src/activities/reader/EpubReaderPercentSelectionActivity.cpp b/src/activities/reader/EpubReaderPercentSelectionActivity.cpp index 7e122dff..2e2c5b63 100644 --- a/src/activities/reader/EpubReaderPercentSelectionActivity.cpp +++ b/src/activities/reader/EpubReaderPercentSelectionActivity.cpp @@ -34,22 +34,34 @@ void EpubReaderPercentSelectionActivity::adjustPercent(const int delta) { void EpubReaderPercentSelectionActivity::loop() { // Back cancels, confirm selects, arrows adjust the percent. - if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { - ActivityResult result; - result.isCancelled = true; - setResult(std::move(result)); - finish(); - return; - } + ButtonEventManager::ButtonEvent ev; + while (buttonEvents.consumeEvent(ev)) { + if (ev.button == MappedInputManager::Button::Back && ev.type == ButtonEventManager::PressType::Short) { + ActivityResult result; + result.isCancelled = true; + setResult(std::move(result)); + finish(); + return; + } - if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - setResult(PercentResult{percent}); - finish(); - return; - } + if (ev.button == MappedInputManager::Button::Confirm && ev.type == ButtonEventManager::PressType::Short) { + setResult(PercentResult{percent}); + finish(); + return; + } - buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Left}, [this] { adjustPercent(-kSmallStep); }); - buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Right}, [this] { adjustPercent(kSmallStep); }); + if ((ev.button == MappedInputManager::Button::PageBack || ev.button == MappedInputManager::Button::Left) && + ev.type == ButtonEventManager::PressType::Short) { + adjustPercent(-kSmallStep); + return; + } + + if ((ev.button == MappedInputManager::Button::PageForward || ev.button == MappedInputManager::Button::Right) && + ev.type == ButtonEventManager::PressType::Short) { + adjustPercent(kSmallStep); + return; + } + } buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Up}, [this] { adjustPercent(kLargeStep); }); buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Down}, [this] { adjustPercent(-kLargeStep); }); diff --git a/src/activities/reader/MdReaderActivity.cpp b/src/activities/reader/MdReaderActivity.cpp index 468c44a7..ccd5330d 100644 --- a/src/activities/reader/MdReaderActivity.cpp +++ b/src/activities/reader/MdReaderActivity.cpp @@ -23,7 +23,6 @@ namespace { constexpr size_t CHUNK_SIZE = 8 * 1024; constexpr size_t MAX_LINE_LENGTH = 64 * 1024; -constexpr unsigned long HEADING_SKIP_MS = 700; constexpr uint32_t CACHE_MAGIC = 0x4D4B4449; // "MKDI" constexpr uint8_t CACHE_VERSION = 3; // Bumped: nested list indent + task checkboxes @@ -39,6 +38,8 @@ static std::string flattenHeadingText(const MdParser::ParsedLine& parsed) { void MdReaderActivity::onEnter() { Activity::onEnter(); + inputDrainGuard.arm(); + if (!txt) { return; } @@ -222,54 +223,60 @@ void MdReaderActivity::onExit() { } void MdReaderActivity::loop() { - if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= ReaderUtils::GO_HOME_MS) { - activityManager.goToFileBrowser(txt ? txt->getPath() : ""); + if (inputDrainGuard.shouldDrain(mappedInput)) { + buttonEvents.drain(); return; } - if (mappedInput.wasReleased(MappedInputManager::Button::Back) && - mappedInput.getHeldTime() < ReaderUtils::GO_HOME_MS) { - onGoHome(); - return; - } + ButtonEventManager::ButtonEvent ev; + while (buttonEvents.consumeEvent(ev)) { + if (ev.button == MappedInputManager::Button::Back) { + if (ev.type == ButtonEventManager::PressType::Long) { + activityManager.goToFileBrowser(txt ? txt->getPath() : ""); + return; + } + if (ev.type == ButtonEventManager::PressType::Short) { + onGoHome(); + return; + } + } - if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) && !headings.empty()) { - currentHeadingIndex = getHeadingIndexForOffset(pageOffsets[currentPage]); - ReaderUtils::enforceExitFullRefresh(renderer); - startActivityForResult( - std::make_unique(renderer, mappedInput, headings, currentHeadingIndex), - [this](const ActivityResult& result) { - if (!result.isCancelled) { - currentPage = std::get(result.data).page; - currentHeadingIndex = getHeadingIndexForOffset(pageOffsets[currentPage]); - requestUpdate(); - } - }); - return; - } - - auto [prevTriggered, nextTriggered] = ReaderUtils::detectPageTurn(mappedInput); - if (!prevTriggered && !nextTriggered) { - return; - } - - const bool headingSkip = mappedInput.getHeldTime() > HEADING_SKIP_MS; - if (headingSkip && !headings.empty()) { - jumpToHeading(nextTriggered); - return; - } - - if (prevTriggered && currentPage > 0) { - currentPage--; - currentHeadingIndex = getHeadingIndexForOffset(pageOffsets[currentPage]); - requestUpdate(); - } else if (nextTriggered) { - if (currentPage < totalPages - 1) { - currentPage++; + if (!headings.empty() && ev.button == MappedInputManager::Button::Confirm && + ev.type == ButtonEventManager::PressType::Short) { currentHeadingIndex = getHeadingIndexForOffset(pageOffsets[currentPage]); - requestUpdate(); - } else { - onGoHome(); + ReaderUtils::enforceExitFullRefresh(renderer); + startActivityForResult( + std::make_unique(renderer, mappedInput, headings, currentHeadingIndex), + [this](const ActivityResult& result) { + if (!result.isCancelled) { + currentPage = std::get(result.data).page; + currentHeadingIndex = getHeadingIndexForOffset(pageOffsets[currentPage]); + requestUpdate(); + } + }); + return; + } + + if ((ev.button == MappedInputManager::Button::PageBack || ev.button == MappedInputManager::Button::Left) && + ev.type == ButtonEventManager::PressType::Short) { + if (currentPage > 0) { + currentPage--; + currentHeadingIndex = getHeadingIndexForOffset(pageOffsets[currentPage]); + requestUpdate(); + } + return; + } + + if ((ev.button == MappedInputManager::Button::PageForward || ev.button == MappedInputManager::Button::Right) && + ev.type == ButtonEventManager::PressType::Short) { + if (currentPage < totalPages - 1) { + currentPage++; + currentHeadingIndex = getHeadingIndexForOffset(pageOffsets[currentPage]); + requestUpdate(); + } else { + onGoHome(); + } + return; } } } diff --git a/src/activities/reader/MdReaderActivity.h b/src/activities/reader/MdReaderActivity.h index c262e9c6..614364bc 100644 --- a/src/activities/reader/MdReaderActivity.h +++ b/src/activities/reader/MdReaderActivity.h @@ -6,6 +6,7 @@ #include #include "CrossPointSettings.h" +#include "ReaderUtils.h" #include "activities/Activity.h" struct MdHeading { @@ -72,6 +73,7 @@ class MdReaderActivity final : public Activity { void assignHeadingPageNumbers(); int getHeadingIndexForOffset(size_t offset) const; void jumpToHeading(bool next); + ReaderUtils::InputDrainGuard inputDrainGuard; // Word-wrap a parsed markdown line into one or more RenderedLines. // Returns true if all content was emitted, false if truncated by maxLines. diff --git a/src/activities/reader/MdReaderTocSelectionActivity.cpp b/src/activities/reader/MdReaderTocSelectionActivity.cpp index 89054c7d..771f634f 100644 --- a/src/activities/reader/MdReaderTocSelectionActivity.cpp +++ b/src/activities/reader/MdReaderTocSelectionActivity.cpp @@ -34,24 +34,41 @@ void MdReaderTocSelectionActivity::loop() { const int pageItems = getPageItems(); const int totalItems = getTotalItems(); - if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - if (selectorIndex >= 0 && selectorIndex < totalItems) { - setResult(PageResult{static_cast(headings[selectorIndex].pageIndex)}); - } else { + ButtonEventManager::ButtonEvent ev; + while (buttonEvents.consumeEvent(ev)) { + if (ev.button == MappedInputManager::Button::Confirm && ev.type == ButtonEventManager::PressType::Short) { + if (selectorIndex >= 0 && selectorIndex < totalItems) { + setResult(PageResult{static_cast(headings[selectorIndex].pageIndex)}); + } else { + ActivityResult result; + result.isCancelled = true; + setResult(std::move(result)); + } + finish(); + return; + } + + if (ev.button == MappedInputManager::Button::Back && ev.type == ButtonEventManager::PressType::Short) { ActivityResult result; result.isCancelled = true; setResult(std::move(result)); + finish(); + return; } - finish(); - return; - } - if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { - ActivityResult result; - result.isCancelled = true; - setResult(std::move(result)); - finish(); - return; + if ((ev.button == MappedInputManager::Button::PageBack || ev.button == MappedInputManager::Button::Left) && + ev.type == ButtonEventManager::PressType::Short) { + selectorIndex = ButtonNavigator::previousIndex(selectorIndex, totalItems); + requestUpdate(); + return; + } + + if ((ev.button == MappedInputManager::Button::PageForward || ev.button == MappedInputManager::Button::Right) && + ev.type == ButtonEventManager::PressType::Short) { + selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems); + requestUpdate(); + return; + } } buttonNavigator.onNextRelease([this, totalItems] { diff --git a/src/activities/reader/QrDisplayActivity.cpp b/src/activities/reader/QrDisplayActivity.cpp index 9976feb7..72602de6 100644 --- a/src/activities/reader/QrDisplayActivity.cpp +++ b/src/activities/reader/QrDisplayActivity.cpp @@ -16,10 +16,13 @@ void QrDisplayActivity::onEnter() { void QrDisplayActivity::onExit() { Activity::onExit(); } void QrDisplayActivity::loop() { - if (mappedInput.wasReleased(MappedInputManager::Button::Back) || - mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - finish(); - return; + ButtonEventManager::ButtonEvent ev; + while (buttonEvents.consumeEvent(ev)) { + if ((ev.button == MappedInputManager::Button::Back || ev.button == MappedInputManager::Button::Confirm) && + ev.type == ButtonEventManager::PressType::Short) { + finish(); + return; + } } } diff --git a/src/activities/reader/ReaderUtils.h b/src/activities/reader/ReaderUtils.h index 9650bd25..b0383257 100644 --- a/src/activities/reader/ReaderUtils.h +++ b/src/activities/reader/ReaderUtils.h @@ -6,6 +6,7 @@ #include +#include "ButtonEventManager.h" #include "MappedInputManager.h" namespace ReaderUtils { @@ -90,13 +91,21 @@ inline PageTurnResult detectPageTurn(const MappedInputManager& input) { // Only treat wasReleased as a page turn when the button's short-press action is default. // Non-default short-press actions are dispatched by the global dispatcher in main.cpp; // counting wasReleased as well would double-fire the action. + // Also suppress immediate page-turns if a double-click action is configured for the button, + // because the button event system delays short events until the double-click window expires. using BA = CrossPointSettings::BUTTON_ACTION; - const bool prev = - (SETTINGS.btnShortPageBack == BA::BTN_DEFAULT && input.wasReleased(MappedInputManager::Button::PageBack)) || - (SETTINGS.btnShortLeft == BA::BTN_DEFAULT && input.wasReleased(MappedInputManager::Button::Left)); - const bool next = - (SETTINGS.btnShortPageForward == BA::BTN_DEFAULT && input.wasReleased(MappedInputManager::Button::PageForward)) || - (SETTINGS.btnShortRight == BA::BTN_DEFAULT && input.wasReleased(MappedInputManager::Button::Right)); + const bool prev = (SETTINGS.btnShortPageBack == BA::BTN_DEFAULT && + !ButtonEventManager::hasDoubleAction(MappedInputManager::Button::PageBack) && + input.wasReleased(MappedInputManager::Button::PageBack)) || + (SETTINGS.btnShortLeft == BA::BTN_DEFAULT && + !ButtonEventManager::hasDoubleAction(MappedInputManager::Button::Left) && + input.wasReleased(MappedInputManager::Button::Left)); + const bool next = (SETTINGS.btnShortPageForward == BA::BTN_DEFAULT && + !ButtonEventManager::hasDoubleAction(MappedInputManager::Button::PageForward) && + input.wasReleased(MappedInputManager::Button::PageForward)) || + (SETTINGS.btnShortRight == BA::BTN_DEFAULT && + !ButtonEventManager::hasDoubleAction(MappedInputManager::Button::Right) && + input.wasReleased(MappedInputManager::Button::Right)); return {prev, next}; } diff --git a/src/activities/reader/StarredPagesActivity.cpp b/src/activities/reader/StarredPagesActivity.cpp index 6735a15b..1fcd8715 100644 --- a/src/activities/reader/StarredPagesActivity.cpp +++ b/src/activities/reader/StarredPagesActivity.cpp @@ -78,33 +78,41 @@ void StarredPagesActivity::deleteSelected() { void StarredPagesActivity::loop() { const int totalItems = static_cast(bookmarkStore.getAll().size()); - if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { - ActivityResult result; - result.isCancelled = true; - setResult(std::move(result)); - finish(); - return; + ButtonEventManager::ButtonEvent ev; + while (buttonEvents.consumeEvent(ev)) { + if (ev.button == MappedInputManager::Button::Back && ev.type == ButtonEventManager::PressType::Short) { + ActivityResult result; + result.isCancelled = true; + setResult(std::move(result)); + finish(); + return; + } + + if (totalItems > 0 && ev.button == MappedInputManager::Button::Confirm && + ev.type == ButtonEventManager::PressType::Short) { + const auto& bm = bookmarkStore.getAll()[selectorIndex]; + setResult(StarredPageResult{bm.spineIndex, bm.pageNumber}); + finish(); + return; + } + + if (totalItems > 0 && + (ev.button == MappedInputManager::Button::PageBack || ev.button == MappedInputManager::Button::Left) && + ev.type == ButtonEventManager::PressType::Short) { + startRename(); + return; + } + + if (totalItems > 0 && + (ev.button == MappedInputManager::Button::PageForward || ev.button == MappedInputManager::Button::Right) && + ev.type == ButtonEventManager::PressType::Short) { + deleteSelected(); + return; + } } if (totalItems == 0) return; - if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - const auto& bm = bookmarkStore.getAll()[selectorIndex]; - setResult(StarredPageResult{bm.spineIndex, bm.pageNumber}); - finish(); - return; - } - - if (mappedInput.wasReleased(MappedInputManager::Button::Left)) { - startRename(); - return; - } - - if (mappedInput.wasReleased(MappedInputManager::Button::Right)) { - deleteSelected(); - return; - } - const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, false); buttonNavigator.onNextRelease([this, totalItems] { diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index bed59e7b..a88d6549 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -137,54 +137,59 @@ void TxtReaderActivity::onExit() { void TxtReaderActivity::loop() { if (inputDrainGuard.shouldDrain(mappedInput)) { + buttonEvents.drain(); return; } - // Long press BACK (1s+) goes to home screen - if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= ReaderUtils::GO_HOME_MS) { - ReaderUtils::enforceExitFullRefresh(renderer); - onGoHome(); - return; - } + ButtonEventManager::ButtonEvent ev; + while (buttonEvents.consumeEvent(ev)) { + if (ev.button == MappedInputManager::Button::Back) { + if (ev.type == ButtonEventManager::PressType::Long) { + ReaderUtils::enforceExitFullRefresh(renderer); + onGoHome(); + return; + } + if (ev.type == ButtonEventManager::PressType::Short) { + ReaderUtils::enforceExitFullRefresh(renderer); + finish(); + return; + } + } - // Short press BACK returns to the calling activity - if (mappedInput.wasReleased(MappedInputManager::Button::Back) && - mappedInput.getHeldTime() < ReaderUtils::GO_HOME_MS) { - ReaderUtils::enforceExitFullRefresh(renderer); - finish(); - return; - } + if (!bookmarkStore.isEmpty() && ev.button == MappedInputManager::Button::Confirm && + ev.type == ButtonEventManager::PressType::Short) { + ReaderUtils::enforceExitFullRefresh(renderer); + startActivityForResult(std::make_unique(renderer, mappedInput, bookmarkStore), + [this](const ActivityResult& result) { + if (!result.isCancelled) { + const auto& starred = std::get(result.data); + currentPage = starred.pageNumber; + if (currentPage >= totalPages) currentPage = totalPages - 1; + if (currentPage < 0) currentPage = 0; + } + requestUpdate(); + }); + return; + } - // Open starred pages list via Confirm button - if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) && !bookmarkStore.isEmpty()) { - ReaderUtils::enforceExitFullRefresh(renderer); - startActivityForResult(std::make_unique(renderer, mappedInput, bookmarkStore), - [this](const ActivityResult& result) { - if (!result.isCancelled) { - const auto& starred = std::get(result.data); - currentPage = starred.pageNumber; - if (currentPage >= totalPages) currentPage = totalPages - 1; - if (currentPage < 0) currentPage = 0; - } - requestUpdate(); - }); - return; - } + if ((ev.button == MappedInputManager::Button::PageBack || ev.button == MappedInputManager::Button::Left) && + ev.type == ButtonEventManager::PressType::Short) { + if (currentPage > 0) { + currentPage--; + requestUpdate(); + } + return; + } - auto [prevTriggered, nextTriggered] = ReaderUtils::detectPageTurn(mappedInput); - if (!prevTriggered && !nextTriggered) { - return; - } - - if (prevTriggered && currentPage > 0) { - currentPage--; - requestUpdate(); - } else if (nextTriggered) { - if (currentPage < totalPages - 1) { - currentPage++; - requestUpdate(); - } else { - finish(); + if ((ev.button == MappedInputManager::Button::PageForward || ev.button == MappedInputManager::Button::Right) && + ev.type == ButtonEventManager::PressType::Short) { + if (currentPage < totalPages - 1) { + currentPage++; + requestUpdate(); + } else { + finish(); + } + return; } } } diff --git a/src/activities/reader/XtcReaderActivity.cpp b/src/activities/reader/XtcReaderActivity.cpp index f9176e51..f384339b 100644 --- a/src/activities/reader/XtcReaderActivity.cpp +++ b/src/activities/reader/XtcReaderActivity.cpp @@ -22,11 +22,6 @@ #include "XtcReaderChapterSelectionActivity.h" #include "fontIds.h" -namespace { -constexpr unsigned long skipPageMs = 700; -constexpr unsigned long goHomeMs = 1000; -} // namespace - void XtcReaderActivity::onEnter() { Activity::onEnter(); @@ -62,42 +57,70 @@ void XtcReaderActivity::onExit() { void XtcReaderActivity::loop() { if (inputDrainGuard.shouldDrain(mappedInput)) { + buttonEvents.drain(); return; } - // Enter chapter selection activity - if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - if (xtc && xtc->hasChapters() && !xtc->getChapters().empty()) { - ReaderUtils::enforceExitFullRefresh(renderer); - startActivityForResult( - std::make_unique(renderer, mappedInput, xtc, currentPage), - [this](const ActivityResult& result) { - if (!result.isCancelled) { - currentPage = std::get(result.data).page; - } - }); + bool delayedPrevTurn = false; + bool delayedNextTurn = false; + using BA = CrossPointSettings::BUTTON_ACTION; + + ButtonEventManager::ButtonEvent ev; + while (buttonEvents.consumeEvent(ev)) { + if (ev.button == MappedInputManager::Button::Confirm && ev.type == ButtonEventManager::PressType::Short) { + if (xtc && xtc->hasChapters() && !xtc->getChapters().empty()) { + ReaderUtils::enforceExitFullRefresh(renderer); + startActivityForResult( + std::make_unique(renderer, mappedInput, xtc, currentPage), + [this](const ActivityResult& result) { + if (!result.isCancelled) { + currentPage = std::get(result.data).page; + } + }); + return; + } + } + + if (ev.button == MappedInputManager::Button::Back) { + if (ev.type == ButtonEventManager::PressType::Long) { + ReaderUtils::enforceExitFullRefresh(renderer); + onGoHome(); + return; + } + if (ev.type == ButtonEventManager::PressType::Short) { + ReaderUtils::enforceExitFullRefresh(renderer); + finish(); + return; + } + } + + if (ev.type == ButtonEventManager::PressType::Short) { + if ((ev.button == MappedInputManager::Button::PageBack && SETTINGS.btnShortPageBack == BA::BTN_DEFAULT && + ButtonEventManager::hasDoubleAction(MappedInputManager::Button::PageBack)) || + (ev.button == MappedInputManager::Button::Left && SETTINGS.btnShortLeft == BA::BTN_DEFAULT && + ButtonEventManager::hasDoubleAction(MappedInputManager::Button::Left))) { + delayedPrevTurn = true; + continue; + } + if ((ev.button == MappedInputManager::Button::PageForward && SETTINGS.btnShortPageForward == BA::BTN_DEFAULT && + ButtonEventManager::hasDoubleAction(MappedInputManager::Button::PageForward)) || + (ev.button == MappedInputManager::Button::Right && SETTINGS.btnShortRight == BA::BTN_DEFAULT && + ButtonEventManager::hasDoubleAction(MappedInputManager::Button::Right))) { + delayedNextTurn = true; + continue; + } } } - // Long press BACK (1s+) goes to home screen - if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= goHomeMs) { - ReaderUtils::enforceExitFullRefresh(renderer); - onGoHome(); - return; + auto [prevTriggered, nextTriggered] = ReaderUtils::detectPageTurn(mappedInput); + if (!prevTriggered && !nextTriggered) { + if (!delayedPrevTurn && !delayedNextTurn) { + return; + } + prevTriggered = delayedPrevTurn; + nextTriggered = delayedNextTurn; } - // Short press BACK returns to the calling activity - if (mappedInput.wasReleased(MappedInputManager::Button::Back) && mappedInput.getHeldTime() < goHomeMs) { - ReaderUtils::enforceExitFullRefresh(renderer); - finish(); - return; - } - - const bool prevTriggered = mappedInput.wasReleased(MappedInputManager::Button::PageBack) || - mappedInput.wasReleased(MappedInputManager::Button::Left); - const bool nextTriggered = mappedInput.wasReleased(MappedInputManager::Button::PageForward) || - mappedInput.wasReleased(MappedInputManager::Button::Right); - if (!prevTriggered && !nextTriggered) { return; } @@ -113,21 +136,13 @@ void XtcReaderActivity::loop() { return; } - const bool skipPages = mappedInput.getHeldTime() > skipPageMs; - const int skipAmount = skipPages ? 10 : 1; - if (prevTriggered) { - if (currentPage >= static_cast(skipAmount)) { - currentPage -= skipAmount; - } else { - currentPage = 0; + if (currentPage > 0) { + currentPage--; + requestUpdate(); } - requestUpdate(); } else if (nextTriggered) { - currentPage += skipAmount; - if (currentPage >= xtc->getPageCount()) { - currentPage = xtc->getPageCount(); // Allow showing "End of book" - } + currentPage++; requestUpdate(); } } diff --git a/src/activities/reader/XtcReaderChapterSelectionActivity.cpp b/src/activities/reader/XtcReaderChapterSelectionActivity.cpp index 61d283de..5f78ba9f 100644 --- a/src/activities/reader/XtcReaderChapterSelectionActivity.cpp +++ b/src/activities/reader/XtcReaderChapterSelectionActivity.cpp @@ -5,6 +5,7 @@ #include +#include "ButtonEventManager.h" #include "MappedInputManager.h" #include "components/UITheme.h" #include "fontIds.h" @@ -47,28 +48,58 @@ void XtcReaderChapterSelectionActivity::onEnter() { void XtcReaderChapterSelectionActivity::onExit() { Activity::onExit(); } void XtcReaderChapterSelectionActivity::loop() { + if (!xtc) { + return; + } + const int pageItems = getPageItems(); const int totalItems = static_cast(xtc->getChapters().size()); - if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - const auto& chapters = xtc->getChapters(); - if (!chapters.empty() && selectorIndex >= 0 && selectorIndex < static_cast(chapters.size())) { - setResult(PageResult{chapters[selectorIndex].startPage}); + ButtonEventManager::ButtonEvent ev; + while (buttonEvents.consumeEvent(ev)) { + if (ev.button == MappedInputManager::Button::Confirm && ev.type == ButtonEventManager::PressType::Short) { + const auto& chapters = xtc->getChapters(); + if (!chapters.empty() && selectorIndex >= 0 && selectorIndex < static_cast(chapters.size())) { + setResult(PageResult{chapters[selectorIndex].startPage}); + finish(); + } + return; + } + if (ev.button == MappedInputManager::Button::Back && ev.type == ButtonEventManager::PressType::Short) { + ActivityResult result; + result.isCancelled = true; + setResult(std::move(result)); finish(); + return; + } + if ((ev.button == MappedInputManager::Button::PageBack || ev.button == MappedInputManager::Button::Left) && + ev.type == ButtonEventManager::PressType::Short) { + selectorIndex = ButtonNavigator::previousIndex(selectorIndex, totalItems); + requestUpdate(); + return; + } + if ((ev.button == MappedInputManager::Button::PageForward || ev.button == MappedInputManager::Button::Right) && + ev.type == ButtonEventManager::PressType::Short) { + selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems); + requestUpdate(); + return; } - } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { - ActivityResult result; - result.isCancelled = true; - setResult(std::move(result)); - finish(); } buttonNavigator.onNextRelease([this, totalItems] { + if (ButtonEventManager::hasDoubleAction(MappedInputManager::Button::Right) || + ButtonEventManager::hasDoubleAction(MappedInputManager::Button::PageForward)) { + return; + } selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems); requestUpdate(); }); buttonNavigator.onPreviousRelease([this, totalItems] { + if (ButtonEventManager::hasDoubleAction(MappedInputManager::Button::Left) || + ButtonEventManager::hasDoubleAction(MappedInputManager::Button::PageBack)) { + return; + } selectorIndex = ButtonNavigator::previousIndex(selectorIndex, totalItems); requestUpdate(); }); diff --git a/src/activities/util/BmpViewerActivity.cpp b/src/activities/util/BmpViewerActivity.cpp index 6285ef3b..c8592e35 100644 --- a/src/activities/util/BmpViewerActivity.cpp +++ b/src/activities/util/BmpViewerActivity.cpp @@ -388,38 +388,38 @@ void BmpViewerActivity::loop() { // Keep CPU awake/polling so 1st click works Activity::loop(); - // Long press BACK (1s+) goes to home screen - if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= ReaderUtils::GO_HOME_MS) { - onGoHome(); - return; - } - - // Short press BACK returns to the calling activity - if (mappedInput.wasReleased(MappedInputManager::Button::Back) && - mappedInput.getHeldTime() < ReaderUtils::GO_HOME_MS) { - finish(); - return; - } - - // Confirm: toggle between 1-bit B&W and 4-level grayscale display. - // For decoded images this always applies; for BMPs it only makes sense when the bitmap - // actually carries greyscale data (1-bit BMPs have nothing to toggle). const bool toggleSupported = isBmpFile(filePath) ? bmpHasGreyscale : true; - if (toggleSupported && mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - toggleDisplayMode(); - return; - } + ButtonEventManager::ButtonEvent ev; + while (buttonEvents.consumeEvent(ev)) { + if (ev.button == MappedInputManager::Button::Back) { + if (ev.type == ButtonEventManager::PressType::Long) { + onGoHome(); + return; + } + if (ev.type == ButtonEventManager::PressType::Short) { + finish(); + return; + } + } + + if (ev.button == MappedInputManager::Button::Confirm && ev.type == ButtonEventManager::PressType::Short) { + if (toggleSupported) { + toggleDisplayMode(); + } + return; + } #ifdef ENABLE_IMAGE_DITHERING_EXTENSION - if (!isBmpFile(filePath) && grayscaleDisplay && mappedInput.wasReleased(MappedInputManager::Button::Left)) { - cycleDitherMode(); - return; - } + if (ev.button == MappedInputManager::Button::Left && ev.type == ButtonEventManager::PressType::Short && + !isBmpFile(filePath) && grayscaleDisplay) { + cycleDitherMode(); + return; + } #endif - // Next/Right button: set this image as the sleep screen - if (mappedInput.wasReleased(MappedInputManager::Button::Right)) { - setAsSleepScreen(); - return; + if (ev.button == MappedInputManager::Button::Right && ev.type == ButtonEventManager::PressType::Short) { + setAsSleepScreen(); + return; + } } } \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index f857808e..90f500c8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -16,6 +16,7 @@ #include #include +#include #include "ButtonEventManager.h" #include "CrossPointSettings.h" @@ -387,88 +388,95 @@ void loop() { { using BA = CrossPointSettings::BUTTON_ACTION; using B = MappedInputManager::Button; + auto actionFor = [&](const ButtonEventManager::ButtonEvent& ev) -> uint8_t { + switch (ev.button) { + case B::Back: + switch (ev.type) { + case ButtonEventManager::PressType::Short: + return SETTINGS.btnShortBack; + case ButtonEventManager::PressType::Double: + return SETTINGS.btnDoubleBack; + case ButtonEventManager::PressType::Long: + return SETTINGS.btnLongBack; + } + break; + case B::Confirm: + switch (ev.type) { + case ButtonEventManager::PressType::Short: + return SETTINGS.btnShortConfirm; + case ButtonEventManager::PressType::Double: + return SETTINGS.btnDoubleConfirm; + case ButtonEventManager::PressType::Long: + return SETTINGS.btnLongConfirm; + } + break; + case B::Left: + switch (ev.type) { + case ButtonEventManager::PressType::Short: + return SETTINGS.btnShortLeft; + case ButtonEventManager::PressType::Double: + return SETTINGS.btnDoubleLeft; + case ButtonEventManager::PressType::Long: + return SETTINGS.btnLongLeft; + } + break; + case B::Right: + switch (ev.type) { + case ButtonEventManager::PressType::Short: + return SETTINGS.btnShortRight; + case ButtonEventManager::PressType::Double: + return SETTINGS.btnDoubleRight; + case ButtonEventManager::PressType::Long: + return SETTINGS.btnLongRight; + } + break; + case B::PageBack: + switch (ev.type) { + case ButtonEventManager::PressType::Short: + return SETTINGS.btnShortPageBack; + case ButtonEventManager::PressType::Double: + return SETTINGS.btnDoublePageBack; + case ButtonEventManager::PressType::Long: + return SETTINGS.btnLongPageBack; + } + break; + case B::PageForward: + switch (ev.type) { + case ButtonEventManager::PressType::Short: + return SETTINGS.btnShortPageForward; + case ButtonEventManager::PressType::Double: + return SETTINGS.btnDoublePageForward; + case ButtonEventManager::PressType::Long: + return SETTINGS.btnLongPageForward; + } + break; + case B::Power: + switch (ev.type) { + case ButtonEventManager::PressType::Short: + return SETTINGS.btnShortPower; + case ButtonEventManager::PressType::Double: + return SETTINGS.btnDoublePower; + case ButtonEventManager::PressType::Long: + return SETTINGS.btnLongPower; + } + break; + default: + break; // Up/Down have no FSMs — ButtonEventManager never emits these + } + return BA::BTN_DEFAULT; + }; ButtonEventManager::ButtonEvent ev; + std::vector defaultEvents; + defaultEvents.reserve(8); while (buttonEventManager.consumeEvent(ev)) { - auto actionFor = [&](B btn) -> uint8_t { - switch (btn) { - case B::Back: - switch (ev.type) { - case ButtonEventManager::PressType::Short: - return SETTINGS.btnShortBack; - case ButtonEventManager::PressType::Double: - return SETTINGS.btnDoubleBack; - case ButtonEventManager::PressType::Long: - return SETTINGS.btnLongBack; - } - break; - case B::Confirm: - switch (ev.type) { - case ButtonEventManager::PressType::Short: - return SETTINGS.btnShortConfirm; - case ButtonEventManager::PressType::Double: - return SETTINGS.btnDoubleConfirm; - case ButtonEventManager::PressType::Long: - return SETTINGS.btnLongConfirm; - } - break; - case B::Left: - switch (ev.type) { - case ButtonEventManager::PressType::Short: - return SETTINGS.btnShortLeft; - case ButtonEventManager::PressType::Double: - return SETTINGS.btnDoubleLeft; - case ButtonEventManager::PressType::Long: - return SETTINGS.btnLongLeft; - } - break; - case B::Right: - switch (ev.type) { - case ButtonEventManager::PressType::Short: - return SETTINGS.btnShortRight; - case ButtonEventManager::PressType::Double: - return SETTINGS.btnDoubleRight; - case ButtonEventManager::PressType::Long: - return SETTINGS.btnLongRight; - } - break; - case B::PageBack: - switch (ev.type) { - case ButtonEventManager::PressType::Short: - return SETTINGS.btnShortPageBack; - case ButtonEventManager::PressType::Double: - return SETTINGS.btnDoublePageBack; - case ButtonEventManager::PressType::Long: - return SETTINGS.btnLongPageBack; - } - break; - case B::PageForward: - switch (ev.type) { - case ButtonEventManager::PressType::Short: - return SETTINGS.btnShortPageForward; - case ButtonEventManager::PressType::Double: - return SETTINGS.btnDoublePageForward; - case ButtonEventManager::PressType::Long: - return SETTINGS.btnLongPageForward; - } - break; - case B::Power: - switch (ev.type) { - case ButtonEventManager::PressType::Short: - return SETTINGS.btnShortPower; - case ButtonEventManager::PressType::Double: - return SETTINGS.btnDoublePower; - case ButtonEventManager::PressType::Long: - return SETTINGS.btnLongPower; - } - break; - default: - break; // Up/Down have no FSMs — ButtonEventManager never emits these + const uint8_t action = actionFor(ev); + if (action == BA::BTN_DEFAULT) { + if (ev.type == ButtonEventManager::PressType::Double) { + continue; } - return BA::BTN_DEFAULT; - }; - - const uint8_t action = actionFor(ev.button); - if (action == BA::BTN_DEFAULT) continue; + defaultEvents.push_back(ev); + continue; + } switch (static_cast(action)) { case BA::BTN_PAGE_FORWARD: @@ -525,6 +533,10 @@ void loop() { break; } } + + for (auto it = defaultEvents.rbegin(); it != defaultEvents.rend(); ++it) { + buttonEventManager.pushEventFront(it->button, it->type); + } } const unsigned long activityStartTime = millis();