Skip to content

Commit 30635e5

Browse files
committed
Add replaceInstrument() to Plugin API
Expose instrument replacement functionality to plugins through new Score.replaceInstrument() method. Allows plugins to change a part's instrument (name, clef, transposition, sound) with full undo/redo support. Changes: - Add Q_INVOKABLE Score::replaceInstrument(Part*, QString) to plugin API - Use Part::MAIN_INSTRUMENT_TICK (not Fraction(0,1)) to correctly identify main instrument vs. mid-score instrument changes - Bridge plugin API to existing NotationParts::replaceInstrument() infrastructure Usage example: var part = curScore.parts[0]; curScore.replaceInstrument(part, "violin");
1 parent 8cf1081 commit 30635e5

File tree

6 files changed

+264
-1
lines changed

6 files changed

+264
-1
lines changed

src/engraving/api/tests/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
set(MODULE_TEST engraving_api_tests)
2222

2323
set(MODULE_TEST_SRC
24+
${CMAKE_CURRENT_LIST_DIR}/environment.cpp
2425
${CMAKE_CURRENT_LIST_DIR}/util_tests.cpp
26+
${CMAKE_CURRENT_LIST_DIR}/score_tests.cpp
2527
)
2628

2729
set(MODULE_TEST_LINK
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* SPDX-License-Identifier: GPL-3.0-only
3+
* MuseScore-Studio-CLA-applies
4+
*
5+
* MuseScore Studio
6+
* Music Composition & Notation
7+
*
8+
* Copyright (C) 2025 MuseScore Limited
9+
*
10+
* This program is free software: you can redistribute it and/or modify
11+
* it under the terms of the GNU General Public License version 3 as
12+
* published by the Free Software Foundation.
13+
*
14+
* This program is distributed in the hope that it will be useful,
15+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17+
* GNU General Public License for more details.
18+
*
19+
* You should have received a copy of the GNU General Public License
20+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
21+
*/
22+
23+
#include "testing/environment.h"
24+
25+
#include "draw/drawmodule.h"
26+
#include "engraving/engravingmodule.h"
27+
28+
#include "engraving/dom/mscore.h"
29+
30+
#include "log.h"
31+
32+
static muse::testing::SuiteEnvironment engraving_api_se(
33+
{
34+
new muse::draw::DrawModule(),
35+
new mu::engraving::EngravingModule()
36+
},
37+
nullptr,
38+
[]() {
39+
LOGI() << "engraving API tests suite post init";
40+
41+
mu::engraving::MScore::testMode = true;
42+
mu::engraving::MScore::noGui = true;
43+
}
44+
);
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
/*
2+
* SPDX-License-Identifier: GPL-3.0-only
3+
* MuseScore-Studio-CLA-applies
4+
*
5+
* MuseScore Studio
6+
* Music Composition & Notation
7+
*
8+
* Copyright (C) 2025 MuseScore Limited
9+
*
10+
* This program is free software: you can redistribute it and/or modify
11+
* it under the terms of the GNU General Public License version 3 as
12+
* published by the Free Software Foundation.
13+
*
14+
* This program is distributed in the hope that it will be useful,
15+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17+
* GNU General Public License for more details.
18+
*
19+
* You should have received a copy of the GNU General Public License
20+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
21+
*/
22+
23+
#include <gtest/gtest.h>
24+
25+
#include "engraving/compat/scoreaccess.h"
26+
#include "engraving/dom/factory.h"
27+
#include "engraving/dom/instrument.h"
28+
#include "engraving/dom/part.h"
29+
#include "engraving/dom/staff.h"
30+
#include "engraving/editing/editpart.h"
31+
32+
using namespace mu::engraving;
33+
34+
class Engraving_ApiScoreTests : public ::testing::Test
35+
{
36+
public:
37+
};
38+
39+
//---------------------------------------------------------
40+
// testReplaceInstrumentAtDomLevel
41+
// Test that ChangePart correctly replaces the instrument
42+
// This tests the underlying mechanism used by the Plugin API
43+
//---------------------------------------------------------
44+
45+
TEST_F(Engraving_ApiScoreTests, replaceInstrumentAtDomLevel)
46+
{
47+
// [GIVEN] A score with a part
48+
MasterScore* score = compat::ScoreAccess::createMasterScore(nullptr);
49+
50+
// Create a part with a default instrument
51+
Part* part = new Part(score);
52+
score->appendPart(part);
53+
score->appendStaff(Factory::createStaff(part));
54+
55+
ASSERT_EQ(score->parts().size(), 1);
56+
ASSERT_NE(part, nullptr);
57+
58+
// Set initial instrument
59+
Instrument initialInstrument;
60+
initialInstrument.setId(u"test.initial");
61+
initialInstrument.setTrackName(u"Initial Instrument");
62+
part->setInstrument(initialInstrument);
63+
64+
// Verify initial instrument
65+
EXPECT_EQ(part->instrumentId(), QString("test.initial"));
66+
67+
// [WHEN] We replace the instrument using ChangePart
68+
Instrument newInstrument;
69+
newInstrument.setId(u"test.replaced");
70+
newInstrument.setTrackName(u"Replaced Instrument");
71+
72+
score->startCmd(TranslatableString::untranslatable("Replace instrument test"));
73+
score->undo(new ChangePart(part, new Instrument(newInstrument), u"Replaced Part"));
74+
score->endCmd();
75+
76+
// [THEN] The part's instrument should be changed
77+
EXPECT_EQ(part->instrumentId(), QString("test.replaced"));
78+
EXPECT_EQ(part->instrument()->trackName(), muse::String(u"Replaced Instrument"));
79+
80+
delete score;
81+
}
82+
83+
//---------------------------------------------------------
84+
// testReplaceInstrumentUndo
85+
// Test that instrument replacement can be undone
86+
//---------------------------------------------------------
87+
88+
TEST_F(Engraving_ApiScoreTests, replaceInstrumentUndo)
89+
{
90+
// [GIVEN] A score with a part
91+
MasterScore* score = compat::ScoreAccess::createMasterScore(nullptr);
92+
93+
Part* part = new Part(score);
94+
score->appendPart(part);
95+
score->appendStaff(Factory::createStaff(part));
96+
97+
// Set initial instrument
98+
Instrument initialInstrument;
99+
initialInstrument.setId(u"test.original");
100+
initialInstrument.setTrackName(u"Original Instrument");
101+
part->setInstrument(initialInstrument);
102+
103+
EXPECT_EQ(part->instrumentId(), QString("test.original"));
104+
105+
// [WHEN] We replace the instrument
106+
Instrument newInstrument;
107+
newInstrument.setId(u"test.new");
108+
newInstrument.setTrackName(u"New Instrument");
109+
110+
score->startCmd(TranslatableString::untranslatable("Replace instrument test"));
111+
score->undo(new ChangePart(part, new Instrument(newInstrument), u"New Part"));
112+
score->endCmd();
113+
114+
// Verify it changed
115+
EXPECT_EQ(part->instrumentId(), QString("test.new"));
116+
117+
// [WHEN] We undo
118+
score->undoRedo(true, nullptr);
119+
120+
// [THEN] The instrument should be back to original
121+
EXPECT_EQ(part->instrumentId(), QString("test.original"));
122+
123+
delete score;
124+
}
125+
126+
//---------------------------------------------------------
127+
// testReplaceInstrumentRedo
128+
// Test that instrument replacement can be redone after undo
129+
//---------------------------------------------------------
130+
131+
TEST_F(Engraving_ApiScoreTests, replaceInstrumentRedo)
132+
{
133+
// [GIVEN] A score with a part
134+
MasterScore* score = compat::ScoreAccess::createMasterScore(nullptr);
135+
136+
Part* part = new Part(score);
137+
score->appendPart(part);
138+
score->appendStaff(Factory::createStaff(part));
139+
140+
// Set initial instrument
141+
Instrument initialInstrument;
142+
initialInstrument.setId(u"test.initial");
143+
part->setInstrument(initialInstrument);
144+
145+
// Replace the instrument
146+
Instrument newInstrument;
147+
newInstrument.setId(u"test.replaced");
148+
149+
score->startCmd(TranslatableString::untranslatable("Replace instrument test"));
150+
score->undo(new ChangePart(part, new Instrument(newInstrument), u"Replaced"));
151+
score->endCmd();
152+
153+
EXPECT_EQ(part->instrumentId(), QString("test.replaced"));
154+
155+
// Undo
156+
score->undoRedo(true, nullptr);
157+
EXPECT_EQ(part->instrumentId(), QString("test.initial"));
158+
159+
// [WHEN] We redo
160+
score->undoRedo(false, nullptr);
161+
162+
// [THEN] The instrument should be replaced again
163+
EXPECT_EQ(part->instrumentId(), QString("test.replaced"));
164+
165+
delete score;
166+
}

src/engraving/api/v1/score.cpp

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,18 @@
2626
#include "dom/factory.h"
2727
#include "dom/instrtemplate.h"
2828
#include "dom/measure.h"
29+
#include "dom/part.h"
2930
#include "dom/score.h"
3031
#include "dom/segment.h"
3132
#include "dom/text.h"
3233
#include "editing/editsystemlocks.h"
3334
#include "types/typesconv.h"
3435

36+
// notation
37+
#include "notation/inotation.h"
38+
#include "notation/inotationparts.h"
39+
#include "notation/notationtypes.h"
40+
3541
// api
3642
#include "apistructs.h"
3743
#include "cursor.h"
@@ -158,6 +164,40 @@ void Score::appendPartByMusicXmlId(const QString& instrumentMusicXmlId)
158164
score()->appendPart(t);
159165
}
160166

