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