From 077049741b37363cbc84cffcba79d2c2dfd20b91 Mon Sep 17 00:00:00 2001 From: ajuncosa Date: Tue, 30 Jun 2026 17:00:13 +0200 Subject: [PATCH 1/9] Allow melismas under rests --- src/notation/internal/notationinteraction.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/notation/internal/notationinteraction.cpp b/src/notation/internal/notationinteraction.cpp index 4dfef306f2f84..12a3383464b5d 100644 --- a/src/notation/internal/notationinteraction.cpp +++ b/src/notation/internal/notationinteraction.cpp @@ -7977,11 +7977,6 @@ void NotationInteraction::addMelisma() } } segment = segment->prev1(SegmentType::ChordRest); - // if the segment has a rest in this track, stop going back - EngravingItem* e = segment ? segment->element(track) : 0; - if (e && !e->isChord()) { - break; - } } // one-chord melisma? From 99f9316d9c1058aaef542e37f3899597158030da Mon Sep 17 00:00:00 2001 From: ajuncosa Date: Tue, 30 Jun 2026 17:19:12 +0200 Subject: [PATCH 2/9] Fix crashes in lyrics due to early deletion of elements during UndoableTransaction::unwind --- src/engraving/dom/score.h | 2 +- src/engraving/editing/cmd.cpp | 4 ++-- src/engraving/editing/textedit.cpp | 4 +++- src/engraving/editing/transaction/transaction.cpp | 7 +++++-- src/engraving/editing/transaction/transaction.h | 2 +- src/engraving/editing/transaction/undostack.cpp | 6 ++++-- src/engraving/editing/transaction/undostack.h | 2 +- 7 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/engraving/dom/score.h b/src/engraving/dom/score.h index efc3e4e210198..f6e3934958027 100644 --- a/src/engraving/dom/score.h +++ b/src/engraving/dom/score.h @@ -462,7 +462,7 @@ class Score : public EngravingObject, public muse::Contextable void cmdFullMeasureRest(); void startCmd(const TranslatableString& actionName); // start undoable command - void endCmd(bool rollback = false, bool layoutAllParts = false); // end undoable command + void endCmd(bool rollback = false, bool layoutAllParts = false, bool keepRolledBackElements = false); // end undoable command void update(); void undoRedo(bool undo, EditData*); diff --git a/src/engraving/editing/cmd.cpp b/src/engraving/editing/cmd.cpp index 1208a30971f05..f9f3bf4541c32 100644 --- a/src/engraving/editing/cmd.cpp +++ b/src/engraving/editing/cmd.cpp @@ -316,9 +316,9 @@ void Score::undoRedo(bool undo, EditData* ed) /// and (always) updating the redraw area. //--------------------------------------------------------- -void Score::endCmd(bool rollback, bool layoutAllParts) +void Score::endCmd(bool rollback, bool layoutAllParts, bool keepRolledBackElements) { - masterScore()->transactionManager()->endTransaction(rollback, layoutAllParts); + masterScore()->transactionManager()->endTransaction(rollback, layoutAllParts, keepRolledBackElements); } //--------------------------------------------------------- diff --git a/src/engraving/editing/textedit.cpp b/src/engraving/editing/textedit.cpp index 78216a37d51ee..b23bd4cc397f7 100644 --- a/src/engraving/editing/textedit.cpp +++ b/src/engraving/editing/textedit.cpp @@ -183,7 +183,9 @@ void TextBase::endEdit(EditData& ed) undo->reopen(); if (newlyAdded) { - score()->endCmd(true); // rollback the "add element" command + // Rollback the "AddElement" command, but don't delete the rolled-back element(s): + score()->endCmd(true, false, /*keepRolledBackElements*/ true); + // Defer deletion (to ~TextEditData): ted->deleteText = true; } else { score()->undoRemoveElement(this); diff --git a/src/engraving/editing/transaction/transaction.cpp b/src/engraving/editing/transaction/transaction.cpp index 421e292efcaef..6900f4052bc08 100644 --- a/src/engraving/editing/transaction/transaction.cpp +++ b/src/engraving/editing/transaction/transaction.cpp @@ -148,7 +148,7 @@ void TransactionManager::beginTransaction(const muse::TranslatableString& descri m_currentTransaction = std::unique_ptr(new Transaction(stack->activeTransaction())); } -void TransactionManager::endTransaction(bool rollback, bool layoutAllParts) +void TransactionManager::endTransaction(bool rollback, bool layoutAllParts, bool keepRolledBackElements) { UndoStack* stack = undoStack(); @@ -167,7 +167,10 @@ void TransactionManager::endTransaction(bool rollback, bool layoutAllParts) } if (rollback) { - stack->activeTransaction()->unwind(); + /* When keepRolledBackElements is true, unwind removes the rolled-back elements from + * the score but does not free them: an external owner is responsible for the deferred + * deletion, since the elements are still referenced after this returns. */ + stack->activeTransaction()->unwind(!keepRolledBackElements); } // NOTE: perform update and re-layout within the transaction, so that diff --git a/src/engraving/editing/transaction/transaction.h b/src/engraving/editing/transaction/transaction.h index 2b887a9e40eef..39732b7ddfa90 100644 --- a/src/engraving/editing/transaction/transaction.h +++ b/src/engraving/editing/transaction/transaction.h @@ -133,7 +133,7 @@ class TransactionManager void transaction(const muse::TranslatableString& description, std::function func); void beginTransaction(const muse::TranslatableString& description); - void endTransaction(bool rollback = false, bool layoutAllParts = false); + void endTransaction(bool rollback = false, bool layoutAllParts = false, bool keepRolledBackElements = false); /// Returns the current transaction, if any is active. /// Where possible, prefer using `transaction()` instead of this method. diff --git a/src/engraving/editing/transaction/undostack.cpp b/src/engraving/editing/transaction/undostack.cpp index 2f7ae5a4e48ef..22931fae09440 100644 --- a/src/engraving/editing/transaction/undostack.cpp +++ b/src/engraving/editing/transaction/undostack.cpp @@ -260,13 +260,15 @@ void UndoableTransaction::cleanup(bool wasDone) } } -void UndoableTransaction::unwind() +void UndoableTransaction::unwind(bool cleanUp) { while (!m_commands.empty()) { UndoableCommand* command = muse::takeLast(m_commands); LOG_UNDO() << "unwind: " << command->name(); command->undo(); - command->cleanup(false); + if (cleanUp) { + command->cleanup(false); + } delete command; } } diff --git a/src/engraving/editing/transaction/undostack.h b/src/engraving/editing/transaction/undostack.h index 4e6ab185a03bb..6636d9a0c7983 100644 --- a/src/engraving/editing/transaction/undostack.h +++ b/src/engraving/editing/transaction/undostack.h @@ -52,7 +52,7 @@ class UndoableTransaction void appendCommand(UndoableCommand* cmd) { m_commands.push_back(cmd); } void append(UndoableTransaction&& other); - void unwind(); + void unwind(bool cleanUp = true); void cleanup(bool wasDone); const std::vector& commands() const { return m_commands; } From df2a909275ccbf011a96514dc40ec86e4ac2f25b Mon Sep 17 00:00:00 2001 From: ajuncosa Date: Mon, 6 Jul 2026 10:44:36 +0200 Subject: [PATCH 3/9] Fix incorrect lyrics syllablevertical positioning when removing an empty lyrics text input (after a dash or melisma line) --- src/engraving/editing/textedit.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/engraving/editing/textedit.cpp b/src/engraving/editing/textedit.cpp index b23bd4cc397f7..52bb97e1fb1ee 100644 --- a/src/engraving/editing/textedit.cpp +++ b/src/engraving/editing/textedit.cpp @@ -207,7 +207,8 @@ void TextBase::endEdit(EditData& ed) Lyrics* prev = Navigation::prevLyrics(toLyrics(this)); if (prev) { prev->setNeedRemoveInvalidSegments(); - renderer()->layoutItem(prev); + prev->triggerLayout(); + score()->update(); } } From 17d61a858ba63a681729231ea5633b68f51a83cd Mon Sep 17 00:00:00 2001 From: ajuncosa Date: Tue, 7 Jul 2026 13:26:13 +0200 Subject: [PATCH 4/9] Fix creation of melisma and syllable separators across MM rests --- src/engraving/dom/utils.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/engraving/dom/utils.cpp b/src/engraving/dom/utils.cpp index 5a82ae6ac0c55..da67bd57e50ac 100644 --- a/src/engraving/dom/utils.cpp +++ b/src/engraving/dom/utils.cpp @@ -1572,6 +1572,13 @@ bool segmentsAreAdjacent(const Segment* firstSeg, const Segment* secondSeg) const Measure* firstMasterMeasure = master->tick2measure(firstMeasure->tick()); const Measure* secondMasterMeasure = master->tick2measure(secondMeasure->tick()); + if (firstMasterMeasure) { + firstMasterMeasure = firstMasterMeasure->coveringMMRestOrThis(); + } + if (secondMasterMeasure) { + secondMasterMeasure = secondMasterMeasure->coveringMMRestOrThis(); + } + Score* score = firstSeg->score(); const RepeatList& repeatList = score->repeatList(true, false); From a468cc8f02de6e204c811dda665b37048a40239d Mon Sep 17 00:00:00 2001 From: ajuncosa Date: Tue, 7 Jul 2026 18:30:54 +0200 Subject: [PATCH 5/9] Fix underscore removal when a syllable is entered after a one-note melisma --- src/engraving/dom/lyrics.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/engraving/dom/lyrics.cpp b/src/engraving/dom/lyrics.cpp index e943295d9f5f2..a6ae914eefd02 100644 --- a/src/engraving/dom/lyrics.cpp +++ b/src/engraving/dom/lyrics.cpp @@ -299,7 +299,7 @@ bool Lyrics::isEditAllowed(EditData& ed) const void Lyrics::adjustPrevious() { Lyrics* prev = Navigation::prevLyrics(toLyrics(this)); - if (prev) { + if (prev && prev->ticks().isNotZero()) { // search for lyric spanners to split at this point if necessary if (prev->tick() + prev->ticks() >= tick()) { // the previous lyric has a spanner attached that goes through this one @@ -312,10 +312,10 @@ void Lyrics::adjustPrevious() } else { prev->undoChangeProperty(Pid::LYRIC_TICKS, Fraction::eps()); } - prev->setNeedRemoveInvalidSegments(); - prev->triggerLayout(); } } + prev->setNeedRemoveInvalidSegments(); + prev->triggerLayout(); } } From fedd47d0ac7e0a3f5be246602c7171bfa3734309 Mon Sep 17 00:00:00 2001 From: ajuncosa Date: Tue, 7 Jul 2026 19:27:04 +0200 Subject: [PATCH 6/9] Revert "Fix incorrect lyrics syllable vertical positioning when removing an empty lyrics text input (after a dash or melisma line)" This reverts commit 1216b4d0df2d36d8c54370836a31ca42e4ee8f79. --- src/engraving/editing/textedit.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/engraving/editing/textedit.cpp b/src/engraving/editing/textedit.cpp index 52bb97e1fb1ee..b23bd4cc397f7 100644 --- a/src/engraving/editing/textedit.cpp +++ b/src/engraving/editing/textedit.cpp @@ -207,8 +207,7 @@ void TextBase::endEdit(EditData& ed) Lyrics* prev = Navigation::prevLyrics(toLyrics(this)); if (prev) { prev->setNeedRemoveInvalidSegments(); - prev->triggerLayout(); - score()->update(); + renderer()->layoutItem(prev); } } From e61024ecce66962e4d4c091689c0ab77f937f6ae Mon Sep 17 00:00:00 2001 From: ajuncosa Date: Tue, 7 Jul 2026 19:47:13 +0200 Subject: [PATCH 7/9] Fix incorrect lyrics syllable vertical positioning --- src/engraving/rendering/score/lyricslayout.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/engraving/rendering/score/lyricslayout.cpp b/src/engraving/rendering/score/lyricslayout.cpp index 65cea9b2bc100..f85bd9544ba11 100644 --- a/src/engraving/rendering/score/lyricslayout.cpp +++ b/src/engraving/rendering/score/lyricslayout.cpp @@ -120,8 +120,14 @@ void LyricsLayout::layout(Lyrics* item, LayoutContext& ctx) ChordRest* cr = item->chordRest(); double x = -cr->x(); + /* Preserve the vertical position: a lyric's Y is set in computeVerticalPositions(). Necessary + * because layoutBaseTextBase1() ends with ldata->move(defaultPos), which accumulates a downward + * shift of the lyric if the current function is called in a partially-relaid system without a + * following computeVerticalPositions() call. */ + const double posY = ldata->pos().y(); TextLayout::layoutBaseTextBase1(item, ctx); TextLayout::computeTextHighResShape(item, ldata); + ldata->setPosY(posY); double centerAdjust = 0.0; double leftAdjust = 0.0; From 4038ecf40bad66426db0d7d0b4ba6e904731ae17 Mon Sep 17 00:00:00 2001 From: ajuncosa Date: Thu, 9 Jul 2026 17:08:21 +0200 Subject: [PATCH 8/9] Fix measure/mmrest comparisons in collectRepeatListElements so repeat segments will be properly calculated with mmrest in voltas --- src/engraving/dom/repeatlist.cpp | 5 +++-- src/engraving/dom/utils.cpp | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/engraving/dom/repeatlist.cpp b/src/engraving/dom/repeatlist.cpp index 3ba4c9a40d76f..3d71e46060cd8 100644 --- a/src/engraving/dom/repeatlist.cpp +++ b/src/engraving/dom/repeatlist.cpp @@ -526,9 +526,10 @@ void RepeatList::collectRepeatListElements() if (mb->isMeasure()) { Measure* m = toMeasure(mb); sectionEndMeasureBase = mb; // ending measure of section is the most recently encountered actual Measure + Measure* underlyingMeasure = m->isMMRest() ? m->mmRestFirst() : m; // Volta ? - if ((!preProcessedVoltas.empty()) && (preProcessedVoltas.front()->startMeasure() == m)) { + if ((!preProcessedVoltas.empty()) && (preProcessedVoltas.front()->startMeasure() == underlyingMeasure)) { if (volta != nullptr) { //if (volta->endMeasure()->tick() < m->tick()) { // The previous volta was supposed to end before us (open volta case) -> insert the end @@ -546,7 +547,7 @@ void RepeatList::collectRepeatListElements() // Start if (m->repeatStart()) { if (volta != nullptr) { - if (volta->startMeasure() != m) { + if (volta->startMeasure() != underlyingMeasure) { // Volta and Start repeat are not on the same measure // assume the previous volta was supposed to end before us (open volta case) -> insert the end // Warning: This might "break" a volta prematurely if its explicit notated end is later than this point diff --git a/src/engraving/dom/utils.cpp b/src/engraving/dom/utils.cpp index da67bd57e50ac..63b4676a31c83 100644 --- a/src/engraving/dom/utils.cpp +++ b/src/engraving/dom/utils.cpp @@ -1506,7 +1506,7 @@ std::vector findPreviousRepeatMeasures(const Measure* measure) const MasterScore* master = measure->masterScore(); const Score* score = measure->score(); - const Measure* masterMeasure = master->tick2measure(measure->tick()); + const Measure* masterMeasure = master->tick2measureMM(measure->tick()); const RepeatList& repeatList = master->repeatList(true, false); @@ -1526,7 +1526,7 @@ std::vector findPreviousRepeatMeasures(const Measure* measure) // Get next segment const RepeatSegment* prevSeg = *prevSegIt; const Measure* lastMasterMeasure = prevSeg->lastMeasure(); - Measure* lastMeasure = lastMasterMeasure ? score->tick2measure(lastMasterMeasure->tick()) : nullptr; + Measure* lastMeasure = lastMasterMeasure ? score->tick2measureMM(lastMasterMeasure->tick()) : nullptr; if (!lastMeasure) { continue; } From 0587b44297fc32025f2f11d003a17ad95f1704b7 Mon Sep 17 00:00:00 2001 From: ajuncosa Date: Wed, 15 Jul 2026 11:20:06 +0200 Subject: [PATCH 9/9] Fix crashes due to empty fields in the mscx --- src/engraving/editing/textedit.cpp | 32 ++++++++++++++++++------------ 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/engraving/editing/textedit.cpp b/src/engraving/editing/textedit.cpp index b23bd4cc397f7..3a18b9e53f190 100644 --- a/src/engraving/editing/textedit.cpp +++ b/src/engraving/editing/textedit.cpp @@ -160,14 +160,15 @@ void TextBase::endEdit(EditData& ed) } } - const bool newlyAdded = ted->oldXmlText.isEmpty(); + const bool textStartedEmpty = ted->oldXmlText.isEmpty(); + const UndoableTransaction* addTransaction = !textStartedEmpty ? nullptr + : (textWasEdited ? undo->prev() : undo->last()); + const bool newlyAdded = addTransaction + && addTransaction->hasCommandsMatchingFilter(UndoableCommandFilter::AddElement, this); if (newlyAdded) { - const UndoableTransaction* transaction = textWasEdited ? undo->prev() : undo->last(); - if (transaction && transaction->hasCommandsMatchingFilter(UndoableCommandFilter::AddElement, this)) { - // We have just added this element to a score. - // Combine undo records of text creation with text editing. - undo->mergeTransactions(ted->startUndoIdx - 1); - } + // We have just added this element to a score. + // Combine undo records of text creation with text editing. + undo->mergeTransactions(ted->startUndoIdx - 1); } // TBox'es manage their Text themselves and are not removed if text is empty @@ -176,18 +177,23 @@ void TextBase::endEdit(EditData& ed) if (actualPlainText.isEmpty() && removeTextIfEmpty) { LOGD("actual text is empty"); - // If this assertion fails, no undo command relevant to this text - // resides on undo stack and reopen() would corrupt the previous - // command. Text shouldn't happen to be empty in other cases though. - assert(newlyAdded || textWasEdited); - - undo->reopen(); if (newlyAdded) { + undo->reopen(); // Rollback the "AddElement" command, but don't delete the rolled-back element(s): score()->endCmd(true, false, /*keepRolledBackElements*/ true); // Defer deletion (to ~TextEditData): ted->deleteText = true; } else { + if (textWasEdited) { + // The text was just edited down to empty: the top transaction is this element's own edit: + undo->reopen(); + } else { + /* The text was already empty before editing began (e.g. an empty text from a + * legacy/corrupt file). No undo command relevant to this text resides on undo + * stack and reopen() would corrupt the previous command: */ + score()->startCmd(TranslatableString("undoableAction", "Remove empty text")); + } + score()->undoRemoveElement(this); if (isLyrics()) {