Line data Source code
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) 2026 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 "exchangevoices.h"
24 :
25 : #include "../dom/excerpt.h"
26 : #include "../dom/measure.h"
27 : #include "../dom/rest.h"
28 : #include "../dom/score.h"
29 : #include "../dom/segment.h"
30 : #include "../dom/staff.h"
31 : #include "../types/types.h"
32 :
33 : #include "clonevoice.h"
34 : #include "transaction/undoablecommand.h"
35 :
36 : using namespace mu::engraving;
37 :
38 : namespace {
39 : class ExchangeVoicesInMeasure : public UndoableCommand
40 : {
41 8 : OBJECT_ALLOCATOR(engraving, ExchangeVoicesInMeasure)
42 :
43 : public:
44 8 : ExchangeVoicesInMeasure(Measure* measure, track_idx_t srcTrack, track_idx_t dstTrack, staff_idx_t staff)
45 8 : : m_measure(measure), m_srcTrack(srcTrack), m_dstTrack(dstTrack), m_staff(staff) {}
46 :
47 3 : void undo() override
48 : {
49 3 : doExchange(m_dstTrack, m_srcTrack);
50 3 : }
51 :
52 8 : void redo() override
53 : {
54 8 : doExchange(m_srcTrack, m_dstTrack);
55 8 : }
56 :
57 8 : UNDO_TYPE(CommandType::ExchangeVoice)
58 0 : UNDO_NAME("ExchangeVoice")
59 24 : UNDO_CHANGED_OBJECTS({ m_measure })
60 :
61 : private:
62 : void doExchange(track_idx_t srcTrack, track_idx_t dstTrack);
63 :
64 : Measure* m_measure = nullptr;
65 : track_idx_t m_srcTrack = muse::nidx;
66 : track_idx_t m_dstTrack = muse::nidx;
67 : staff_idx_t m_staff = muse::nidx;
68 : };
69 :
70 11 : void ExchangeVoicesInMeasure::doExchange(track_idx_t srcTrack, track_idx_t dstTrack)
71 : {
72 46 : for (Segment* s = m_measure->first(SegmentType::ChordRest); s; s = s->next(SegmentType::ChordRest)) {
73 35 : s->swapElements(srcTrack, dstTrack);
74 : }
75 :
76 11 : Fraction start = m_measure->tick();
77 11 : Fraction end = start + m_measure->ticks();
78 11 : auto spanners = m_measure->score()->spannerMap().findOverlapping(start.ticks(), end.ticks() - 1);
79 19 : for (auto i = spanners.begin(); i < spanners.end(); i++) {
80 8 : Spanner* sp = i->value;
81 8 : Fraction spStart = sp->tick();
82 8 : Fraction spEnd = spStart + sp->ticks();
83 8 : LOGD("Start %d End %d Diff %d \n Measure Start %d End %d", spStart.ticks(), spEnd.ticks(), (spEnd - spStart).ticks(),
84 : start.ticks(), end.ticks());
85 8 : if (sp->isSlur() && (spStart >= start || spEnd < end)) {
86 6 : if (sp->track() == srcTrack && spStart >= start) {
87 4 : sp->setTrack(dstTrack);
88 2 : } else if (sp->track() == dstTrack && spStart >= start) {
89 1 : sp->setTrack(srcTrack);
90 : }
91 6 : if (sp->track2() == srcTrack && spEnd < end) {
92 5 : sp->setTrack2(dstTrack);
93 1 : } else if (sp->track2() == dstTrack && spEnd < end) {
94 0 : sp->setTrack2(srcTrack);
95 : }
96 : }
97 : }
98 11 : m_measure->checkMultiVoices(m_staff); // probably true, but check for invisible notes & rests
99 11 : }
100 : }
101 :
102 3 : void ExchangeVoices::exchangeVoicesInSelection(Score* score, voice_idx_t srcVoice, voice_idx_t dstVoice)
103 : {
104 3 : if (!score->selection().isRange()) {
105 0 : MScore::setError(MsError::NO_STAFF_SELECTED);
106 0 : return;
107 : }
108 3 : Fraction t1 = score->selection().tickStart();
109 3 : Fraction t2 = score->selection().tickEnd();
110 :
111 3 : Measure* m1 = score->tick2measure(t1);
112 3 : Measure* m2 = score->tick2measure(t2);
113 :
114 3 : if (score->excerpt()) {
115 0 : return;
116 : }
117 :
118 3 : if (t2 > m2->tick()) {
119 3 : m2 = m2->nextMeasure();
120 : }
121 :
122 : for (;;) {
123 8 : exchangeVoices(score, m1, srcVoice, dstVoice, score->selection().staffStart(), score->selection().staffEnd());
124 8 : m1 = m1->nextMeasure();
125 8 : if ((m1 == 0) || (m2 && (m1->tick() == m2->tick()))) {
126 3 : break;
127 : }
128 : }
129 : }
130 :
131 8 : void ExchangeVoices::exchangeVoices(Score* score, Measure* measure, voice_idx_t srcVoice, voice_idx_t dstVoice, staff_idx_t srcStaff,
132 : staff_idx_t dstStaff)
133 : {
134 8 : Fraction tick = measure->tick();
135 :
136 16 : for (staff_idx_t staffIdx = srcStaff; staffIdx < dstStaff; ++staffIdx) {
137 8 : track_idx_t srcStaffTrack = staffIdx * VOICES;
138 8 : track_idx_t srcTrack = srcStaffTrack + srcVoice;
139 8 : track_idx_t dstTrack = srcStaffTrack + dstVoice;
140 8 : int trackDiff = static_cast<int>(dstVoice - srcVoice);
141 :
142 : //handle score and complete measures first
143 8 : score->undo(new ExchangeVoicesInMeasure(measure, srcTrack, dstTrack, staffIdx));
144 :
145 16 : for (Staff* st : score->staff(staffIdx)->staffList()) {
146 8 : track_idx_t staffTrack = st->idx() * VOICES;
147 8 : Measure* measure2 = st->score()->tick2measure(tick);
148 8 : Excerpt* ex = st->score()->excerpt();
149 :
150 8 : if (ex) {
151 0 : const TracksMap& tracks = ex->tracksMapping();
152 0 : std::vector<track_idx_t> srcTrackList = muse::values(tracks, srcTrack);
153 0 : std::vector<track_idx_t> dstTrackList = muse::values(tracks, dstTrack);
154 :
155 0 : for (track_idx_t srcTrack2 : srcTrackList) {
156 : // don't care about other linked staves
157 0 : if (!(staffTrack <= srcTrack2) || !(srcTrack2 < staffTrack + VOICES)) {
158 0 : continue;
159 : }
160 :
161 0 : track_idx_t tempTrack = srcTrack;
162 0 : std::vector<track_idx_t> testTracks = muse::values(tracks, tempTrack + trackDiff);
163 0 : bool hasVoice = false;
164 0 : for (track_idx_t testTrack : testTracks) {
165 0 : if (staffTrack <= testTrack && testTrack < staffTrack + VOICES && muse::contains(dstTrackList, testTrack)) {
166 0 : hasVoice = true;
167 : // voice is simply exchangeable now (deal directly)
168 0 : score->undo(new ExchangeVoicesInMeasure(measure2, srcTrack2, testTrack, staffTrack / 4));
169 : }
170 : }
171 :
172 : // only source voice is in this staff
173 0 : if (!hasVoice) {
174 0 : CloneVoice::cloneVoice(measure->first(), measure2->endTick(), measure2->first(), tempTrack, srcTrack2, false, true);
175 0 : muse::remove(srcTrackList, srcTrack2);
176 : }
177 0 : }
178 :
179 0 : for (track_idx_t dstTrack2 : dstTrackList) {
180 : // don't care about other linked staves
181 0 : if (!(staffTrack <= dstTrack2) || !(dstTrack2 < staffTrack + VOICES)) {
182 0 : continue;
183 : }
184 :
185 0 : track_idx_t tempTrack = dstTrack;
186 0 : std::vector<track_idx_t> testTracks = muse::values(tracks, tempTrack - trackDiff);
187 0 : bool hasVoice = false;
188 0 : for (track_idx_t testTrack : testTracks) {
189 0 : if (staffTrack <= testTrack && testTrack < staffTrack + VOICES && muse::contains(srcTrackList, testTrack)) {
190 0 : hasVoice = true;
191 : }
192 : }
193 :
194 : // only destination voice is in this staff
195 0 : if (!hasVoice) {
196 0 : CloneVoice::cloneVoice(measure->first(), measure2->endTick(), measure2->first(), tempTrack, dstTrack2, false, true);
197 0 : muse::remove(dstTrackList, dstTrack2);
198 : }
199 0 : }
200 8 : } else if (srcStaffTrack != staffTrack) {
201 : // linked staff in same score (all voices present can be assumed)
202 0 : score->undo(new ExchangeVoicesInMeasure(measure2, staffTrack + srcVoice, staffTrack + dstVoice, st->idx()));
203 : }
204 8 : }
205 : }
206 :
207 : // make sure voice 0 is complete
208 :
209 8 : if (srcVoice == 0 || dstVoice == 0) {
210 16 : for (staff_idx_t staffIdx = srcStaff; staffIdx < dstStaff; ++staffIdx) {
211 : // check for complete timeline of voice 0
212 8 : Fraction ctick = measure->tick();
213 8 : track_idx_t track = staffIdx * VOICES;
214 36 : for (Segment* s = measure->first(SegmentType::ChordRest); s; s = s->next(SegmentType::ChordRest)) {
215 28 : ChordRest* cr = toChordRest(s->element(track));
216 28 : if (!cr) {
217 19 : continue;
218 : }
219 9 : if (cr->isRest()) {
220 4 : Rest* r = toRest(cr);
221 4 : if (r->isGap()) {
222 1 : r->undoChangeProperty(Pid::GAP, false);
223 : }
224 : }
225 9 : if (ctick < s->tick()) {
226 0 : score->setRest(ctick, track, s->tick() - ctick, false, 0); // fill gap
227 : }
228 9 : ctick = cr->endTick();
229 : }
230 8 : Fraction etick = measure->endTick();
231 8 : if (ctick < etick) {
232 5 : score->setRest(ctick, track, etick - ctick, false, 0); // fill gap
233 : }
234 : }
235 : }
236 8 : }
|