Skip to content

Fix #26174: Prevent grid lines from overlapping scrollbar in Accidentals#32888

Open
AndreiBarb wants to merge 2 commits intomusescore:masterfrom
AndreiBarb:fix/26174-palette-scrollbar-opacity
Open

Fix #26174: Prevent grid lines from overlapping scrollbar in Accidentals#32888
AndreiBarb wants to merge 2 commits intomusescore:masterfrom
AndreiBarb:fix/26174-palette-scrollbar-opacity

Conversation

@AndreiBarb
Copy link
Copy Markdown

@AndreiBarb AndreiBarb commented Apr 1, 2026

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: 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.

Resolves: #26174

  • 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

  • Refactor
    • Restructured palette grid drag-and-drop interaction handling for improved stability.
    • Optimized "More" button layering in the palette component.

… 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.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 1, 2026

📝 Walkthrough

Walkthrough

The 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

Cohort / File(s) Summary
PaletteGridView Component Restructuring
src/palette/qml/MuseScore/Palette/internal/PaletteGridView.qml
Restructured component hierarchy by extracting PaletteGridLinesCanvas from parent container role and replacing it with top-level DropArea containing all drag-and-drop handlers. PaletteGridLinesCanvas reintroduced as sibling. "More" button z-value changed from grid.z + 1 to fixed z: 1. Drag icon parent reassigned from grid to paletteView in endDrag().

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title directly references issue #26174 and clearly describes the main problem being fixed: preventing grid lines from overlapping the scrollbar.
Description check ✅ Passed The description follows the template, includes issue reference, explains the problem and solution, and has all checklist items completed except the unit test (noted as not applicable).
Linked Issues check ✅ Passed The code changes directly address issue #26174's requirement: the scrollbar now renders above grid lines using z-order adjustments to prevent visual overlap.
Out of Scope Changes check ✅ Passed The changes restructure PaletteGridView.qml's component hierarchy and z-ordering to fix the scrollbar overlap issue, which aligns with the scope of issue #26174.
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.

@cbjeukendrup
Copy link
Copy Markdown
Collaborator

A cleaner alternative (if it works, at least) would be to remove z: 1 inside PaletteGridView.qml, and refactor it a bit by moving the DropArea outside PaletteGridLinesCanvas and move PaletteGridLinesCanvas to the bottom of the file so that it is rendered on top.

@AndreiBarb
Copy link
Copy Markdown
Author

A cleaner alternative (if it works, at least) would be to remove z: 1 inside PaletteGridView.qml, and refactor it a bit by moving the DropArea outside PaletteGridLinesCanvas and move PaletteGridLinesCanvas to the bottom of the file so that it is rendered on top.

I implemented the alternative you said (it was indeed a cleaner way of doing it).

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: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4155bb8a-bca4-42fa-86ab-72f107dfd2a9

📥 Commits

Reviewing files that changed from the base of the PR and between 10290bd and 326b3ac.

📒 Files selected for processing (1)
  • src/palette/qml/MuseScore/Palette/internal/PaletteGridView.qml

color: background.color

z: grid.z + 1
z: 1
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Apr 8, 2026

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 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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@coderabbitai "vtests" apply to MuseScore's engraving (src/engraving), and not to the UI.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@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.

Comment on lines +251 to +317
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 {
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Apr 8, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 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.qml

Repository: 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.

Suggested change
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 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This comment may be pre-existing before this PR, because only the indentation of this code was changed.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 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
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

@RomanPudashkin RomanPudashkin requested a review from Eism April 9, 2026 07:50
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.

Scroll bar doesn't hover above the grid in the Accidentals palette

3 participants