Skip to content

feat: add held back-button routing#138

Open
asturur wants to merge 1 commit into
mainfrom
hold-back-hub
Open

feat: add held back-button routing#138
asturur wants to merge 1 commit into
mainfrom
hold-back-hub

Conversation

@asturur

@asturur asturur commented May 20, 2026

Copy link
Copy Markdown
Member

Summary

  • split Back into short-release cancel vs long-release cancel_hold
  • route long-release Back straight to Hub, or to quit confirm when already on Hub
  • add navigation coverage for the new Back press/release behavior

Testing

  • Not run locally here (Qt/QML toolchain unavailable on this machine)

Summary by CodeRabbit

Release Notes

  • New Features
    • Added cancel/hold input handling: holding the cancel button (Escape) for 1 second now triggers a dedicated hold action, providing distinct behavior from a quick press.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 20, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The PR introduces a hold-to-action state machine for the Escape key: pressing Escape arms a 1-second timer; releasing before timeout triggers cancel, after timeout triggers synthetic cancel_hold (routing to hub). New _closeAllModals() consolidates modal cleanup, and tests validate press/release sequences and hold thresholds across game, system, and hub screens.

Changes

Cancel-hold feature

Layer / File(s) Summary
Action constant definition
rust/zaparoo-core/src/input_actions.rs
New public CANCEL_HOLD constant identifies the synthetic action dispatched after holding cancel for 1 second.
Modal cleanup consolidation
src/ui/app/Main.qml
New _closeAllModals() helper resets all modal visibility, context-menu state, list-picker state, and discover-menu state; _forceHub() now calls it before routing to hub or opening quit confirmation.
Cancel-hold state machine core
src/ui/app/Main.qml
Implements hold-detection via _cancelPending, _cancelHoldReady, _cancelHeldKey properties, a cancelHoldTimer that fires after _cancelHoldMs, _armCancelHold() and _stopCancelHold() handlers, modified handleKey() to defer cancel dispatch until release, updated handleKeyRelease() to dispatch either cancel_hold or cancel based on timer readiness, and cleanup on window inactive.
Cancel-hold action routing
src/ui/app/Main.qml
New handleAction() handler for cancel_hold invokes _forceHub() to apply modal cleanup and hub navigation.
Navigation and hold-state tests
tests/ui/tst_navigation.qml
Test init now resets cancel-hold state, existing back-navigation tests updated to exercise handleKeyRelease() for Escape and Backspace, and three new tests validate hold-to-navigate (games → hub) and hold-to-quit-confirm (hub → quitConfirmModal) with proper state assertions and release-side no-ops.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • ZaparooProject/zaparoo-launcher#25: Both PRs modify src/ui/app/Main.qml input handling for Escape key; the referenced PR suppresses auto-repeated key presses, while this PR adds timed hold-detection and a synthetic cancel_hold action.

Poem

🐰 A hold so true, a second waits,
Escape key guards the hub's gates,
Press it fast for swift retreat,
Hold it long for final beat,
State machines and timers aligned—
Cancel-hold, so well designed! 🎮

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding a held back-button routing feature.
Description check ✅ Passed The description covers Summary, Testing, and checklist items, but lacks Motivation, Screenshots/recordings, and detailed test plan sections from the template.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch hold-back-hub

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/ui/app/Main.qml`:
- Around line 1599-1608: The timer currently only sets root._cancelHoldReady but
tests expect navigation immediately when the hold threshold passes; update the
Timer (cancelHoldTimer) onTriggered handler to not only set
root._cancelHoldReady but also invoke the same action/path used on key release
to perform navigation (i.e., call the existing cancel/release handler that the
key-release path uses) when root._cancelPending is true so navigation occurs
while the key is still held.

In `@tests/ui/tst_navigation.qml`:
- Around line 135-152: The tests expect the cancel-hold behavior to execute when
the cancelHoldTimer fires, so update the cancelHoldTimer handler in Main.qml to
perform the actual navigation/modal open (same logic currently in
handleKeyRelease) instead of only setting _cancelHoldReady; specifically, call
the code that routes to screenHub or sets quitConfirmModalVisible, clear/disable
_cancelHoldPending/_cancelHoldReady and stop the timer, and ensure
handleKeyRelease checks those flags and returns early (no-op) if the timer
already executed to avoid double actions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d6ac5eb4-b87d-420e-ac84-bc77d2bc3dbe

📥 Commits

Reviewing files that changed from the base of the PR and between d27a465 and 169207a.

📒 Files selected for processing (3)
  • rust/zaparoo-core/src/input_actions.rs
  • src/ui/app/Main.qml
  • tests/ui/tst_navigation.qml

Comment thread src/ui/app/Main.qml
Comment on lines +1599 to +1608
Timer {
id: cancelHoldTimer
interval: root._cancelHoldMs
repeat: false
onTriggered: {
if (!root._cancelPending)
return;
root._cancelHoldReady = true;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Timer only sets flag—tests expect navigation on timer fire, not on release.

The timer's onTriggered sets _cancelHoldReady = true but doesn't dispatch the action. The tests (test_cancel_hold_from_games_routes_directly_to_hub, test_cancel_hold_on_hub_opens_quit_confirm) use tryCompare before calling handleKeyRelease, expecting navigation to occur when the timer fires. With the current implementation, navigation only happens on key release.

If the intended UX is "navigate when hold threshold passes (key still held)," the timer should dispatch the action:

Proposed fix for timer-triggered dispatch
 Timer {
     id: cancelHoldTimer
     interval: root._cancelHoldMs
     repeat: false
     onTriggered: {
         if (!root._cancelPending)
             return;
         root._cancelHoldReady = true;
+        root._stopCancelHold();
+        root.handleAction("cancel_hold");
     }
 }

Alternatively, if the intended UX is "navigate on release after holding 1 second," the tests need restructuring to call handleKeyRelease and use a wait() or tryCompare on a different property.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ui/app/Main.qml` around lines 1599 - 1608, The timer currently only sets
root._cancelHoldReady but tests expect navigation immediately when the hold
threshold passes; update the Timer (cancelHoldTimer) onTriggered handler to not
only set root._cancelHoldReady but also invoke the same action/path used on key
release to perform navigation (i.e., call the existing cancel/release handler
that the key-release path uses) when root._cancelPending is true so navigation
occurs while the key is still held.

