-
Notifications
You must be signed in to change notification settings - Fork 2.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement partial & repeat ties #25745
Conversation
@@ -1080,7 +1080,7 @@ std::set<size_t> CompatMidiRender::getNotesIndexesToRender(Chord* chord) | |||
} | |||
|
|||
auto noteShouldBeRendered = [](Note* n) { | |||
while (n->tieBack() && n != n->tieBack()->startNote()) { | |||
while (n->tieBack() && !n->incomingPartialTie() && n != n->tieBack()->startNote()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've seen this check repeated several times: every time you are looking for a tie-back-but-not-incoming-partial. Perhaps this is worth adding a tieBackNonPartial()
method?
src/engraving/dom/edit.cpp
Outdated
static Tie* createAndAddTie(Note* startNote, Note* endNote) | ||
{ | ||
Score* score = startNote->score(); | ||
const bool createPartialTie = !endNote; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I actually find it more readable without this additional bool that's just the negation of another
Tie* tie = toTie(originalNote->tieFor()->linkedClone()); | ||
tie->setScore(score); | ||
newNote->setTieFor(tie); | ||
tie->setStartNote(newNote); | ||
tie->setTrack(newNote->track()); | ||
tieMap.add(originalNote->tieFor(), tie); | ||
} | ||
if (originalNote->tieBack()) { | ||
if (originalNote->tieBack() && originalNote->tieBack()->type() == ElementType::TIE) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above (kind of). Maybe we could modify tieBack
and tieFor
to return only "real" ties, and not LV and/or partial ties?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I considered this, but it seemed to me that there were far more scenarios where we want tieFor
and tieBack
to return LVs & partial ties than when we didn't.
src/engraving/dom/measure.cpp
Outdated
|
||
Segment* seg = first(SegmentType::ChordRest); | ||
while (seg) { | ||
for (track_idx_t track = startTrack; track < ntracks; track++) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since you're checking all the available tracks, I think you can more compactly do
for (EngravingItem* item : seg->elist()) {}
src/engraving/dom/note.cpp
Outdated
tieFor()->endNote()->setTieBack(tieFor()); | ||
} | ||
} | ||
|
||
bool Note::followingJumpItem() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From the name of this function I expected it to return an EngravingItem. You could either rename it into hasFollowingJumpItem
, or it seems like it could also be useful to return the items themselves rather than bools?
src/engraving/dom/note.cpp
Outdated
return false; | ||
} | ||
|
||
String Note::precedingJumpItemName() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this function really should not be here. At a first glance, I thought this was the symmetric of followingJumpItem but it looks like it has a completely different use and the fact that it returns a String left me quite confused.
From what I understand, the string returned by this function has a UI-related purpose which is exclusively referred to partial ties, so it definitely should be in the PartialTie class (perhaps as a static method)
src/engraving/dom/tie.h
Outdated
void toggleEndPoint(const String& id); | ||
|
||
void addTie(TieEndPoint* endPoint); | ||
void removeTie(TieEndPoint* endPoint); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fact that there is a public add
method but also an addTie
is a bit odd. Someone reading the public interface of your class wouldn't know why they are different and which one they should use. Could there be a more descriptive name?
src/engraving/dom/tie.cpp
Outdated
{ | ||
Note* note = endPoint->note(); | ||
Score* score = note ? note->score() : nullptr; | ||
Tie* _startTie = startTie(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
m_startTie
src/engraving/dom/tie.cpp
Outdated
if (std::find_if((*tieEndPoints()).begin(), (*tieEndPoints()).end(), findEndTie) != (*tieEndPoints()).end()) { | ||
continue; | ||
} | ||
score()->undoRemoveElement(tie); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A method of the Tie class should not be removing other ties from the Score, it could be a bit dangerous. Also, from a function named collectPossibleEndPoints I'd probably expect something which returns me a list of TieEndPoint*, not something which actively modifies the current tie and even the score. Here I would try to do a combination of (not necessarily all of them)
- Giving this method a more descriptive name
- Moving out the code which removes other ties into a separate function
- Given that this method performs an undoable action on the score, it looks more like the kind of methods we usually put in edit.cpp, that's also an option
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this code to remove ties is absolutely in the wrong place - it doesn't even cover all the situations I was hoping.
I think we need to remove stale endpoints when the repeat structure is recalculated - either inside or just after RepeatList::unwind
.
src/engraving/dom/tie.h
Outdated
@@ -25,6 +25,64 @@ | |||
#include "slurtie.h" | |||
|
|||
namespace mu::engraving { | |||
class TieEndPointList; | |||
class TieEndPoint |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Side question: would TieJumpPoint be perhaps a better name for this class? I know what you mean by TieEndPoint, but it feels to me like it doesn't communicate the main purpose. Also because we often call "end point" the graphical end points too.
src/engraving/dom/note.cpp
Outdated
case ElementType::PARTIAL_TIE: { | ||
PartialTie* pt = toPartialTie(e); | ||
pt->setTick(tick()); | ||
if (pt->partialSpannerDirection() == PartialSpannerDirection::OUTGOING) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pt->itOutGoing()
?
Add separate style option for hanging ties
3c25b0a
to
ea6001f
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here are some first (somewhat superficial) review comments. I haven't been able yet to take a deeper look at some parts of the code, so I might add some more comments later; if you prefer, you can of course wait until then before fixing these first comments.
src/engraving/dom/barline.cpp
Outdated
@@ -103,6 +103,8 @@ static void undoChangeBarLineType(BarLine* bl, BarLineType barType, bool allStav | |||
// createMMRest will then set for the mmrest directly | |||
Measure* m2 = m->isMMRest() ? m->mmRestLast() : m; | |||
|
|||
BarLineType prevBarType = bl->barLineType(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this used?
@@ -207,6 +206,7 @@ class Measure final : public MeasureBase | |||
Segment* firstActive() const { return m_segments.firstActive(); } | |||
|
|||
Segment* last() const { return m_segments.last(); } | |||
Segment* last(SegmentType t) const { return m_segments.last(t); } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
General remark: I'm sometimes a bit worried about the fact that we keep adding more and more auxiliary methods and overloads to central classes like EngravingItem, Score, Measure, Segment and Chord(Rest). On the one hand, reducing code duplication is good. On the other hand, having huge classes that are used in many places but contain many methods that are used only a few times, is not ideal, for example for compilation time and understandability.
For now I don't suggest to change anything in this regard, but maybe when everyone is back we should have a discussion at some point with the team about what we want to do with this dilemma.
@@ -32,6 +32,9 @@ MenuView { | |||
|
|||
property alias model: view.model | |||
|
|||
property NavigationSection notationViewNavigationSection: null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The UiComponents
module should not contain the word "notation", because it is intended to be as general as possible. Something like navigationSectionOverride
would be okay though.
onNotationViewNavigationSectionChanged: function() { | ||
menuNavPanel.section = root.notationViewNavigationSection | ||
} | ||
onNavigationOrderStartChanged: function() { | ||
menuNavPanel.order = root.navigationOrderStart | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't very idiomatic QML: instead of setting the property imperatively to a fixed value, a binding (inside the NavigationPanel
would be better:
section: root.navigationSectionOverride ?? content.navigationSection
order: root.navigationOrderStart
(which would remove the need for the addition of the id: menuNavPanel
line)
property int navigationOrderStart: 0 | ||
property int navigationOrderEnd: partialTieNavPanel.order | ||
|
||
property QtObject model: partialTiePopupModel |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be good to make this and similar properties in other popups readonly
and alias
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
notationViewNavigationSection
and navigationOrderStart
are set in ElementPopupLoader.qml, so can't be readonly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I was unclear; I was referring to the model
properties specifically here.
</widget> | ||
</item> | ||
<item> | ||
<widget class="QDoubleSpinBox" name="minHangingTieLength"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be good to fix the keyboard navigation order too, to prevent a situation like #25530; see that issue for info
onItemsChanged: function() { | ||
tieMenuLoader.items = partialTiePopupModel.items | ||
tieMenuLoader.show(Qt.point(0, 0)) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could also be done in a more idiomatic way:
- the
tieMenuLoader.items = partialTiePopupModel.items
assignment is not necessary, because insidetieMenuLoader
you create a "binding" by writingitems: partialTiePopupModel.items
, which means thattieMenuLoader.items
is automatically updated whenpartialTiePopupModel.items
is updated. - as a consequence, for the
.show(…)
call, it is better to writeinsideonItemsChanged: function() { tieMenuLoader.show(Qt.point(0, 0)) }
tieMenuLoader
.
var component = prv.componentByType(elementType) | ||
|
||
var popup = loader.loadPopup(component, elementRect) | ||
if (!popup.model.canOpen) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be more ideal to make this decision earlier, in C++, to avoid needlessly creating and destroying popup windows.
showArrow: false | ||
|
||
onOpened: { | ||
tieMenuLoader.show(Qt.point(0, 0)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is a little bit of an ugly hack to have a StyledPopupView with nothing in it, and then launch a ContextMenuLoader from it. It would be nicer if we can avoid the StyledPopupView. That would require a bit of not-entirely-trivial refactoring though in, ElementPopupLoader
. It might be better to save that for a next PR. Also, we should check if this hack does not cause graphical weirdnesses, like an extra border somewhere, or double drop shadow in a corner; if it does, that might increase the priority of the follow-up refactoring PR a bit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I agree it's messy and not ideal. I'd like to address it outside of this PR though.
contentWidth: content.width | ||
contentHeight: content.childrenRect.height | ||
|
||
ColumnLayout { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps this should not be a ColumnLayout but a simple Item
, or perhaps even make the ContextMenuLoader
itself a direct child of root
, if that works (putting the model and NavigationPanel inside the ContextMenuLoader).
@cbjeukendrup I've made the changes requested - I'd like to make any other changes in future PRs so we can get this merged asap! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few more comments; feel free to merge this PR and address them later.
It would be great if you could provide an explanation of the structure of the PR: which information is stored how and where; how is the 'tree of ownership' (e.g. a Note owns a PartialTie), how are the crosslinks within that tree (e.g. a partial tie might have a reference to its counterpart(s)); etc..
Of course I should be able to extract that information from the code itself, but due to the size of the code, that is quite difficult in this case, both mentally and practically. So a small explanation to help me interpret what I'm seeing would increase efficiency a lot, to avoid constantly having to scroll up and down and use Ctrl+F all the time.
That said, I've not been able to look at every line of this PR; even if I'd do a local-only review, i.e. without trying to get a full understanding of the structure of the whole PR, it would take me some hours. So I've scanned it and commented on a few things that caught my eye. But again, feel free to address them in later PRs indeed.
TranslatableString undoCmd = addPartialTie ? TranslatableString("engraving", "Replace full tie with partial tie") : TranslatableString( | ||
"engraving", "Replace partial tie with full tie"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(This line got split in an unfortunate way)
class TieJumpPointList; | ||
class TieJumpPoint |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you explain how these classes are used? Who "owns" them, the Note or the Tie? And who is responsible for creating and delete
ing them?
It might even be good to add some documentation comments to the code itself, because for an uninformed reader it is not immediately clear what's going on.
Also, these classes (and especially their implementation) are big enough to give them their own file.
const TranslatableString tieTo("engraving", "Tie to "); | ||
const String title = tieTo.str + precedingJumpItemName() + u" " + muse::mtrc("engraving", "(m. %1)").arg(measureNo); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are a few issues here:
- the
tieTo
string won't be detected bylupdate
, because you need to write explicitlyTranslatableString(…, …)
- translatable strings ending with a space are not allowed. Normally the Linux build would complain about them, but now it doesn't, because the string is not detected. The reasons are
- translators can easily overlook the trailing whitespace, or fail to understand its importance
- concatenating strings becomes problematic for languages with different word order
tieTo.str
only means"Tie to "
, i.e. it does not return a translated version- without more info, translators can't know what
"m. %1"
is supposed to mean.
Something like the following should work:
//: An explanation of the meaning of this string can go here, in this special `//:` comment on the line above
//: For example, explain what %1 and %2 will be replaced with
return muse::mtrc("engraving", "Tie to %1 (m. %2)")
.arg(precedingJumpItemName(), measureNo);
And, translators will need to know that the strings from precedingJumpItemName
will be inserted into this string: in many languages, after a preposition, a different form of the word is used. To make sure that those strings won't be mixed up with others, you could add a disambiguation, which is the third argument of mtrc
.
//: Used at %1 in the string "Tie to %1 (m. %2)"
return muse::mtrc("engraving", "start of score", "partial tie target");
//: Used at %1 in the string "Tie to %1 (m. %2)"
return muse::mtrc("engraving", "coda", "partial tie target");
bool active() const { return m_active; } | ||
void setActive(bool v) { m_active = v; } | ||
void undoSetActive(bool v); | ||
void setJumpPointList(TieJumpPointList* jumpPointList) { m_jumpPointList = jumpPointList; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not the biggest fan of the pattern where a list item needs to know in which list it is being stored. Perhaps we can find a more elegant solution at some point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes neither am I! As you can see below this is solely down to needing to be able to find the start tie from an end tie. I'd like to find a different way of finding this, as it creates a trail of pointers which need to be kept updated (recipe for disaster)
MasterScore* master = masterScore(); | ||
const Note* note = toNote(parentItem()); | ||
const Chord* chord = note->chord(); | ||
const Measure* measure = chord->measure(); | ||
const MeasureBase* masterMeasureBase = master->measure(measure->index()); | ||
const Measure* masterMeasure = masterMeasureBase && masterMeasureBase->isMeasure() ? toMeasure(masterMeasureBase) : nullptr; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like these could be moved after the two early-returns
@cbjeukendrup hope this clears some things up. This is just information on the relationship between elements - who owns what, and why certain pointers are being stored. PartialTie
Tie
tieJumpPoints()This returns the setJumpPoint() & jumpPoint()This sets TieJumpPointnote()This returns endTie()This returns jumpPointList()This returns a pointer to the TieJumpPointListEach note has a m_jumpPoints
startTie()This returns the start tie. This is the undoAddTieToScore(TieJumpPoint*) & undoRemoveTieFromScore(TieJumpPoint*)These methods create the correct type of tie and add them to the score. So in short - ownership & management of |
Resolves: #25426
This PR introduces partial ties to MuseScore, enabling ties to be connected across repeats. When adding a tie before a repeat item (repeat barlines, markers, jumps, and voltas) if any note on the other side of the repeat destination can be tied, a partial tie will be added.
When more than one endpoint is available, clicking the outgoing tie will open a menu of possible endpoints. These can be toggled on and off.
Screen.Recording.2024-12-05.at.14.33.29.mov
Playback implementation to follow.