diff --git a/src/engraving/dom/lyrics.cpp b/src/engraving/dom/lyrics.cpp index 74976a7f30686..2dab93cfa642b 100644 --- a/src/engraving/dom/lyrics.cpp +++ b/src/engraving/dom/lyrics.cpp @@ -557,6 +557,8 @@ void Score::forAllLyrics(std::function f) void Lyrics::undoChangeProperty(Pid id, const PropertyValue& v, PropertyFlags ps) { if (id == Pid::VERSE && verse() != v.toInt()) { + PartialLyricsLine* prevPartial = findPrevPartialLyricsLineDash(this); + for (Lyrics* l : chordRest()->lyrics()) { if (l->verse() == v.toInt()) { // verse already exists, swap @@ -568,6 +570,10 @@ void Lyrics::undoChangeProperty(Pid id, const PropertyValue& v, PropertyFlags ps } } TextBase::undoChangeProperty(id, v, ps); + if (prevPartial && prevPartial->verse() != v.toInt()) { + // Skip logic to update Lyrics by calling parent class + prevPartial->LyricsLine::undoChangeProperty(id, v, ps); + } return; } diff --git a/src/engraving/dom/lyrics.h b/src/engraving/dom/lyrics.h index ef3cb0e607a01..78568e2e431b9 100644 --- a/src/engraving/dom/lyrics.h +++ b/src/engraving/dom/lyrics.h @@ -96,6 +96,7 @@ class Lyrics final : public TextBase PropertyValue getProperty(Pid propertyId) const override; bool setProperty(Pid propertyId, const PropertyValue&) override; PropertyValue propertyDefault(Pid id) const override; + void undoChangeProperty(Pid id, const PropertyValue&, PropertyFlags ps) override; void triggerLayout() const override; double yRelativeToStaff() const; @@ -110,8 +111,6 @@ class Lyrics final : public TextBase Lyrics(ChordRest* parent); Lyrics(const Lyrics&); - void undoChangeProperty(Pid id, const PropertyValue&, PropertyFlags ps) override; - int m_verse = 0; // row index Fraction m_ticks; // if > 0 then draw an underline to tick() + _ticks (melisma) LyricsSyllabic m_syllabic = LyricsSyllabic::SINGLE; @@ -229,6 +228,7 @@ class PartialLyricsLine final : public LyricsLine bool setProperty(Pid propertyId, const PropertyValue&) override; PropertyValue propertyDefault(Pid propertyId) const override; Sid getPropertyStyle(Pid propertyId) const override; + void undoChangeProperty(Pid id, const PropertyValue&, PropertyFlags ps) override; Lyrics* findLyricsInPreviousRepeatSeg() const; Lyrics* findAdjacentLyricsOrDefault() const; diff --git a/src/engraving/dom/lyricsline.cpp b/src/engraving/dom/lyricsline.cpp index de12764c04b95..7e527a8821059 100644 --- a/src/engraving/dom/lyricsline.cpp +++ b/src/engraving/dom/lyricsline.cpp @@ -292,6 +292,31 @@ Sid PartialLyricsLine::getPropertyStyle(Pid propertyId) const } } +void PartialLyricsLine::undoChangeProperty(Pid id, const PropertyValue& v, PropertyFlags ps) +{ + if (id == Pid::VERSE && verse() != v.toInt()) { + ChordRest* endCR = endElement() + && endElement()->isChordRest() ? toChordRest(endElement()) : nullptr; + Lyrics* endLyrics = nullptr; + if (endCR) { + for (Lyrics* lyr : endCR->lyrics()) { + if (lyr->verse() == verse()) { + endLyrics = lyr; + break; + } + } + } + + LyricsLine::undoChangeProperty(id, v, ps); + if (endLyrics && endLyrics->verse() != v.toInt()) { + endLyrics->undoChangeProperty(id, v, ps); + } + return; + } + + LyricsLine::undoChangeProperty(id, v, ps); +} + void PartialLyricsLine::doComputeEndElement() { LyricsLine::doComputeEndElement(); diff --git a/src/engraving/dom/score.cpp b/src/engraving/dom/score.cpp index 053df9a884adc..0b56ae73f3e47 100644 --- a/src/engraving/dom/score.cpp +++ b/src/engraving/dom/score.cpp @@ -1555,28 +1555,37 @@ void Score::removeElement(EngravingItem* element) ) { MeasureBase* mb = toMeasureBase(element); measures()->remove(mb); - System* system = mb->system(); + System* system = mb->system(); if (!system) { - // vertical boxes are not shown in continuous view so no system #ifndef NDEBUG - bool noSystemMode = lineMode() && element->isVBoxBase(); -#endif + // vertical boxes are not shown in continuous view so no system + const bool noSystemMode = lineMode() && element->isVBoxBase(); assert(noSystemMode || !isOpen()); +#endif return; } - Page* page = system->page(); - if (element->isBox() && system->measures().size() == 1) { - auto i = std::find(page->systems().begin(), page->systems().end(), system); - page->systems().erase(i); - mb->resetExplicitParent(); - if (page->systems().empty()) { + system->removeMeasure(mb); + + // See also InsertRemoveMeasures::removeMeasures() + if (element->isBox() && system->measures().empty()) { + Page* page = system->page(); + if (page) { + muse::remove(page->systems(), system); + } + + muse::remove(m_systems, system); + deleteLater(system); + + if (page && page->systems().empty()) { // Remove this page, since it is now empty. // This involves renumbering and repositioning all subsequent pages. PointF pos = page->pos(); auto ii = std::find(pages().begin(), pages().end(), page); pages().erase(ii); + deleteLater(page); + while (ii != pages().end()) { page = *ii; page->setPageNumber(page->pageNumber() - 1); @@ -1587,7 +1596,6 @@ void Score::removeElement(EngravingItem* element) } } } -// setLayout(mb->tick()); return; } diff --git a/src/engraving/editing/editmeasures.cpp b/src/engraving/editing/editmeasures.cpp index f677450db6957..7466a16853b1b 100644 --- a/src/engraving/editing/editmeasures.cpp +++ b/src/engraving/editing/editmeasures.cpp @@ -339,10 +339,16 @@ void InsertRemoveMeasures::removeMeasures() if (page) { // erase system from page muse::remove(page->systems(), s); - // erase system from score - muse::remove(score->systems(), s); - // finally delete system - score->deleteLater(s); + } + // erase system from score + muse::remove(score->systems(), s); + // finally delete system + score->deleteLater(s); + + if (page && page->systems().empty()) { + // if page is empty, delete it as well + muse::remove(score->pages(), page); + score->deleteLater(page); } } } diff --git a/src/engraving/rendering/score/chordlayout.cpp b/src/engraving/rendering/score/chordlayout.cpp index fb59f8c394173..28b1e5413eb76 100644 --- a/src/engraving/rendering/score/chordlayout.cpp +++ b/src/engraving/rendering/score/chordlayout.cpp @@ -166,12 +166,6 @@ void ChordLayout::layoutPitched(Chord* item, LayoutContext& ctx) } } - for (EngravingItem* e : item->el()) { - if (e->isChordBracket()) { - TLayout::layoutItem(e, ctx); - } - } - // A chord can have its own arpeggio and also be part of another arpeggio's span. We need to lay out both of these arpeggios properly Arpeggio* oldSpanArp = item->spanArpeggio(); Arpeggio* newSpanArp = nullptr; @@ -296,7 +290,9 @@ void ChordLayout::layoutPitched(Chord* item, LayoutContext& ctx) } for (EngravingItem* e : item->el()) { - if (e->isSlur()) { // we cannot at this time as chordpositions are not fixed + // Cannot layout slurs as chord positions are not fixed + // Chord brackets should be the outermost element + if (e->isSlur() || e->isChordBracket()) { continue; } TLayout::layoutItem(e, ctx); @@ -341,6 +337,12 @@ void ChordLayout::layoutPitched(Chord* item, LayoutContext& ctx) createParenGroups(item); ParenthesisLayout::layoutChordParentheses(item, ctx); + for (EngravingItem* e : item->el()) { + if (e->isChordBracket()) { + TLayout::layoutItem(e, ctx); + } + } + fillShape(item, item->mutldata(), ctx.conf()); } diff --git a/src/engraving/rendering/score/parenthesislayout.cpp b/src/engraving/rendering/score/parenthesislayout.cpp index 477a4f505b87e..e153121480341 100644 --- a/src/engraving/rendering/score/parenthesislayout.cpp +++ b/src/engraving/rendering/score/parenthesislayout.cpp @@ -553,7 +553,9 @@ Shape ParenthesisLayout::getParentShape(const EngravingItem* parent) return !s.item() || s.item()->isParenthesis() || (s.item()->isLaissezVibSegment() && isChord) || (s.item()->isHook() && isChord) - || (s.item()->isStem() && isChord); + || (s.item()->isStem() && isChord) + || (s.item()->isArpeggio() && isChord) + || (s.item()->isChordBracket() && isChord); }); return parentShape; diff --git a/src/engraving/rendering/score/slurtielayout.cpp b/src/engraving/rendering/score/slurtielayout.cpp index 73a2eb24f1a30..cc4d7659bffd3 100644 --- a/src/engraving/rendering/score/slurtielayout.cpp +++ b/src/engraving/rendering/score/slurtielayout.cpp @@ -2639,6 +2639,9 @@ void SlurTieLayout::computeBezier(TieSegment* tieSeg, PointF shoulderOffset) { const PointF tieStart = tieSeg->ups(Grip::START).p + tieSeg->ups(Grip::START).off; const PointF tieEnd = tieSeg->ups(Grip::END).p + tieSeg->ups(Grip::END).off; + if (!muse::RealIsEqualOrMore(tieEnd.x(), tieStart.x())) { + return; + } PointF tieEndNormalized = tieEnd - tieStart; // normalize to zero if (muse::RealIsNull(tieEndNormalized.x())) { diff --git a/src/engraving/rw/read460/tread.cpp b/src/engraving/rw/read460/tread.cpp index 3a37f8b3c6084..79e8eab9f5d9b 100644 --- a/src/engraving/rw/read460/tread.cpp +++ b/src/engraving/rw/read460/tread.cpp @@ -781,7 +781,7 @@ void TRead::read(Dynamic* d, XmlReader& e, ReadContext& ctx) } else if (tag == "play") { d->setPlayDynamic(e.readBool()); } else if (ctx.mscVersion() < 470 && tag == "dynamicsSize") { - d->setSymbolScale(e.readDouble()); + readProperty(d, e, ctx, Pid::MUSICAL_SYMBOLS_SCALE); } else if (readProperty(d, tag, e, ctx, Pid::AVOID_BARLINES)) { } else if (readProperty(d, tag, e, ctx, Pid::CENTER_ON_NOTEHEAD)) { } else if (readProperty(d, tag, e, ctx, Pid::ANCHOR_TO_END_OF_PREVIOUS)) { diff --git a/src/engraving/rw/write/twrite.cpp b/src/engraving/rw/write/twrite.cpp index 5bf38ff82ae7b..6c1e1055fbfa7 100644 --- a/src/engraving/rw/write/twrite.cpp +++ b/src/engraving/rw/write/twrite.cpp @@ -1025,7 +1025,7 @@ void TWrite::write(const Chord* item, XmlWriter& xml, WriteContext& ctx) if (item->hook() && item->hook()->isUserModified()) { write(item->hook(), xml, ctx); } - if (item->showStemSlash() && item->isUserModified()) { + if (item->showStemSlash() != item->propertyDefault(Pid::SHOW_STEM_SLASH).toBool()) { xml.tag("showStemSlash", item->showStemSlash()); } if (item->stemSlash() && item->stemSlash()->isUserModified()) { diff --git a/src/engraving/tests/CMakeLists.txt b/src/engraving/tests/CMakeLists.txt index 831d0729b47ac..e63fa9aa31dd5 100644 --- a/src/engraving/tests/CMakeLists.txt +++ b/src/engraving/tests/CMakeLists.txt @@ -98,6 +98,7 @@ set(MODULE_TEST_SRC ${CMAKE_CURRENT_LIST_DIR}/fretdiagram_tests.cpp ${CMAKE_CURRENT_LIST_DIR}/engraving_xml_tests.cpp ${CMAKE_CURRENT_LIST_DIR}/parentheses_tests.cpp + ${CMAKE_CURRENT_LIST_DIR}/lyrics_tests.cpp ${CMAKE_CURRENT_LIST_DIR}/automation/automation_tests.cpp ${CMAKE_CURRENT_LIST_DIR}/mocks/engravingconfigurationmock.h diff --git a/src/engraving/tests/lyrics_data/partialLyricsLineVerse.mscx b/src/engraving/tests/lyrics_data/partialLyricsLineVerse.mscx new file mode 100755 index 0000000000000..3fb51711bfef8 --- /dev/null +++ b/src/engraving/tests/lyrics_data/partialLyricsLineVerse.mscx @@ -0,0 +1,231 @@ + + + 4.7.0 + + + nKXCiaEoEhL_6aillYgKmGN + 480 + 1 + 1 + 1 + 0 + 1 + + + Composer / arranger + + 2026-05-11 + + + + Apple Macintosh + + + + + Untitled score + + Orchestra + + Flutes + +
+ flutes + oboes + clarinets + saxophones + bassoons + +
+
+ horns + trumpets + cornets + flugelhorns + trombones + tubas + +
+
+ timpani +
+
+ keyboard-percussion + + drums + unpitched-metal-percussion + unpitched-wooden-percussion + other-percussion + +
+ keyboards + harps + organs + synths + + +
+ voices + voice-groups +
+
+ orchestral-strings +
+
+ + t4wOfNXvET_MB7K3yCV9bO + + CG/iy0suU5M_8apoiL+qw2P + + stdNormal + + + + + Flute + Fl. + + Flute + 59 + 98 + 60 + 93 + wind.flutes.flute + + + Fluid + + + + + + mKe/djNTwGI_s1XmcbjJpUL + 2 + + + cUnqv/ph9gI_LPTcl7EN5KB + 0 + + + XgRNHq69m7K_S3NLE+JPF5K + 4 + 4 + + + + 1 + 1. + +t9p05H+WZE_dUtgtdQFXUJ + 1 + + + + 1 + + + + + 28VKFi2fDqK_9xUWGpjsqy + measure + 4/4 + + + + + Xr5lOYXqQ3N_cA97Vqf75gL + + + + + -1 + + + + + + 0 + a396oaBKAiJ_VRT1nJG6RCJ + + + + 3/4 + + + + + + 2. + 4xcpnAOTAoL_n4evRtiDRJG + 2 + + + + 1/1 + + + + + 3CiytIHP3RJ_ALMjoc/8kLJ + quarter + + m7fQthHZQJO_7DgH01TN/EM + 67 + 15 + + + + fvBsBY0kU4M_oSukTnvYBhB + quarter + + Rb2Rvj8SSME_zaNY72NA+cI + 67 + 15 + + + + iWINXjjGjhC_hyQB/0KmcXO + quarter + + zkh7KqvN1tI_uzBzYEU/fTD + 67 + 15 + + + + + + -3/4 + + + + + VwNj0yokoxC_HzD3rfGZTsB + quarter + + end + z+ILmWw4Gq_BSmKiWdn0aD + l1 + + + 1 + 6REO+yewDvG_38oVnz2lL2D + l2 + + + o/LBWL7i30E_B9M2co1VMIE + 67 + 15 + + + + + + -1/1 + + + + + + +
+
diff --git a/src/engraving/tests/lyrics_tests.cpp b/src/engraving/tests/lyrics_tests.cpp new file mode 100644 index 0000000000000..cfcd952c173cc --- /dev/null +++ b/src/engraving/tests/lyrics_tests.cpp @@ -0,0 +1,85 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-Studio-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2026 MuseScore Limited and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include + +#include "engraving/dom/lyrics.h" +#include "engraving/dom/utils.h" +#include "utils/scorerw.h" + +using namespace mu::engraving; + +static const String LYRICS_DATA_DIR("lyrics_data/"); + +class Engraving_LyricsTests : public ::testing::Test +{ +}; + +TEST_F(Engraving_LyricsTests, PartialLyricsLineVerse) +{ + // Check partial lyrics dash verses are linked to their ending lyric + MasterScore* score = ScoreRW::readScore(LYRICS_DATA_DIR + u"partialLyricsLineVerse.mscx"); + + Measure* m2 = score->first()->nextMeasure(); + + EXPECT_TRUE(m2); + + ChordRest* endCR = m2->lastChordRest(0); + + EXPECT_TRUE(endCR); + + Lyrics* l1 = nullptr; + Lyrics* l2 = nullptr; + + for (Lyrics* l : endCR->lyrics()) { + if (l->verse() == 0) { + l1 = l; + } + if (l->verse() == 1) { + l2 = l; + } + } + + EXPECT_TRUE(l1); + EXPECT_TRUE(l2); + + PartialLyricsLine* pll = findPrevPartialLyricsLineDash(l1); + + EXPECT_TRUE(pll); + EXPECT_EQ(pll->verse(), 0); + + score->startCmd(TranslatableString::untranslatable("Lyrics tests")); + pll->undoChangeProperty(Pid::VERSE, 1, PropertyFlags::UNSTYLED); + score->endCmd(); + + EXPECT_EQ(pll->verse(), 1); + EXPECT_EQ(l1->verse(), 1); + EXPECT_EQ(l2->verse(), 0); + + score->startCmd(TranslatableString::untranslatable("Lyrics tests")); + l1->undoChangeProperty(Pid::VERSE, 0, PropertyFlags::UNSTYLED); + score->endCmd(); + + EXPECT_EQ(pll->verse(), 0); + EXPECT_EQ(l1->verse(), 0); + EXPECT_EQ(l2->verse(), 1); +} diff --git a/src/importexport/mei/internal/meiimporter.cpp b/src/importexport/mei/internal/meiimporter.cpp index 783d827e6b389..63ba60e9624ba 100644 --- a/src/importexport/mei/internal/meiimporter.cpp +++ b/src/importexport/mei/internal/meiimporter.cpp @@ -450,54 +450,57 @@ bool MeiImporter::addGraceNotesToChord(ChordRest* chordRest, bool isAfter) EngravingItem* MeiImporter::addAnnotation(const libmei::Element& meiElement, Measure* measure) { - const ChordRest* chordRest = this->findStart(meiElement, measure); - if (!chordRest || chordRest->isGrace()) { + ControlElementPosition pos = this->findStart(meiElement, measure); + if (!pos.measure || (pos.chordRest && pos.chordRest->isGrace())) { return nullptr; } - Segment* segment = chordRest->segment(); + Segment* segment = pos.measure->getSegment(SegmentType::ChordRest, pos.tick); EngravingItem* item = nullptr; if (meiElement.m_name == "breath" || meiElement.m_name == "caesura") { // For Breath we need to add a specific segment and add the breath to it (and not to the ChordRest one) - segment = measure->getSegment(SegmentType::Breath, chordRest->endTick()); + Fraction tick = (pos.chordRest) ? pos.chordRest->endTick() : pos.tick; + segment = pos.measure->getSegment(SegmentType::Breath, tick); item = Factory::createBreath(segment); } else if (meiElement.m_name == "dir") { ElementType elementType = Convert::elementTypeForDir(meiElement); switch (elementType) { - case (ElementType::PLAYTECH_ANNOTATION): item = Factory::createPlayTechAnnotation( - chordRest->segment(), PlayingTechniqueType::Natural, TextStyleType::STAFF); + case (ElementType::PLAYTECH_ANNOTATION): + item = Factory::createPlayTechAnnotation(segment, PlayingTechniqueType::Natural, TextStyleType::STAFF); break; - case (ElementType::STAFF_TEXT): item = Factory::createStaffText(chordRest->segment()); + case (ElementType::STAFF_TEXT): + item = Factory::createStaffText(segment); break; - case (ElementType::SYSTEM_TEXT): item = Factory::createSystemText(chordRest->segment()); + case (ElementType::SYSTEM_TEXT): + item = Factory::createSystemText(segment); break; default: - item = Factory::createExpression(chordRest->segment()); + item = Factory::createExpression(segment); } } else if (meiElement.m_name == "dynam") { - item = Factory::createDynamic(chordRest->segment()); + item = Factory::createDynamic(segment); } else if (meiElement.m_name == "fermata") { - item = Factory::createFermata(chordRest->segment()); + item = Factory::createFermata(segment); } else if (meiElement.m_name == "harm") { const libmei::AttLabelled* labeledAtt = dynamic_cast(&meiElement); if (labeledAtt && (labeledAtt->GetLabel() == MEI_FB_HARM)) { - item = Factory::createFiguredBass(chordRest->segment()); + item = Factory::createFiguredBass(segment); } else { - item = Factory::createHarmony(chordRest->segment()); + item = Factory::createHarmony(segment); } } else if (meiElement.m_name == "harpPedal") { - item = Factory::createHarpPedalDiagram(chordRest->segment()); + item = Factory::createHarpPedalDiagram(segment); } else if (meiElement.m_name == "reh") { - item = Factory::createRehearsalMark(chordRest->segment()); + item = Factory::createRehearsalMark(segment); } else if (meiElement.m_name == "tempo") { - item = Factory::createTempoText(chordRest->segment()); + item = Factory::createTempoText(segment); } else { return nullptr; } this->readXmlId(item, meiElement.m_xmlId); - item->setTrack(chordRest->track()); + item->setTrack(pos.track); segment->add(item); return item; @@ -512,41 +515,41 @@ EngravingItem* MeiImporter::addAnnotation(const libmei::Element& meiElement, Mea Spanner* MeiImporter::addSpanner(const libmei::Element& meiElement, Measure* measure, pugi::xml_node node) { - ChordRest* chordRest = this->findStart(meiElement, measure); - if (!chordRest) { + ControlElementPosition pos = this->findStart(meiElement, measure); + if (!pos.measure) { return nullptr; } Spanner* item = nullptr; + Segment* segment = pos.measure->getSegment(SegmentType::ChordRest, pos.tick); if (meiElement.m_name == "dir") { ElementType elementType = Convert::elementTypeForDirWithExt(meiElement); switch (elementType) { - case (ElementType::HAIRPIN): item = Factory::createHairpin( - chordRest->segment()); + case (ElementType::HAIRPIN): item = Factory::createHairpin(segment); break; default: - item = Factory::createTextLine(chordRest->segment()); + item = Factory::createTextLine(segment); } } else if (meiElement.m_name == "hairpin") { - item = Factory::createHairpin(chordRest->segment()); + item = Factory::createHairpin(segment); } else if (meiElement.m_name == "octave") { - item = Factory::createOttava(chordRest->segment()); + item = Factory::createOttava(segment); } else if (meiElement.m_name == "pedal") { - item = Factory::createPedal(chordRest->segment()); + item = Factory::createPedal(segment); } else if (meiElement.m_name == "slur") { - item = Factory::createSlur(chordRest->segment()); + item = Factory::createSlur(segment); } else if (meiElement.m_name == "trill") { - item = Factory::createTrill(chordRest->segment()); + item = Factory::createTrill(segment); } else { return nullptr; } this->readXmlId(item, meiElement.m_xmlId); - item->setTick(chordRest->tick()); - item->setStartElement(chordRest); - item->setTrack(chordRest->track()); - item->setTrack2(chordRest->track()); + item->setTick(pos.tick); + item->setStartElement(pos.chordRest); + item->setTrack(pos.track); + item->setTrack2(pos.track); m_score->addElement(item); @@ -566,7 +569,7 @@ Spanner* MeiImporter::addSpanner(const libmei::Element& meiElement, Measure* mea EngravingItem* MeiImporter::addToChordRest(const libmei::Element& meiElement, Measure* measure, Chord* chord) { - ChordRest* chordRest = (!measure) ? chord : this->findStart(meiElement, measure); + ChordRest* chordRest = (!measure) ? chord : this->findStart(meiElement, measure).chordRest; if (!chordRest) { return nullptr; } @@ -614,23 +617,26 @@ std::string MeiImporter::xmlIdFrom(std::string dataURI) * If there is not @startid but a @tstamp (MEI not written by MuseScore), try to find the corresponding ChordRest */ -ChordRest* MeiImporter::findStart(const libmei::Element& meiElement, Measure* measure) +ControlElementPosition MeiImporter::findStart(const libmei::Element& meiElement, Measure* measure) { + ControlElementPosition pos; const libmei::AttStartId* startIdAtt = dynamic_cast(&meiElement); IF_ASSERT_FAILED(measure && startIdAtt) { - return nullptr; + return pos; } - ChordRest* chordRest = nullptr; if (startIdAtt->HasStartid()) { std::string startId = this->xmlIdFrom(startIdAtt->GetStartid()); // The startid corresponding ChordRest should have been added to the m_startIdChordRests previously if (!m_startIdChordRests.count(startId) || !m_startIdChordRests.at(startId)) { Convert::logs.push_back(String("Could not find element for @startid '%1'").arg(String::fromStdString( startIdAtt->GetStartid()))); - return nullptr; + return pos; } - chordRest = m_startIdChordRests.at(startId); + pos.chordRest = m_startIdChordRests.at(startId); + pos.measure = pos.chordRest->measure(); + pos.tick = pos.chordRest->tick(); + pos.track = pos.chordRest->track(); } else { // No @startid, try a lookup based on the @tstamp. This is only for files not written via MuseScore const libmei::AttTimestampLog* timestampLogAtt = dynamic_cast(&meiElement); @@ -638,7 +644,7 @@ ChordRest* MeiImporter::findStart(const libmei::Element& meiElement, Measure* me const libmei::AttLayerIdent* layerIdentAtt = dynamic_cast(&meiElement); IF_ASSERT_FAILED(timestampLogAtt && staffIdentAtt) { - return nullptr; + return pos; } // If no @tstamp (invalid), put it on 1.0; @@ -648,14 +654,13 @@ ChordRest* MeiImporter::findStart(const libmei::Element& meiElement, Measure* me staffIdentAtt->GetStaff().at(0)) : 0; const int layer = (layerIdentAtt && layerIdentAtt->HasLayer()) ? this->getVoiceIndex(staffIdx, layerIdentAtt->GetLayer()) : 0; - chordRest = measure->findChordRest(measure->tick() + tstampFraction, staffIdx * VOICES + layer); - if (!chordRest) { - Convert::logs.push_back(String("Could not find element corresponding to @tstamp '%1'").arg(timestampLogAtt->GetTstamp())); - return nullptr; - } + pos.measure = measure; + pos.tick = measure->tick() + tstampFraction; + pos.track = staffIdx * VOICES + layer; + pos.chordRest = measure->findChordRest(pos.tick, pos.track); } - return chordRest; + return pos; } /** @@ -664,20 +669,23 @@ ChordRest* MeiImporter::findStart(const libmei::Element& meiElement, Measure* me * If there is not @endid but a @tstamp2 (MEI not written by MuseScore), try to find the corresponding ChordRest */ -ChordRest* MeiImporter::findEnd(pugi::xml_node controlNode, const ChordRest* startChordRest) +ControlElementPosition MeiImporter::findEnd(pugi::xml_node controlNode, Spanner* spanner) { + ControlElementPosition pos; libmei::InstStartEndId startEndIdAtt; startEndIdAtt.ReadStartEndId(controlNode); - ChordRest* chordRest = nullptr; if (startEndIdAtt.HasEndid()) { std::string endId = this->xmlIdFrom(startEndIdAtt.GetEndid()); // The @endid corresponding ChordRest should have been added to the m_endIdChordRests previously if (!m_endIdChordRests.count(endId) || !m_endIdChordRests.at(endId)) { Convert::logs.push_back(String("Could not find element for @endid '%1'").arg(String::fromStdString(startEndIdAtt.GetEndid()))); - return nullptr; + return pos; } - chordRest = m_endIdChordRests.at(endId); + pos.chordRest = m_endIdChordRests.at(endId); + pos.measure = pos.chordRest->measure(); + pos.tick = pos.chordRest->tick(); + pos.track = pos.chordRest->track(); } else { // No @endid, try a lookup based on the @tstamp2. This is only for files not written via MuseScore libmei::InstTimestamp2Log timestamp2LogAtt; @@ -687,40 +695,43 @@ ChordRest* MeiImporter::findEnd(pugi::xml_node controlNode, const ChordRest* sta libmei::InstLayerIdent layerIdentAtt; layerIdentAtt.ReadLayerIdent(controlNode); - // We need at least a @tstamp2 and a startChordRest with its Measure - if (!timestamp2LogAtt.HasTstamp2() || !startChordRest || !startChordRest->measure()) { - return nullptr; + // We need at least a @tstamp2 and a spanner with its startMeasure + Measure* startM = (spanner->startElement()) + ? spanner->startElement()->findMeasure() + : m_score->tick2measure(spanner->tick()); + + if (!timestamp2LogAtt.HasTstamp2() || !startM) { + return pos; } libmei::data_MEASUREBEAT tstamp2Value = timestamp2LogAtt.GetTstamp2(); // Find the end Measure - Measure* measure = startChordRest->measure(); + Measure* measure = startM; for (int i = tstamp2Value.first; i > 0; --i) { if (!measure->next() || !measure->next()->isMeasure()) { - return nullptr; + return pos; } measure = toMeasure(measure->next()); } + pos.measure = measure; Fraction tstampFraction = Convert::tstampToFraction(tstamp2Value.second, measure->timesig()); - // Use the startChordRest staffIdx unless given in @staff + // Use the spanner staffIdx unless given in @staff staff_idx_t staffIdx = (staffIdentAtt.HasStaff() && staffIdentAtt.GetStaff().size() > 0) ? this->getStaffIndex( - staffIdentAtt.GetStaff().at(0)) : startChordRest->staffIdx(); - // Use the startChordRest voice unless given in @layer - track_idx_t layer - = (layerIdentAtt.HasLayer()) ? this->getVoiceIndex(static_cast(staffIdx), - layerIdentAtt.GetLayer()) : startChordRest->voice(); + staffIdentAtt.GetStaff().at(0)) : track2staff(spanner->track()); + // Use the spanner voice unless given in @layer + track_idx_t layer = (layerIdentAtt.HasLayer()) + ? static_cast(this->getVoiceIndex(static_cast(staffIdx), + layerIdentAtt.GetLayer())) + : track2voice(spanner->track()); - chordRest = measure->findChordRest(measure->tick() + tstampFraction, staffIdx * VOICES + layer); - if (!chordRest) { - Convert::logs.push_back(String("Could not find element corresponding to @tstamp2 '%1m+%2'").arg(tstamp2Value.first).arg( - tstamp2Value.second)); - return nullptr; - } + pos.tick = measure->tick() + tstampFraction; + pos.track = staffIdx * VOICES + layer; + pos.chordRest = measure->findChordRest(pos.tick, pos.track); } - return chordRest; + return pos; } /** @@ -3572,17 +3583,19 @@ void MeiImporter::addSpannerEnds() endNote->addSpannerBack(gliss); // All other Spanners - } else if (spannerMapEntry.first->startCR()) { - ChordRest* chordRest = this->findEnd(spannerMapEntry.second, spannerMapEntry.first->startCR()); - if (!chordRest) { + } else { + ControlElementPosition pos = this->findEnd(spannerMapEntry.second, spannerMapEntry.first); + if (!pos.measure) { continue; } - spannerMapEntry.first->setTick2(chordRest->tick()); - spannerMapEntry.first->setEndElement(chordRest); - spannerMapEntry.first->setTrack2(chordRest->track()); + spannerMapEntry.first->setTick2(pos.tick); + spannerMapEntry.first->setEndElement(pos.chordRest); + spannerMapEntry.first->setTrack2(pos.track); if (spannerMapEntry.first->isOttava() || spannerMapEntry.first->isTrill()) { // Set the tick2 to include the duration of the ChordRest - spannerMapEntry.first->setTick2(chordRest->endTick()); + if (pos.chordRest) { + spannerMapEntry.first->setTick2(pos.chordRest->endTick()); + } // Special handling of ottavas if (spannerMapEntry.first->isOttava()) { Ottava* ottava = toOttava(spannerMapEntry.first); diff --git a/src/importexport/mei/internal/meiimporter.h b/src/importexport/mei/internal/meiimporter.h index 2e070e7f8cadd..fc15624bb9d3b 100644 --- a/src/importexport/mei/internal/meiimporter.h +++ b/src/importexport/mei/internal/meiimporter.h @@ -57,6 +57,13 @@ struct ClefTypeList; namespace mu::iex::mei { class UIDRegister; +struct ControlElementPosition { + engraving::Measure* measure = nullptr; + engraving::Fraction tick; + int track = 0; + engraving::ChordRest* chordRest = nullptr; +}; + enum GraceReading { GraceNone = 0, GraceAsGrp, @@ -188,8 +195,8 @@ class MeiImporter engraving::EngravingItem* addToChordRest(const libmei::Element& meiElement, engraving::Measure* measure, engraving::Chord* chord = nullptr); std::string xmlIdFrom(std::string dataURI); - engraving::ChordRest* findStart(const libmei::Element& meiElement, engraving::Measure* measure); - engraving::ChordRest* findEnd(pugi::xml_node controlNode, const engraving::ChordRest* startChordRest); + ControlElementPosition findStart(const libmei::Element& meiElement, engraving::Measure* measure); + ControlElementPosition findEnd(pugi::xml_node controlNode, engraving::Spanner* spanner); engraving::Note* findStartNote(const libmei::Element& meiElement); engraving::Note* findEndNote(pugi::xml_node controlNode); const std::vector findPlistChordRests(pugi::xml_node controlNode); diff --git a/src/importexport/musicxml/internal/export/exportmusicxml.cpp b/src/importexport/musicxml/internal/export/exportmusicxml.cpp index f76fb208fa358..ba98207859ccb 100644 --- a/src/importexport/musicxml/internal/export/exportmusicxml.cpp +++ b/src/importexport/musicxml/internal/export/exportmusicxml.cpp @@ -5760,13 +5760,17 @@ void ExportMusicXml::textLine(TextLineBase const* const tl, staff_idx_t staff, c String lineEnd; switch (hookType) { + case HookType::HOOK_90: + lineEnd = (hookHeight < 0.0) ? u"up" : u"down"; + rest += String(u" end-length=\"%1\"").arg(std::abs(hookHeight * 10)); + break; case HookType::HOOK_90T: lineEnd = u"both"; rest += String(u" end-length=\"%1\"").arg(std::abs(hookHeight * 20)); break; - case HookType::HOOK_90: - lineEnd = (hookHeight < 0.0) ? u"up" : u"down"; - rest += String(u" end-length=\"%1\"").arg(std::abs(hookHeight * 10)); + case HookType::ARROW: + case HookType::ARROW_FILLED: + lineEnd = u"arrow"; break; case HookType::NONE: lineEnd = u"none"; diff --git a/src/importexport/musicxml/internal/import/importmusicxmlpass2.cpp b/src/importexport/musicxml/internal/import/importmusicxmlpass2.cpp index c1bd2205583aa..c76630beb639e 100644 --- a/src/importexport/musicxml/internal/import/importmusicxmlpass2.cpp +++ b/src/importexport/musicxml/internal/import/importmusicxmlpass2.cpp @@ -5131,7 +5131,7 @@ void MusicXmlParserDirection::bracket(const String& type, const int number, } else if (lineEnd == "both") { textLine->setBeginHookType(HookType::HOOK_90T); } else if (lineEnd == "arrow") { - m_logger->logError(String(u"line-end \"arrow\" not supported")); + textLine->setBeginHookType(HookType::ARROW_FILLED); } else if (lineEnd == "none") { textLine->setBeginHookType(HookType::NONE); } @@ -5192,7 +5192,7 @@ void MusicXmlParserDirection::bracket(const String& type, const int number, } else if (lineEnd == "both") { textLine->setEndHookType(HookType::HOOK_90T); } else if (lineEnd == "arrow") { - m_logger->logError(String(u"line-end \"arrow\" not supported")); + textLine->setEndHookType(HookType::ARROW_FILLED); } else if (lineEnd == "none") { textLine->setEndHookType(HookType::NONE); } diff --git a/vtest/scores/chordBrackets-3.mscz b/vtest/scores/chordBrackets-3.mscz new file mode 100644 index 0000000000000..8fd58ff6c3638 Binary files /dev/null and b/vtest/scores/chordBrackets-3.mscz differ