From 64d161e88bef241a07364626448f618a261dc14b Mon Sep 17 00:00:00 2001 From: Istiak Tridip <13367189+istiak-tridip@users.noreply.github.com> Date: Mon, 9 Feb 2026 15:19:34 +0600 Subject: [PATCH] feat: unify navigation handling with system-wide continuous navigation (#600) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR unifies navigation handling & adds system-wide support for continuous navigation. ## Summary Holding down a navigation button now continuously advances through items until the button is released. This removes the need for repeated press-and-release actions and makes navigation faster and smoother, especially in long menus or documents. When page-based navigation is available, it will navigate through pages. If not, it will progress through menu items or similar list-based UI elements. Additionally, this PR fixes inconsistencies in wrap-around behavior and navigation index calculations. Places where the navigation system was updated: - Home Page - Settings Pages - My Library Page - WiFi Selection Page - OPDS Browser Page - Keyboard - File Transfer Page - XTC Chapter Selector Page - EPUB Chapter Selector Page I’ve tested this on the device as much as possible and tried to match the existing behavior. Please let me know if I missed anything. Thanks 🙏 ![crosspoint](https://github.com/user-attachments/assets/6a3c7482-f45e-4a77-b156-721bb3b679e6) --- Following the request from @osteotek and @daveallie for system-wide support, the old PR (#379) has been closed in favor of this consolidated, system-wide implementation. --- ### AI Usage Did you use AI tools to help write this code? _**PARTIALLY**_ --------- Co-authored-by: Dave Allie --- .../browser/OpdsBookBrowserActivity.cpp | 44 ++++--- .../browser/OpdsBookBrowserActivity.h | 2 + src/activities/home/HomeActivity.cpp | 21 ++- src/activities/home/HomeActivity.h | 2 + src/activities/home/MyLibraryActivity.cpp | 41 +++--- src/activities/home/MyLibraryActivity.h | 3 + src/activities/home/RecentBooksActivity.cpp | 39 +++--- src/activities/home/RecentBooksActivity.h | 2 + .../network/NetworkModeSelectionActivity.cpp | 17 +-- .../network/NetworkModeSelectionActivity.h | 3 + .../network/WifiSelectionActivity.cpp | 24 ++-- .../network/WifiSelectionActivity.h | 2 + .../EpubReaderChapterSelectionActivity.cpp | 45 +++---- .../EpubReaderChapterSelectionActivity.h | 2 + .../reader/EpubReaderMenuActivity.cpp | 21 +-- .../reader/EpubReaderMenuActivity.h | 2 + .../EpubReaderPercentSelectionActivity.cpp | 22 +--- .../EpubReaderPercentSelectionActivity.h | 2 + .../XtcReaderChapterSelectionActivity.cpp | 53 +++----- .../XtcReaderChapterSelectionActivity.h | 2 + .../settings/CalibreSettingsActivity.cpp | 15 ++- .../settings/CalibreSettingsActivity.h | 2 + .../settings/KOReaderSettingsActivity.cpp | 15 ++- .../settings/KOReaderSettingsActivity.h | 2 + src/activities/settings/SettingsActivity.cpp | 38 +++--- src/activities/settings/SettingsActivity.h | 2 + src/activities/util/KeyboardEntryActivity.cpp | 70 ++++------ src/activities/util/KeyboardEntryActivity.h | 2 + src/main.cpp | 2 + src/util/ButtonNavigator.cpp | 124 ++++++++++++++++++ src/util/ButtonNavigator.h | 53 ++++++++ 31 files changed, 408 insertions(+), 266 deletions(-) create mode 100644 src/util/ButtonNavigator.cpp create mode 100644 src/util/ButtonNavigator.h diff --git a/src/activities/browser/OpdsBookBrowserActivity.cpp b/src/activities/browser/OpdsBookBrowserActivity.cpp index f33bda04..340b5444 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 4155799c..5ae4ea5d 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -196,13 +196,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; @@ -226,12 +231,6 @@ void HomeActivity::loop() { } else if (menuSelectedIndex == 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 1f714217..8ec68777 100644 --- a/src/activities/home/HomeActivity.h +++ b/src/activities/home/HomeActivity.h @@ -8,6 +8,7 @@ #include "../Activity.h" #include "./MyLibraryActivity.h" +#include "util/ButtonNavigator.h" struct RecentBook; struct Rect; @@ -15,6 +16,7 @@ struct Rect; class HomeActivity final : public Activity { TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; + ButtonNavigator buttonNavigator; int selectorIndex = 0; bool updateRequired = false; bool recentsLoading = false; diff --git a/src/activities/home/MyLibraryActivity.cpp b/src/activities/home/MyLibraryActivity.cpp index e0a36b97..52b2fe13 100644 --- a/src/activities/home/MyLibraryActivity.cpp +++ b/src/activities/home/MyLibraryActivity.cpp @@ -11,7 +11,6 @@ #include "util/StringUtils.h" namespace { -constexpr int SKIP_PAGE_MS = 700; constexpr unsigned long GO_HOME_MS = 1000; } // namespace @@ -109,13 +108,6 @@ void MyLibraryActivity::loop() { return; } - const bool upReleased = mappedInput.wasReleased(MappedInputManager::Button::Left) || - mappedInput.wasReleased(MappedInputManager::Button::Up); - ; - const bool downReleased = mappedInput.wasReleased(MappedInputManager::Button::Right) || - mappedInput.wasReleased(MappedInputManager::Button::Down); - - const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS; const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, false); if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { @@ -157,21 +149,26 @@ void MyLibraryActivity::loop() { } int listSize = static_cast(files.size()); - if (upReleased) { - if (skipPage) { - selectorIndex = std::max(static_cast((selectorIndex / pageItems - 1) * pageItems), 0); - } else { - selectorIndex = (selectorIndex + listSize - 1) % listSize; - } + + buttonNavigator.onNextRelease([this, listSize] { + selectorIndex = ButtonNavigator::nextIndex(static_cast(selectorIndex), listSize); updateRequired = true; - } else if (downReleased) { - if (skipPage) { - selectorIndex = std::min(static_cast((selectorIndex / pageItems + 1) * pageItems), listSize - 1); - } else { - selectorIndex = (selectorIndex + 1) % listSize; - } + }); + + buttonNavigator.onPreviousRelease([this, listSize] { + selectorIndex = ButtonNavigator::previousIndex(static_cast(selectorIndex), listSize); updateRequired = true; - } + }); + + buttonNavigator.onNextContinuous([this, listSize, pageItems] { + selectorIndex = ButtonNavigator::nextPageIndex(static_cast(selectorIndex), listSize, pageItems); + updateRequired = true; + }); + + buttonNavigator.onPreviousContinuous([this, listSize, pageItems] { + selectorIndex = ButtonNavigator::previousPageIndex(static_cast(selectorIndex), listSize, pageItems); + updateRequired = true; + }); } void MyLibraryActivity::displayTaskLoop() { @@ -217,4 +214,4 @@ size_t MyLibraryActivity::findEntry(const std::string& name) const { for (size_t i = 0; i < files.size(); i++) if (files[i] == name) return i; return 0; -} +} \ No newline at end of file diff --git a/src/activities/home/MyLibraryActivity.h b/src/activities/home/MyLibraryActivity.h index 70e9e29c..0713524d 100644 --- a/src/activities/home/MyLibraryActivity.h +++ b/src/activities/home/MyLibraryActivity.h @@ -8,11 +8,14 @@ #include #include "../Activity.h" +#include "RecentBooksStore.h" +#include "util/ButtonNavigator.h" class MyLibraryActivity final : public Activity { private: TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; + ButtonNavigator buttonNavigator; size_t selectorIndex = 0; bool updateRequired = false; diff --git a/src/activities/home/RecentBooksActivity.cpp b/src/activities/home/RecentBooksActivity.cpp index 4add9b84..657d05c9 100644 --- a/src/activities/home/RecentBooksActivity.cpp +++ b/src/activities/home/RecentBooksActivity.cpp @@ -12,7 +12,6 @@ #include "util/StringUtils.h" namespace { -constexpr int SKIP_PAGE_MS = 700; constexpr unsigned long GO_HOME_MS = 1000; } // namespace @@ -70,13 +69,6 @@ void RecentBooksActivity::onExit() { } void RecentBooksActivity::loop() { - const bool upReleased = mappedInput.wasReleased(MappedInputManager::Button::Left) || - mappedInput.wasReleased(MappedInputManager::Button::Up); - ; - const bool downReleased = mappedInput.wasReleased(MappedInputManager::Button::Right) || - mappedInput.wasReleased(MappedInputManager::Button::Down); - - const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS; const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, true); if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { @@ -92,21 +84,26 @@ void RecentBooksActivity::loop() { } int listSize = static_cast(recentBooks.size()); - if (upReleased) { - if (skipPage) { - selectorIndex = std::max(static_cast((selectorIndex / pageItems - 1) * pageItems), 0); - } else { - selectorIndex = (selectorIndex + listSize - 1) % listSize; - } + + buttonNavigator.onNextRelease([this, listSize] { + selectorIndex = ButtonNavigator::nextIndex(static_cast(selectorIndex), listSize); updateRequired = true; - } else if (downReleased) { - if (skipPage) { - selectorIndex = std::min(static_cast((selectorIndex / pageItems + 1) * pageItems), listSize - 1); - } else { - selectorIndex = (selectorIndex + 1) % listSize; - } + }); + + buttonNavigator.onPreviousRelease([this, listSize] { + selectorIndex = ButtonNavigator::previousIndex(static_cast(selectorIndex), listSize); updateRequired = true; - } + }); + + buttonNavigator.onNextContinuous([this, listSize, pageItems] { + selectorIndex = ButtonNavigator::nextPageIndex(static_cast(selectorIndex), listSize, pageItems); + updateRequired = true; + }); + + buttonNavigator.onPreviousContinuous([this, listSize, pageItems] { + selectorIndex = ButtonNavigator::previousPageIndex(static_cast(selectorIndex), listSize, pageItems); + updateRequired = true; + }); } void RecentBooksActivity::displayTaskLoop() { diff --git a/src/activities/home/RecentBooksActivity.h b/src/activities/home/RecentBooksActivity.h index 4490aeac..fee89981 100644 --- a/src/activities/home/RecentBooksActivity.h +++ b/src/activities/home/RecentBooksActivity.h @@ -9,11 +9,13 @@ #include "../Activity.h" #include "RecentBooksStore.h" +#include "util/ButtonNavigator.h" class RecentBooksActivity final : public Activity { private: TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; + ButtonNavigator buttonNavigator; size_t selectorIndex = 0; bool updateRequired = false; diff --git a/src/activities/network/NetworkModeSelectionActivity.cpp b/src/activities/network/NetworkModeSelectionActivity.cpp index e6713ea0..bee13d8c 100644 --- a/src/activities/network/NetworkModeSelectionActivity.cpp +++ b/src/activities/network/NetworkModeSelectionActivity.cpp @@ -73,18 +73,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 b80cf65d..5475251e 100644 --- a/src/activities/network/WifiSelectionActivity.cpp +++ b/src/activities/network/WifiSelectionActivity.cpp @@ -420,20 +420,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 ae3a032c..9a11b1a3 100644 --- a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp +++ b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp @@ -6,11 +6,6 @@ #include "components/UITheme.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 - int EpubReaderChapterSelectionActivity::getTotalItems() const { return epub->getTocItemsCount(); } int EpubReaderChapterSelectionActivity::getPageItems() const { @@ -77,12 +72,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(); @@ -95,21 +84,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 c43469d0..325d562a 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/EpubReaderMenuActivity.cpp b/src/activities/reader/EpubReaderMenuActivity.cpp index 732cff5e..58ec6c4e 100644 --- a/src/activities/reader/EpubReaderMenuActivity.cpp +++ b/src/activities/reader/EpubReaderMenuActivity.cpp @@ -48,16 +48,19 @@ void EpubReaderMenuActivity::loop() { return; } + // Handle navigation + buttonNavigator.onNext([this] { + selectedIndex = ButtonNavigator::nextIndex(selectedIndex, static_cast(menuItems.size())); + updateRequired = true; + }); + + buttonNavigator.onPrevious([this] { + selectedIndex = ButtonNavigator::previousIndex(selectedIndex, static_cast(menuItems.size())); + updateRequired = true; + }); + // Use local variables for items we need to check after potential deletion - if (mappedInput.wasReleased(MappedInputManager::Button::Up) || - mappedInput.wasReleased(MappedInputManager::Button::Left)) { - selectedIndex = (selectedIndex + menuItems.size() - 1) % menuItems.size(); - updateRequired = true; - } else if (mappedInput.wasReleased(MappedInputManager::Button::Down) || - mappedInput.wasReleased(MappedInputManager::Button::Right)) { - selectedIndex = (selectedIndex + 1) % menuItems.size(); - updateRequired = true; - } else if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { const auto selectedAction = menuItems[selectedIndex].action; if (selectedAction == MenuAction::ROTATE_SCREEN) { // Cycle orientation preview locally; actual rotation happens on menu exit. diff --git a/src/activities/reader/EpubReaderMenuActivity.h b/src/activities/reader/EpubReaderMenuActivity.h index c24a610e..1f34b208 100644 --- a/src/activities/reader/EpubReaderMenuActivity.h +++ b/src/activities/reader/EpubReaderMenuActivity.h @@ -9,6 +9,7 @@ #include #include "../ActivityWithSubactivity.h" +#include "util/ButtonNavigator.h" class EpubReaderMenuActivity final : public ActivityWithSubactivity { public: @@ -48,6 +49,7 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity { bool updateRequired = false; TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; + ButtonNavigator buttonNavigator; std::string title = "Reader Menu"; uint8_t pendingOrientation = 0; const std::vector orientationLabels = {"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}; diff --git a/src/activities/reader/EpubReaderPercentSelectionActivity.cpp b/src/activities/reader/EpubReaderPercentSelectionActivity.cpp index 74dd5229..ec7293d8 100644 --- a/src/activities/reader/EpubReaderPercentSelectionActivity.cpp +++ b/src/activities/reader/EpubReaderPercentSelectionActivity.cpp @@ -79,25 +79,11 @@ void EpubReaderPercentSelectionActivity::loop() { return; } - if (mappedInput.wasReleased(MappedInputManager::Button::Left)) { - adjustPercent(-kSmallStep); - return; - } + buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Left}, [this] { adjustPercent(-kSmallStep); }); + buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Right}, [this] { adjustPercent(kSmallStep); }); - if (mappedInput.wasReleased(MappedInputManager::Button::Right)) { - adjustPercent(kSmallStep); - return; - } - - if (mappedInput.wasReleased(MappedInputManager::Button::Up)) { - adjustPercent(kLargeStep); - return; - } - - if (mappedInput.wasReleased(MappedInputManager::Button::Down)) { - adjustPercent(-kLargeStep); - return; - } + buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Up}, [this] { adjustPercent(kLargeStep); }); + buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Down}, [this] { adjustPercent(-kLargeStep); }); } void EpubReaderPercentSelectionActivity::renderScreen() { diff --git a/src/activities/reader/EpubReaderPercentSelectionActivity.h b/src/activities/reader/EpubReaderPercentSelectionActivity.h index 56238935..8d3ec96f 100644 --- a/src/activities/reader/EpubReaderPercentSelectionActivity.h +++ b/src/activities/reader/EpubReaderPercentSelectionActivity.h @@ -7,6 +7,7 @@ #include "MappedInputManager.h" #include "activities/ActivityWithSubactivity.h" +#include "util/ButtonNavigator.h" class EpubReaderPercentSelectionActivity final : public ActivityWithSubactivity { public: @@ -31,6 +32,7 @@ class EpubReaderPercentSelectionActivity final : public ActivityWithSubactivity // FreeRTOS task and mutex for rendering. TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; + ButtonNavigator buttonNavigator; // Callback invoked when the user confirms a percent. const std::function onSelect; diff --git a/src/activities/reader/XtcReaderChapterSelectionActivity.cpp b/src/activities/reader/XtcReaderChapterSelectionActivity.cpp index 51f4a6db..378924f0 100644 --- a/src/activities/reader/XtcReaderChapterSelectionActivity.cpp +++ b/src/activities/reader/XtcReaderChapterSelectionActivity.cpp @@ -8,10 +8,6 @@ #include "components/UITheme.h" #include "fontIds.h" -namespace { -constexpr int SKIP_PAGE_MS = 700; -} // namespace - int XtcReaderChapterSelectionActivity::getPageItems() const { constexpr int lineHeight = 30; @@ -78,13 +74,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(); @@ -93,29 +84,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 86a1a070..7b7a0ed4 100644 --- a/src/activities/settings/CalibreSettingsActivity.cpp +++ b/src/activities/settings/CalibreSettingsActivity.cpp @@ -63,15 +63,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/KOReaderSettingsActivity.cpp b/src/activities/settings/KOReaderSettingsActivity.cpp index 278ce7cd..a72151d6 100644 --- a/src/activities/settings/KOReaderSettingsActivity.cpp +++ b/src/activities/settings/KOReaderSettingsActivity.cpp @@ -64,15 +64,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 967a8342..14a2f8a0 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -16,10 +16,6 @@ const char* SettingsActivity::categoryNames[categoryCount] = {"Display", "Reader", "Controls", "System"}; -namespace { -constexpr int changeTabsMs = 700; -} // namespace - void SettingsActivity::taskTrampoline(void* param) { auto* self = static_cast(param); self->displayTaskLoop(); @@ -116,28 +112,28 @@ void SettingsActivity::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 changeTab = mappedInput.getHeldTime() > changeTabsMs; - // Handle navigation - if (upReleased && changeTab) { + buttonNavigator.onNextRelease([this] { + selectedSettingIndex = ButtonNavigator::nextIndex(selectedSettingIndex, settingsCount + 1); + updateRequired = true; + }); + + buttonNavigator.onPreviousRelease([this] { + selectedSettingIndex = ButtonNavigator::previousIndex(selectedSettingIndex, settingsCount + 1); + updateRequired = true; + }); + + buttonNavigator.onNextContinuous([this, &hasChangedCategory] { hasChangedCategory = true; - selectedCategoryIndex = (selectedCategoryIndex > 0) ? (selectedCategoryIndex - 1) : (categoryCount - 1); + selectedCategoryIndex = ButtonNavigator::nextIndex(selectedCategoryIndex, categoryCount); updateRequired = true; - } else if (downReleased && changeTab) { + }); + + buttonNavigator.onPreviousContinuous([this, &hasChangedCategory] { hasChangedCategory = true; - selectedCategoryIndex = (selectedCategoryIndex < categoryCount - 1) ? (selectedCategoryIndex + 1) : 0; + selectedCategoryIndex = ButtonNavigator::previousIndex(selectedCategoryIndex, categoryCount); updateRequired = true; - } else if (upReleased || leftReleased) { - selectedSettingIndex = (selectedSettingIndex > 0) ? (selectedSettingIndex - 1) : (settingsCount); - updateRequired = true; - } else if (rightReleased || downReleased) { - selectedSettingIndex = (selectedSettingIndex < settingsCount) ? (selectedSettingIndex + 1) : 0; - updateRequired = true; - } + }); if (hasChangedCategory) { selectedSettingIndex = (selectedSettingIndex == 0) ? 0 : 1; diff --git a/src/activities/settings/SettingsActivity.h b/src/activities/settings/SettingsActivity.h index 70248bb0..04ead1e0 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; @@ -124,6 +125,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 int selectedSettingIndex = 0; diff --git a/src/activities/util/KeyboardEntryActivity.cpp b/src/activities/util/KeyboardEntryActivity.cpp index bc0bdff8..40f2eaa6 100644 --- a/src/activities/util/KeyboardEntryActivity.cpp +++ b/src/activities/util/KeyboardEntryActivity.cpp @@ -142,37 +142,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 @@ -191,20 +178,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 @@ -223,18 +204,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 0f05bae5..8e94fd3c 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 33515bce..80dd36ac 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -27,6 +27,7 @@ #include "activities/util/FullScreenMessageActivity.h" #include "components/UITheme.h" #include "fontIds.h" +#include "util/ButtonNavigator.h" HalDisplay display; HalGPIO gpio; @@ -304,6 +305,7 @@ void setup() { SETTINGS.loadFromFile(); KOREADER_STORE.loadFromFile(); UITheme::getInstance().reload(); + 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