Fix #26174: Prevent grid lines from overlapping scrollbar in Accidentals#32888
Fix #26174: Prevent grid lines from overlapping scrollbar in Accidentals#32888AndreiBarb wants to merge 2 commits intomusescore:masterfrom
Conversation
… Accidentals When opening the "More" popup Accidentals, the grid lines separating the elements would visually render on top of the vertical scrollbar. This commit explicitly sets a "z: 100" property on the vertical StyledScrollBar within the popup's view. Now, instead of being obscured by the separator lines, the scrollbar correctly renders above the grid elements.
📝 WalkthroughWalkthroughThe component hierarchy in PaletteGridView is restructured by extracting PaletteGridLinesCanvas from its parent role and replacing it with a top-level DropArea that inherits the drag-and-drop logic. PaletteGridLinesCanvas is reintroduced as a sibling element. The "More" button's z-value changes from relative to fixed, and drag operations are adjusted to reference the updated parent hierarchy. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
A cleaner alternative (if it works, at least) would be to remove |
I implemented the alternative you said (it was indeed a cleaner way of doing it). |
There was a problem hiding this comment.
Actionable comments posted: 2
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 4155bb8a-bca4-42fa-86ab-72f107dfd2a9
📒 Files selected for processing (1)
src/palette/qml/MuseScore/Palette/internal/PaletteGridView.qml
| color: background.color | ||
|
|
||
| z: grid.z + 1 | ||
| z: 1 |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Please add a vtest for the Accidentals “More” popup layering.
This fix depends on child order and z values, so it is easy to regress without a visual check. A small vtest that opens “More” with grid enabled and verifies the vertical scrollbar paints above the separators would lock this down.
Also applies to: 693-702
There was a problem hiding this comment.
@coderabbitai "vtests" apply to MuseScore's engraving (src/engraving), and not to the UI.
There was a problem hiding this comment.
@cbjeukendrup, thanks for the correction! I'll remember that vtests are scoped to src/engraving and are not applicable to UI/QML changes. Apologies for the misleading suggestion.
✏️ Learnings added
Learnt from: cbjeukendrup
Repo: musescore/MuseScore PR: 32888
File: src/palette/qml/MuseScore/Palette/internal/PaletteGridView.qml:170-170
Timestamp: 2026-04-08T19:03:21.705Z
Learning: In the MuseScore project, "vtests" (visual tests) apply exclusively to engraving code under `src/engraving`. They are not applicable to UI/QML changes (e.g., files under `src/palette/qml` or other QML components). Do not suggest vtests for UI-layer fixes.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
🧠 Learnings used
Learnt from: mathesoncalum
Repo: musescore/MuseScore PR: 32779
File: src/engraving/dom/masterscore.h:173-173
Timestamp: 2026-04-06T11:15:07.564Z
Learning: In `src/engraving/dom/masterscore.h`, the `initAutomation()` method on class `MasterScore` is intentionally marked with a `// TODO: Placeholder?` comment. It is a convenience method used during in-development automation work, and whether it will be part of the final API is yet to be determined. Do not flag this TODO as stale or suggest its removal.
Learnt from: luapmartin
Repo: musescore/MuseScore PR: 32861
File: src/framework/multiwindows/tests/environment.cpp:27-34
Timestamp: 2026-03-31T10:32:15.267Z
Learning: In the MuseScore project (C++), the formatting style for `static muse::testing::SuiteEnvironment` initializations with an opening brace on its own line and closing parenthesis aligned to the left (as seen in `src/framework/multiwindows/tests/environment.cpp`) is acceptable and passes the project's codestyle checks. Do not flag this pattern as a formatting issue.
| onEntered: function(drag) { | ||
| onDragOverPaletteFinished(); | ||
|
|
||
| if (action !== Qt.MoveAction) { | ||
| internal = false; | ||
| } | ||
| // first check if controller allows dropping this item here | ||
| const mimeData = PaletteUtils.dropEventMimeData(drag); | ||
| internal = (drag.source.parentModelIndex === paletteView.paletteRootIndex); | ||
| action = paletteView.paletteController.dropAction(mimeData, drag.proposedAction, paletteView.paletteRootIndex, internal); | ||
| proposedAction = drag.proposedAction; | ||
|
|
||
| const accept = (action & drag.supportedActions) && (internal || !paletteView.externalDropBlocked); | ||
| if (action !== Qt.MoveAction) { | ||
| internal = false; | ||
| } | ||
|
|
||
| if (accept) { | ||
| drag.accept(action); | ||
| } else { | ||
| drag.accepted = false; | ||
| } | ||
| const accept = (action & drag.supportedActions) && (internal || !paletteView.externalDropBlocked); | ||
|
|
||
| // If event is accepted, process the drag in a usual way | ||
| if (drag.accepted) { | ||
| onDrag(drag); | ||
| } | ||
| if (accept) { | ||
| drag.accept(action); | ||
| } else { | ||
| drag.accepted = false; | ||
| } | ||
|
|
||
| onPositionChanged: function(drag) { | ||
| onDrag(drag) | ||
| // If event is accepted, process the drag in a usual way | ||
| if (drag.accepted) { | ||
| onDrag(drag); | ||
| } | ||
| } | ||
|
|
||
| function onDragOverPaletteFinished() { | ||
| if (placeholder.active) { | ||
| placeholder.removePlaceholder(); | ||
| paletteView.state = "default"; | ||
| } | ||
| onPositionChanged: function(drag) { | ||
| onDrag(drag) | ||
| } | ||
|
|
||
| if (drag.source && drag.source.parentModelIndex === paletteView.paletteRootIndex) { | ||
| drag.source.internalDrag = false; | ||
| } | ||
| function onDragOverPaletteFinished() { | ||
| if (placeholder.active) { | ||
| placeholder.removePlaceholder(); | ||
| paletteView.state = "default"; | ||
| } | ||
|
|
||
| onExited: onDragOverPaletteFinished(); | ||
| if (drag.source && drag.source.parentModelIndex === paletteView.paletteRootIndex) { | ||
| drag.source.internalDrag = false; | ||
| } | ||
| } | ||
|
|
||
| onDropped: function(drop) { | ||
| if (!action) { | ||
| onDragOverPaletteFinished(); | ||
| return; | ||
| } | ||
| onExited: onDragOverPaletteFinished(); | ||
|
|
||
| const destIndex = placeholder.active ? placeholder.index : paletteView.paletteModel.rowCount(paletteView.paletteRootIndex); | ||
| onDropped: function(drop) { | ||
| if (!action) { | ||
| onDragOverPaletteFinished(); | ||
| return; | ||
| } | ||
|
|
||
| // Moving cells here causes Drag.onDragFinished be not called properly. | ||
| // Therefore record the necessary information to move cells later. | ||
| const data = { | ||
| action: action, | ||
| srcParentModelIndex: drag.source.parentModelIndex, | ||
| srcRowIndex: drag.source.rowIndex, | ||
| paletteView: paletteView, | ||
| destIndex: destIndex, | ||
| mimeData: PaletteUtils.dropEventMimeData(drop) | ||
| }; | ||
|
|
||
| if (typeof data.srcParentModelIndex !== "undefined") { | ||
| drag.source.dropData = data; | ||
| } else { | ||
| data.paletteView.insertCell(data.destIndex, data.mimeData, data.action); | ||
| } | ||
|
|
||
| drop.accept(action); | ||
| const destIndex = placeholder.active ? placeholder.index : paletteView.paletteModel.rowCount(paletteView.paletteRootIndex); | ||
| onDragOverPaletteFinished(); | ||
|
|
||
| // Moving cells here causes Drag.onDragFinished be not called properly. | ||
| // Therefore record the necessary information to move cells later. | ||
| const data = { | ||
| action: action, | ||
| srcParentModelIndex: drag.source.parentModelIndex, | ||
| srcRowIndex: drag.source.rowIndex, | ||
| paletteView: paletteView, | ||
| destIndex: destIndex, | ||
| mimeData: PaletteUtils.dropEventMimeData(drop) | ||
| }; | ||
|
|
||
| if (typeof data.srcParentModelIndex !== "undefined") { | ||
| drag.source.dropData = data; | ||
| } else { |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -n -C2 'onEntered: function\(drag\)|function onDragOverPaletteFinished\(|onExited:|onDropped: function\(drop\)|\bdrag\.source\b' src/palette/qml/MuseScore/Palette/internal/PaletteGridView.qmlRepository: musescore/MuseScore
Length of output: 2160
Pass the current drop event through the helper; drag is undefined here.
onDragOverPaletteFinished() is defined without parameters (line 282) but line 288 reads drag.source, and onDropped: function(drop) still uses drag.source at lines 308–309 and 316 instead of drop.source. In QML this throws ReferenceError at runtime, so entering or dropping over the palette aborts reorder/external-drop handling.
Proposed fix
onEntered: function(drag) {
- onDragOverPaletteFinished();
+ onDragOverPaletteFinished(drag);
// first check if controller allows dropping this item here
const mimeData = PaletteUtils.dropEventMimeData(drag);
internal = (drag.source.parentModelIndex === paletteView.paletteRootIndex);
action = paletteView.paletteController.dropAction(mimeData, drag.proposedAction, paletteView.paletteRootIndex, internal);
proposedAction = drag.proposedAction;
if (action !== Qt.MoveAction) {
internal = false;
}
const accept = (action & drag.supportedActions) && (internal || !paletteView.externalDropBlocked);
if (accept) {
drag.accept(action);
} else {
drag.accepted = false;
}
// If event is accepted, process the drag in a usual way
if (drag.accepted) {
onDrag(drag);
}
}
onPositionChanged: function(drag) {
onDrag(drag)
}
- function onDragOverPaletteFinished() {
+ function onDragOverPaletteFinished(event) {
if (placeholder.active) {
placeholder.removePlaceholder();
paletteView.state = "default";
}
- if (drag.source && drag.source.parentModelIndex === paletteView.paletteRootIndex) {
- drag.source.internalDrag = false;
+ if (event && event.source && event.source.parentModelIndex === paletteView.paletteRootIndex) {
+ event.source.internalDrag = false;
}
}
- onExited: onDragOverPaletteFinished();
+ onExited: function(drag) {
+ onDragOverPaletteFinished(drag);
+ }
onDropped: function(drop) {
if (!action) {
- onDragOverPaletteFinished();
+ onDragOverPaletteFinished(drop);
return;
}
const destIndex = placeholder.active ? placeholder.index : paletteView.paletteModel.rowCount(paletteView.paletteRootIndex);
- onDragOverPaletteFinished();
+ onDragOverPaletteFinished(drop);
// Moving cells here causes Drag.onDragFinished be not called properly.
// Therefore record the necessary information to move cells later.
const data = {
action: action,
- srcParentModelIndex: drag.source.parentModelIndex,
- srcRowIndex: drag.source.rowIndex,
+ srcParentModelIndex: drop.source ? drop.source.parentModelIndex : undefined,
+ srcRowIndex: drop.source ? drop.source.rowIndex : undefined,
paletteView: paletteView,
destIndex: destIndex,
mimeData: PaletteUtils.dropEventMimeData(drop)
};
- if (typeof data.srcParentModelIndex !== "undefined") {
- drag.source.dropData = data;
+ if (drop.source && typeof data.srcParentModelIndex !== "undefined") {
+ drop.source.dropData = data;
} else {
data.paletteView.insertCell(data.destIndex, data.mimeData, data.action);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| onEntered: function(drag) { | |
| onDragOverPaletteFinished(); | |
| if (action !== Qt.MoveAction) { | |
| internal = false; | |
| } | |
| // first check if controller allows dropping this item here | |
| const mimeData = PaletteUtils.dropEventMimeData(drag); | |
| internal = (drag.source.parentModelIndex === paletteView.paletteRootIndex); | |
| action = paletteView.paletteController.dropAction(mimeData, drag.proposedAction, paletteView.paletteRootIndex, internal); | |
| proposedAction = drag.proposedAction; | |
| const accept = (action & drag.supportedActions) && (internal || !paletteView.externalDropBlocked); | |
| if (action !== Qt.MoveAction) { | |
| internal = false; | |
| } | |
| if (accept) { | |
| drag.accept(action); | |
| } else { | |
| drag.accepted = false; | |
| } | |
| const accept = (action & drag.supportedActions) && (internal || !paletteView.externalDropBlocked); | |
| // If event is accepted, process the drag in a usual way | |
| if (drag.accepted) { | |
| onDrag(drag); | |
| } | |
| if (accept) { | |
| drag.accept(action); | |
| } else { | |
| drag.accepted = false; | |
| } | |
| onPositionChanged: function(drag) { | |
| onDrag(drag) | |
| // If event is accepted, process the drag in a usual way | |
| if (drag.accepted) { | |
| onDrag(drag); | |
| } | |
| } | |
| function onDragOverPaletteFinished() { | |
| if (placeholder.active) { | |
| placeholder.removePlaceholder(); | |
| paletteView.state = "default"; | |
| } | |
| onPositionChanged: function(drag) { | |
| onDrag(drag) | |
| } | |
| if (drag.source && drag.source.parentModelIndex === paletteView.paletteRootIndex) { | |
| drag.source.internalDrag = false; | |
| } | |
| function onDragOverPaletteFinished() { | |
| if (placeholder.active) { | |
| placeholder.removePlaceholder(); | |
| paletteView.state = "default"; | |
| } | |
| onExited: onDragOverPaletteFinished(); | |
| if (drag.source && drag.source.parentModelIndex === paletteView.paletteRootIndex) { | |
| drag.source.internalDrag = false; | |
| } | |
| } | |
| onDropped: function(drop) { | |
| if (!action) { | |
| onDragOverPaletteFinished(); | |
| return; | |
| } | |
| onExited: onDragOverPaletteFinished(); | |
| const destIndex = placeholder.active ? placeholder.index : paletteView.paletteModel.rowCount(paletteView.paletteRootIndex); | |
| onDropped: function(drop) { | |
| if (!action) { | |
| onDragOverPaletteFinished(); | |
| return; | |
| } | |
| // Moving cells here causes Drag.onDragFinished be not called properly. | |
| // Therefore record the necessary information to move cells later. | |
| const data = { | |
| action: action, | |
| srcParentModelIndex: drag.source.parentModelIndex, | |
| srcRowIndex: drag.source.rowIndex, | |
| paletteView: paletteView, | |
| destIndex: destIndex, | |
| mimeData: PaletteUtils.dropEventMimeData(drop) | |
| }; | |
| if (typeof data.srcParentModelIndex !== "undefined") { | |
| drag.source.dropData = data; | |
| } else { | |
| data.paletteView.insertCell(data.destIndex, data.mimeData, data.action); | |
| } | |
| drop.accept(action); | |
| const destIndex = placeholder.active ? placeholder.index : paletteView.paletteModel.rowCount(paletteView.paletteRootIndex); | |
| onDragOverPaletteFinished(); | |
| // Moving cells here causes Drag.onDragFinished be not called properly. | |
| // Therefore record the necessary information to move cells later. | |
| const data = { | |
| action: action, | |
| srcParentModelIndex: drag.source.parentModelIndex, | |
| srcRowIndex: drag.source.rowIndex, | |
| paletteView: paletteView, | |
| destIndex: destIndex, | |
| mimeData: PaletteUtils.dropEventMimeData(drop) | |
| }; | |
| if (typeof data.srcParentModelIndex !== "undefined") { | |
| drag.source.dropData = data; | |
| } else { | |
| onEntered: function(drag) { | |
| onDragOverPaletteFinished(drag); | |
| // first check if controller allows dropping this item here | |
| const mimeData = PaletteUtils.dropEventMimeData(drag); | |
| internal = (drag.source.parentModelIndex === paletteView.paletteRootIndex); | |
| action = paletteView.paletteController.dropAction(mimeData, drag.proposedAction, paletteView.paletteRootIndex, internal); | |
| proposedAction = drag.proposedAction; | |
| if (action !== Qt.MoveAction) { | |
| internal = false; | |
| } | |
| const accept = (action & drag.supportedActions) && (internal || !paletteView.externalDropBlocked); | |
| if (accept) { | |
| drag.accept(action); | |
| } else { | |
| drag.accepted = false; | |
| } | |
| // If event is accepted, process the drag in a usual way | |
| if (drag.accepted) { | |
| onDrag(drag); | |
| } | |
| } | |
| onPositionChanged: function(drag) { | |
| onDrag(drag) | |
| } | |
| function onDragOverPaletteFinished(event) { | |
| if (placeholder.active) { | |
| placeholder.removePlaceholder(); | |
| paletteView.state = "default"; | |
| } | |
| if (event && event.source && event.source.parentModelIndex === paletteView.paletteRootIndex) { | |
| event.source.internalDrag = false; | |
| } | |
| } | |
| onExited: function(drag) { | |
| onDragOverPaletteFinished(drag); | |
| } | |
| onDropped: function(drop) { | |
| if (!action) { | |
| onDragOverPaletteFinished(drop); | |
| return; | |
| } | |
| const destIndex = placeholder.active ? placeholder.index : paletteView.paletteModel.rowCount(paletteView.paletteRootIndex); | |
| onDragOverPaletteFinished(drop); | |
| // Moving cells here causes Drag.onDragFinished be not called properly. | |
| // Therefore record the necessary information to move cells later. | |
| const data = { | |
| action: action, | |
| srcParentModelIndex: drop.source ? drop.source.parentModelIndex : undefined, | |
| srcRowIndex: drop.source ? drop.source.rowIndex : undefined, | |
| paletteView: paletteView, | |
| destIndex: destIndex, | |
| mimeData: PaletteUtils.dropEventMimeData(drop) | |
| }; | |
| if (drop.source && typeof data.srcParentModelIndex !== "undefined") { | |
| drop.source.dropData = data; | |
| } else { |
There was a problem hiding this comment.
This comment may be pre-existing before this PR, because only the indentation of this code was changed.
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check the git log to see the original version of onDragOverPaletteFinished before this PR
git log --oneline -5
git show HEAD:src/palette/qml/MuseScore/Palette/internal/PaletteGridView.qml 2>/dev/null | grep -n -A 10 "onDragOverPaletteFinished"
Length of output: 2238
@cbjeukendrup, you're right — I apologize for the false positive. The git history confirms this code is identical to what existed before this PR; only the indentation changed when the DropArea was lifted out of PaletteGridLinesCanvas.
More importantly, my analysis was wrong: in QML, DropArea exposes drag as a grouped property on the component itself (distinct from the drag parameter in signal handlers). So drag.source inside onDragOverPaletteFinished() correctly refers to DropArea.drag.source, which is perfectly valid QML and accessible from any function within the DropArea scope — no parameter passing needed.
This review comment can be disregarded.
( >_< ) (ears folded in embarrassment)
🧠 Learnings used
Learnt from: mathesoncalum
Repo: musescore/MuseScore PR: 32779
File: src/notationscene/qml/MuseScore/NotationScene/abstractnotationpaintview.cpp:399-400
Timestamp: 2026-04-06T11:27:52.780Z
Learning: In `src/notationscene/qml/MuseScore/NotationScene/abstractnotationpaintview.cpp`, the `notationAutomation()` disconnect calls in `onUnloadNotation` (for `automationModeEnabledChanged` and `automationLinesDataChanged`) intentionally omit a null check on the return value, matching the pre-existing pattern of the inlined `m_notation->masterNotation()->automation()` chain. Do not flag missing null checks on these disconnect call sites.
Learnt from: cbjeukendrup
Repo: musescore/MuseScore PR: 32888
File: src/palette/qml/MuseScore/Palette/internal/PaletteGridView.qml:170-170
Timestamp: 2026-04-08T19:03:21.705Z
Learning: In the MuseScore codebase, vtests (visual tests) are intended for engraving code under `src/engraving` only. When reviewing changes to UI/QML code (any `.qml` files under `src/**/qml/`), do not recommend adding/updating vtests; instead focus on UI/QML-appropriate checks and test strategies for that layer.
| @@ -167,7 +167,7 @@ StyledGridView { | |||
| visible: paletteView.showMoreButton | |||
| color: background.color | |||
|
|
|||
| z: grid.z + 1 | |||
| z: 1 | |||
There was a problem hiding this comment.
This usage of z can also be eliminated by placing moreButtonRect at the end of the file, preventing similar issues with the More button in the future.
When opening the "More" popup for Accidentals, the grid lines separating the elements would visually render on top of the vertical scrollbar.
This commit explicitly sets a
z: 100property on the vertical StyledScrollBar within the popup's view. Now, instead of being obscured by the separator lines, the scrollbar correctly renders above the grid elements.Resolves: #26174
Summary by CodeRabbit