LCOV - code coverage report
Current view: top level - src/engraving/editing - edittuplet.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 95.3 % 85 81
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 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 "edittuplet.h"
      24              : 
      25              : #include <algorithm>
      26              : #include <map>
      27              : #include <set>
      28              : #include <unordered_set>
      29              : 
      30              : #include "../dom/chord.h"
      31              : #include "../dom/chordrest.h"
      32              : #include "../dom/durationtype.h"
      33              : #include "../dom/input.h"
      34              : #include "../dom/mscore.h"
      35              : #include "../dom/note.h"
      36              : #include "../dom/score.h"
      37              : #include "../dom/select.h"
      38              : 
      39              : #include "log.h"
      40              : 
      41              : using namespace mu::engraving;
      42              : 
      43           11 : static Fraction dividingRatio(const ChordRest* chordRest, const Fraction& ratio, bool autoBaseLen)
      44              : {
      45           11 :     Fraction result = ratio;
      46           11 :     if (autoBaseLen) {
      47           11 :         result.setDenominator(Tuplet::computeTupletDenominator(ratio.numerator(), chordRest->ticks()));
      48              :     }
      49           11 :     return result;
      50              : }
      51              : 
      52              : // Selected chords/rests a tuplet can start at, in score order
      53           49 : static std::vector<ChordRest*> selectedTupletChordRests(const Score* score)
      54              : {
      55              :     // Score order, not pointer order
      56           49 :     const std::set<ChordRest*> selectedChordRests = score->getSelectedChordRests();
      57           49 :     std::vector<ChordRest*> sortedChordRests(selectedChordRests.begin(), selectedChordRests.end());
      58           49 :     std::sort(sortedChordRests.begin(), sortedChordRests.end(), [](const ChordRest* a, const ChordRest* b) {
      59          223 :         return a->tick() != b->tick() ? a->tick() < b->tick() : a->track() < b->track();
      60              :     });
      61              : 
      62           49 :     std::vector<ChordRest*> chordRests;
      63              : 
      64              :     // Make sure each linked group is handled only once
      65           49 :     std::unordered_set<const LinkedObjects*> handledLinks;
      66              : 
      67          187 :     for (ChordRest* chordRest : sortedChordRests) {
      68          138 :         if (chordRest->links() && !handledLinks.insert(chordRest->links()).second) {
      69            4 :             continue;
      70              :         }
      71              : 
      72          134 :         if (!chordRest->isGrace() && !(chordRest->isChord() && toChord(chordRest)->isTrillCueNote())) {
      73          134 :             chordRests.push_back(chordRest);
      74              :         }
      75              :     }
      76              : 
      77           49 :     return chordRests;
      78           49 : }
      79              : 
      80              : // The selected chords/rests that get a tuplet: one whose tuplet doesn't fit is left as it is (as is a measure rest,
      81              : // which has no duration to use as the subdivision), and one that the tuplet before it in its track will replace gets none
      82           27 : static std::vector<ChordRest*> tupletStarts(const Score* score, const Fraction& ratio)
      83              : {
      84           27 :     std::vector<ChordRest*> starts;
      85           27 :     std::map<track_idx_t, Fraction> tupletEndTicks;
      86          106 :     for (ChordRest* chordRest : selectedTupletChordRests(score)) {
      87           79 :         const auto previous = tupletEndTicks.find(chordRest->track());
      88           79 :         if (previous != tupletEndTicks.end() && chordRest->tick() < previous->second) {
      89           41 :             continue;
      90              :         }
      91           53 :         if (!score->canAddTuplet(chordRest, ratio, chordRest->durationType())) {
      92           15 :             continue;
      93              :         }
      94           38 :         tupletEndTicks[chordRest->track()] = score->tupletEndTick(chordRest, ratio, chordRest->durationType());
      95           38 :         starts.push_back(chordRest);
      96           27 :     }
      97              : 
      98           27 :     return starts;
      99           27 : }
     100              : 
     101            5 : static void selectTupletStart(Score* score, const Tuplet* tuplet)
     102              : {
     103            5 :     if (tuplet->elements().empty()) {
     104            0 :         return;
     105              :     }
     106              : 
     107            5 :     DurationElement* firstElement = tuplet->elements().front();
     108              :     // SINGLE also moves the input position
     109            5 :     if (firstElement->isChord()) {
     110            5 :         const std::vector<Note*>& notes = toChord(firstElement)->notes();
     111            5 :         IF_ASSERT_FAILED(!notes.empty()) {
     112            0 :             score->select(firstElement, SelectType::SINGLE);
     113              :         } else {
     114            5 :             score->select(notes.front(), SelectType::SINGLE);
     115           15 :             score->select({ notes.begin() + 1, notes.end() }, SelectType::ADD);
     116              :         }
     117              :     } else {
     118            0 :         score->select(firstElement, SelectType::SINGLE);
     119              :     }
     120              : }
     121              : 
     122           17 : bool EditTuplet::checkAddTupletsWithSelectedDurations(const Score* score, const Fraction& ratio)
     123              : {
     124           17 :     if (!selectedTupletChordRests(score).empty() && tupletStarts(score, ratio).empty()) {
     125            7 :         MScore::setError(MsError::NO_SPACE_FOR_TUPLET);
     126            7 :         return false;
     127              :     }
     128              : 
     129           10 :     return true;
     130              : }
     131              : 
     132           15 : std::vector<Tuplet*> EditTuplet::addTuplets(Transaction&, Score* score, const Fraction& ratio, TupletNumberType numberType,
     133              :                                             TupletBracketType bracketType, bool autoBaseLen, TupletSelectedDuration selectedDurationAs)
     134              : {
     135           15 :     const bool asSubdivision = selectedDurationAs == TupletSelectedDuration::SUBDIVISION;
     136           15 :     Selection& selection = score->selection();
     137           15 :     const bool wasRangeSelection = selection.isRange();
     138           15 :     const Fraction startTick = selection.tickStart();
     139           15 :     const Fraction endTick = selection.tickEnd();
     140           15 :     const staff_idx_t startStaff = selection.staffStart();
     141           15 :     const staff_idx_t endStaff = selection.staffEnd();
     142           15 :     Box* startBox = selection.startBox();
     143           15 :     Box* endBox = selection.endBox();
     144              : 
     145              :     // ticks taken before any tuplet is created
     146           15 :     std::vector<std::pair<ChordRest*, Fraction> > starts;
     147           45 :     for (ChordRest* chordRest : asSubdivision ? tupletStarts(score, ratio) : selectedTupletChordRests(score)) {
     148           30 :         starts.emplace_back(chordRest, chordRest->tick());
     149           15 :     }
     150              : 
     151           15 :     std::vector<Tuplet*> tuplets;
     152              :     // a subdivision tuplet can end after the range
     153           15 :     Fraction rangeEndTick = endTick;
     154           45 :     for (const auto& [chordRest, tick] : starts) {
     155              :         // an earlier tuplet may have replaced it, e.g. on a linked staff
     156           30 :         if (asSubdivision && score->findCR(tick, chordRest->track()) != chordRest) {
     157            0 :             continue;
     158              :         }
     159              : 
     160           30 :         Tuplet* tuplet = nullptr;
     161           30 :         if (asSubdivision) {
     162           19 :             tuplet = score->addTuplet(chordRest, ratio, chordRest->durationType(), numberType, bracketType);
     163           19 :             if (tuplet) {
     164           19 :                 rangeEndTick = std::max(rangeEndTick, tuplet->endTick());
     165              :             }
     166              :         } else {
     167           11 :             tuplet = score->addTuplet(chordRest, dividingRatio(chordRest, ratio, autoBaseLen), numberType, bracketType);
     168              :         }
     169           30 :         if (tuplet) {
     170           30 :             tuplets.push_back(tuplet);
     171              :         }
     172              :     }
     173              : 
     174           15 :     if (wasRangeSelection) {
     175           10 :         selection.setRangeTicks(startTick, rangeEndTick, startStaff, endStaff, startBox, endBox);
     176           10 :         selection.updateSelectedElements();
     177            5 :     } else if (!tuplets.empty()) {
     178            5 :         selectTupletStart(score, tuplets.front());
     179              :     }
     180              : 
     181           15 :     if (!tuplets.empty()) {
     182           15 :         score->inputState().setDuration(tuplets.front()->baseLen());
     183              :     }
     184              : 
     185           15 :     return tuplets;
     186           15 : }
        

Generated by: LCOV version 2.0-1