LCOV - code coverage report
Current view: top level - src/engraving/editing - edittie.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 74.9 % 199 149
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) 2025 MuseScore Limited and others
       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 "edittie.h"
      24              : 
      25              : #include "../dom/chord.h"
      26              : #include "../dom/factory.h"
      27              : #include "../dom/input.h"
      28              : #include "../dom/laissezvib.h"
      29              : #include "../dom/note.h"
      30              : #include "../dom/partialtie.h"
      31              : #include "../dom/score.h"
      32              : #include "../dom/select.h"
      33              : #include "../dom/tie.h"
      34              : #include "../dom/tiejumppointlist.h"
      35              : #include "../dom/utils.h"
      36              : 
      37              : #include "editchord.h"
      38              : #include "noteinput.h"
      39              : #include "transaction/transaction.h"
      40              : 
      41              : #include "log.h"
      42              : 
      43              : using namespace mu::engraving;
      44              : 
      45           46 : void ChangeTieJumpPointActive::flip()
      46              : {
      47           46 :     TieJumpPoint* jumpPoint = m_jumpPointList->findJumpPoint(m_id);
      48           46 :     if (!jumpPoint) {
      49            0 :         return;
      50              :     }
      51           46 :     bool oldActive = jumpPoint->active();
      52              : 
      53           46 :     if (m_active && jumpPoint->endTie() && jumpPoint->endTie()->jumpPoint() != jumpPoint) {
      54           12 :         jumpPoint->endTie()->setJumpPoint(jumpPoint);
      55              :     }
      56              : 
      57           46 :     jumpPoint->setActive(m_active);
      58           46 :     m_active = oldActive;
      59              : }
      60              : 
      61           11 : std::vector<Note*> EditTie::cmdTieNoteList(const Selection& selection, bool noteEntryMode)
      62              : {
      63           11 :     EngravingItem* el = selection.element();
      64           11 :     if (Note* n = InputState::note(el)) {
      65           10 :         if (noteEntryMode) {
      66            2 :             return n->chord()->notes();
      67              :         } else {
      68           24 :             return { n };
      69              :         }
      70              :     } else {
      71            1 :         ChordRest* cr = InputState::chordRest(el);
      72            1 :         if (cr && cr->isChord()) {
      73            0 :             return toChord(cr)->notes();
      74              :         }
      75              :     }
      76            1 :     return selection.noteList();
      77              : }
      78              : 
      79           11 : static Tie* createAndAddTie(Note* startNote, Note* endNote)
      80              : {
      81           11 :     Score* score = startNote->score();
      82           11 :     Tie* tie = endNote ? Factory::createTie(startNote) : Factory::createPartialTie(startNote);
      83           11 :     tie->setStartNote(startNote);
      84           11 :     tie->setTrack(startNote->track());
      85           11 :     tie->setTick(startNote->chord()->segment()->tick());
      86           11 :     if (endNote) {
      87            9 :         if (endNote->tieBack()) {
      88            1 :             score->undoRemoveElement(endNote->tieBack());
      89              :         }
      90            9 :         tie->setEndNote(endNote);
      91            9 :         tie->setTicks(endNote->chord()->segment()->tick() - startNote->chord()->segment()->tick());
      92              :     }
      93           11 :     score->undoAddElement(tie);
      94              : 
      95           11 :     tie->addTiesToJumpPoints();
      96           11 :     if (!tie->endNote() && tie->tieJumpPoints() && tie->tieJumpPoints()->empty()) {
      97            0 :         score->undoRemoveElement(tie);
      98            0 :         tie = nullptr;
      99              :     }
     100              : 
     101           11 :     return tie;
     102              : }
     103              : 
     104            1 : void EditTie::cmdAddTie(Score* score, bool addToChord)
     105              : {
     106            1 :     std::vector<Note*> noteList = cmdTieNoteList(score->selection(), score->noteEntryMode());
     107            1 :     if (noteList.empty()) {
     108            0 :         LOGD("no notes selected");
     109            0 :         return;
     110              :     }
     111              : 
     112            1 :     std::sort(noteList.begin(), noteList.end(), [](const Note* a, const Note* b) { return a->track() < b->track(); });
     113            1 :     track_idx_t track = noteList.at(0)->track();
     114              : 
     115            1 :     std::vector<EngravingItem*> toSelect;
     116              : 
     117            1 :     score->startCmd(TranslatableString("undoableAction", "Add tie"));
     118            1 :     Chord* lastAddedChord = nullptr;
     119              : 
     120            1 :     Transaction& tx = score->transactionManager()->currentOrDummyTransaction();
     121              : 
     122            1 :     InputState& is = score->inputState();
     123              : 
     124            2 :     for (Note* note : noteList) {
     125            1 :         if (note->tieFor()) {
     126            0 :             LOGD("cmdAddTie: note %p has already tie? noteFor: %p", note, note->tieFor());
     127            0 :             if (addToChord) {
     128            0 :                 continue;
     129              :             } else {
     130            0 :                 score->undoRemoveElement(note->tieFor());
     131              :             }
     132              :         }
     133              : 
     134            1 :         ChordRest* cr = nullptr;
     135            1 :         Chord* c = note->chord();
     136            1 :         int staffMove = c->staffMove();
     137              : 
     138              :         // set cursor at position after note
     139            1 :         if (c->isGraceBefore()) {
     140              :             // tie grace note before to main note
     141            1 :             cr = toChord(c->ownershipParent());
     142            1 :             addToChord = true;
     143              :         } else {
     144            0 :             is.setTrack(note->chord()->track());
     145            0 :             is.setSegment(note->chord()->segment());
     146            0 :             is.moveToNextInputPos();
     147            0 :             if (is.beyondScore()) {
     148            0 :                 score->appendMeasures(1);
     149            0 :                 is.moveToNextInputPos();
     150              :             }
     151            0 :             is.setLastSegment(is.segment());
     152              : 
     153            0 :             if (!is.cr()) {
     154            0 :                 score->expandVoice();
     155              :             }
     156            0 :             cr = is.cr();
     157              :         }
     158            1 :         if (!cr) {
     159            0 :             break;
     160              :         }
     161              : 
     162            1 :         bool addFlag = lastAddedChord != nullptr;
     163            1 :         if (c->track() != track) {
     164            0 :             addFlag = false;
     165            0 :             track = c->track();
     166              :         }
     167              :         // try to re-use existing note or chord
     168            1 :         Note* n = nullptr;
     169            1 :         if (addToChord && cr->isChord()) {
     170            1 :             Chord* chord = toChord(cr);
     171            1 :             Note* nn = chord->findNote(note->pitch());
     172            1 :             if (nn && nn->tpc() == note->tpc()) {
     173            1 :                 n = nn;                     // re-use note
     174              :             } else {
     175            0 :                 addFlag = true;             // re-use chord
     176              :             }
     177              :         }
     178              : 
     179              :         // if no note to re-use, create one
     180            1 :         NoteVal nval(note->noteVal());
     181            1 :         if (!n) {
     182            0 :             n = NoteInput::addPitch(tx, score, nval, addFlag);
     183            0 :             if (staffMove != 0) {
     184            0 :                 score->undo(new ChangeChordStaffMove(n->chord(), staffMove));
     185              :             }
     186              :         } else {
     187            1 :             score->select(n);
     188              :         }
     189              : 
     190            1 :         if (n) {
     191            1 :             if (!lastAddedChord) {
     192            1 :                 lastAddedChord = n->chord();
     193              :             }
     194              :             // n is not necessarily next note if duration span over measure
     195            1 :             Note* nnote = searchTieNote(note);
     196            1 :             while (nnote) {
     197              :                 // DEBUG: if duration spans over measure
     198              :                 // this does not set line for intermediate notes
     199              :                 // tpc was set correctly already
     200              :                 //n->setLine(note->line());
     201              :                 //n->setTpc(note->tpc());
     202            1 :                 createAndAddTie(note, nnote);
     203              : 
     204            1 :                 if (!addFlag || nnote->chord()->tick() >= lastAddedChord->tick() || nnote->chord()->isGrace()) {
     205            1 :                     break;
     206              :                 } else {
     207            0 :                     note = nnote;
     208            0 :                     is.setLastSegment(is.segment());
     209            0 :                     nnote = NoteInput::addPitch(tx, score, nval, true);
     210              :                 }
     211              :             }
     212            1 :             if (staffMove != 0) {
     213            0 :                 for (Note* tiedNote : n->tiedNotes()) {
     214            0 :                     score->undo(new ChangeChordStaffMove(tiedNote->chord(), staffMove));
     215            0 :                 }
     216              :             }
     217              :         }
     218            1 :         toSelect.push_back(n);
     219              :     }
     220            1 :     if (lastAddedChord) {
     221            1 :         NoteInput::nextInputPos(tx, score, lastAddedChord, false);
     222              :     }
     223            2 :     for (EngravingItem* e : toSelect) {
     224            1 :         if (score->canReselectItem(e)) {
     225            0 :             score->select(e, SelectType::ADD);
     226              :         }
     227              :     }
     228            1 :     score->endCmd();
     229            1 : }
     230              : 
     231           10 : Tie* EditTie::cmdToggleTie(Score* score)
     232              : {
     233           10 :     std::vector<Note*> noteList = cmdTieNoteList(score->selection(), score->noteEntryMode());
     234              : 
     235           10 :     if (noteList.empty()) {
     236            0 :         LOGD("no notes selected");
     237            0 :         return nullptr;
     238              :     }
     239              : 
     240           10 :     std::vector<Note*> tieNoteList(noteList.size());
     241           10 :     bool singleTick = true;
     242           10 :     bool someHaveExistingNextNoteToTieTo = false;
     243           10 :     bool allHaveExistingNextNoteToTieTo = true;
     244           21 :     for (size_t i = 0; i < noteList.size(); ++i) {
     245           11 :         Note* n = noteList[i];
     246           11 :         if (n->chord()->tick() != noteList.front()->tick()) {
     247            1 :             singleTick = false;
     248              :         }
     249           11 :         if (n->tieFor()) {
     250            0 :             tieNoteList[i] = nullptr;
     251              :         } else {
     252           11 :             Note* tieNote = searchTieNote(n);
     253           11 :             tieNoteList[i] = tieNote;
     254           11 :             if (tieNote || n->chord()->hasFollowingJumpItem()) {
     255           10 :                 someHaveExistingNextNoteToTieTo = true;
     256              :             } else {
     257            1 :                 allHaveExistingNextNoteToTieTo = false;
     258              :             }
     259              :         }
     260              :     }
     261              : 
     262           10 :     const bool shouldTieListSelection = noteList.size() >= 2 && !singleTick;
     263              : 
     264           10 :     if (singleTick /* i.e. all notes are in the same tick */ && !allHaveExistingNextNoteToTieTo) {
     265            0 :         cmdAddTie(score);
     266            0 :         return nullptr;
     267              :     }
     268              : 
     269              :     const TranslatableString actionName = someHaveExistingNextNoteToTieTo
     270           10 :                                           ? TranslatableString("undoableAction", "Add tie")
     271           10 :                                           : TranslatableString("undoableAction", "Remove tie");
     272              : 
     273           10 :     score->startCmd(actionName);
     274              : 
     275           10 :     Tie* tie = nullptr;
     276              : 
     277           21 :     for (size_t i = 0; i < noteList.size(); ++i) {
     278           11 :         Note* note = noteList[i];
     279           11 :         Note* tieToNote = tieNoteList[i];
     280              : 
     281           11 :         if (!note) {
     282            1 :             continue;
     283              :         }
     284              : 
     285              :         // Tie to adjacent unselected note
     286           10 :         if (someHaveExistingNextNoteToTieTo && tieToNote) {
     287            7 :             Note* startNote = note->tick() <= tieToNote->tick() ? note : tieToNote;
     288            7 :             Note* endNote = startNote == tieToNote ? note : tieToNote;
     289            7 :             tie = createAndAddTie(startNote, endNote);
     290            7 :             continue;
     291            7 :         }
     292              : 
     293            3 :         Tie* oldTie = note->tieFor();
     294            3 :         Chord* chord = note->chord();
     295            3 :         if (oldTie) {
     296              :             // Toggle existing tie off
     297            0 :             if (oldTie->tieJumpPoints()) {
     298            0 :                 oldTie->undoRemoveTiesFromJumpPoints();
     299              :             }
     300            0 :             score->undoRemoveElement(oldTie);
     301            0 :             continue;
     302              :         }
     303              : 
     304            3 :         if (chord->hasFollowingJumpItem()) {
     305              :             // Create partial tie
     306            2 :             tie = createAndAddTie(note, nullptr);
     307            2 :             continue;
     308              :         }
     309              : 
     310            1 :         if (!shouldTieListSelection || i > noteList.size() - 2) {
     311            0 :             continue;
     312              :         }
     313              : 
     314              :         // Tie to next appropriate note in selection
     315            1 :         Note* note2 = nullptr;
     316              : 
     317            1 :         for (size_t j = i + 1; j < noteList.size(); ++j) {
     318            1 :             Note* candidateNote = noteList[j];
     319            1 :             if (!candidateNote) {
     320            0 :                 continue;
     321              :             }
     322            1 :             const bool samePart = note->part() == candidateNote->part();
     323            1 :             const bool samePitch = note->pitch() == candidateNote->pitch();
     324            1 :             const bool sameUnisonIdx = note->unisonIndex() == candidateNote->unisonIndex();
     325            1 :             const bool diffTick = note->tick() != candidateNote->tick();
     326            1 :             if (samePart && samePitch && sameUnisonIdx && diffTick) {
     327            1 :                 note2 = candidateNote;
     328            1 :                 noteList[j] = nullptr;
     329            1 :                 break;
     330              :             }
     331              :         }
     332              : 
     333            1 :         if (!(note && note2)) {
     334            0 :             continue;
     335              :         }
     336              : 
     337            1 :         Note* startNote = note->tick() <= note2->tick() ? note : note2;
     338            1 :         Note* endNote = startNote == note2 ? note : note2;
     339              : 
     340            1 :         tie = createAndAddTie(startNote, endNote);
     341              :     }
     342              : 
     343           10 :     score->endCmd();
     344              : 
     345           10 :     return tie;
     346           10 : }
     347              : 
     348            1 : void EditTie::cmdToggleLaissezVib(Score* score)
     349              : {
     350            1 :     const std::vector<Note*> noteList = score->selection().noteList();
     351              : 
     352            1 :     if (noteList.empty()) {
     353            0 :         LOGD("no notes selected");
     354            0 :         return;
     355              :     }
     356              : 
     357            1 :     score->startCmd(TranslatableString("undoableAction", "Toggle laissez vibrer"));
     358              : 
     359            2 :     for (Note* note: noteList) {
     360            1 :         if (LaissezVib* lv = note->laissezVib()) {
     361            0 :             score->undoRemoveElement(lv);
     362            1 :         } else if (note->tieFor()) {
     363            0 :             continue;
     364              :         } else {
     365            1 :             LaissezVib* lvTie = Factory::createLaissezVib(note);
     366            1 :             score->undoAddElement(lvTie);
     367              :         }
     368              :     }
     369              : 
     370            1 :     score->endCmd();
     371            1 : }
        

Generated by: LCOV version 2.0-1