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 "editvoice.h"
24 : #include "editchord.h"
25 :
26 : #include <vector>
27 :
28 : #include "global/containers.h"
29 :
30 : #include "../dom/articulation.h"
31 : #include "../dom/chord.h"
32 : #include "../dom/excerpt.h"
33 : #include "../dom/factory.h"
34 : #include "../dom/lyrics.h"
35 : #include "../dom/masterscore.h"
36 : #include "../dom/measure.h"
37 : #include "../dom/note.h"
38 : #include "../dom/partialtie.h"
39 : #include "../dom/rest.h"
40 : #include "../dom/score.h"
41 : #include "../dom/segment.h"
42 : #include "../dom/select.h"
43 : #include "../dom/slur.h"
44 : #include "../dom/spanner.h"
45 : #include "../dom/staff.h"
46 : #include "../dom/tie.h"
47 : #include "../dom/tuplet.h"
48 :
49 : using namespace mu;
50 : using namespace mu::engraving;
51 :
52 : //---------------------------------------------------------
53 : // changeSelectedElementsVoice
54 : // moves selected notes into specified voice if possible
55 : //---------------------------------------------------------
56 :
57 4 : void EditVoice::changeSelectedElementsVoice(Transaction&, Score* score, voice_idx_t voice)
58 : {
59 25 : auto isOperationValid = [](Score* score, track_idx_t origTrack, track_idx_t dstTrack) {
60 25 : Excerpt* excerpt = score->excerpt();
61 25 : if (!excerpt) {
62 25 : return true;
63 : }
64 :
65 0 : const track_idx_t dstTrackMapped = muse::key(excerpt->tracksMapping(), dstTrack, muse::nidx);
66 0 : if (dstTrackMapped == muse::nidx) {
67 0 : return false;
68 : }
69 :
70 0 : const track_idx_t origTrackMapped = muse::key(excerpt->tracksMapping(), origTrack, muse::nidx);
71 0 : if (origTrackMapped == dstTrack) {
72 0 : return false;
73 : }
74 :
75 0 : return true;
76 : };
77 :
78 : // Track groups of parentheses to recreate in new chord
79 : struct MovedParenGroup {
80 : Chord* dstChord = nullptr;
81 : bool generated = false;
82 : bool leftVisible = true;
83 : bool rightVisible = true;
84 : PointF leftOffset = PointF();
85 : PointF rightOffset = PointF();
86 : std::vector<Note*> notes;
87 : };
88 4 : std::map<const NoteParenthesisInfo*, MovedParenGroup> movedParenGroups;
89 :
90 4 : std::vector<EngravingItem*> newElements;
91 4 : std::vector<EngravingItem*> oel = score->selection().elements(); // make copy
92 50 : for (EngravingItem* e : oel) {
93 46 : if (e->isNote()) {
94 29 : Note* note = toNote(e);
95 29 : Chord* chord = note->chord();
96 :
97 : // move grace notes with main chord only
98 29 : if (chord->isGrace() || chord->isTrillCueNote()) {
99 9 : continue;
100 : }
101 :
102 29 : if (chord->voice() == voice) {
103 4 : continue;
104 : }
105 :
106 25 : Score* elementScore = score;
107 25 : if (score->excerpt() && !chord->staff()->isVoiceVisible(voice)) {
108 : // We are on a linked stave with the desired voice not visible
109 : // Get note in main score to continue
110 1 : note = toNote(note->findLinkedInScore(score->masterScore()));
111 1 : chord = note->chord();
112 1 : elementScore = score->masterScore();
113 : }
114 25 : Segment* s = chord->segment();
115 25 : Measure* m = s->measure();
116 25 : size_t notes = chord->notes().size();
117 25 : track_idx_t dstTrack = chord->staffIdx() * VOICES + voice;
118 25 : ChordRest* dstCR = toChordRest(s->element(dstTrack));
119 25 : Chord* dstChord = nullptr;
120 :
121 25 : if (!isOperationValid(elementScore, chord->track(), dstTrack)) {
122 0 : continue;
123 : }
124 :
125 : // set up destination chord
126 :
127 25 : if (dstCR && dstCR->globalTicks() == chord->globalTicks()) {
128 : // existing chord/rest in destination with correct duration
129 : // this case allows for tuplets, unlike the more general case below
130 14 : if (dstCR->isChord()) {
131 : // can simply move note in
132 10 : dstChord = toChord(dstCR);
133 : } else {
134 : // replace rest with chord, then move note in
135 4 : dstChord = Factory::createChord(s);
136 4 : dstChord->setTrack(dstTrack);
137 4 : dstChord->setDurationType(chord->durationType());
138 4 : dstChord->setTicks(chord->ticks());
139 4 : dstChord->setTuplet(dstCR->tuplet());
140 4 : elementScore->undoRemoveElement(dstCR);
141 : }
142 11 : } else if (!chord->tuplet()) {
143 : // rests or gap in destination
144 : // insert new chord if the rests / gap are long enough
145 : // then move note in
146 9 : bool hasIncompatibleTuplet = false;
147 9 : const Chord* cBefore = nullptr;
148 9 : const Chord* cAfterStart = nullptr;
149 31 : for (const Segment* s2 = m->first(SegmentType::ChordRest); s2; s2 = s2->next(SegmentType::ChordRest)) {
150 24 : const ChordRest* cr2 = toChordRest(s2->element(dstTrack));
151 24 : if (!cr2) {
152 10 : continue;
153 : }
154 14 : if (const Tuplet* topTuplet = cr2->topTuplet()) {
155 0 : if (topTuplet->tick() < s->tick()
156 0 : && topTuplet->endTick() > s->tick()) {
157 0 : hasIncompatibleTuplet = true;
158 0 : break;
159 : }
160 0 : if (topTuplet->tick() < chord->endTick()
161 0 : && topTuplet->endTick() > chord->endTick()) {
162 0 : hasIncompatibleTuplet = true;
163 0 : break;
164 : }
165 : }
166 14 : if (!cr2->isChord()) {
167 8 : continue;
168 : }
169 6 : if (s2->tick() < s->tick()) {
170 2 : cBefore = toChord(cr2);
171 : }
172 6 : if (s2->tick() >= s->tick()) {
173 4 : cAfterStart = toChord(cr2);
174 : }
175 6 : if (s2->tick() >= chord->endTick()) {
176 2 : break;
177 : }
178 : }
179 9 : if (hasIncompatibleTuplet) {
180 0 : continue;
181 : }
182 9 : if (cBefore && cBefore->endTick() > s->tick()) {
183 : // previous chord overlaps
184 1 : continue;
185 : }
186 8 : if (cAfterStart && cAfterStart->tick() < chord->endTick()) {
187 : // next chord overlaps
188 2 : continue;
189 : }
190 : // big enough gap found
191 6 : dstChord = Factory::createChord(s);
192 6 : dstChord->setTrack(dstTrack);
193 6 : dstChord->setDurationType(chord->durationType());
194 6 : dstChord->setTicks(chord->ticks());
195 : // makeGapVoice will not back-fill an empty voice
196 6 : if (voice && !dstCR) {
197 4 : elementScore->expandVoice(s, /*m->first(SegmentType::ChordRest,*/ dstTrack);
198 : }
199 6 : elementScore->makeGapVoice(s, dstTrack, chord->ticks(), s->tick());
200 : }
201 :
202 22 : if (!dstChord) {
203 2 : continue;
204 : }
205 :
206 : // move note to destination chord
207 :
208 : // create & add new note
209 20 : Note* newNote = Factory::copyNote(*note);
210 20 : newNote->setSelected(false);
211 20 : newNote->setOwnershipParent(dstChord);
212 20 : elementScore->undoAddElement(newNote);
213 20 : newElements.push_back(newNote);
214 : // add new chord if one was created
215 20 : if (dstChord != dstCR) {
216 10 : elementScore->undoAddCR(dstChord, m, s->tick());
217 : }
218 56 : for (EngravingObject* linked : note->linkList()) {
219 36 : Note* linkedNote = toNote(linked);
220 36 : Note* linkedNewNote = linked == note ? newNote : toNote(newNote->findLinkedInStaff(linkedNote->staff()));
221 : // reconnect the tie to this note, if any
222 36 : Tie* tie = linkedNote->tieBack();
223 36 : if (tie) {
224 0 : Note* startNote = tie->isPartialTie() && !toPartialTie(tie)->isOutgoing() ? nullptr : tie->startNote();
225 0 : elementScore->undoChangeSpannerElements(tie, startNote, linkedNewNote);
226 : }
227 : // reconnect the tie from this note, if any
228 36 : tie = linkedNote->tieFor();
229 36 : if (tie) {
230 0 : elementScore->undoChangeSpannerElements(tie, linkedNewNote, tie->endNote());
231 : }
232 :
233 : // Reconnect note anchored spanners
234 36 : for (EngravingItem* item : linkedNote->spannerBack()) {
235 0 : if (!item || !item->isSpanner()) {
236 0 : continue;
237 : }
238 0 : Spanner* spanner = toSpanner(item);
239 0 : elementScore->undoChangeSpannerElements(spanner, spanner->startElement(), linkedNewNote);
240 : }
241 :
242 36 : for (EngravingItem* item : linkedNote->spannerFor()) {
243 0 : if (!item || !item->isSpanner()) {
244 0 : continue;
245 : }
246 0 : Spanner* spanner = toSpanner(item);
247 0 : elementScore->undoChangeSpannerElements(spanner, linkedNewNote, spanner->endElement());
248 : }
249 20 : }
250 :
251 : // Move paren to corresponding group in new chord
252 20 : if (!chord->noteParentheses().empty()) {
253 0 : const NoteParenthesisInfo* noteParenInfo = chord->findNoteParenthesisInfo(note);
254 0 : if (noteParenInfo) {
255 0 : Parenthesis* leftParen = noteParenInfo->leftParen();
256 0 : Parenthesis* rightParen = noteParenInfo->rightParen();
257 0 : DO_ASSERT(leftParen && rightParen);
258 0 : if (leftParen && rightParen) {
259 0 : bool generated = leftParen->generated();
260 0 : MovedParenGroup& movedGroup = movedParenGroups[noteParenInfo];
261 0 : movedGroup.dstChord = dstChord;
262 0 : movedGroup.generated = generated;
263 0 : movedGroup.notes.push_back(newNote);
264 0 : movedGroup.leftOffset = leftParen->offset();
265 0 : movedGroup.rightOffset = rightParen->offset();
266 0 : movedGroup.leftVisible = leftParen->visible();
267 0 : movedGroup.rightVisible = rightParen->visible();
268 0 : EditChord::removeChordParentheses(chord, { note }, /*addToLinked*/ true, generated);
269 : }
270 : }
271 : }
272 :
273 : // remove original note
274 20 : if (notes > 1) {
275 12 : elementScore->undoRemoveElement(note);
276 8 : } else if (notes == 1) {
277 : // take care of slurs
278 8 : int currentTick = chord->tick().ticks();
279 8 : for (auto it : elementScore->spannerMap().findOverlapping(currentTick, currentTick + 1)) {
280 0 : Spanner* spanner = it.value;
281 0 : if (!spanner->isSlur()) {
282 0 : continue;
283 : }
284 0 : Slur* slur = toSlur(spanner);
285 0 : if (slur->startElement() == chord && slur->endElement() == chord) {
286 0 : elementScore->undoChangeSpannerElements(slur, dstChord, dstChord);
287 0 : slur->undoChangeProperty(Pid::VOICE, voice);
288 0 : } else if (slur->startElement() == chord) {
289 0 : elementScore->undoChangeSpannerElements(slur, dstChord, slur->endElement());
290 0 : } else if (slur->endElement() == chord) {
291 0 : elementScore->undoChangeSpannerElements(slur, slur->startElement(), dstChord);
292 : }
293 : }
294 : // move articulations
295 12 : for (Articulation* artic : chord->articulations()) {
296 4 : if (dstChord->hasArticulation(artic)) {
297 3 : continue;
298 : }
299 1 : elementScore->undoChangeParent(artic, dstChord, dstChord->staffIdx());
300 : }
301 : // create rest to leave behind
302 8 : Rest* r = Factory::createRest(s);
303 8 : r->setTrack(chord->track());
304 8 : r->setDurationType(chord->durationType());
305 8 : r->setTicks(chord->ticks());
306 8 : r->setTuplet(chord->tuplet());
307 : // if there were grace notes, move them
308 8 : while (!chord->graceNotes().empty()) {
309 0 : Chord* gc = chord->graceNotes().front();
310 0 : Chord* ngc = Factory::copyChord(*gc);
311 0 : elementScore->undoRemoveElement(gc);
312 0 : ngc->setOwnershipParent(dstChord);
313 0 : ngc->setTrack(dstChord->track());
314 0 : elementScore->undoAddElement(ngc);
315 : }
316 : // remove chord, replace with rest
317 8 : elementScore->undoRemoveElement(chord);
318 8 : elementScore->undoAddCR(r, m, s->tick());
319 : }
320 :
321 : // Move lyrics
322 20 : for (Lyrics* lyric : chord->lyrics()) {
323 0 : if (!lyric || dstChord->lyrics(lyric->verse(), lyric->placement())) {
324 0 : continue;
325 : }
326 :
327 0 : Lyrics* newLyric = Factory::copyLyrics(*lyric);
328 0 : newLyric->setOwnershipParent(dstChord);
329 0 : newLyric->setSelected(false);
330 0 : newLyric->setTrack(dstTrack);
331 0 : elementScore->undoAddElement(newLyric);
332 0 : newElements.push_back(newLyric);
333 :
334 0 : elementScore->undoRemoveElement(lyric);
335 : }
336 17 : } else if (e->hasVoiceAssignmentProperties()) {
337 0 : if (e->isSpannerSegment()) {
338 0 : e = toSpannerSegment(e)->spanner();
339 : }
340 :
341 0 : track_idx_t dstTrack = e->staffIdx() * VOICES + voice;
342 0 : if (!isOperationValid(score, e->track(), dstTrack)) {
343 0 : continue;
344 : }
345 :
346 0 : e->undoChangeProperty(Pid::VOICE, voice);
347 0 : e->undoChangeProperty(Pid::VOICE_ASSIGNMENT, VoiceAssignment::CURRENT_VOICE_ONLY);
348 0 : newElements.push_back(e);
349 : }
350 : }
351 :
352 : // Recreate parenthesis groups
353 4 : for (auto& pair : movedParenGroups) {
354 0 : MovedParenGroup& movedGroup = pair.second;
355 0 : IF_ASSERT_FAILED(!movedGroup.notes.empty()) {
356 0 : continue;
357 : }
358 0 : EditChord::addChordParentheses(movedGroup.dstChord, movedGroup.notes, /*addToLinked*/ true, movedGroup.generated);
359 :
360 0 : const NoteParenthesisInfo* info = movedGroup.notes.front()->parenthesisInfo();
361 0 : Parenthesis* leftParen = info ? info->leftParen() : nullptr;
362 0 : Parenthesis* rightParen = info ? info->rightParen() : nullptr;
363 :
364 0 : IF_ASSERT_FAILED(info && leftParen && rightParen) {
365 0 : continue;
366 : }
367 :
368 0 : leftParen->undoSetVisible(movedGroup.leftVisible);
369 0 : rightParen->undoSetVisible(movedGroup.rightVisible);
370 0 : leftParen->undoChangeProperty(Pid::OFFSET, movedGroup.leftOffset);
371 0 : rightParen->undoChangeProperty(Pid::OFFSET, movedGroup.rightOffset);
372 : }
373 :
374 4 : if (!newElements.empty()) {
375 4 : score->selection().clear();
376 4 : score->select(newElements, SelectType::ADD, muse::nidx);
377 : }
378 :
379 4 : score->setLayoutAll();
380 4 : }
381 :
382 0 : void EditVoice::changeSelectedElementsVoiceAssignment(Transaction&, Score* score, VoiceAssignment voiceAssignment)
383 : {
384 0 : std::vector<EngravingItem*> newElements;
385 :
386 0 : for (EngravingItem* e : score->selection().elements()) {
387 0 : if (e->hasVoiceAssignmentProperties()) {
388 0 : e->undoChangeProperty(Pid::VOICE_ASSIGNMENT, voiceAssignment);
389 0 : newElements.push_back(e);
390 : }
391 : }
392 :
393 0 : if (!newElements.empty()) {
394 0 : score->selection().clear();
395 0 : score->select(newElements, SelectType::ADD, muse::nidx);
396 : }
397 0 : }
|