Skip to content

Fix Issue #26867: Proper Export Menu Keyboard Navagation#32990

Open
sshadid-code wants to merge 1 commit intomusescore:masterfrom
sshadid-code:export-menu-fix
Open

Fix Issue #26867: Proper Export Menu Keyboard Navagation#32990
sshadid-code wants to merge 1 commit intomusescore:masterfrom
sshadid-code:export-menu-fix

Conversation

@sshadid-code
Copy link
Copy Markdown

@sshadid-code sshadid-code commented Apr 12, 2026

Implemented navigation handling for ExportScoresListView, allowing up and down navigation through the list and focus management without unexpected behavior of looping after 20 items without manual scrolling

Resolves: #26867

Explicitly set navigation.column to 0 to ensure proper handling of the left and right arrow keys. The addition of Connections handles automatically scrolling the export menu while navigating with the arrow keys. This alone completely handles using arrow key navigation down the list of export options. Added navigation.onNavigationEvent to disallow navigating down from the final option and up from the first option. Otherwise such inputs would cause improper navigation behavior. This code also handles a bug where attempting to navigate back up the list would skip to the first option after 11 up inputs past the currently displayed top of the list.

No unit test was made in place of using extensive manual testing. This was due to time constraints and the small number of inputs possible in the export menu's part selection.

  • I signed the CLA
  • The title of the PR describes the problem it addresses
  • Each commit's message describes its purpose and effects, and references the issue it resolves
  • If changes are extensive, there is a sequence of easily reviewable commits
  • The code in the PR follows the coding rules
  • There are no unnecessary changes
  • The code compiles and runs on my machine, preferably after each commit individually
  • I created a unit test or vtest to verify the changes I made (if applicable)

Summary by CodeRabbit

  • Bug Fixes
    • Improved keyboard navigation in the export scores list view with proper focus management and boundary handling for up/down navigation.

Implemented navigation handling for ExportScoresListView, allowing up and down navigation through the list and focus management without unexpected behavior of looping after 20 items without manual scrolling
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 12, 2026

📝 Walkthrough

Walkthrough

Enhanced keyboard navigation in the export dialog's parts list by adding event handlers to the checkbox navigation. The implementation computes target indices when navigating with Up/Down keys, clamps bounds to prevent cycling, repositions the list view for visibility, and defers focus updates to the appropriate item.

Changes

