LCOV - code coverage report
Current view: top level - src/engraving/editing - splitjoinmeasure.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 73.7 % 190 140
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 "splitjoinmeasure.h"
      24              : 
      25              : #include "dom/chordrest.h"
      26              : #include "dom/factory.h"
      27              : #include "dom/masterscore.h"
      28              : #include "dom/measure.h"
      29              : #include "dom/range.h"
      30              : #include "dom/score.h"
      31              : #include "dom/segment.h"
      32              : #include "dom/spanner.h"
      33              : #include "dom/staff.h"
      34              : #include "dom/stafftypechange.h"
      35              : #include "dom/timesig.h"
      36              : 
      37              : #include "addremoveelement.h"
      38              : #include "editspanner.h"
      39              : #include "transaction/transaction.h"
      40              : 
      41              : using namespace mu::engraving;
      42              : 
      43              : //---------------------------------------------------------
      44              : //   splitMeasure
      45              : //---------------------------------------------------------
      46              : 
      47           22 : void SplitJoinMeasure::splitMeasure(Transaction& tx, MasterScore* masterScore, const Fraction& tick)
      48              : {
      49           22 :     Segment* segment = masterScore->tick2segment(tick, false, SegmentType::ChordRest);
      50              : 
      51           22 :     if (segment->rtick().isZero()) {
      52            0 :         MScore::setError(MsError::CANNOT_SPLIT_MEASURE_FIRST_BEAT);
      53            0 :         return;
      54              :     }
      55           22 :     if (segment->splitsTuplet()) {
      56            0 :         MScore::setError(MsError::CANNOT_SPLIT_MEASURE_TUPLET);
      57            0 :         return;
      58              :     }
      59              : 
      60           22 :     Measure* measure = masterScore->tick2measure(tick);
      61              : 
      62           46 :     for (size_t staffIdx = 0; staffIdx < masterScore->nstaves(); ++staffIdx) {
      63           24 :         if (measure->isMeasureRepeatGroup(static_cast<int>(staffIdx))) {
      64            0 :             MScore::setError(MsError::CANNOT_SPLIT_MEASURE_REPEAT);
      65            0 :             return;
      66              :         }
      67              :     }
      68              : 
      69           22 :     ScoreRange range;
      70           22 :     range.read(measure->first(), measure->last());
      71              : 
      72           22 :     Fraction stick = measure->tick();
      73           22 :     Fraction etick = measure->endTick();
      74              : 
      75           22 :     std::vector<std::tuple<Spanner*, Fraction, Fraction> > spanners;
      76           34 :     for (auto i : masterScore->spanner()) {
      77           12 :         Spanner* s = i.second;
      78           12 :         EngravingItem* start = s->startElement();
      79           12 :         EngravingItem* end = s->endElement();
      80           12 :         if (s->tick() >= stick && s->tick() < etick) {
      81            6 :             start = nullptr;
      82              :         }
      83           12 :         if (s->tick2() >= stick && s->tick2() < etick) {
      84            4 :             end = nullptr;
      85              :         }
      86           12 :         if (start != s->startElement() || end != s->endElement()) {
      87            8 :             spanners.push_back(std::make_tuple(s, s->tick(), s->ticks()));
      88            8 :             tx.push(new ChangeStartEndSpanner(s, start, end));
      89              :         }
      90           12 :         if (s->tick() < stick && s->tick2() > stick) {
      91            1 :             spanners.push_back(std::make_tuple(s, s->tick(), s->ticks()));
      92              :         }
      93              :     }
      94              : 
      95           22 :     MeasureBase* nm = measure->next();
      96              : 
      97              :     // create empty measures:
      98           22 :     Score::InsertMeasureOptions options;
      99           22 :     options.createMeasureRests = true;
     100           22 :     options.moveSignaturesClef = false;
     101           22 :     options.moveStaffTypeChanges = false;
     102           22 :     options.ignoreBarLines = true;
     103              : 
     104           22 :     masterScore->insertMeasure(nm, options);
     105           22 :     Measure* m2 = toMeasure(nm ? nm->prev() : masterScore->lastMeasure());
     106           22 :     masterScore->insertMeasure(m2, options);
     107           22 :     Measure* m1 = toMeasure(m2->prev());
     108              : 
     109           46 :     for (Score* s : masterScore->scoreList()) {
     110           24 :         Measure* m = s->tick2measure(tick);
     111           24 :         s->undoRemoveMeasures(m, m, true, false);
     112           22 :     }
     113           22 :     masterScore->undoInsertTime(measure->tick(), -measure->ticks());
     114              : 
     115           22 :     m1->setTick(measure->tick());
     116           22 :     m2->setTick(tick);
     117              : 
     118           22 :     Fraction ticks1 = tick - measure->tick();
     119           22 :     Fraction ticks2 = measure->ticks() - ticks1;
     120              : 
     121           22 :     m1->setTimesig(measure->timesig());
     122           22 :     m2->setTimesig(measure->timesig());
     123              : 
     124           22 :     ticks1.reduce();
     125           22 :     ticks2.reduce();
     126              :     // Now make sure this reduction doesn't go 'beyond' the original measure's
     127              :     // actual denominator for both resultant measures.
     128           22 :     if (ticks1.denominator() < measure->ticks().denominator()) {
     129           14 :         if (measure->ticks().denominator() % m1->timesig().denominator() == 0) {
     130           13 :             int mult = measure->ticks().denominator() / ticks1.denominator();
     131              :             // *= operator automatically reduces via GCD, so rather do literal multiplication:
     132           13 :             ticks1.setDenominator(ticks1.denominator() * mult);
     133           13 :             ticks1.setNumerator(ticks1.numerator() * mult);
     134              :         }
     135              :     }
     136           22 :     if (ticks2.denominator() < measure->ticks().denominator()) {
     137           11 :         if (measure->ticks().denominator() % m2->timesig().denominator() == 0) {
     138           11 :             int mult = measure->ticks().denominator() / ticks2.denominator();
     139           11 :             ticks2.setDenominator(ticks2.denominator() * mult);
     140           11 :             ticks2.setNumerator(ticks2.numerator() * mult);
     141              :         }
     142              :     }
     143           22 :     if (ticks1.denominator() > 128 || ticks2.denominator() > 128) {
     144            0 :         MScore::setError(MsError::CANNOT_SPLIT_MEASURE_TOO_SHORT);
     145            0 :         return;
     146              :     }
     147              : 
     148           22 :     m1->adjustToLen(ticks1, false);
     149           22 :     m2->adjustToLen(ticks2, false);
     150              : 
     151              :     // set correct barline types if needed
     152           46 :     for (Score* linkedScore : masterScore->scoreList()) {
     153           24 :         Measure* sM1 = linkedScore->tick2measure(m1->tick());
     154           24 :         Measure* sM2 = linkedScore->tick2measure(m2->tick());
     155              :         // TODO: handle other end barline types; they may differ per staff
     156           24 :         if (measure->endBarLineType() == BarLineType::END_REPEAT) {
     157            0 :             sM2->undoChangeProperty(Pid::REPEAT_END, true);
     158              :         }
     159           24 :         if (measure->getProperty(Pid::REPEAT_START).toBool()) {
     160            0 :             sM1->undoChangeProperty(Pid::REPEAT_START, true);
     161              :         }
     162           22 :     }
     163              : 
     164           22 :     range.write(masterScore, m1->tick());
     165              : 
     166           31 :     for (const auto& [s, t, ticks] : spanners) {
     167            9 :         if (s->tick() != t) {
     168            1 :             s->undoChangeProperty(Pid::SPANNER_TICK, t);
     169              :         }
     170            9 :         if (s->ticks() != ticks) {
     171            1 :             s->undoChangeProperty(Pid::SPANNER_TICKS, ticks);
     172              :         }
     173              :     }
     174              : 
     175           46 :     for (size_t staffIdx = 0; staffIdx < masterScore->nstaves(); ++staffIdx) {
     176           24 :         Staff* staff = masterScore->staves().at(staffIdx);
     177           24 :         if (staff->isStaffTypeStartFrom(stick)) {
     178            0 :             StaffTypeChange* stc = Factory::createStaffTypeChange(m1);
     179            0 :             stc->setTrack(staffIdx * VOICES);
     180            0 :             masterScore->addElement(stc);
     181              :         }
     182              :     }
     183           22 : }
     184              : 
     185              : //---------------------------------------------------------
     186              : //   joinMeasure
     187              : //---------------------------------------------------------
     188              : 
     189           11 : void SplitJoinMeasure::joinMeasures(Transaction& tx, MasterScore* masterScore, const Fraction& tick1, const Fraction& tick2)
     190              : {
     191           11 :     Measure* m1 = masterScore->tick2measure(tick1);
     192           11 :     Measure* m2 = masterScore->tick2measure(tick2);
     193              : 
     194           11 :     if (!m1 || !m2) {
     195            0 :         return;
     196              :     }
     197              : 
     198           22 :     for (size_t staffIdx = 0; staffIdx < masterScore->nstaves(); ++staffIdx) {
     199           11 :         if (m1->isMeasureRepeatGroupWithPrevM(static_cast<int>(staffIdx))
     200           11 :             || m2->isMeasureRepeatGroupWithNextM(static_cast<int>(staffIdx))) {
     201            0 :             MScore::setError(MsError::CANNOT_SPLIT_MEASURE_REPEAT);
     202            0 :             return;
     203              :         }
     204           11 :         if (masterScore->staves().at(staffIdx)->staffTypeRange(tick2).first > tick1.ticks()) {
     205            0 :             MScore::setError(MsError::CANNOT_JOIN_MEASURE_STAFFTYPE_CHANGE);
     206            0 :             return;
     207              :         }
     208              :     }
     209              : 
     210           11 :     if (m1->isMMRest()) {
     211            0 :         m1 = m1->mmRestFirst();
     212              :     }
     213           11 :     if (m2->isMMRest()) {
     214            0 :         m2 = m2->mmRestLast();
     215              :     }
     216              : 
     217           11 :     masterScore->deselectAll();
     218              : 
     219           11 :     ScoreRange range;
     220           11 :     range.read(m1->first(), m2->last());
     221              : 
     222           11 :     Fraction startTick = m1->tick();
     223           11 :     Fraction endTick = m2->endTick();
     224              : 
     225           11 :     auto spanners = masterScore->spannerMap().findContained(startTick.ticks(), endTick.ticks());
     226           22 :     for (auto i : spanners) {
     227           11 :         masterScore->doUndoRemoveElement(i.value);
     228              :     }
     229              : 
     230           11 :     std::map<Spanner*, Fraction> spannersEndingInRange;
     231           11 :     std::multimap<int, Spanner*> spannerMap = masterScore->spanner();
     232           23 :     for (std::pair<int, Spanner*> i : spannerMap) {
     233           12 :         Spanner* s = i.second;
     234           12 :         if (s->tick() >= startTick && s->tick() < endTick) {
     235            4 :             s->setStartElement(0);
     236              :         }
     237           12 :         if (s->tick2() >= startTick && s->tick2() < endTick) {
     238            3 :             s->setEndElement(0);
     239              :         }
     240              :         // Maintain length of spanners starting outside and ending within join range
     241           12 :         if (s->tick() < startTick && s->tick2() < endTick) {
     242            3 :             spannersEndingInRange.emplace(s, s->tick2());
     243              :         }
     244              :         // Remove spanners starting within join range and ending outside
     245              :         // These will be replaced on range.write
     246           12 :         if (s->tick() > startTick && s->tick() < endTick && s->tick2() > endTick) {
     247            4 :             masterScore->doUndoRemoveElement(s);
     248              :         }
     249              :     }
     250              : 
     251           11 :     MeasureBase* next = m2->next();
     252           11 :     masterScore->deleteMeasures(m1, m2, true);
     253              : 
     254           11 :     Score::InsertMeasureOptions options;
     255           11 :     options.createMeasureRests = true;
     256           11 :     options.moveSignaturesClef = false;
     257           11 :     options.moveStaffTypeChanges = false;
     258           11 :     options.ignoreBarLines = true;
     259           11 :     Measure* joinedMeasure = toMeasure(masterScore->insertMeasure(next, options));
     260              : 
     261           14 :     for (auto spannerToFixup : spannersEndingInRange) {
     262            3 :         Spanner* sp = spannerToFixup.first;
     263            3 :         Fraction spEndTick = spannerToFixup.second;
     264              : 
     265            3 :         sp->setTick2(spEndTick);
     266              :     }
     267              : 
     268           22 :     for (size_t staffIdx = 0; staffIdx < masterScore->nstaves(); ++staffIdx) {
     269           11 :         Staff* staff = masterScore->staves().at(staffIdx);
     270           11 :         if (staff->isStaffTypeStartFrom(tick1)) {
     271            0 :             StaffTypeChange* stc = engraving::Factory::createStaffTypeChange(joinedMeasure);
     272            0 :             stc->setTrack(staffIdx * VOICES);
     273            0 :             masterScore->addElement(stc);
     274              :         }
     275              :     }
     276              : 
     277           11 :     const Fraction newTimesig = m1->timesig();
     278           11 :     Fraction newLen;
     279           22 :     for (Measure* mm = m1; mm; mm = mm->nextMeasure()) {
     280           22 :         newLen += mm->ticks();
     281           22 :         if (mm == m2) {
     282           11 :             break;
     283              :         }
     284              :     }
     285              : 
     286              :     // The loop since measures are not currently linked in MuseScore
     287              :     // change nominal time sig, as inserted measure took it from next measure
     288           22 :     for (Score* linkedScore : masterScore->scoreList()) {
     289           11 :         Measure* ins = linkedScore->tick2measure(startTick);
     290           11 :         ins->undoChangeProperty(Pid::TIMESIG_NOMINAL, newTimesig);
     291              :         // set correct barline types if needed
     292              :         // TODO: handle other end barline types; they may differ per staff
     293           11 :         if (m2->endBarLineType() == BarLineType::END_REPEAT) {
     294            0 :             ins->undoChangeProperty(Pid::REPEAT_END, true);
     295              :         }
     296           11 :         if (m1->getProperty(Pid::REPEAT_START).toBool()) {
     297            0 :             ins->undoChangeProperty(Pid::REPEAT_START, true);
     298              :         }
     299           11 :     }
     300           11 :     Measure* inserted = (next ? next->prevMeasure() : masterScore->lastMeasure());
     301           11 :     inserted->adjustToLen(newLen, /* appendRests... */ false);
     302              : 
     303           11 :     range.write(masterScore, m1->tick());
     304              : 
     305              :     // if there are some Time Signatures in joined measures, move last one to the next measure (if there is not one already)
     306           11 :     Segment* ts = inserted->last()->prev(SegmentType::TimeSig);
     307           11 :     if (ts && ts->rtick().isNotZero()) {
     308            0 :         for (Segment* insMSeg = inserted->last()->prev(SegmentType::TimeSig); insMSeg && insMSeg->rtick().isNotZero();
     309            0 :              insMSeg = insMSeg->prev(SegmentType::TimeSig)) {
     310            0 :             for (staff_idx_t staffIdx = 0; staffIdx < masterScore->nstaves(); ++staffIdx) {
     311            0 :                 if (TimeSig* insTimeSig = toTimeSig(insMSeg->element(staffIdx * VOICES))) {
     312            0 :                     TimeSig* lts = nullptr;
     313            0 :                     for (EngravingObject* l : insTimeSig->linkList()) {
     314            0 :                         Score* linkedScore = l->score();
     315            0 :                         TimeSig* timeSig = toTimeSig(l);
     316            0 :                         Segment* tSeg = timeSig->segment();
     317            0 :                         track_idx_t track = timeSig->track();
     318            0 :                         Measure* sNext = next ? linkedScore->tick2measure(next->tick()) : nullptr;
     319            0 :                         Segment* nextTSeg = sNext ? sNext->undoGetSegmentR(SegmentType::TimeSig, Fraction(0, 1)) : nullptr;
     320            0 :                         if (sNext && !nextTSeg->element(track)) {
     321            0 :                             TimeSig* nsig = Factory::copyTimeSig(*timeSig);
     322            0 :                             nsig->setScore(linkedScore);
     323            0 :                             nsig->setTrack(track);
     324            0 :                             nsig->setOwnershipParent(nextTSeg);
     325              : 
     326            0 :                             linkedScore->doUndoAddElement(nsig);
     327              : 
     328            0 :                             if (!lts) {
     329            0 :                                 lts = nsig;
     330              :                             } else {
     331            0 :                                 tx.push(new Link(lts, nsig));
     332              :                             }
     333              :                         }
     334            0 :                         linkedScore->doUndoRemoveElement(timeSig);
     335            0 :                         if (tSeg->empty()) {
     336            0 :                             linkedScore->doUndoRemoveElement(tSeg);
     337              :                         }
     338            0 :                     }
     339              :                 }
     340              :             }
     341              :         }
     342              :     }
     343           11 : }
        

Generated by: LCOV version 2.0-1