diff --git a/src/engraving/dom/masterscore.cpp b/src/engraving/dom/masterscore.cpp index 36d4f792a00b9..de814683d363a 100644 --- a/src/engraving/dom/masterscore.cpp +++ b/src/engraving/dom/masterscore.cpp @@ -231,6 +231,11 @@ void MasterScore::addExcerpt(Excerpt* ex, size_t index) initParts(ex); } + // Avoid adding duplicates + if (std::find(excerpts().begin(), excerpts().end(), ex) != excerpts().end()) { + return; + } + excerpts().insert(excerpts().begin() + (index == muse::nidx ? excerpts().size() : index), ex); setExcerptsChanged(true); } diff --git a/src/engraving/editing/editexcerpt.cpp b/src/engraving/editing/editexcerpt.cpp index 64e85d4a76c37..8af4b60423c12 100644 --- a/src/engraving/editing/editexcerpt.cpp +++ b/src/engraving/editing/editexcerpt.cpp @@ -120,6 +120,21 @@ void SwapExcerpt::flip(EditData*) // ChangeExcerptTitle //--------------------------------------------------------- +ChangeExcerptTitle::ChangeExcerptTitle(Excerpt* x, const String& t) + : excerpt(x), title(t) +{ + // Ensure excerpt is in master's list (required for saving to disk). + // "Potential" excerpts shown in Parts dialog are not in the list + // until explicitly created, so renaming them wouldn't persist. + MasterScore* master = excerpt->masterScore(); + if (master) { + const std::vector& excerpts = master->excerpts(); + if (std::find(excerpts.begin(), excerpts.end(), excerpt) == excerpts.end()) { + master->initAndAddExcerpt(excerpt, true); + } + } +} + void ChangeExcerptTitle::flip(EditData*) { String s = title; diff --git a/src/engraving/editing/editexcerpt.h b/src/engraving/editing/editexcerpt.h index 0a2ad705d337a..38243ac53d611 100644 --- a/src/engraving/editing/editexcerpt.h +++ b/src/engraving/editing/editexcerpt.h @@ -99,8 +99,7 @@ class ChangeExcerptTitle : public UndoCommand void flip(EditData*) override; public: - ChangeExcerptTitle(Excerpt* x, const String& t) - : excerpt(x), title(t) {} + ChangeExcerptTitle(Excerpt* x, const String& t); UNDO_TYPE(CommandType::ChangeExcerptTitle) UNDO_NAME("ChangeExcerptTitle") diff --git a/src/engraving/tests/parts_tests.cpp b/src/engraving/tests/parts_tests.cpp index f2c6daa7e440b..95c913306a344 100644 --- a/src/engraving/tests/parts_tests.cpp +++ b/src/engraving/tests/parts_tests.cpp @@ -41,6 +41,8 @@ #include "engraving/dom/segment.h" #include "engraving/dom/spanner.h" #include "engraving/dom/staff.h" +#include "engraving/editing/undo.h" +#include "engraving/editing/editexcerpt.h" #include "utils/scorerw.h" #include "utils/scorecomp.h" @@ -1387,3 +1389,63 @@ TEST_F(Engraving_PartsTests, staffStyles) } #endif + +//--------------------------------------------------------- +// renamePotentialExcerpt +// Test that renaming a "potential" excerpt (not yet in +// master's excerpts list) persists after save/reload. +// This tests the fix for the bug where part names changed +// in the Parts dialog weren't saved if the part hadn't +// been opened as a tab. +// +// The fix: ChangeExcerptTitle auto-adds the excerpt to +// master's list if not present, ensuring it gets saved. +//--------------------------------------------------------- + +TEST_F(Engraving_PartsTests, renamePotentialExcerpt) +{ + MasterScore* score = ScoreRW::readScore(PARTS_DATA_DIR + u"part-all.mscx"); + ASSERT_TRUE(score); + + // Create a "potential" excerpt from first part - NOT added to excerpts list yet + // This simulates what the Parts dialog does for parts that haven't been "generated" + std::vector parts = { score->parts().at(0) }; + std::vector potentialExcerpts = Excerpt::createExcerptsFromParts(parts, score); + ASSERT_EQ(potentialExcerpts.size(), 1u); + Excerpt* excerpt = potentialExcerpts.at(0); + + // Verify excerpt is NOT in master's list (simulating a "potential" excerpt) + EXPECT_TRUE(std::find(score->excerpts().begin(), score->excerpts().end(), excerpt) == score->excerpts().end()); + + // Rename the excerpt - ChangeExcerptTitle should auto-add to master's list + const String newName = u"RenamedPart"; + score->startCmd(TranslatableString::untranslatable("Test rename")); + score->undo(new ChangeExcerptTitle(excerpt, newName)); + score->endCmd(); + + EXPECT_EQ(excerpt->name(), newName); + // Verify excerpt is now in master's list (auto-added by ChangeExcerptTitle) + EXPECT_TRUE(std::find(score->excerpts().begin(), score->excerpts().end(), excerpt) != score->excerpts().end()); + + // Save to temp file + String tempFile = PARTS_DATA_DIR + u"part-rename-potential-test.mscx"; + EXPECT_TRUE(ScoreRW::saveScore(score, ScoreRW::rootPath() + u"/" + tempFile)); + delete score; + + // Reload and verify the name persisted + score = ScoreRW::readScore(tempFile); + ASSERT_TRUE(score); + ASSERT_FALSE(score->excerpts().empty()) << "No excerpts found - excerpt was not saved"; + + // Find the renamed excerpt + bool found = false; + for (Excerpt* ex : score->excerpts()) { + if (ex->name() == newName) { + found = true; + break; + } + } + EXPECT_TRUE(found) << "Renamed excerpt not found after reload"; + + delete score; +} diff --git a/src/notation/internal/excerptnotation.cpp b/src/notation/internal/excerptnotation.cpp index 7f5f12f613d83..bb0a91f438235 100644 --- a/src/notation/internal/excerptnotation.cpp +++ b/src/notation/internal/excerptnotation.cpp @@ -23,6 +23,7 @@ #include "excerptnotation.h" #include "engraving/dom/excerpt.h" +#include "engraving/dom/masterscore.h" #include "engraving/editing/editexcerpt.h" using namespace mu::notation; @@ -101,17 +102,18 @@ void ExcerptNotation::undoSetName(const QString& name) return; } - if (!score()) { + engraving::MasterScore* master = m_excerpt->masterScore(); + if (!master) { setName(name); + notifyAboutNotationChanged(); return; } //: Means: "edit the name of a part score" - undoStack()->prepareChanges(muse::TranslatableString("undoableAction", "Rename part")); - - score()->undo(new engraving::ChangeExcerptTitle(m_excerpt, name)); + master->startCmd(muse::TranslatableString("undoableAction", "Rename part")); + master->undo(new engraving::ChangeExcerptTitle(m_excerpt, name)); + master->endCmd(); - undoStack()->commitChanges(); notifyAboutNotationChanged(); }