From 727186f20820f9f5b12376f89df1803aa57884d8 Mon Sep 17 00:00:00 2001 From: Istiak Tridip <13367189+istiak-tridip@users.noreply.github.com> Date: Wed, 28 Jan 2026 02:22:00 +0600 Subject: [PATCH 01/14] feat: ButtonNavigator class --- src/main.cpp | 2 + src/util/ButtonNavigator.cpp | 85 ++++++++++++++++++++++++++++++++++++ src/util/ButtonNavigator.h | 47 ++++++++++++++++++++ 3 files changed, 134 insertions(+) create mode 100644 src/util/ButtonNavigator.cpp create mode 100644 src/util/ButtonNavigator.h diff --git a/src/main.cpp b/src/main.cpp index 2308f0a2..a726e78a 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); if (gpio.isWakeupByPowerButton()) { // For normal wakeups, verify power button press duration diff --git a/src/util/ButtonNavigator.cpp b/src/util/ButtonNavigator.cpp new file mode 100644 index 00000000..f7006ca5 --- /dev/null +++ b/src/util/ButtonNavigator.cpp @@ -0,0 +1,85 @@ +#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::onNextPress(const Callback& callback) { onPress(getNextButtons(), callback); } + +void ButtonNavigator::onPreviousPress(const Callback& callback) { onPress(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) { + if (!mappedInput) return; + + bool buttonPressed = false; + for (const MappedInputManager::Button button : buttons) { + if (mappedInput->wasPressed(button)) { + buttonPressed = true; + break; + } + } + + if (buttonPressed && !recentlyNavigatedContinuously()) { + callback(); + } +} + +void ButtonNavigator::onContinuous(const Buttons& buttons, const Callback& callback) { + if (!mappedInput) return; + + bool buttonPressed = false; + for (const MappedInputManager::Button button : buttons) { + if (mappedInput->isPressed(button)) { + buttonPressed = true; + break; + } + } + + if (buttonPressed && shouldNavigateContinuously()) { + 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; +} + +bool ButtonNavigator::recentlyNavigatedContinuously() const { + const int elapsedTime = millis() - lastContinuousNavTime; + if (elapsedTime < 50) { + return true; + } + + return false; +} + +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; +} \ No newline at end of file diff --git a/src/util/ButtonNavigator.h b/src/util/ButtonNavigator.h new file mode 100644 index 00000000..14a1d928 --- /dev/null +++ b/src/util/ButtonNavigator.h @@ -0,0 +1,47 @@ +#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; + [[nodiscard]] bool recentlyNavigatedContinuously() const; + + [[nodiscard]] static Buttons getNextButtons() { + return {MappedInputManager::Button::Down, MappedInputManager::Button::Right}; + } + [[nodiscard]] static Buttons getPreviousButtons() { + return {MappedInputManager::Button::Up, MappedInputManager::Button::Left}; + } + + 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 onNextPress(const Callback& callback); + void onPreviousPress(const Callback& callback); + void onPress(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); +}; \ No newline at end of file From 7aa21f2386b81dbb0757c94976729be484e03ef3 Mon Sep 17 00:00:00 2001 From: Istiak Tridip <13367189+istiak-tridip@users.noreply.github.com> Date: Wed, 28 Jan 2026 02:23:42 +0600 Subject: [PATCH 02/14] refactor: home activity --- src/activities/home/HomeActivity.cpp | 21 ++++++++++----------- src/activities/home/HomeActivity.h | 2 ++ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index 58b29505..8c1d90a5 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -161,13 +161,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; @@ -188,12 +193,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; From 178c826b52350d78bcdf47d714e82379b7162cb7 Mon Sep 17 00:00:00 2001 From: Istiak Tridip <13367189+istiak-tridip@users.noreply.github.com> Date: Wed, 28 Jan 2026 02:33:04 +0600 Subject: [PATCH 03/14] refactor: settings activities --- .../settings/CalibreSettingsActivity.cpp | 15 ++++++++------- .../settings/CalibreSettingsActivity.h | 2 ++ .../settings/CategorySettingsActivity.cpp | 14 +++++++------- .../settings/CategorySettingsActivity.h | 2 ++ .../settings/KOReaderSettingsActivity.cpp | 15 ++++++++------- .../settings/KOReaderSettingsActivity.h | 2 ++ src/activities/settings/SettingsActivity.cpp | 16 +++++++--------- src/activities/settings/SettingsActivity.h | 2 ++ 8 files changed, 38 insertions(+), 30 deletions(-) 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; From 73c5f0584366289443d6c485972d19a1f1fb83af Mon Sep 17 00:00:00 2001 From: Istiak Tridip <13367189+istiak-tridip@users.noreply.github.com> Date: Wed, 28 Jan 2026 03:29:15 +0600 Subject: [PATCH 04/14] refactor: epub chapter selection --- .../EpubReaderChapterSelectionActivity.cpp | 45 ++++++++---------- .../EpubReaderChapterSelectionActivity.h | 2 + src/util/ButtonNavigator.cpp | 47 ++++++++++++++++++- src/util/ButtonNavigator.h | 7 +++ 4 files changed, 75 insertions(+), 26 deletions(-) diff --git a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp index 1b35e143..3ead68ca 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/util/ButtonNavigator.cpp b/src/util/ButtonNavigator.cpp index f7006ca5..531722ca 100644 --- a/src/util/ButtonNavigator.cpp +++ b/src/util/ButtonNavigator.cpp @@ -15,6 +15,10 @@ void ButtonNavigator::onNextPress(const Callback& callback) { onPress(getNextBut void ButtonNavigator::onPreviousPress(const Callback& callback) { onPress(getPreviousButtons(), callback); } +void ButtonNavigator::onNextRelease(const Callback& callback) const { onRelease(getNextButtons(), callback); } + +void ButtonNavigator::onPreviousRelease(const Callback& callback) const { onRelease(getPreviousButtons(), callback); } + void ButtonNavigator::onNextContinuous(const Callback& callback) { onContinuous(getNextButtons(), callback); } void ButtonNavigator::onPreviousContinuous(const Callback& callback) { onContinuous(getPreviousButtons(), callback); } @@ -34,6 +38,21 @@ void ButtonNavigator::onPress(const Buttons& buttons, const Callback& callback) callback(); } } +void ButtonNavigator::onRelease(const Buttons& buttons, const Callback& callback) const { + if (!mappedInput) return; + + bool buttonReleased = false; + for (const MappedInputManager::Button button : buttons) { + if (mappedInput->wasReleased(button)) { + buttonReleased = true; + break; + } + } + + if (buttonReleased && !recentlyNavigatedContinuously()) { + callback(); + } +} void ButtonNavigator::onContinuous(const Buttons& buttons, const Callback& callback) { if (!mappedInput) return; @@ -82,4 +101,30 @@ int ButtonNavigator::previousIndex(const int currentIndex, const int totalItems) // Calculate the previous index with wrap-around return (currentIndex + totalItems - 1) % totalItems; -} \ No newline at end of file +} + +int ButtonNavigator::nextPageIndex(const int currentIndex, const int totalItems, const int itemsPerPage) { + if (totalItems <= 0 || itemsPerPage <= 0) return 0; + + 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; + + 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 index 14a1d928..0b08d85c 100644 --- a/src/util/ButtonNavigator.h +++ b/src/util/ButtonNavigator.h @@ -38,10 +38,17 @@ class ButtonNavigator final { void onPreviousPress(const Callback& callback); void onPress(const Buttons& buttons, const Callback& callback); + void onNextRelease(const Callback& callback) const; + void onPreviousRelease(const Callback& callback) const; + void onRelease(const Buttons& buttons, const Callback& callback) const; + 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); }; \ No newline at end of file From b7b36f02e252c2c1a195f30958b2d1ec9940ba0c Mon Sep 17 00:00:00 2001 From: Istiak Tridip <13367189+istiak-tridip@users.noreply.github.com> Date: Wed, 28 Jan 2026 18:28:46 +0600 Subject: [PATCH 05/14] fix: button navigator triggers --- src/util/ButtonNavigator.cpp | 51 +++++++++++------------------------- src/util/ButtonNavigator.h | 8 +++--- 2 files changed, 20 insertions(+), 39 deletions(-) diff --git a/src/util/ButtonNavigator.cpp b/src/util/ButtonNavigator.cpp index 531722ca..c041aa1d 100644 --- a/src/util/ButtonNavigator.cpp +++ b/src/util/ButtonNavigator.cpp @@ -15,9 +15,9 @@ void ButtonNavigator::onNextPress(const Callback& callback) { onPress(getNextBut void ButtonNavigator::onPreviousPress(const Callback& callback) { onPress(getPreviousButtons(), callback); } -void ButtonNavigator::onNextRelease(const Callback& callback) const { onRelease(getNextButtons(), callback); } +void ButtonNavigator::onNextRelease(const Callback& callback) { onRelease(getNextButtons(), callback); } -void ButtonNavigator::onPreviousRelease(const Callback& callback) const { onRelease(getPreviousButtons(), callback); } +void ButtonNavigator::onPreviousRelease(const Callback& callback) { onRelease(getPreviousButtons(), callback); } void ButtonNavigator::onNextContinuous(const Callback& callback) { onContinuous(getNextButtons(), callback); } @@ -26,49 +26,39 @@ void ButtonNavigator::onPreviousContinuous(const Callback& callback) { onContinu void ButtonNavigator::onPress(const Buttons& buttons, const Callback& callback) { if (!mappedInput) return; - bool buttonPressed = false; for (const MappedInputManager::Button button : buttons) { if (mappedInput->wasPressed(button)) { - buttonPressed = true; - break; + callback(); + return; } } - - if (buttonPressed && !recentlyNavigatedContinuously()) { - callback(); - } } -void ButtonNavigator::onRelease(const Buttons& buttons, const Callback& callback) const { + +void ButtonNavigator::onRelease(const Buttons& buttons, const Callback& callback) { if (!mappedInput) return; - bool buttonReleased = false; for (const MappedInputManager::Button button : buttons) { if (mappedInput->wasReleased(button)) { - buttonReleased = true; - break; - } - } + if (lastContinuousNavTime == 0) { + callback(); + } - if (buttonReleased && !recentlyNavigatedContinuously()) { - callback(); + lastContinuousNavTime = 0; + return; + } } } void ButtonNavigator::onContinuous(const Buttons& buttons, const Callback& callback) { if (!mappedInput) return; - bool buttonPressed = false; for (const MappedInputManager::Button button : buttons) { - if (mappedInput->isPressed(button)) { - buttonPressed = true; - break; + if (mappedInput->isPressed(button) && shouldNavigateContinuously()) { + callback(); + lastContinuousNavTime = millis(); + return; } } - - if (buttonPressed && shouldNavigateContinuously()) { - callback(); - lastContinuousNavTime = millis(); - } } bool ButtonNavigator::shouldNavigateContinuously() const { @@ -80,15 +70,6 @@ bool ButtonNavigator::shouldNavigateContinuously() const { return buttonHeldLongEnough && navigationIntervalElapsed; } -bool ButtonNavigator::recentlyNavigatedContinuously() const { - const int elapsedTime = millis() - lastContinuousNavTime; - if (elapsedTime < 50) { - return true; - } - - return false; -} - int ButtonNavigator::nextIndex(const int currentIndex, const int totalItems) { if (totalItems <= 0) return 0; diff --git a/src/util/ButtonNavigator.h b/src/util/ButtonNavigator.h index 0b08d85c..3e88f315 100644 --- a/src/util/ButtonNavigator.h +++ b/src/util/ButtonNavigator.h @@ -12,11 +12,11 @@ class ButtonNavigator final { const uint16_t continuousStartMs; const uint16_t continuousIntervalMs; uint32_t lastContinuousNavTime = 0; + bool continuousNavHold = false; static const MappedInputManager* mappedInput; [[nodiscard]] bool shouldNavigateContinuously() const; - [[nodiscard]] bool recentlyNavigatedContinuously() const; [[nodiscard]] static Buttons getNextButtons() { return {MappedInputManager::Button::Down, MappedInputManager::Button::Right}; @@ -38,9 +38,9 @@ class ButtonNavigator final { void onPreviousPress(const Callback& callback); void onPress(const Buttons& buttons, const Callback& callback); - void onNextRelease(const Callback& callback) const; - void onPreviousRelease(const Callback& callback) const; - void onRelease(const Buttons& buttons, const Callback& callback) const; + 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); From 3d9c3bf101222b8ba29114283f021c67a3aa552e Mon Sep 17 00:00:00 2001 From: Istiak Tridip <13367189+istiak-tridip@users.noreply.github.com> Date: Wed, 28 Jan 2026 22:07:34 +0600 Subject: [PATCH 06/14] refactor: xtc chapter selection --- .../XtcReaderChapterSelectionActivity.cpp | 53 ++++++++----------- .../XtcReaderChapterSelectionActivity.h | 2 + 2 files changed, 23 insertions(+), 32 deletions(-) diff --git a/src/activities/reader/XtcReaderChapterSelectionActivity.cpp b/src/activities/reader/XtcReaderChapterSelectionActivity.cpp index b2cfecaa..6ef6c796 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; From bde92c288f071e5024e007b8017e4eb84dddf2b8 Mon Sep 17 00:00:00 2001 From: Istiak Tridip <13367189+istiak-tridip@users.noreply.github.com> Date: Wed, 28 Jan 2026 22:07:48 +0600 Subject: [PATCH 07/14] refactor: opds activity --- .../browser/OpdsBookBrowserActivity.cpp | 44 ++++++++++--------- .../browser/OpdsBookBrowserActivity.h | 2 + 2 files changed, 25 insertions(+), 21 deletions(-) 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; From 3c727dd8f948db31d22a0d8b6674b3d3290ef501 Mon Sep 17 00:00:00 2001 From: Istiak Tridip <13367189+istiak-tridip@users.noreply.github.com> Date: Wed, 28 Jan 2026 22:46:25 +0600 Subject: [PATCH 08/14] refactor: my library activity --- src/activities/home/MyLibraryActivity.cpp | 39 +++++++++++------------ src/activities/home/MyLibraryActivity.h | 2 ++ 2 files changed, 21 insertions(+), 20 deletions(-) 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; From 91313d8505c63d93ff4dddede4df5e8807f16222 Mon Sep 17 00:00:00 2001 From: Istiak Tridip <13367189+istiak-tridip@users.noreply.github.com> Date: Wed, 28 Jan 2026 22:55:58 +0600 Subject: [PATCH 09/14] refactor: wifi selection activity --- .../network/WifiSelectionActivity.cpp | 24 ++++++++----------- .../network/WifiSelectionActivity.h | 2 ++ 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/activities/network/WifiSelectionActivity.cpp b/src/activities/network/WifiSelectionActivity.cpp index 5c45223b..8b717583 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; From a6b4ed5cf92333b67eae085be8b310261e9c07c4 Mon Sep 17 00:00:00 2001 From: Istiak Tridip <13367189+istiak-tridip@users.noreply.github.com> Date: Thu, 29 Jan 2026 00:43:22 +0600 Subject: [PATCH 10/14] refaxtor: keyboard activity --- src/activities/util/KeyboardEntryActivity.cpp | 87 +++++++++---------- src/activities/util/KeyboardEntryActivity.h | 2 + 2 files changed, 41 insertions(+), 48 deletions(-) diff --git a/src/activities/util/KeyboardEntryActivity.cpp b/src/activities/util/KeyboardEntryActivity.cpp index 3a6befac..828faaae 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 + const auto upButtonCallback = [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)) { + const auto downButtonCallback = [this] { + selectedRow = ButtonNavigator::nextIndex(selectedRow, NUM_ROWS); + + const int maxCol = getRowLength(selectedRow) - 1; + if (selectedCol > maxCol) selectedCol = maxCol; + updateRequired = true; + }; + + const auto leftButtonCallback = [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; + }; + + const auto rightButtonCallback = [this] { const int maxCol = getRowLength(selectedRow) - 1; // Special bottom row case @@ -219,18 +200,28 @@ 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; - } + }; + + constexpr auto upButton = MappedInputManager::Button::Up; + constexpr auto downButton = MappedInputManager::Button::Down; + constexpr auto leftButton = MappedInputManager::Button::Left; + constexpr auto rightButton = MappedInputManager::Button::Right; + + buttonNavigator.onPress({upButton}, upButtonCallback); + buttonNavigator.onContinuous({upButton}, upButtonCallback); + + buttonNavigator.onPress({downButton}, downButtonCallback); + buttonNavigator.onContinuous({downButton}, downButtonCallback); + + buttonNavigator.onPress({leftButton}, leftButtonCallback); + buttonNavigator.onContinuous({leftButton}, leftButtonCallback); + + buttonNavigator.onPress({rightButton}, rightButtonCallback); + buttonNavigator.onContinuous({rightButton}, rightButtonCallback); // 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 From fe119239ad2cbafcd6a2037fb7ec68f8914b07b5 Mon Sep 17 00:00:00 2001 From: Istiak Tridip <13367189+istiak-tridip@users.noreply.github.com> Date: Thu, 29 Jan 2026 02:33:56 +0600 Subject: [PATCH 11/14] feat: `ButtonNavigator::onPressAndContinuous` helper function --- src/activities/util/KeyboardEntryActivity.cpp | 33 +++++-------------- src/util/ButtonNavigator.cpp | 6 ++++ src/util/ButtonNavigator.h | 17 +++++----- 3 files changed, 22 insertions(+), 34 deletions(-) diff --git a/src/activities/util/KeyboardEntryActivity.cpp b/src/activities/util/KeyboardEntryActivity.cpp index 828faaae..301e67b0 100644 --- a/src/activities/util/KeyboardEntryActivity.cpp +++ b/src/activities/util/KeyboardEntryActivity.cpp @@ -139,23 +139,23 @@ void KeyboardEntryActivity::handleKeyPress() { void KeyboardEntryActivity::loop() { // Handle navigation - const auto upButtonCallback = [this] { + buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Up}, [this] { selectedRow = ButtonNavigator::previousIndex(selectedRow, NUM_ROWS); const int maxCol = getRowLength(selectedRow) - 1; if (selectedCol > maxCol) selectedCol = maxCol; updateRequired = true; - }; + }); - const auto downButtonCallback = [this] { + 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; - }; + }); - const auto leftButtonCallback = [this] { + buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Left}, [this] { const int maxCol = getRowLength(selectedRow) - 1; // Special bottom row case @@ -179,9 +179,9 @@ void KeyboardEntryActivity::loop() { } updateRequired = true; - }; + }); - const auto rightButtonCallback = [this] { + buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Right}, [this] { const int maxCol = getRowLength(selectedRow) - 1; // Special bottom row case @@ -204,24 +204,7 @@ void KeyboardEntryActivity::loop() { selectedCol = ButtonNavigator::nextIndex(selectedCol, maxCol + 1); } updateRequired = true; - }; - - constexpr auto upButton = MappedInputManager::Button::Up; - constexpr auto downButton = MappedInputManager::Button::Down; - constexpr auto leftButton = MappedInputManager::Button::Left; - constexpr auto rightButton = MappedInputManager::Button::Right; - - buttonNavigator.onPress({upButton}, upButtonCallback); - buttonNavigator.onContinuous({upButton}, upButtonCallback); - - buttonNavigator.onPress({downButton}, downButtonCallback); - buttonNavigator.onContinuous({downButton}, downButtonCallback); - - buttonNavigator.onPress({leftButton}, leftButtonCallback); - buttonNavigator.onContinuous({leftButton}, leftButtonCallback); - - buttonNavigator.onPress({rightButton}, rightButtonCallback); - buttonNavigator.onContinuous({rightButton}, rightButtonCallback); + }); // Selection if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { diff --git a/src/util/ButtonNavigator.cpp b/src/util/ButtonNavigator.cpp index c041aa1d..f79229dd 100644 --- a/src/util/ButtonNavigator.cpp +++ b/src/util/ButtonNavigator.cpp @@ -6,11 +6,17 @@ 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); } diff --git a/src/util/ButtonNavigator.h b/src/util/ButtonNavigator.h index 3e88f315..2f9afbc1 100644 --- a/src/util/ButtonNavigator.h +++ b/src/util/ButtonNavigator.h @@ -12,19 +12,10 @@ class ButtonNavigator final { const uint16_t continuousStartMs; const uint16_t continuousIntervalMs; uint32_t lastContinuousNavTime = 0; - bool continuousNavHold = false; - static const MappedInputManager* mappedInput; [[nodiscard]] bool shouldNavigateContinuously() const; - [[nodiscard]] static Buttons getNextButtons() { - return {MappedInputManager::Button::Down, MappedInputManager::Button::Right}; - } - [[nodiscard]] static Buttons getPreviousButtons() { - return {MappedInputManager::Button::Up, MappedInputManager::Button::Left}; - } - public: explicit ButtonNavigator(const uint16_t continuousIntervalMs = 500, const uint16_t continuousStartMs = 500) : continuousStartMs(continuousStartMs), continuousIntervalMs(continuousIntervalMs) {} @@ -33,6 +24,7 @@ class ButtonNavigator final { 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); @@ -51,4 +43,11 @@ class ButtonNavigator final { [[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 From cebfc54266ae41b1580c9b7cecef4730f7ff0cd3 Mon Sep 17 00:00:00 2001 From: Istiak Tridip <13367189+istiak-tridip@users.noreply.github.com> Date: Thu, 29 Jan 2026 03:48:24 +0600 Subject: [PATCH 12/14] refactor: file transfer activity --- .../network/NetworkModeSelectionActivity.cpp | 17 +++++++---------- .../network/NetworkModeSelectionActivity.h | 3 +++ 2 files changed, 10 insertions(+), 10 deletions(-) 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; From 45bba9a8225ad644967aad579cf1204a87d2350d Mon Sep 17 00:00:00 2001 From: Istiak Tridip <13367189+istiak-tridip@users.noreply.github.com> Date: Thu, 29 Jan 2026 04:07:47 +0600 Subject: [PATCH 13/14] fix: replace for loop with `std::any_of` --- src/util/ButtonNavigator.cpp | 41 +++++++++++++++++------------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/src/util/ButtonNavigator.cpp b/src/util/ButtonNavigator.cpp index f79229dd..585de13f 100644 --- a/src/util/ButtonNavigator.cpp +++ b/src/util/ButtonNavigator.cpp @@ -30,40 +30,37 @@ void ButtonNavigator::onNextContinuous(const Callback& callback) { onContinuous( void ButtonNavigator::onPreviousContinuous(const Callback& callback) { onContinuous(getPreviousButtons(), callback); } void ButtonNavigator::onPress(const Buttons& buttons, const Callback& callback) { - if (!mappedInput) return; + const bool wasPressed = std::any_of(buttons.begin(), buttons.end(), [](const MappedInputManager::Button button) { + return mappedInput != nullptr && mappedInput->wasPressed(button); + }); - for (const MappedInputManager::Button button : buttons) { - if (mappedInput->wasPressed(button)) { - callback(); - return; - } + if (wasPressed) { + callback(); } } void ButtonNavigator::onRelease(const Buttons& buttons, const Callback& callback) { - if (!mappedInput) return; + const bool wasReleased = std::any_of(buttons.begin(), buttons.end(), [](const MappedInputManager::Button button) { + return mappedInput != nullptr && mappedInput->wasReleased(button); + }); - for (const MappedInputManager::Button button : buttons) { - if (mappedInput->wasReleased(button)) { - if (lastContinuousNavTime == 0) { - callback(); - } - - lastContinuousNavTime = 0; - return; + if (wasReleased) { + if (lastContinuousNavTime == 0) { + callback(); } + + lastContinuousNavTime = 0; } } void ButtonNavigator::onContinuous(const Buttons& buttons, const Callback& callback) { - if (!mappedInput) return; + const bool isPressed = std::any_of(buttons.begin(), buttons.end(), [this](const MappedInputManager::Button button) { + return mappedInput != nullptr && mappedInput->isPressed(button) && shouldNavigateContinuously(); + }); - for (const MappedInputManager::Button button : buttons) { - if (mappedInput->isPressed(button) && shouldNavigateContinuously()) { - callback(); - lastContinuousNavTime = millis(); - return; - } + if (isPressed) { + callback(); + lastContinuousNavTime = millis(); } } From 66e69e750e900a7e196e6dec1abc0d601699e7bc Mon Sep 17 00:00:00 2001 From: Istiak Tridip <13367189+istiak-tridip@users.noreply.github.com> Date: Thu, 29 Jan 2026 20:41:59 +0600 Subject: [PATCH 14/14] feat: fall back to index navigation when items fit on one page --- src/util/ButtonNavigator.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/util/ButtonNavigator.cpp b/src/util/ButtonNavigator.cpp index 585de13f..d9844138 100644 --- a/src/util/ButtonNavigator.cpp +++ b/src/util/ButtonNavigator.cpp @@ -90,6 +90,11 @@ int ButtonNavigator::previousIndex(const int currentIndex, const int 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; @@ -103,6 +108,11 @@ int ButtonNavigator::nextPageIndex(const int currentIndex, const int totalItems, 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;