LCOV - code coverage report
Current view: top level - src/engraving/editing - editvoice.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 52.4 % 212 111
Test Date: 2026-11-05 04:07:02

            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 : }
        

Generated by: LCOV version 2.0-1