Comment on lines +135 to +152
function test_cancel_hold_from_games_routes_directly_to_hub(): void {
main.activeScreen = main.screenGames;
main.handleKey(Qt.Key_Escape);
tryCompare(main, "activeScreen", main.screenHub);
compare(main._cancelHoldPending, false, "Hold timer must clear after firing");
main.handleKeyRelease(Qt.Key_Escape);
compare(main.activeScreen, main.screenHub, "Release after long-hold must not fire a second cancel");
}

function test_cancel_hold_on_hub_opens_quit_confirm(): void {
main.activeScreen = main.screenHub;
main.handleKey(Qt.Key_Escape);
tryCompare(main, "quitConfirmModalVisible", true);
compare(main.activeScreen, main.screenHub, "Long-hold Back on Hub must stay on Hub and ask for quit");
compare(main._cancelHoldPending, false, "Hold timer must clear after opening quit confirm");
main.handleKeyRelease(Qt.Key_Escape);
compare(main.quitConfirmModalVisible, true, "Release after long-hold must not close the quit confirm");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Tests expect timer-triggered navigation but implementation dispatches on release.

Both test_cancel_hold_from_games_routes_directly_to_hub and test_cancel_hold_on_hub_opens_quit_confirm call tryCompare immediately after handleKey(Qt.Key_Escape), before any handleKeyRelease. This expects the timer's onTriggered to navigate/open the modal. However, the current cancelHoldTimer only sets _cancelHoldReady = true—the actual action dispatch happens in handleKeyRelease.

These tests will timeout because the screen/modal state never changes until release.

If the fix is applied to cancelHoldTimer (dispatching on timer fire), these tests are correct. If the intended behavior is release-triggered, restructure:

Alternative test structure for release-triggered behavior
 function test_cancel_hold_from_games_routes_directly_to_hub(): void {
     main.activeScreen = main.screenGames;
     main.handleKey(Qt.Key_Escape);
+    wait(root._cancelHoldMs + 50);  // Wait past hold threshold
+    compare(main.activeScreen, main.screenGames, "Must not navigate while key is still held");
+    main.handleKeyRelease(Qt.Key_Escape);
-    tryCompare(main, "activeScreen", main.screenHub);
+    compare(main.activeScreen, main.screenHub);
     compare(main._cancelHoldPending, false, "Hold timer must clear after firing");
-    main.handleKeyRelease(Qt.Key_Escape);
-    compare(main.activeScreen, main.screenHub, "Release after long-hold must not fire a second cancel");
 }

This comment is related to the cancelHoldTimer issue flagged in Main.qml.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/ui/tst_navigation.qml` around lines 135 - 152, The tests expect the
cancel-hold behavior to execute when the cancelHoldTimer fires, so update the
cancelHoldTimer handler in Main.qml to perform the actual navigation/modal open
(same logic currently in handleKeyRelease) instead of only setting
_cancelHoldReady; specifically, call the code that routes to screenHub or sets
quitConfirmModalVisible, clear/disable _cancelHoldPending/_cancelHoldReady and
stop the timer, and ensure handleKeyRelease checks those flags and returns early
(no-op) if the timer already executed to avoid double actions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant