feat: unify navigation handling with system-wide continuous navigation (#600)
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 🙏  --- 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 <dave@daveallie.com>
This commit is contained in:
124
src/util/ButtonNavigator.cpp
Normal file
124
src/util/ButtonNavigator.cpp
Normal file
@@ -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;
|
||||
}
|
||||
53
src/util/ButtonNavigator.h
Normal file
53
src/util/ButtonNavigator.h
Normal file
@@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
#include "MappedInputManager.h"
|
||||
|
||||
class ButtonNavigator final {
|
||||
using Callback = std::function<void()>;
|
||||
using Buttons = std::vector<MappedInputManager::Button>;
|
||||
|
||||
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};
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user