167+
/** APIDOC
168+
* Replaces the instrument for a given part with a new instrument.
169+
* This changes the instrument definition including its name, clef, and sound.
170+
* @method
171+
* @param {Part} part - The Part object whose instrument should be replaced.
172+
* @param {string} instrumentId - ID of the new instrument from instruments.xml.
173+
*/
174+
void Score::replaceInstrument(apiv1::Part* part, const QString& instrumentId)
175+
{
176+
if (!part) {
177+
LOGW("replaceInstrument: part is null");
178+
return;
179+
}
180+
181+
const InstrumentTemplate* t = searchTemplate(instrumentId);
182+
if (!t) {
183+
LOGW("replaceInstrument: <%s> not found", qPrintable(instrumentId));
184+
return;
185+
}
186+
187+
mu::notation::INotationPartsPtr parts = notation() ? notation()->parts() : nullptr;
188+
if (!parts) {
189+
LOGW("replaceInstrument: notation parts is null");
190+
return;
191+
}
192+
193+
mu::notation::InstrumentKey instrumentKey;
194+
instrumentKey.partId = muse::ID(part->part()->id());
195+
instrumentKey.instrumentId = muse::String::fromQString(part->part()->instrumentId());
196+
instrumentKey.tick = mu::engraving::Part::MAIN_INSTRUMENT_TICK;
197+
198+
parts->replaceInstrument(instrumentKey, mu::engraving::Instrument::fromTemplate(t));
199+
}
200+
161201
//---------------------------------------------------------
162202
// Score::firstSegment
163203
//---------------------------------------------------------

