refactor: implement state machine for button press detection in MyLibraryActivity

Button Behavior Matrix:
┌─────────────────┬──────────────┬──────────────────┐
│ Action          │ Condition    │ Timing           │
├─────────────────┼──────────────┼──────────────────┤
│ Switch Tab      │ Long press   │ ≥450ms hold      │
│ Skip Page       │ Double press │ <120ms between   │
│                 │              │ 1st PRESS &      │
│                 │              │ 2nd RELEASE      │
│ Move Item       │ Short press  │ <450ms press     │
└─────────────────┴──────────────┴──────────────────┘

Key Fixes:
- Each action has clear, exclusive condition
This commit is contained in:
pablohc 2026-01-31 00:54:47 +01:00
parent ebcd813ff6
commit 38b17ec95d
2 changed files with 145 additions and 38 deletions

View File

@ -19,9 +19,10 @@ constexpr int LINE_HEIGHT = 30;
constexpr int LEFT_MARGIN = 20; constexpr int LEFT_MARGIN = 20;
constexpr int RIGHT_MARGIN = 40; // Extra space for scroll indicator constexpr int RIGHT_MARGIN = 40; // Extra space for scroll indicator
// Timing thresholds // Timing thresholds for button behavior
constexpr int SKIP_PAGE_MS = 700; constexpr int LONG_PRESS_MS = 450; // Long press: change tab
constexpr unsigned long GO_HOME_MS = 1000; constexpr int DOUBLE_PRESS_MS = 120; // Double press: skip page
constexpr unsigned long GO_HOME_MS = 1000; // Long press back: go to root
void sortFileList(std::vector<std::string>& strs) { void sortFileList(std::vector<std::string>& strs) {
std::sort(begin(strs), end(strs), [](const std::string& str1, const std::string& str2) { std::sort(begin(strs), end(strs), [](const std::string& str1, const std::string& str2) {
@ -143,6 +144,51 @@ void MyLibraryActivity::taskTrampoline(void* param) {
self->displayTaskLoop(); self->displayTaskLoop();
} }
// Action execution: Move one item (short press timeout)
void MyLibraryActivity::executeMoveItem(bool isPrevButton) {
const int itemCount = getCurrentItemCount();
if (itemCount > 0) {
if (isPrevButton) {
selectorIndex = (selectorIndex + itemCount - 1) % itemCount;
} else {
selectorIndex = (selectorIndex + 1) % itemCount;
}
}
}
// Action execution: Skip page (double press)
void MyLibraryActivity::executeSkipPage(bool isPrevButton) {
const int itemCount = getCurrentItemCount();
const int pageItems = getPageItems();
if (itemCount > 0) {
if (isPrevButton) {
int targetPage = (selectorIndex / pageItems) - 1;
if (targetPage < 0) {
targetPage = ((itemCount - 1) / pageItems);
}
selectorIndex = targetPage * pageItems;
} else {
int targetPage = (selectorIndex / pageItems) + 1;
int maxPage = (itemCount - 1) / pageItems;
if (targetPage > maxPage) {
targetPage = 0;
}
selectorIndex = targetPage * pageItems;
}
}
}
// Action execution: Switch tab (long press)
void MyLibraryActivity::executeSwitchTab(bool isPrevButton) {
if (isPrevButton && currentTab == Tab::Files) {
currentTab = Tab::Recent;
selectorIndex = 0;
} else if (!isPrevButton && currentTab == Tab::Recent) {
currentTab = Tab::Files;
selectorIndex = 0;
}
}
void MyLibraryActivity::onEnter() { void MyLibraryActivity::onEnter() {
Activity::onEnter(); Activity::onEnter();
@ -185,6 +231,9 @@ void MyLibraryActivity::loop() {
const int itemCount = getCurrentItemCount(); const int itemCount = getCurrentItemCount();
const int pageItems = getPageItems(); const int pageItems = getPageItems();
// Get current time for all timing operations
unsigned long currentTime = millis();
// Long press BACK (1s+) in Files tab goes to root folder // Long press BACK (1s+) in Files tab goes to root folder
if (currentTab == Tab::Files && mappedInput.isPressed(MappedInputManager::Button::Back) && if (currentTab == Tab::Files && mappedInput.isPressed(MappedInputManager::Button::Back) &&
mappedInput.getHeldTime() >= GO_HOME_MS) { mappedInput.getHeldTime() >= GO_HOME_MS) {
@ -197,13 +246,6 @@ void MyLibraryActivity::loop() {
return; 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 // Confirm button - open selected item
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
if (currentTab == Tab::Recent) { if (currentTab == Tab::Recent) {
@ -253,38 +295,84 @@ void MyLibraryActivity::loop() {
return; return;
} }
// Tab switching: Left/Right always control tabs // Navigation buttons (UP/LEFT and DOWN/RIGHT have same behavior)
if (leftReleased && currentTab == Tab::Files) { const bool upPressed = mappedInput.isPressed(MappedInputManager::Button::Up);
currentTab = Tab::Recent; const bool leftPressed = mappedInput.isPressed(MappedInputManager::Button::Left);
selectorIndex = 0; const bool downPressed = mappedInput.isPressed(MappedInputManager::Button::Down);
updateRequired = true; const bool rightPressed = mappedInput.isPressed(MappedInputManager::Button::Right);
return; const bool upReleased = mappedInput.wasReleased(MappedInputManager::Button::Up);
const bool leftReleased = mappedInput.wasReleased(MappedInputManager::Button::Left);
const bool downReleased = mappedInput.wasReleased(MappedInputManager::Button::Down);
const bool rightReleased = mappedInput.wasReleased(MappedInputManager::Button::Right);
// Navigation: UP/LEFT move backward, DOWN/RIGHT move forward
const bool prevPressed = upPressed || leftPressed;
const bool nextPressed = downPressed || rightPressed;
const bool prevReleased = upReleased || leftReleased;
const bool nextReleased = downReleased || rightReleased;
// State machine for button press detection
// ==========================================
// IDLE: Wait for first press
if (buttonState == ButtonState::Idle) {
if (prevPressed || nextPressed) {
buttonState = ButtonState::FirstPress;
firstPressTime = currentTime;
isPrevButtonPressed = prevPressed;
} }
if (rightReleased && currentTab == Tab::Recent) { }
currentTab = Tab::Files; // FIRST_PRESS: Button is held, check for long press or release
selectorIndex = 0; else if (buttonState == ButtonState::FirstPress) {
const unsigned long holdDuration = currentTime - firstPressTime;
// Check for long press (>=450ms) - switch tab
if (holdDuration >= LONG_PRESS_MS) {
executeSwitchTab(isPrevButtonPressed);
buttonState = ButtonState::WaitingForReleaseAfterLongPress;
updateRequired = true; updateRequired = true;
return; return;
} }
// Navigation: Up/Down moves through items only // Check for release (<450ms) - transition to waiting for second press
const bool prevReleased = upReleased; if ((isPrevButtonPressed && prevReleased) || (!isPrevButtonPressed && nextReleased)) {
const bool nextReleased = downReleased; buttonState = ButtonState::WaitingForSecondPress;
firstReleaseTime = currentTime;
}
}
// WAITING_FOR_SECOND_PRESS: First button released, waiting for second press
else if (buttonState == ButtonState::WaitingForSecondPress) {
const unsigned long waitDuration = currentTime - firstReleaseTime;
if (prevReleased && itemCount > 0) { // Timeout (>=120ms without second press) - execute move_item
if (skipPage) { if (waitDuration >= DOUBLE_PRESS_MS) {
selectorIndex = ((selectorIndex / pageItems - 1) * pageItems + itemCount) % itemCount; executeMoveItem(isPrevButtonPressed);
} else { buttonState = ButtonState::Idle;
selectorIndex = (selectorIndex + itemCount - 1) % itemCount;
}
updateRequired = true; updateRequired = true;
} else if (nextReleased && itemCount > 0) { return;
if (skipPage) {
selectorIndex = ((selectorIndex / pageItems + 1) * pageItems) % itemCount;
} else {
selectorIndex = (selectorIndex + 1) % itemCount;
} }
// Second press detected (<120ms) - double press
if (prevPressed || nextPressed) {
buttonState = ButtonState::DoublePressDetected;
}
}
// DOUBLE_PRESS_DETECTED: Second press detected, wait for release
else if (buttonState == ButtonState::DoublePressDetected) {
// Wait for second button release
if (prevReleased || nextReleased) {
executeSkipPage(isPrevButtonPressed);
buttonState = ButtonState::Idle;
updateRequired = true; updateRequired = true;
return;
}
}
// WAITING_FOR_RELEASE_AFTER_LONG_PRESS: Ignore release after long press
else if (buttonState == ButtonState::WaitingForReleaseAfterLongPress) {
// Wait for button to be released, then go back to idle
if ((isPrevButtonPressed && prevReleased) || (!isPrevButtonPressed && nextReleased)) {
buttonState = ButtonState::Idle;
}
} }
} }

View File

@ -21,6 +21,25 @@ class MyLibraryActivity final : public Activity {
int selectorIndex = 0; int selectorIndex = 0;
bool updateRequired = false; bool updateRequired = false;
// State machine for button press detection
enum class ButtonState {
Idle,
FirstPress,
WaitingForSecondPress,
DoublePressDetected,
WaitingForReleaseAfterLongPress
};
ButtonState buttonState = ButtonState::Idle;
unsigned long firstPressTime = 0; // Time of first PRESS
unsigned long firstReleaseTime = 0; // Time of first RELEASE
bool isPrevButtonPressed = false; // Which button was pressed first
// Action execution functions
void executeMoveItem(bool isPrevButton);
void executeSkipPage(bool isPrevButton);
void executeSwitchTab(bool isPrevButton);
// Recent tab state // Recent tab state
std::vector<std::string> bookTitles; // Display titles for each book std::vector<std::string> bookTitles; // Display titles for each book
std::vector<std::string> bookPaths; // Paths for each visible book (excludes missing) std::vector<std::string> bookPaths; // Paths for each visible book (excludes missing)