diff --git a/src/activities/browser/OpdsBookBrowserActivity.cpp b/src/activities/browser/OpdsBookBrowserActivity.cpp index 2bde74de..4343daed 100644 --- a/src/activities/browser/OpdsBookBrowserActivity.cpp +++ b/src/activities/browser/OpdsBookBrowserActivity.cpp @@ -17,7 +17,6 @@ namespace { constexpr int PAGE_ITEMS = 23; -constexpr int SKIP_PAGE_MS = 700; } // namespace void OpdsBookBrowserActivity::taskTrampoline(void* param) { @@ -118,12 +117,6 @@ void OpdsBookBrowserActivity::loop() { // Handle browsing state if (state == BrowserState::BROWSING) { - const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::Up) || - mappedInput.wasReleased(MappedInputManager::Button::Left); - const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::Down) || - mappedInput.wasReleased(MappedInputManager::Button::Right); - const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS; - if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (!entries.empty()) { const auto& entry = entries[selectorIndex]; @@ -135,20 +128,29 @@ void OpdsBookBrowserActivity::loop() { } } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { navigateBack(); - } else if (prevReleased && !entries.empty()) { - if (skipPage) { - selectorIndex = ((selectorIndex / PAGE_ITEMS - 1) * PAGE_ITEMS + entries.size()) % entries.size(); - } else { - selectorIndex = (selectorIndex + entries.size() - 1) % entries.size(); - } - updateRequired = true; - } else if (nextReleased && !entries.empty()) { - if (skipPage) { - selectorIndex = ((selectorIndex / PAGE_ITEMS + 1) * PAGE_ITEMS) % entries.size(); - } else { - selectorIndex = (selectorIndex + 1) % entries.size(); - } - updateRequired = true; + } + + // Handle navigation + if (!entries.empty()) { + buttonNavigator.onNextRelease([this] { + selectorIndex = ButtonNavigator::nextIndex(selectorIndex, entries.size()); + updateRequired = true; + }); + + buttonNavigator.onPreviousRelease([this] { + selectorIndex = ButtonNavigator::previousIndex(selectorIndex, entries.size()); + updateRequired = true; + }); + + buttonNavigator.onNextContinuous([this] { + selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, entries.size(), PAGE_ITEMS); + updateRequired = true; + }); + + buttonNavigator.onPreviousContinuous([this] { + selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, entries.size(), PAGE_ITEMS); + updateRequired = true; + }); } } } diff --git a/src/activities/browser/OpdsBookBrowserActivity.h b/src/activities/browser/OpdsBookBrowserActivity.h index b08d9c2a..e778f6b7 100644 --- a/src/activities/browser/OpdsBookBrowserActivity.h +++ b/src/activities/browser/OpdsBookBrowserActivity.h @@ -9,6 +9,7 @@ #include #include "../ActivityWithSubactivity.h" +#include "util/ButtonNavigator.h" /** * Activity for browsing and downloading books from an OPDS server. @@ -37,6 +38,7 @@ class OpdsBookBrowserActivity final : public ActivityWithSubactivity { private: TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; + ButtonNavigator buttonNavigator; bool updateRequired = false; BrowserState state = BrowserState::LOADING; diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index 678af7cb..6c6cdb19 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -162,13 +162,18 @@ void HomeActivity::freeCoverBuffer() { } void HomeActivity::loop() { - const bool prevPressed = mappedInput.wasPressed(MappedInputManager::Button::Up) || - mappedInput.wasPressed(MappedInputManager::Button::Left); - const bool nextPressed = mappedInput.wasPressed(MappedInputManager::Button::Down) || - mappedInput.wasPressed(MappedInputManager::Button::Right); - const int menuCount = getMenuItemCount(); + buttonNavigator.onNext([this, menuCount] { + selectorIndex = ButtonNavigator::nextIndex(selectorIndex, menuCount); + updateRequired = true; + }); + + buttonNavigator.onPrevious([this, menuCount] { + selectorIndex = ButtonNavigator::previousIndex(selectorIndex, menuCount); + updateRequired = true; + }); + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { // Calculate dynamic indices based on which options are available int idx = 0; @@ -189,12 +194,6 @@ void HomeActivity::loop() { } else if (selectorIndex == settingsIdx) { onSettingsOpen(); } - } else if (prevPressed) { - selectorIndex = (selectorIndex + menuCount - 1) % menuCount; - updateRequired = true; - } else if (nextPressed) { - selectorIndex = (selectorIndex + 1) % menuCount; - updateRequired = true; } } diff --git a/src/activities/home/HomeActivity.h b/src/activities/home/HomeActivity.h index 52963514..66dfeaa6 100644 --- a/src/activities/home/HomeActivity.h +++ b/src/activities/home/HomeActivity.h @@ -6,10 +6,12 @@ #include #include "../Activity.h" +#include "util/ButtonNavigator.h" class HomeActivity final : public Activity { TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; + ButtonNavigator buttonNavigator; int selectorIndex = 0; bool updateRequired = false; bool hasContinueReading = false; diff --git a/src/activities/home/MyLibraryActivity.cpp b/src/activities/home/MyLibraryActivity.cpp index 29c6ea73..6d9d79fc 100644 --- a/src/activities/home/MyLibraryActivity.cpp +++ b/src/activities/home/MyLibraryActivity.cpp @@ -21,7 +21,6 @@ constexpr int LEFT_MARGIN = 20; constexpr int RIGHT_MARGIN = 40; // Extra space for scroll indicator // Timing thresholds -constexpr int SKIP_PAGE_MS = 700; constexpr unsigned long GO_HOME_MS = 1000; void sortFileList(std::vector& strs) { @@ -178,13 +177,9 @@ void MyLibraryActivity::loop() { return; } - const bool upReleased = mappedInput.wasReleased(MappedInputManager::Button::Up); - const bool downReleased = mappedInput.wasReleased(MappedInputManager::Button::Down); const bool leftReleased = mappedInput.wasReleased(MappedInputManager::Button::Left); const bool rightReleased = mappedInput.wasReleased(MappedInputManager::Button::Right); - const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS; - // Confirm button - open selected item if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (currentTab == Tab::Recent) { @@ -249,24 +244,28 @@ void MyLibraryActivity::loop() { } // Navigation: Up/Down moves through items only - const bool prevReleased = upReleased; - const bool nextReleased = downReleased; + constexpr auto upButton = MappedInputManager::Button::Up; + constexpr auto downButton = MappedInputManager::Button::Down; - if (prevReleased && itemCount > 0) { - if (skipPage) { - selectorIndex = ((selectorIndex / pageItems - 1) * pageItems + itemCount) % itemCount; - } else { - selectorIndex = (selectorIndex + itemCount - 1) % itemCount; - } + buttonNavigator.onRelease({downButton}, [this, itemCount] { + selectorIndex = ButtonNavigator::nextIndex(selectorIndex, itemCount); updateRequired = true; - } else if (nextReleased && itemCount > 0) { - if (skipPage) { - selectorIndex = ((selectorIndex / pageItems + 1) * pageItems) % itemCount; - } else { - selectorIndex = (selectorIndex + 1) % itemCount; - } + }); + + buttonNavigator.onRelease({upButton}, [this, itemCount] { + selectorIndex = ButtonNavigator::previousIndex(selectorIndex, itemCount); updateRequired = true; - } + }); + + buttonNavigator.onContinuous({downButton}, [this, itemCount, pageItems] { + selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, itemCount, pageItems); + updateRequired = true; + }); + + buttonNavigator.onContinuous({upButton}, [this, itemCount, pageItems] { + selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, itemCount, pageItems); + updateRequired = true; + }); } void MyLibraryActivity::displayTaskLoop() { diff --git a/src/activities/home/MyLibraryActivity.h b/src/activities/home/MyLibraryActivity.h index 39a27ed7..1241ba7c 100644 --- a/src/activities/home/MyLibraryActivity.h +++ b/src/activities/home/MyLibraryActivity.h @@ -9,6 +9,7 @@ #include "../Activity.h" #include "RecentBooksStore.h" +#include "util/ButtonNavigator.h" class MyLibraryActivity final : public Activity { public: @@ -17,6 +18,7 @@ class MyLibraryActivity final : public Activity { private: TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; + ButtonNavigator buttonNavigator; Tab currentTab = Tab::Recent; int selectorIndex = 0; diff --git a/src/activities/network/NetworkModeSelectionActivity.cpp b/src/activities/network/NetworkModeSelectionActivity.cpp index 50767084..eac880d2 100644 --- a/src/activities/network/NetworkModeSelectionActivity.cpp +++ b/src/activities/network/NetworkModeSelectionActivity.cpp @@ -72,18 +72,15 @@ void NetworkModeSelectionActivity::loop() { } // Handle navigation - const bool prevPressed = mappedInput.wasPressed(MappedInputManager::Button::Up) || - mappedInput.wasPressed(MappedInputManager::Button::Left); - const bool nextPressed = mappedInput.wasPressed(MappedInputManager::Button::Down) || - mappedInput.wasPressed(MappedInputManager::Button::Right); + buttonNavigator.onNext([this] { + selectedIndex = ButtonNavigator::nextIndex(selectedIndex, MENU_ITEM_COUNT); + updateRequired = true; + }); - if (prevPressed) { - selectedIndex = (selectedIndex + MENU_ITEM_COUNT - 1) % MENU_ITEM_COUNT; + buttonNavigator.onPrevious([this] { + selectedIndex = ButtonNavigator::previousIndex(selectedIndex, MENU_ITEM_COUNT); updateRequired = true; - } else if (nextPressed) { - selectedIndex = (selectedIndex + 1) % MENU_ITEM_COUNT; - updateRequired = true; - } + }); } void NetworkModeSelectionActivity::displayTaskLoop() { diff --git a/src/activities/network/NetworkModeSelectionActivity.h b/src/activities/network/NetworkModeSelectionActivity.h index 1b93b825..5441e1af 100644 --- a/src/activities/network/NetworkModeSelectionActivity.h +++ b/src/activities/network/NetworkModeSelectionActivity.h @@ -6,6 +6,7 @@ #include #include "../Activity.h" +#include "util/ButtonNavigator.h" // Enum for network mode selection enum class NetworkMode { JOIN_NETWORK, CONNECT_CALIBRE, CREATE_HOTSPOT }; @@ -22,6 +23,8 @@ enum class NetworkMode { JOIN_NETWORK, CONNECT_CALIBRE, CREATE_HOTSPOT }; class NetworkModeSelectionActivity final : public Activity { TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; + ButtonNavigator buttonNavigator; + int selectedIndex = 0; bool updateRequired = false; const std::function onModeSelected; diff --git a/src/activities/network/WifiSelectionActivity.cpp b/src/activities/network/WifiSelectionActivity.cpp index becd41a3..0d3fedd2 100644 --- a/src/activities/network/WifiSelectionActivity.cpp +++ b/src/activities/network/WifiSelectionActivity.cpp @@ -419,20 +419,16 @@ void WifiSelectionActivity::loop() { return; } - // Handle UP/DOWN navigation - if (mappedInput.wasPressed(MappedInputManager::Button::Up) || - mappedInput.wasPressed(MappedInputManager::Button::Left)) { - if (selectedNetworkIndex > 0) { - selectedNetworkIndex--; - updateRequired = true; - } - } else if (mappedInput.wasPressed(MappedInputManager::Button::Down) || - mappedInput.wasPressed(MappedInputManager::Button::Right)) { - if (!networks.empty() && selectedNetworkIndex < static_cast(networks.size()) - 1) { - selectedNetworkIndex++; - updateRequired = true; - } - } + // Handle navigation + buttonNavigator.onNext([this] { + selectedNetworkIndex = ButtonNavigator::nextIndex(selectedNetworkIndex, networks.size()); + updateRequired = true; + }); + + buttonNavigator.onPrevious([this] { + selectedNetworkIndex = ButtonNavigator::previousIndex(selectedNetworkIndex, networks.size()); + updateRequired = true; + }); } } diff --git a/src/activities/network/WifiSelectionActivity.h b/src/activities/network/WifiSelectionActivity.h index 0a7e7166..ae1702ea 100644 --- a/src/activities/network/WifiSelectionActivity.h +++ b/src/activities/network/WifiSelectionActivity.h @@ -10,6 +10,7 @@ #include #include "activities/ActivityWithSubactivity.h" +#include "util/ButtonNavigator.h" // Structure to hold WiFi network information struct WifiNetworkInfo { @@ -45,6 +46,7 @@ enum class WifiSelectionState { class WifiSelectionActivity final : public ActivityWithSubactivity { TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; + ButtonNavigator buttonNavigator; bool updateRequired = false; WifiSelectionState state = WifiSelectionState::SCANNING; int selectedNetworkIndex = 0; diff --git a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp index a77b37d0..25ae13cd 100644 --- a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp +++ b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp @@ -7,11 +7,6 @@ #include "MappedInputManager.h" #include "fontIds.h" -namespace { -// Time threshold for treating a long press as a page-up/page-down -constexpr int SKIP_PAGE_MS = 700; -} // namespace - bool EpubReaderChapterSelectionActivity::hasSyncOption() const { return KOREADER_STORE.hasCredentials(); } int EpubReaderChapterSelectionActivity::getTotalItems() const { @@ -119,12 +114,6 @@ void EpubReaderChapterSelectionActivity::loop() { return; } - const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::Up) || - mappedInput.wasReleased(MappedInputManager::Button::Left); - const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::Down) || - mappedInput.wasReleased(MappedInputManager::Button::Right); - - const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS; const int pageItems = getPageItems(); const int totalItems = getTotalItems(); @@ -145,21 +134,27 @@ void EpubReaderChapterSelectionActivity::loop() { } } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { onGoBack(); - } else if (prevReleased) { - if (skipPage) { - selectorIndex = ((selectorIndex / pageItems - 1) * pageItems + totalItems) % totalItems; - } else { - selectorIndex = (selectorIndex + totalItems - 1) % totalItems; - } - updateRequired = true; - } else if (nextReleased) { - if (skipPage) { - selectorIndex = ((selectorIndex / pageItems + 1) * pageItems) % totalItems; - } else { - selectorIndex = (selectorIndex + 1) % totalItems; - } - updateRequired = true; } + + buttonNavigator.onNextRelease([this, totalItems] { + selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems); + updateRequired = true; + }); + + buttonNavigator.onPreviousRelease([this, totalItems] { + selectorIndex = ButtonNavigator::previousIndex(selectorIndex, totalItems); + updateRequired = true; + }); + + buttonNavigator.onNextContinuous([this, totalItems, pageItems] { + selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, totalItems, pageItems); + updateRequired = true; + }); + + buttonNavigator.onPreviousContinuous([this, totalItems, pageItems] { + selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, totalItems, pageItems); + updateRequired = true; + }); } void EpubReaderChapterSelectionActivity::displayTaskLoop() { diff --git a/src/activities/reader/EpubReaderChapterSelectionActivity.h b/src/activities/reader/EpubReaderChapterSelectionActivity.h index 255f0cea..5ddb324b 100644 --- a/src/activities/reader/EpubReaderChapterSelectionActivity.h +++ b/src/activities/reader/EpubReaderChapterSelectionActivity.h @@ -7,12 +7,14 @@ #include #include "../ActivityWithSubactivity.h" +#include "util/ButtonNavigator.h" class EpubReaderChapterSelectionActivity final : public ActivityWithSubactivity { std::shared_ptr epub; std::string epubPath; TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; + ButtonNavigator buttonNavigator; int currentSpineIndex = 0; int currentPage = 0; int totalPagesInSpine = 0; diff --git a/src/activities/reader/XtcReaderChapterSelectionActivity.cpp b/src/activities/reader/XtcReaderChapterSelectionActivity.cpp index ad806a30..4732506c 100644 --- a/src/activities/reader/XtcReaderChapterSelectionActivity.cpp +++ b/src/activities/reader/XtcReaderChapterSelectionActivity.cpp @@ -5,10 +5,6 @@ #include "MappedInputManager.h" #include "fontIds.h" -namespace { -constexpr int SKIP_PAGE_MS = 700; -} // namespace - int XtcReaderChapterSelectionActivity::getPageItems() const { constexpr int startY = 60; constexpr int lineHeight = 30; @@ -75,13 +71,8 @@ void XtcReaderChapterSelectionActivity::onExit() { } void XtcReaderChapterSelectionActivity::loop() { - const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::Up) || - mappedInput.wasReleased(MappedInputManager::Button::Left); - const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::Down) || - mappedInput.wasReleased(MappedInputManager::Button::Right); - - const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS; const int pageItems = getPageItems(); + const int totalItems = static_cast(xtc->getChapters().size()); if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { const auto& chapters = xtc->getChapters(); @@ -90,29 +81,27 @@ void XtcReaderChapterSelectionActivity::loop() { } } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { onGoBack(); - } else if (prevReleased) { - const int total = static_cast(xtc->getChapters().size()); - if (total == 0) { - return; - } - if (skipPage) { - selectorIndex = ((selectorIndex / pageItems - 1) * pageItems + total) % total; - } else { - selectorIndex = (selectorIndex + total - 1) % total; - } - updateRequired = true; - } else if (nextReleased) { - const int total = static_cast(xtc->getChapters().size()); - if (total == 0) { - return; - } - if (skipPage) { - selectorIndex = ((selectorIndex / pageItems + 1) * pageItems) % total; - } else { - selectorIndex = (selectorIndex + 1) % total; - } - updateRequired = true; } + + buttonNavigator.onNextRelease([this, totalItems] { + selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems); + updateRequired = true; + }); + + buttonNavigator.onPreviousRelease([this, totalItems] { + selectorIndex = ButtonNavigator::previousIndex(selectorIndex, totalItems); + updateRequired = true; + }); + + buttonNavigator.onNextContinuous([this, totalItems, pageItems] { + selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, totalItems, pageItems); + updateRequired = true; + }); + + buttonNavigator.onPreviousContinuous([this, totalItems, pageItems] { + selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, totalItems, pageItems); + updateRequired = true; + }); } void XtcReaderChapterSelectionActivity::displayTaskLoop() { diff --git a/src/activities/reader/XtcReaderChapterSelectionActivity.h b/src/activities/reader/XtcReaderChapterSelectionActivity.h index f0fe06bb..c4de4f0b 100644 --- a/src/activities/reader/XtcReaderChapterSelectionActivity.h +++ b/src/activities/reader/XtcReaderChapterSelectionActivity.h @@ -7,11 +7,13 @@ #include #include "../Activity.h" +#include "util/ButtonNavigator.h" class XtcReaderChapterSelectionActivity final : public Activity { std::shared_ptr xtc; TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; + ButtonNavigator buttonNavigator; uint32_t currentPage = 0; int selectorIndex = 0; bool updateRequired = false; diff --git a/src/activities/settings/CalibreSettingsActivity.cpp b/src/activities/settings/CalibreSettingsActivity.cpp index d1df9d0e..df5430e8 100644 --- a/src/activities/settings/CalibreSettingsActivity.cpp +++ b/src/activities/settings/CalibreSettingsActivity.cpp @@ -62,15 +62,16 @@ void CalibreSettingsActivity::loop() { return; } - if (mappedInput.wasPressed(MappedInputManager::Button::Up) || - mappedInput.wasPressed(MappedInputManager::Button::Left)) { - selectedIndex = (selectedIndex + MENU_ITEMS - 1) % MENU_ITEMS; - updateRequired = true; - } else if (mappedInput.wasPressed(MappedInputManager::Button::Down) || - mappedInput.wasPressed(MappedInputManager::Button::Right)) { + // Handle navigation + buttonNavigator.onNext([this] { selectedIndex = (selectedIndex + 1) % MENU_ITEMS; updateRequired = true; - } + }); + + buttonNavigator.onPrevious([this] { + selectedIndex = (selectedIndex + MENU_ITEMS - 1) % MENU_ITEMS; + updateRequired = true; + }); } void CalibreSettingsActivity::handleSelection() { diff --git a/src/activities/settings/CalibreSettingsActivity.h b/src/activities/settings/CalibreSettingsActivity.h index 49695c62..53de46bc 100644 --- a/src/activities/settings/CalibreSettingsActivity.h +++ b/src/activities/settings/CalibreSettingsActivity.h @@ -6,6 +6,7 @@ #include #include "activities/ActivityWithSubactivity.h" +#include "util/ButtonNavigator.h" /** * Submenu for OPDS Browser settings. @@ -24,6 +25,7 @@ class CalibreSettingsActivity final : public ActivityWithSubactivity { private: TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; + ButtonNavigator buttonNavigator; bool updateRequired = false; int selectedIndex = 0; diff --git a/src/activities/settings/CategorySettingsActivity.cpp b/src/activities/settings/CategorySettingsActivity.cpp index 7fd5ef5f..adfe7527 100644 --- a/src/activities/settings/CategorySettingsActivity.cpp +++ b/src/activities/settings/CategorySettingsActivity.cpp @@ -62,15 +62,15 @@ void CategorySettingsActivity::loop() { } // Handle navigation - if (mappedInput.wasPressed(MappedInputManager::Button::Up) || - mappedInput.wasPressed(MappedInputManager::Button::Left)) { - selectedSettingIndex = (selectedSettingIndex > 0) ? (selectedSettingIndex - 1) : (settingsCount - 1); + buttonNavigator.onNext([this] { + selectedSettingIndex = (selectedSettingIndex + 1) % settingsCount; updateRequired = true; - } else if (mappedInput.wasPressed(MappedInputManager::Button::Down) || - mappedInput.wasPressed(MappedInputManager::Button::Right)) { - selectedSettingIndex = (selectedSettingIndex < settingsCount - 1) ? (selectedSettingIndex + 1) : 0; + }); + + buttonNavigator.onPrevious([this] { + selectedSettingIndex = (selectedSettingIndex + settingsCount - 1) % settingsCount; updateRequired = true; - } + }); } void CategorySettingsActivity::toggleCurrentSetting() { diff --git a/src/activities/settings/CategorySettingsActivity.h b/src/activities/settings/CategorySettingsActivity.h index a7d1f0ce..fbd02cae 100644 --- a/src/activities/settings/CategorySettingsActivity.h +++ b/src/activities/settings/CategorySettingsActivity.h @@ -8,6 +8,7 @@ #include #include "activities/ActivityWithSubactivity.h" +#include "util/ButtonNavigator.h" class CrossPointSettings; @@ -44,6 +45,7 @@ struct SettingInfo { class CategorySettingsActivity final : public ActivityWithSubactivity { TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; + ButtonNavigator buttonNavigator; bool updateRequired = false; int selectedSettingIndex = 0; const char* categoryName; diff --git a/src/activities/settings/KOReaderSettingsActivity.cpp b/src/activities/settings/KOReaderSettingsActivity.cpp index 71003433..86684bf5 100644 --- a/src/activities/settings/KOReaderSettingsActivity.cpp +++ b/src/activities/settings/KOReaderSettingsActivity.cpp @@ -63,15 +63,16 @@ void KOReaderSettingsActivity::loop() { return; } - if (mappedInput.wasPressed(MappedInputManager::Button::Up) || - mappedInput.wasPressed(MappedInputManager::Button::Left)) { - selectedIndex = (selectedIndex + MENU_ITEMS - 1) % MENU_ITEMS; - updateRequired = true; - } else if (mappedInput.wasPressed(MappedInputManager::Button::Down) || - mappedInput.wasPressed(MappedInputManager::Button::Right)) { + // Handle navigation + buttonNavigator.onNext([this] { selectedIndex = (selectedIndex + 1) % MENU_ITEMS; updateRequired = true; - } + }); + + buttonNavigator.onPrevious([this] { + selectedIndex = (selectedIndex + MENU_ITEMS - 1) % MENU_ITEMS; + updateRequired = true; + }); } void KOReaderSettingsActivity::handleSelection() { diff --git a/src/activities/settings/KOReaderSettingsActivity.h b/src/activities/settings/KOReaderSettingsActivity.h index 2bedf034..24f2f820 100644 --- a/src/activities/settings/KOReaderSettingsActivity.h +++ b/src/activities/settings/KOReaderSettingsActivity.h @@ -6,6 +6,7 @@ #include #include "activities/ActivityWithSubactivity.h" +#include "util/ButtonNavigator.h" /** * Submenu for KOReader Sync settings. @@ -24,6 +25,7 @@ class KOReaderSettingsActivity final : public ActivityWithSubactivity { private: TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; + ButtonNavigator buttonNavigator; bool updateRequired = false; int selectedIndex = 0; diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 7316db05..c0d66f98 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -111,17 +111,15 @@ void SettingsActivity::loop() { } // Handle navigation - if (mappedInput.wasPressed(MappedInputManager::Button::Up) || - mappedInput.wasPressed(MappedInputManager::Button::Left)) { - // Move selection up (with wrap-around) - selectedCategoryIndex = (selectedCategoryIndex > 0) ? (selectedCategoryIndex - 1) : (categoryCount - 1); + buttonNavigator.onNext([this] { + selectedCategoryIndex = (selectedCategoryIndex + 1) % categoryCount; updateRequired = true; - } else if (mappedInput.wasPressed(MappedInputManager::Button::Down) || - mappedInput.wasPressed(MappedInputManager::Button::Right)) { - // Move selection down (with wrap around) - selectedCategoryIndex = (selectedCategoryIndex < categoryCount - 1) ? (selectedCategoryIndex + 1) : 0; + }); + + buttonNavigator.onPrevious([this] { + selectedCategoryIndex = (selectedCategoryIndex + categoryCount - 1) % categoryCount; updateRequired = true; - } + }); } void SettingsActivity::enterCategory(int categoryIndex) { diff --git a/src/activities/settings/SettingsActivity.h b/src/activities/settings/SettingsActivity.h index 821dda42..fef979e5 100644 --- a/src/activities/settings/SettingsActivity.h +++ b/src/activities/settings/SettingsActivity.h @@ -8,6 +8,7 @@ #include #include "activities/ActivityWithSubactivity.h" +#include "util/ButtonNavigator.h" class CrossPointSettings; struct SettingInfo; @@ -15,6 +16,7 @@ struct SettingInfo; class SettingsActivity final : public ActivityWithSubactivity { TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; + ButtonNavigator buttonNavigator; bool updateRequired = false; int selectedCategoryIndex = 0; // Currently selected category const std::function onGoHome; diff --git a/src/activities/util/KeyboardEntryActivity.cpp b/src/activities/util/KeyboardEntryActivity.cpp index 3a6befac..301e67b0 100644 --- a/src/activities/util/KeyboardEntryActivity.cpp +++ b/src/activities/util/KeyboardEntryActivity.cpp @@ -138,37 +138,24 @@ void KeyboardEntryActivity::handleKeyPress() { } void KeyboardEntryActivity::loop() { - // Navigation - if (mappedInput.wasPressed(MappedInputManager::Button::Up)) { - if (selectedRow > 0) { - selectedRow--; - // Clamp column to valid range for new row - const int maxCol = getRowLength(selectedRow) - 1; - if (selectedCol > maxCol) selectedCol = maxCol; - } else { - // Wrap to bottom row - selectedRow = NUM_ROWS - 1; - const int maxCol = getRowLength(selectedRow) - 1; - if (selectedCol > maxCol) selectedCol = maxCol; - } - updateRequired = true; - } + // Handle navigation + buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Up}, [this] { + selectedRow = ButtonNavigator::previousIndex(selectedRow, NUM_ROWS); - if (mappedInput.wasPressed(MappedInputManager::Button::Down)) { - if (selectedRow < NUM_ROWS - 1) { - selectedRow++; - const int maxCol = getRowLength(selectedRow) - 1; - if (selectedCol > maxCol) selectedCol = maxCol; - } else { - // Wrap to top row - selectedRow = 0; - const int maxCol = getRowLength(selectedRow) - 1; - if (selectedCol > maxCol) selectedCol = maxCol; - } + const int maxCol = getRowLength(selectedRow) - 1; + if (selectedCol > maxCol) selectedCol = maxCol; updateRequired = true; - } + }); - if (mappedInput.wasPressed(MappedInputManager::Button::Left)) { + buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Down}, [this] { + selectedRow = ButtonNavigator::nextIndex(selectedRow, NUM_ROWS); + + const int maxCol = getRowLength(selectedRow) - 1; + if (selectedCol > maxCol) selectedCol = maxCol; + updateRequired = true; + }); + + buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Left}, [this] { const int maxCol = getRowLength(selectedRow) - 1; // Special bottom row case @@ -187,20 +174,14 @@ void KeyboardEntryActivity::loop() { // At done button, move to backspace selectedCol = BACKSPACE_COL; } - updateRequired = true; - return; - } - - if (selectedCol > 0) { - selectedCol--; } else { - // Wrap to end of current row - selectedCol = maxCol; + selectedCol = ButtonNavigator::previousIndex(selectedCol, maxCol + 1); } - updateRequired = true; - } - if (mappedInput.wasPressed(MappedInputManager::Button::Right)) { + updateRequired = true; + }); + + buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Right}, [this] { const int maxCol = getRowLength(selectedRow) - 1; // Special bottom row case @@ -219,18 +200,11 @@ void KeyboardEntryActivity::loop() { // At done button, wrap to beginning of row selectedCol = SHIFT_COL; } - updateRequired = true; - return; - } - - if (selectedCol < maxCol) { - selectedCol++; } else { - // Wrap to beginning of current row - selectedCol = 0; + selectedCol = ButtonNavigator::nextIndex(selectedCol, maxCol + 1); } updateRequired = true; - } + }); // Selection if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { diff --git a/src/activities/util/KeyboardEntryActivity.h b/src/activities/util/KeyboardEntryActivity.h index 1c1b1f3e..3b48ea19 100644 --- a/src/activities/util/KeyboardEntryActivity.h +++ b/src/activities/util/KeyboardEntryActivity.h @@ -9,6 +9,7 @@ #include #include "../Activity.h" +#include "util/ButtonNavigator.h" /** * Reusable keyboard entry activity for text input. @@ -65,6 +66,7 @@ class KeyboardEntryActivity : public Activity { bool isPassword; TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; + ButtonNavigator buttonNavigator; bool updateRequired = false; // Keyboard state diff --git a/src/main.cpp b/src/main.cpp index 89c4e13c..c12a97be 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -25,6 +25,7 @@ #include "activities/settings/SettingsActivity.h" #include "activities/util/FullScreenMessageActivity.h" #include "fontIds.h" +#include "util/ButtonNavigator.h" HalDisplay display; HalGPIO gpio; @@ -293,6 +294,7 @@ void setup() { SETTINGS.loadFromFile(); KOREADER_STORE.loadFromFile(); + ButtonNavigator::setMappedInputManager(mappedInputManager); switch (gpio.getWakeupReason()) { case HalGPIO::WakeupReason::PowerButton: diff --git a/src/util/ButtonNavigator.cpp b/src/util/ButtonNavigator.cpp new file mode 100644 index 00000000..d9844138 --- /dev/null +++ b/src/util/ButtonNavigator.cpp @@ -0,0 +1,124 @@ +#include "ButtonNavigator.h" + +const MappedInputManager* ButtonNavigator::mappedInput = nullptr; + +void ButtonNavigator::onNext(const Callback& callback) { + onNextPress(callback); + onNextContinuous(callback); +} + +void ButtonNavigator::onPrevious(const Callback& callback) { + onPreviousPress(callback); + onPreviousContinuous(callback); +} + +void ButtonNavigator::onPressAndContinuous(const Buttons& buttons, const Callback& callback) { + onPress(buttons, callback); + onContinuous(buttons, callback); +} + +void ButtonNavigator::onNextPress(const Callback& callback) { onPress(getNextButtons(), callback); } + +void ButtonNavigator::onPreviousPress(const Callback& callback) { onPress(getPreviousButtons(), callback); } + +void ButtonNavigator::onNextRelease(const Callback& callback) { onRelease(getNextButtons(), callback); } + +void ButtonNavigator::onPreviousRelease(const Callback& callback) { onRelease(getPreviousButtons(), callback); } + +void ButtonNavigator::onNextContinuous(const Callback& callback) { onContinuous(getNextButtons(), callback); } + +void ButtonNavigator::onPreviousContinuous(const Callback& callback) { onContinuous(getPreviousButtons(), callback); } + +void ButtonNavigator::onPress(const Buttons& buttons, const Callback& callback) { + const bool wasPressed = std::any_of(buttons.begin(), buttons.end(), [](const MappedInputManager::Button button) { + return mappedInput != nullptr && mappedInput->wasPressed(button); + }); + + if (wasPressed) { + callback(); + } +} + +void ButtonNavigator::onRelease(const Buttons& buttons, const Callback& callback) { + const bool wasReleased = std::any_of(buttons.begin(), buttons.end(), [](const MappedInputManager::Button button) { + return mappedInput != nullptr && mappedInput->wasReleased(button); + }); + + if (wasReleased) { + if (lastContinuousNavTime == 0) { + callback(); + } + + lastContinuousNavTime = 0; + } +} + +void ButtonNavigator::onContinuous(const Buttons& buttons, const Callback& callback) { + const bool isPressed = std::any_of(buttons.begin(), buttons.end(), [this](const MappedInputManager::Button button) { + return mappedInput != nullptr && mappedInput->isPressed(button) && shouldNavigateContinuously(); + }); + + if (isPressed) { + callback(); + lastContinuousNavTime = millis(); + } +} + +bool ButtonNavigator::shouldNavigateContinuously() const { + if (!mappedInput) return false; + + const bool buttonHeldLongEnough = mappedInput->getHeldTime() > continuousStartMs; + const bool navigationIntervalElapsed = (millis() - lastContinuousNavTime) > continuousIntervalMs; + + return buttonHeldLongEnough && navigationIntervalElapsed; +} + +int ButtonNavigator::nextIndex(const int currentIndex, const int totalItems) { + if (totalItems <= 0) return 0; + + // Calculate the next index with wrap-around + return (currentIndex + 1) % totalItems; +} + +int ButtonNavigator::previousIndex(const int currentIndex, const int totalItems) { + if (totalItems <= 0) return 0; + + // Calculate the previous index with wrap-around + return (currentIndex + totalItems - 1) % totalItems; +} + +int ButtonNavigator::nextPageIndex(const int currentIndex, const int totalItems, const int itemsPerPage) { + if (totalItems <= 0 || itemsPerPage <= 0) return 0; + + // When items fit on one page, use index navigation instead + if (totalItems <= itemsPerPage) { + return nextIndex(currentIndex, totalItems); + } + + const int lastPageIndex = (totalItems - 1) / itemsPerPage; + const int currentPageIndex = currentIndex / itemsPerPage; + + if (currentPageIndex < lastPageIndex) { + return (currentPageIndex + 1) * itemsPerPage; + } + + return 0; +} + +int ButtonNavigator::previousPageIndex(const int currentIndex, const int totalItems, const int itemsPerPage) { + if (totalItems <= 0 || itemsPerPage <= 0) return 0; + + // When items fit on one page, use index navigation instead + if (totalItems <= itemsPerPage) { + return previousIndex(currentIndex, totalItems); + } + + const int lastPageIndex = (totalItems - 1) / itemsPerPage; + const int currentPageIndex = currentIndex / itemsPerPage; + + if (currentPageIndex > 0) { + return (currentPageIndex - 1) * itemsPerPage; + } + + return lastPageIndex * itemsPerPage; +} diff --git a/src/util/ButtonNavigator.h b/src/util/ButtonNavigator.h new file mode 100644 index 00000000..2f9afbc1 --- /dev/null +++ b/src/util/ButtonNavigator.h @@ -0,0 +1,53 @@ +#pragma once + +#include +#include + +#include "MappedInputManager.h" + +class ButtonNavigator final { + using Callback = std::function; + using Buttons = std::vector; + + const uint16_t continuousStartMs; + const uint16_t continuousIntervalMs; + uint32_t lastContinuousNavTime = 0; + static const MappedInputManager* mappedInput; + + [[nodiscard]] bool shouldNavigateContinuously() const; + + public: + explicit ButtonNavigator(const uint16_t continuousIntervalMs = 500, const uint16_t continuousStartMs = 500) + : continuousStartMs(continuousStartMs), continuousIntervalMs(continuousIntervalMs) {} + + static void setMappedInputManager(const MappedInputManager& mappedInputManager) { mappedInput = &mappedInputManager; } + + void onNext(const Callback& callback); + void onPrevious(const Callback& callback); + void onPressAndContinuous(const Buttons& buttons, const Callback& callback); + + void onNextPress(const Callback& callback); + void onPreviousPress(const Callback& callback); + void onPress(const Buttons& buttons, const Callback& callback); + + void onNextRelease(const Callback& callback); + void onPreviousRelease(const Callback& callback); + void onRelease(const Buttons& buttons, const Callback& callback); + + void onNextContinuous(const Callback& callback); + void onPreviousContinuous(const Callback& callback); + void onContinuous(const Buttons& buttons, const Callback& callback); + + [[nodiscard]] static int nextIndex(int currentIndex, int totalItems); + [[nodiscard]] static int previousIndex(int currentIndex, int totalItems); + + [[nodiscard]] static int nextPageIndex(int currentIndex, int totalItems, int itemsPerPage); + [[nodiscard]] static int previousPageIndex(int currentIndex, int totalItems, int itemsPerPage); + + [[nodiscard]] static Buttons getNextButtons() { + return {MappedInputManager::Button::Down, MappedInputManager::Button::Right}; + } + [[nodiscard]] static Buttons getPreviousButtons() { + return {MappedInputManager::Button::Up, MappedInputManager::Button::Left}; + } +}; \ No newline at end of file