src/engraving/api/v1/score.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,15 @@ class Score : public apiv1::ScoreElement, public muse::Injectable
240240
/// \since MuseScore 3.5
241241
Q_INVOKABLE void appendPartByMusicXmlId(const QString& instrumentMusicXmlId);
242242

243+
/// Replaces the instrument for a given part with a new instrument.
244+
/// This changes the instrument definition including its name, clef, and sound.
245+
/// \param part - The Part object whose instrument should be replaced.
246+
/// \param instrumentId - ID of the new instrument, as listed in
247+
/// [`instruments.xml`](https://github.com/musescore/MuseScore/blob/3.x/share/instruments/instruments.xml)
248+
/// file.
249+
/// \since MuseScore 4.7
250+
Q_INVOKABLE void replaceInstrument(apiv1::Part* part, const QString& instrumentId);
251+
243252
/// Appends a number of measures to this score.
244253
Q_INVOKABLE void appendMeasures(int n) { score()->appendMeasures(n); }
245254
Q_INVOKABLE void addText(const QString& type, const QString& text);

src/notation/internal/notationparts.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -716,7 +716,9 @@ void NotationParts::replaceInstrument(const InstrumentKey& instrumentKey, const
716716

717717
startEdit(TranslatableString("undoableAction", "Replace instrument"));
718718

719-
if (isMainInstrumentForPart(instrumentKey, part)) {
719+
bool isMain = isMainInstrumentForPart(instrumentKey, part);
720+
721+
if (isMain) {
720722
QString newInstrumentPartName = formatInstrumentTitle(newInstrument.trackName(), newInstrument.trait());
721723
score()->undo(new mu::engraving::ChangePart(part, new mu::engraving::Instrument(newInstrument), newInstrumentPartName));
722724

0 commit comments

Comments
 (0)