Cohort / File(s) Summary
Export Dialog Navigation Enhancement
src/project/qml/MuseScore/Project/internal/Export/ExportScoresListView.qml
Added navigation.column: 0 property, implemented checkBox.navigation.onNavigationEvent handler with Up/Down navigation logic including target index computation, bounds clamping, and list repositioning, plus a Connections block to handle navigation active state changes.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~15 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title describes the main purpose (fixing keyboard navigation in export menu) but contains a typo ('Navagation' instead of 'Navigation') and adequately conveys the primary change.
Description check ✅ Passed The PR description addresses the issue (#26867), explains the changes made, documents why unit tests were skipped, and completes the provided template checklist.
Linked Issues check ✅ Passed The code changes directly address issue #26867 by implementing keyboard navigation support that prevents list cycling after repeated key presses and enables navigation through all export parts.
Out of Scope Changes check ✅ Passed All changes are focused on fixing keyboard navigation in ExportScoresListView, directly supporting the stated objective with no unrelated modifications.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d81967cb-cce1-473e-b3a7-1db674586929

📥 Commits

Reviewing files that changed from the base of the PR and between 6f59207 and a0c9514.

📒 Files selected for processing (1)
  • src/project/qml/MuseScore/Project/internal/Export/ExportScoresListView.qml

Comment on lines +95 to +139
navigation.onNavigationEvent: function(event) {
let targetIndex = delegateItem.index

switch (event.type) {
case NavigationEvent.Up:
if (delegateItem.index === 0) {
event.accepted = true
return
}
targetIndex = delegateItem.index - 1
break

case NavigationEvent.Down:
if (delegateItem.index === listView.count - 1) {
event.accepted = true
return
}
targetIndex = delegateItem.index + 1
break

default:
return
}

listView.positionViewAtIndex(targetIndex, ListView.Contain)

Qt.callLater(function() {
let item = listView.itemAtIndex(targetIndex)
if (item) {
item.requestActiveFocus()
}
})

event.accepted = true
}

Connections {
target: checkBox.navigation

function onActiveChanged() {
if (target.active) {
listView.positionViewAtIndex(index, ListView.Contain)
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Please add a UI/QML regression test for this keyboard path.

Given this now owns custom boundary and focus behavior, add automated coverage for: (1) repeated Down to last item without wrap, (2) repeated Up to first item without wrap, and (3) deep-list traversal beyond initial viewport.

Based on learnings: In the MuseScore codebase, vtests are intended for src/engraving only; for .qml UI changes, use UI/QML-appropriate checks instead of vtests.

@mathesoncalum mathesoncalum requested a review from Eism April 13, 2026 08:50
}
})

event.accepted = true
Copy link
Copy Markdown
Contributor

@krasko78 krasko78 Apr 13, 2026

Choose a reason for hiding this comment

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

Lines 115-128 should not be needed. When the event is not accepted, the navigation system will move the focus to the checkbox above/below (what lines 121-126 do). This will trigger onActiveChanged and inside it the list view will be scrolled (i.e. what line 119 does will be done by line 136). Then targetIndex won't be needed either. P.S. Nope, I am wrong. Upon further testing I see that my suggestion does not work as I expected. Moving down works well but when you start going up, at some point it skips items and goes directly to the first item (the main score). I just noticed that the same buggy behavior exists with the Shortcuts list in Preferences that I gave you as an example. :) I'll investigate this when I can. Disregard my suggestion. P.S. ListView acts weirdly with regard to the instantiation of delegates outside of the visible area.

Your code does work by first scrolling the list view and then navigating. Do we need lines 121-128? If the list view gets scrolled (line 119) and the event is not accepted, then the navigation system should correctly move the focus. Also positionViewAtIndex ends up called twice (once in onNavigationEvent and again in onActiveChanged) but at least everything works. The second call won't do anything and should be fast. onActiveChanged can be removed to avoid the double call, but it does help with the other navigation keys not handled by the code such as PgUp, PgDn, Home and End.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Your code does work by first scrolling the list view and then navigating. Do we need lines 121-128? If the list view gets scrolled (line 119) and the event is not accepted, then the navigation system should correctly move the focus. Also positionViewAtIndex ends up called twice (once in onNavigationEvent and again in onActiveChanged) but at least everything works. The second call won't do anything and should be fast. onActiveChanged can be removed to avoid the double call, but it does help with the other navigation keys not handled by the code such as PgUp, PgDn, Home and End.

From what I can tell navigation.onActiveChanged (which I updated to no longer use Connection) could be removed if either
A. the alternate navigation options such as left arrow, right arrow, PgUp, and PgDn could be disabled
B. the alternate navigation options could be included in the navigation.onNavigationEvent code

I've attempted to take approach B but have been unable to find what events these alternate navigation options are tied to. This may very well be due to my lack of understanding of the MuseScore codebase and/or my lack of familiarity with QML. Without knowing how to implement option A or B, navigation.onActiveChanged remains required.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

That's fine. I concluded my comment saying onActiveChanged should stay because it helps with the navigation with PgUp, PgDn, Home and End.

}

Connections {
target: checkBox.navigation
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Since this code is inside the checkbox, there is no need for Connections. Just like with the previous handler for navigation.onNavigationEvent above, here we can simply write

navigation.onActiveChanged: {
. . .
}

Then target.active will become navigation.active. You can take a look at the other navigation.onActiveChanged instances in the code.

I also suggest changing index to delegateItem.Index. index works but is more work for Qt to find it since it will first look for it in the checkbox and then up in the parents. Qualifying it with the ID of the object it belongs to eliminates this (and makes it clearer).

@krasko78
Copy link
Copy Markdown
Contributor

Nice work, @sshadid-code ! I've left a couple of suggestions for simplification. :)
I think the title of the PR is not very accurate since it does not deal with a menu.

@sshadid-code
Copy link
Copy Markdown
Author

Nice work, @sshadid-code ! I've left a couple of suggestions for simplification. :) I think the title of the PR is not very accurate since it does not deal with a menu.

Thank @krasko78. I'll aim to implement and test these changes by the end of the day (potentially by tomorrow depending on my schedule). If there are any other changes you think I should implement for the PR to be accepted please let me know.

@krasko78
Copy link
Copy Markdown
Contributor

I edited my first comment because I discovered I was wrong. PR will have to be reviewed by Eism so you may wait for his feedback.

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.

Maximum of 20 instruments available with screen reader in the export dialogue box

3 participants