Skip to content

Fix #31056 and add tie and slur masking over key and time signatures#33024

Open
ajuncosa wants to merge 7 commits intomusescore:masterfrom
ajuncosa:tie-masks
Open

Fix #31056 and add tie and slur masking over key and time signatures#33024
ajuncosa wants to merge 7 commits intomusescore:masterfrom
ajuncosa:tie-masks

Conversation

@ajuncosa
Copy link
Copy Markdown

Resolves: #31056

  • Add tie and slur masking over key and time signatures.
  • Fix You cannot "put back" a tie (only a slur) #31056, which was due to spanner segments not identifying changes in Z as modifications from the default value, therefore deeming it unnecessary to save them in the XML.

@ajuncosa ajuncosa requested a review from mike-spa April 15, 2026 12:40
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 15, 2026

📝 Walkthrough

Walkthrough

This change extends Z-order management for spanner segments and implements a masking system for slurs and ties. The modifications make setZ() virtual in EngravingItem and add setZ() overrides to both SpannerSegment and Spanner classes to propagate depth changes. New style settings (maskSlursOverTimeAndKeySignatures and maskTiesOverTimeAndKeySignatures) are introduced, with corresponding masking logic added to MaskLayout to filter and mask overlapping slur and tie segments against key and time signature items. UI controls are added to the Slurs and Ties style dialog sections, with accessor methods provided through SlursAndTiesPageModel.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 34.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: fixing issue #31056 and adding tie/slur masking over key/time signatures, which matches the primary objectives of the changeset.
Description check ✅ Passed The description provides the issue reference (#31056) and explains the core changes (tie/slur masking and Z-coordinate fix), though it lacks completion of the template checklist items.
Linked Issues check ✅ Passed The PR meets issue #31056 requirements by implementing persistent tie masking through Z-coordinate serialization and spanner-segment Z-order handling, ensuring masking persists across file saves/reopens.
Out of Scope Changes check ✅ Passed All code changes directly support the linked issue objectives: Z-coordinate virtualization and propagation for spanner segments, masking logic for ties/slurs over key/time signatures, and UI controls for the new masking options.

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

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

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.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/engraving/rendering/score/tdraw.cpp (1)

3094-3101: ⚠️ Potential issue | 🔴 Critical

Balance the painter state on the early-return path.

painter->save() now runs before the cross-measure guard, but the return on Line 3099 exits without restore(). That leaves the painter stack/mask state dirty for subsequent drawing.

🐛 Proposed fix
-    painter->save();
-
-    setMask(item, painter);
-
     // hide tie toward the second chord of a cross-measure value
     if (item->tie()->endNote() && item->tie()->endNote()->chord()->crossMeasure() == CrossMeasure::SECOND) {
         return;
     }
+    painter->save();
+    DEFER {
+        painter->restore();
+    };
+
+    setMask(item, painter);
@@
-    painter->restore();

Also applies to: 3143-3144

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/engraving/rendering/score/tdraw.cpp` around lines 3094 - 3101, The
painter state is saved by painter->save() before the cross-measure guard but not
restored on the early return; update the guard that checks
item->tie()->endNote() && item->tie()->endNote()->chord()->crossMeasure() ==
CrossMeasure::SECOND to call painter->restore() (and undo any mask set by
setMask(item, painter) if necessary) before returning, and apply the same change
to the similar early-return guard later in the file (the other occurrence around
the same cross-measure check).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/engraving/rendering/score/masklayout.cpp`:
- Around line 83-91: The loop is skipping continuation slur/tie segments because
of the isSingleBeginType() check; remove that check so continuation segments are
processed too. In the loop over system->spannerSegments(), keep the visibility
and staff show checks (system->staff(spannerSeg->staffIdx())->show() and
spannerSeg->visible()), but drop the !spannerSeg->isSingleBeginType() condition
and still call computeSlurTieMasks(toSlurTieSegment(spannerSeg),
SegmentType::KeySig | SegmentType::TimeSig) when (maskSlurs &&
spannerSeg->isSlurSegment()) || (maskTies && spannerSeg->isTieSegment()) so
masks are written on continuation SlurTieSegment instances as well.

In `@src/notationscene/qml/MuseScore/NotationScene/styledialog/SlursSection.qml`:
- Around line 137-146: The onClicked handler for the CheckBox
maskSlursOverTimeAndKeySignaturesCheckbox writes the inverse of the new state,
causing the model to revert; change the handler to use onToggled (or assign
directly in onClicked without negation) and set
root.pageModel.maskSlursOverTimeAndKeySignatures.value =
maskSlursOverTimeAndKeySignaturesCheckbox.checked (i.e., assign the current
checked state directly) so the page model reflects the new checkbox state.

In `@src/notationscene/qml/MuseScore/NotationScene/styledialog/TiesSection.qml`:
- Around line 180-189: Replace the indirect negation handler on the CheckBox by
using onToggled and directly assigning the new state to the model: in the
CheckBox with id maskTiesOverTimeAndKeySignaturesCheckbox, replace the onClicked
block that sets root.pageModel.maskTiesOverTimeAndKeySignatures.value =
!maskTiesOverTimeAndKeySignaturesCheckbox.checked with an onToggled handler that
assigns root.pageModel.maskTiesOverTimeAndKeySignatures.value = checked (or the
toggled parameter), removing the unnecessary negation and aligning with Qt Quick
idioms.

---

Outside diff comments:
In `@src/engraving/rendering/score/tdraw.cpp`:
- Around line 3094-3101: The painter state is saved by painter->save() before
the cross-measure guard but not restored on the early return; update the guard
that checks item->tie()->endNote() &&
item->tie()->endNote()->chord()->crossMeasure() == CrossMeasure::SECOND to call
painter->restore() (and undo any mask set by setMask(item, painter) if
necessary) before returning, and apply the same change to the similar
early-return guard later in the file (the other occurrence around the same
cross-measure check).
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 34b737f5-57a9-472a-9118-7f8738d93782

📥 Commits

Reviewing files that changed from the base of the PR and between e7d9ae7 and dc670b8.

📒 Files selected for processing (10)
  • src/engraving/dom/spanner.cpp
  • src/engraving/rendering/score/masklayout.cpp
  • src/engraving/rendering/score/masklayout.h
  • src/engraving/rendering/score/tdraw.cpp
  • src/engraving/style/styledef.cpp
  • src/engraving/style/styledef.h
  • src/notationscene/qml/MuseScore/NotationScene/styledialog/SlursSection.qml
  • src/notationscene/qml/MuseScore/NotationScene/styledialog/TiesSection.qml
  • src/notationscene/qml/MuseScore/NotationScene/styledialog/slursandtiespagemodel.cpp
  • src/notationscene/qml/MuseScore/NotationScene/styledialog/slursandtiespagemodel.h

Comment thread src/engraving/rendering/score/masklayout.cpp
Comment on lines +137 to +146
CheckBox {
id: maskSlursOverTimeAndKeySignaturesCheckbox
text: qsTrc("notation/editstyle/slursandties", "Mask slurs over time and key signatures")
checked: root.pageModel.maskSlursOverTimeAndKeySignatures.value
width: parent.width
Layout.columnSpan: 2

onClicked: {
root.pageModel.maskSlursOverTimeAndKeySignatures.value = !maskSlursOverTimeAndKeySignaturesCheckbox.checked
}
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 | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In Qt Quick Controls 2, for a CheckBox/AbstractButton, is the clickedsignal emitted before or after thecheckedstate changes? IsonToggled: modelProperty = checked the recommended pattern for syncing a bound boolean setting?

💡 Result:

In Qt Quick Controls 2, for CheckBox and AbstractButton, the clicked signal is emitted when the button is interactively clicked (after press and release). The toggled signal (from QtQuick.Controls 2.2+) is emitted when a checkable button is interactively toggled. The checked state changes before both signals are emitted: on user interaction, the control first toggles the checked property (via toggle or equivalent), then emits toggled(checked) and clicked. Thus, clicked and toggled are emitted after the checked state changes. onToggled: modelProperty = checked is a recommended pattern for syncing a bound boolean model property. Direct binding checked: modelProperty breaks on user click (assignment overrides binding), so use onToggled (or onCheckedChanged) to update the model with the new checked value. Keep an initial binding or Component.onCompleted sync for model→UI, and use Connections for model change notifications if needed. This ensures two-way sync without binding loss.

Citations:


🏁 Script executed:

fd -t f "SlursSection.qml"

Repository: musescore/MuseScore

Length of output: 137


🏁 Script executed:

sed -n '137,146p' src/notationscene/qml/MuseScore/NotationScene/styledialog/SlursSection.qml

Repository: musescore/MuseScore

Length of output: 582


Write the checkbox state back directly, not its inverse.

With checked bound to the page model, negating maskSlursOverTimeAndKeySignaturesCheckbox.checked here writes the previous value back after the control toggles, causing the setting to snap back instead of changing. In Qt Quick Controls, the checked state changes before the clicked signal fires, so the negation reverses the new toggled state. Use onToggled and assign checked directly instead.

♻️ Proposed fix
             CheckBox {
                 id: maskSlursOverTimeAndKeySignaturesCheckbox
                 text: qsTrc("notation/editstyle/slursandties", "Mask slurs over time and key signatures")
                 checked: root.pageModel.maskSlursOverTimeAndKeySignatures.value
                 width: parent.width
                 Layout.columnSpan: 2

-                onClicked: {
-                    root.pageModel.maskSlursOverTimeAndKeySignatures.value = !maskSlursOverTimeAndKeySignaturesCheckbox.checked
-                }
+                onToggled: root.pageModel.maskSlursOverTimeAndKeySignatures.value = checked
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/notationscene/qml/MuseScore/NotationScene/styledialog/SlursSection.qml`
around lines 137 - 146, The onClicked handler for the CheckBox
maskSlursOverTimeAndKeySignaturesCheckbox writes the inverse of the new state,
causing the model to revert; change the handler to use onToggled (or assign
directly in onClicked without negation) and set
root.pageModel.maskSlursOverTimeAndKeySignatures.value =
maskSlursOverTimeAndKeySignaturesCheckbox.checked (i.e., assign the current
checked state directly) so the page model reflects the new checkbox state.

Comment on lines +180 to +189
CheckBox {
id: maskTiesOverTimeAndKeySignaturesCheckbox
text: qsTrc("notation/editstyle/slursandties", "Mask ties over time and key signatures")
checked: root.pageModel.maskTiesOverTimeAndKeySignatures.value
width: parent.width
Layout.columnSpan: 2

onClicked: {
root.pageModel.maskTiesOverTimeAndKeySignatures.value = !maskTiesOverTimeAndKeySignaturesCheckbox.checked
}
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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, check if the file exists
git ls-files | grep -i tiessection.qml

Repository: musescore/MuseScore

Length of output: 136


🏁 Script executed:

# Once we find the file, let's examine the exact lines mentioned
cat -n "src/notationscene/qml/MuseScore/NotationScene/styledialog/TiesSection.qml" | sed -n '175,195p'

Repository: musescore/MuseScore

Length of output: 978


🏁 Script executed:

# Also check the broader context to understand the pattern
cat -n "src/notationscene/qml/MuseScore/NotationScene/styledialog/TiesSection.qml" | sed -n '170,210p'

Repository: musescore/MuseScore

Length of output: 1856


🏁 Script executed:

# Search for similar checkbox patterns in the codebase to see how two-way binding is typically done
rg -A 5 "CheckBox.*{" --type qml src/notationscene/qml/ | head -60

Repository: musescore/MuseScore

Length of output: 148


🏁 Script executed:

# Search for CheckBox patterns in qml files with better regex
rg "onClicked|onToggled" --type qml src/notationscene/qml/ -B 3 -A 2 | head -80

Repository: musescore/MuseScore

Length of output: 8633


🏁 Script executed:

# Also look for other checkbox assignments to understand the pattern
rg "\.value = " --type qml src/notationscene/qml/ -B 2 -A 0 | grep -A 2 "CheckBox\|checked"

Repository: musescore/MuseScore

Length of output: 36131


🏁 Script executed:

# Search more broadly for checkbox state binding patterns
rg "checked:" --type qml src/notationscene/qml/ -A 3 | head -60

Repository: musescore/MuseScore

Length of output: 6525


Use onToggled with direct state assignment instead of negating checked.

Line 188 negates the checkbox state when writing to the model, creating an indirect toggle pattern. The idiomatic Qt Quick approach is to use onToggled (which fires with the new state) and assign directly to the model. This eliminates a redundant binding cycle and aligns with standard checkbox binding practices.

Proposed fix
             CheckBox {
                 id: maskTiesOverTimeAndKeySignaturesCheckbox
                 text: qsTrc("notation/editstyle/slursandties", "Mask ties over time and key signatures")
                 checked: root.pageModel.maskTiesOverTimeAndKeySignatures.value
                 width: parent.width
                 Layout.columnSpan: 2

-                onClicked: {
-                    root.pageModel.maskTiesOverTimeAndKeySignatures.value = !maskTiesOverTimeAndKeySignaturesCheckbox.checked
-                }
+                onToggled: root.pageModel.maskTiesOverTimeAndKeySignatures.value = checked
             }
📝 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
CheckBox {
id: maskTiesOverTimeAndKeySignaturesCheckbox
text: qsTrc("notation/editstyle/slursandties", "Mask ties over time and key signatures")
checked: root.pageModel.maskTiesOverTimeAndKeySignatures.value
width: parent.width
Layout.columnSpan: 2
onClicked: {
root.pageModel.maskTiesOverTimeAndKeySignatures.value = !maskTiesOverTimeAndKeySignaturesCheckbox.checked
}
CheckBox {
id: maskTiesOverTimeAndKeySignaturesCheckbox
text: qsTrc("notation/editstyle/slursandties", "Mask ties over time and key signatures")
checked: root.pageModel.maskTiesOverTimeAndKeySignatures.value
width: parent.width
Layout.columnSpan: 2
onToggled: root.pageModel.maskTiesOverTimeAndKeySignatures.value = checked
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/notationscene/qml/MuseScore/NotationScene/styledialog/TiesSection.qml`
around lines 180 - 189, Replace the indirect negation handler on the CheckBox by
using onToggled and directly assigning the new state to the model: in the
CheckBox with id maskTiesOverTimeAndKeySignaturesCheckbox, replace the onClicked
block that sets root.pageModel.maskTiesOverTimeAndKeySignatures.value =
!maskTiesOverTimeAndKeySignaturesCheckbox.checked with an onToggled handler that
assigns root.pageModel.maskTiesOverTimeAndKeySignatures.value = checked (or the
toggled parameter), removing the unnecessary negation and aligning with Qt Quick
idioms.

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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/engraving/dom/spanner.cpp (1)

394-401: ⚠️ Potential issue | 🔴 Critical

Keep the base-class modified check for Z-order persistence.

Returning only modified drops EngravingItem::isUserModified(), so a spanner segment whose only explicit edit is Pid::Z can still look unmodified after Pid::Z was delegated above. That leaves the XML persistence bug unfixed for the exact mask-ordering case this PR is targeting.

Proposed fix
-    return modified; // ? modified : EngravingItem::isUserModified();
+    return modified || EngravingItem::isUserModified();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/engraving/dom/spanner.cpp` around lines 394 - 401,
SpannerSegment::isUserModified currently returns only the local `modified`
boolean, which omits the base-class check and thus can drop edits like Pid::Z;
change the function to combine the local check with the base-class result by
returning `modified || EngravingItem::isUserModified()` so that edits detected
by EngravingItem (e.g., Z-order) are preserved; update the return expression in
SpannerSegment::isUserModified accordingly.
♻️ Duplicate comments (3)
src/notationscene/qml/MuseScore/NotationScene/styledialog/TiesSection.qml (1)

180-189: ⚠️ Potential issue | 🟠 Major

Write the toggled state back directly.

This has the same issue as the slur checkbox: checked is already updated when onClicked runs, so negating it writes the old value back and prevents the style from changing.

In Qt Quick Controls 2, when a CheckBox is clicked, does the `checked` property change before the `clicked`/`onClicked` handler runs, and is `onToggled: modelProperty = checked` the recommended way to sync a boolean model value?
Proposed fix
             CheckBox {
                 id: maskTiesOverTimeAndKeySignaturesCheckbox
                 text: qsTrc("notation/editstyle/slursandties", "Mask ties over time and key signatures")
                 checked: root.pageModel.maskTiesOverTimeAndKeySignatures.value
                 width: parent.width
                 Layout.columnSpan: 2

-                onClicked: {
-                    root.pageModel.maskTiesOverTimeAndKeySignatures.value = !maskTiesOverTimeAndKeySignaturesCheckbox.checked
-                }
+                onToggled: root.pageModel.maskTiesOverTimeAndKeySignatures.value = checked
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/notationscene/qml/MuseScore/NotationScene/styledialog/TiesSection.qml`
around lines 180 - 189, The CheckBox handler is negating the already-updated
checked state and thus writes the old value back; in the CheckBox with id
maskTiesOverTimeAndKeySignaturesCheckbox, stop inverting checked in the
onClicked handler and instead assign the model directly from the current checked
value (or use onToggled) so
root.pageModel.maskTiesOverTimeAndKeySignatures.value =
maskTiesOverTimeAndKeySignaturesCheckbox.checked (or use onToggled:
root.pageModel.maskTiesOverTimeAndKeySignatures.value = checked).
src/notationscene/qml/MuseScore/NotationScene/styledialog/SlursSection.qml (1)

137-146: ⚠️ Potential issue | 🟠 Major

Write the toggled state back directly.

checked has already changed by the time onClicked runs, so negating it here restores the old value and the setting never sticks. Use onToggled and assign checked directly instead.

In Qt Quick Controls 2, when a CheckBox is clicked, does the `checked` property change before the `clicked`/`onClicked` handler runs, and is `onToggled: modelProperty = checked` the recommended way to sync a boolean model value?
Proposed fix
             CheckBox {
                 id: maskSlursOverTimeAndKeySignaturesCheckbox
                 text: qsTrc("notation/editstyle/slursandties", "Mask slurs over time and key signatures")
                 checked: root.pageModel.maskSlursOverTimeAndKeySignatures.value
                 width: parent.width
                 Layout.columnSpan: 2

-                onClicked: {
-                    root.pageModel.maskSlursOverTimeAndKeySignatures.value = !maskSlursOverTimeAndKeySignaturesCheckbox.checked
-                }
+                onToggled: root.pageModel.maskSlursOverTimeAndKeySignatures.value = checked
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/notationscene/qml/MuseScore/NotationScene/styledialog/SlursSection.qml`
around lines 137 - 146, The checkbox handler is reverting the new state because
onClicked runs after checked already toggled; update the handler to use
onToggled and write the checked value directly to the model (replace the
onClicked block on maskSlursOverTimeAndKeySignaturesCheckbox with an onToggled
handler that sets root.pageModel.maskSlursOverTimeAndKeySignatures.value =
checked), ensuring the UI state and model stay in sync.
src/engraving/rendering/score/masklayout.cpp (1)

83-91: ⚠️ Potential issue | 🟠 Major

Continuation slur/tie segments are still skipped.

The isSingleBeginType() guard means only the first system segment ever gets a mask. A slur or tie that continues onto a later system still won't be masked over that system's header key/time signature.

Proposed fix
         if (maskSlurs || maskTies) {
             for (SpannerSegment* spannerSeg : system->spannerSegments()) {
-                if (!spannerSeg->isSlurTieSegment() || !spannerSeg->isSingleBeginType()
+                if (!spannerSeg->isSlurTieSegment()
                     || !system->staff(spannerSeg->staffIdx())->show() || !spannerSeg->visible()) {
                     continue;
                 }
                 if ((maskSlurs && spannerSeg->isSlurSegment()) || (maskTies && spannerSeg->isTieSegment())) {
                     computeSlurTieMasks(toSlurTieSegment(spannerSeg), SegmentType::KeySig | SegmentType::TimeSig);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/engraving/rendering/score/masklayout.cpp` around lines 83 - 91, The loop
currently skips continuation slur/tie segments due to the isSingleBeginType()
guard; remove that check so continuation segments are considered as well—i.e.,
keep the filters isSlurTieSegment(), staff(...)->show(), and visible() but drop
the isSingleBeginType() requirement so
computeSlurTieMasks(toSlurTieSegment(spannerSeg), SegmentType::KeySig |
SegmentType::TimeSig) runs for slur/tie segments on later systems (when
maskSlurs or maskTies apply).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/engraving/dom/spanner.cpp`:
- Around line 394-401: SpannerSegment::isUserModified currently returns only the
local `modified` boolean, which omits the base-class check and thus can drop
edits like Pid::Z; change the function to combine the local check with the
base-class result by returning `modified || EngravingItem::isUserModified()` so
that edits detected by EngravingItem (e.g., Z-order) are preserved; update the
return expression in SpannerSegment::isUserModified accordingly.

---

Duplicate comments:
In `@src/engraving/rendering/score/masklayout.cpp`:
- Around line 83-91: The loop currently skips continuation slur/tie segments due
to the isSingleBeginType() guard; remove that check so continuation segments are
considered as well—i.e., keep the filters isSlurTieSegment(),
staff(...)->show(), and visible() but drop the isSingleBeginType() requirement
so computeSlurTieMasks(toSlurTieSegment(spannerSeg), SegmentType::KeySig |
SegmentType::TimeSig) runs for slur/tie segments on later systems (when
maskSlurs or maskTies apply).

In `@src/notationscene/qml/MuseScore/NotationScene/styledialog/SlursSection.qml`:
- Around line 137-146: The checkbox handler is reverting the new state because
onClicked runs after checked already toggled; update the handler to use
onToggled and write the checked value directly to the model (replace the
onClicked block on maskSlursOverTimeAndKeySignaturesCheckbox with an onToggled
handler that sets root.pageModel.maskSlursOverTimeAndKeySignatures.value =
checked), ensuring the UI state and model stay in sync.

In `@src/notationscene/qml/MuseScore/NotationScene/styledialog/TiesSection.qml`:
- Around line 180-189: The CheckBox handler is negating the already-updated
checked state and thus writes the old value back; in the CheckBox with id
maskTiesOverTimeAndKeySignaturesCheckbox, stop inverting checked in the
onClicked handler and instead assign the model directly from the current checked
value (or use onToggled) so
root.pageModel.maskTiesOverTimeAndKeySignatures.value =
maskTiesOverTimeAndKeySignaturesCheckbox.checked (or use onToggled:
root.pageModel.maskTiesOverTimeAndKeySignatures.value = checked).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d9ab3254-3b26-4a84-abb2-4d259c926bf9

📥 Commits

Reviewing files that changed from the base of the PR and between e33c150 and 92c9618.

📒 Files selected for processing (14)
  • src/engraving/dom/engravingitem.h
  • src/engraving/dom/slurtie.cpp
  • src/engraving/dom/spanner.cpp
  • src/engraving/dom/spanner.h
  • src/engraving/rendering/score/masklayout.cpp
  • src/engraving/rendering/score/masklayout.h
  • src/engraving/rendering/score/tdraw.cpp
  • src/engraving/style/styledef.cpp
  • src/engraving/style/styledef.h
  • src/engraving/tests/readwriteundoreset_data/slurs.mscx
  • src/notationscene/qml/MuseScore/NotationScene/styledialog/SlursSection.qml
  • src/notationscene/qml/MuseScore/NotationScene/styledialog/TiesSection.qml
  • src/notationscene/qml/MuseScore/NotationScene/styledialog/slursandtiespagemodel.cpp
  • src/notationscene/qml/MuseScore/NotationScene/styledialog/slursandtiespagemodel.h
💤 Files with no reviewable changes (2)
  • src/engraving/dom/slurtie.cpp
  • src/engraving/tests/readwriteundoreset_data/slurs.mscx

continue;
}
if ((maskSlurs && spannerSeg->isSlurSegment()) || (maskTies && spannerSeg->isTieSegment())) {
computeSlurTieMasks(toSlurTieSegment(spannerSeg), SegmentType::KeySig | SegmentType::TimeSig);
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.

No reason to pass the segment type from here, it can be written within computeSlurTieMask

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.

You're right, I kept it in case we wanted to separately personalise the types to mask in the future, but it's unnecessary for now. I'll update.

TRACEFUNC;

Spanner* spanner = slurTieSegment->spanner();
staff_idx_t spannerStaffTrackIdx = spanner->staffIdx() * VOICES;
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.

track_idx_t spannerTrackIdx = spanner->track();

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.

Wouldn't this be a bit too specific? If the time and key signatures are always on voice 0, the ties/slurs corresponding to other voices on the same staff wouldn't be masked, so you could end up with something like this:

Captura de pantalla 2026-04-16 a las 19 58 56

PS: this makes me wonder about cross-staff notation, maybe we shouldn't filter by staff either?

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.

Ha! You're right, good point

Copy link
Copy Markdown
Contributor

@mike-spa mike-spa Apr 17, 2026

Choose a reason for hiding this comment

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

I think you can use trackZeroVoice for that. And for cross-staff notation (very good observation, I would have brought it up later) you can either use Spanner::track2 which is precisely meant for that, or you can get start- and end chord and use their vStaffIdx(), and collect the key- and timeSigs from both staves

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.

Good to know, I'll have a look!

continue;
}

Shape itemShape = item->shape().translated(itemPos);
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.

You could move this above the if and then do if (!slurTieShape.bbox.intersects(itemShape).padded(...))

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

♻️ Duplicate comments (1)
src/engraving/rendering/score/masklayout.cpp (1)

376-380: ⚠️ Potential issue | 🟠 Major

Filter key/time-sig items by the spanner's staff/track.

seg->elist() returns items across all tracks of that segment. For a slur/tie on one staff this ends up masking key/time signatures that belong to other staves' tracks, which can produce spurious or out-of-staff masks — and for cross-staff spanners the correct staves may be missed entirely.

Use the spanner's own track (and, for slurs, track2() and/or the vStaffIdx() of start/end CRs for cross-staff) to restrict the mask targets, e.g. iterate only the relevant staff2track(staffIdx) ranges on the segment.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/engraving/rendering/score/masklayout.cpp` around lines 376 - 380,
seg->elist() currently collects items from all tracks causing key/time
signatures from other tracks to be masked; restrict itemsToMaskOver to only the
spanner's relevant track(s): use the spanner's track (and for slurs/ties check
track2() and the vStaffIdx() of the start/end CRs for cross-staff cases) to
compute the staff2track(staffIdx) range(s) for this spanner and iterate only
those tracks' items on the segment instead of seg->elist(); update the loop that
fills itemsToMaskOver to query per-track item lists (or filter seg->elist() by
item->track() matching the computed track range) so only same-staff (or intended
cross-staff) key/time-sign items are considered.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/engraving/dom/spanner.cpp`:
- Line 469: The call ls->setZ(z()) triggers the virtual SpannerSegment::setZ
which forwards to m_spanner->setZ and re-writes Z on every segment; instead call
the non-virtual base implementation directly to only initialize the new segment.
Replace the virtual dispatch (ls->setZ(z())) with an explicit base-class call
(e.g., ls->Segment::setZ(z())) so only the new segment's Z is set; adjust the
call site where ls is created/initialized to use Segment::setZ rather than the
SpannerSegment override.

In `@src/engraving/rendering/score/masklayout.cpp`:
- Around line 421-431: Rename the ambiguous parameters in
MaskLayout::createFilteredItemShape to clarify filtering direction: change
overlyingItemShape -> filterShape and maskedItemShape -> targetShape (or
sourceShape -> maskedShape if you prefer), and update all call sites
accordingly; keep the internal logic and uses of filteredItemShape,
collisionPadding, ShapeElement, elements(), intersects, padded and add unchanged
so behavior is preserved while improving readability at invocation sites.
- Line 371: Compute spanner->endSegment() once before the loop and reuse it
instead of calling it on every iteration: call Spanner::endSegment() into a
local const Segment* (e.g., endSeg) prior to the for loop, then change the loop
condition from seg && seg != spanner->endSegment() to seg && seg != endSeg so
the expensive tick2segment/lookups inside Spanner::endSegment() are not executed
on each iteration.

---

Duplicate comments:
In `@src/engraving/rendering/score/masklayout.cpp`:
- Around line 376-380: seg->elist() currently collects items from all tracks
causing key/time signatures from other tracks to be masked; restrict
itemsToMaskOver to only the spanner's relevant track(s): use the spanner's track
(and for slurs/ties check track2() and the vStaffIdx() of the start/end CRs for
cross-staff cases) to compute the staff2track(staffIdx) range(s) for this
spanner and iterate only those tracks' items on the segment instead of
seg->elist(); update the loop that fills itemsToMaskOver to query per-track item
lists (or filter seg->elist() by item->track() matching the computed track
range) so only same-staff (or intended cross-staff) key/time-sign items are
considered.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: bed90766-d2f5-4794-839b-f5def53a5360

📥 Commits

Reviewing files that changed from the base of the PR and between 92c9618 and 6f73a6f.

📒 Files selected for processing (3)
  • src/engraving/dom/spanner.cpp
  • src/engraving/rendering/score/masklayout.cpp
  • src/engraving/rendering/score/masklayout.h

ls->setSelected(selected());
ls->setTrack(track());
// ls->setAutoplace(autoplace());
ls->setZ(z());
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

Minor: unnecessary propagation to sibling segments.

ls->setZ(z()) invokes SpannerSegment::setZ, which in turn calls m_spanner->setZ(val) and re-writes Z on every already-existing segment (and the spanner) to the same value. Since only the new segment needs initialization, calling the non-virtual base directly avoids this redundant fan-out:

♻️ Proposed tweak
-    ls->setZ(z());
+    ls->EngravingItem::setZ(z());
📝 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
ls->setZ(z());
ls->EngravingItem::setZ(z());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/engraving/dom/spanner.cpp` at line 469, The call ls->setZ(z()) triggers
the virtual SpannerSegment::setZ which forwards to m_spanner->setZ and re-writes
Z on every segment; instead call the non-virtual base implementation directly to
only initialize the new segment. Replace the virtual dispatch (ls->setZ(z()))
with an explicit base-class call (e.g., ls->Segment::setZ(z())) so only the new
segment's Z is set; adjust the call site where ls is created/initialized to use
Segment::setZ rather than the SpannerSegment override.


Spanner* spanner = slurTieSegment->spanner();
std::vector<const EngravingItem*> itemsToMaskOver;
for (const Segment* seg = spanner->startSegment(); seg && seg != spanner->endSegment(); seg = seg->next1()) {
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 | 🟡 Minor

Hoist spanner->endSegment() out of the loop condition.

Spanner::endSegment() is not trivial — it performs tick2segment lookups and may fall back to further segment searches. Evaluating it on every iteration is wasteful; compute it once before the loop.

♻️ Proposed fix
-    for (const Segment* seg = spanner->startSegment(); seg && seg != spanner->endSegment(); seg = seg->next1()) {
+    const Segment* const endSeg = spanner->endSegment();
+    for (const Segment* seg = spanner->startSegment(); seg && seg != endSeg; seg = seg->next1()) {
📝 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
for (const Segment* seg = spanner->startSegment(); seg && seg != spanner->endSegment(); seg = seg->next1()) {
const Segment* const endSeg = spanner->endSegment();
for (const Segment* seg = spanner->startSegment(); seg && seg != endSeg; seg = seg->next1()) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/engraving/rendering/score/masklayout.cpp` at line 371, Compute
spanner->endSegment() once before the loop and reuse it instead of calling it on
every iteration: call Spanner::endSegment() into a local const Segment* (e.g.,
endSeg) prior to the for loop, then change the loop condition from seg && seg !=
spanner->endSegment() to seg && seg != endSeg so the expensive
tick2segment/lookups inside Spanner::endSegment() are not executed on each
iteration.

Comment on lines +421 to +431
Shape MaskLayout::createFilteredItemShape(const Shape& overlyingItemShape, const Shape& maskedItemShape, const double collisionPadding)
{
Shape filteredItemShape;
filteredItemShape.elements().reserve(overlyingItemShape.elements().size());
for (const ShapeElement& el : overlyingItemShape.elements()) {
if (maskedItemShape.intersects(el.padded(collisionPadding))) {
filteredItemShape.add(el);
}
}
return filteredItemShape;
}
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

Nice extraction.

Good deduplication of the filtering logic shared between barline-for-text and slur/tie masking. The parameter names overlyingItemShape / maskedItemShape read a bit ambiguously (both are "items"); renaming to something like filterShape / targetShape (or sourceShape / maskedShape) could make the filtering direction clearer at call sites, but this is cosmetic.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/engraving/rendering/score/masklayout.cpp` around lines 421 - 431, Rename
the ambiguous parameters in MaskLayout::createFilteredItemShape to clarify
filtering direction: change overlyingItemShape -> filterShape and
maskedItemShape -> targetShape (or sourceShape -> maskedShape if you prefer),
and update all call sites accordingly; keep the internal logic and uses of
filteredItemShape, collisionPadding, ShapeElement, elements(), intersects,
padded and add unchanged so behavior is preserved while improving readability at
invocation sites.

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.

You cannot "put back" a tie (only a slur)

2 participants