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 "transaction.h"
24 :
25 : #include "../../dom/masterscore.h"
26 :
27 : #include "undoablecommand.h"
28 : #include "undostack.h"
29 :
30 : #include "log.h"
31 :
32 : using namespace mu::engraving;
33 :
34 1467 : static ScoreChanges buildScoreChanges(const CmdState& cmdState, const UndoableTransaction::ChangesInfo& changes,
35 : const std::vector<UndoableCommand*>& commands)
36 : {
37 1467 : int startTick = cmdState.startTick().ticks();
38 1467 : int endTick = cmdState.endTick().ticks();
39 1467 : staff_idx_t startStaff = cmdState.startStaff();
40 1467 : staff_idx_t endStaff = cmdState.endStaff();
41 :
42 24813 : for (const UndoableCommand* cmd : commands) {
43 23346 : std::optional<ChangedRange> range = cmd->changedRange();
44 23346 : if (!range) {
45 23342 : continue;
46 : }
47 :
48 4 : int tickFrom = range->tickFrom.ticks();
49 4 : int tickTo = range->tickTo.ticks();
50 :
51 4 : if (startTick == -1 || startTick > tickFrom) {
52 4 : startTick = tickFrom;
53 : }
54 4 : if (endTick == -1 || endTick < tickTo) {
55 4 : endTick = tickTo;
56 : }
57 :
58 4 : if (range->staffIdxFrom != muse::nidx && (startStaff == muse::nidx || startStaff > range->staffIdxFrom)) {
59 4 : startStaff = range->staffIdxFrom;
60 : }
61 4 : if (range->staffIdxTo != muse::nidx && (endStaff == muse::nidx || endStaff < range->staffIdxTo)) {
62 4 : endStaff = range->staffIdxTo;
63 : }
64 : }
65 :
66 26082 : for (const auto& pair : changes.changedObjects) {
67 24615 : if (!pair.first->isEngravingItem()) {
68 209 : continue;
69 : }
70 :
71 24406 : int tick = toEngravingItem(pair.first)->tick().ticks();
72 :
73 24406 : if (startTick > tick) {
74 19 : startTick = tick;
75 : }
76 :
77 24406 : if (endTick < tick) {
78 885 : endTick = tick;
79 : }
80 : }
81 :
82 : return { startTick, endTick,
83 : startStaff, endStaff,
84 1467 : changes.isTextEditing,
85 1467 : std::move(changes.changedObjects),
86 1467 : std::move(changes.changedObjectTypes),
87 1467 : std::move(changes.changedPropertyIdSet),
88 1467 : std::move(changes.changedStyleIdSet) };
89 1467 : }
90 :
91 : //---------------------------------------------------------
92 : // Transaction
93 : //---------------------------------------------------------
94 :
95 1277 : Transaction::Transaction(UndoableTransaction* undoableTransaction)
96 1277 : : m_undoableTransaction(undoableTransaction)
97 : {
98 1277 : }
99 :
100 6142 : Transaction& Transaction::dummy()
101 : {
102 6142 : static Transaction dummyTransaction(nullptr);
103 6142 : dummyTransaction.m_isDummy = true;
104 6142 : return dummyTransaction;
105 : }
106 :
107 22499 : void Transaction::push(UndoableCommand* cmd)
108 : {
109 22499 : if (m_isDummy) {
110 1580 : cmd->redo();
111 1580 : delete cmd;
112 1580 : return;
113 : }
114 :
115 : // NOTE: append the command _before_ performing it. Some commands' redo()
116 : // may themselves push further commands; those must end up _after_ this
117 : // command in the transaction, so that undo reverses them first.
118 20919 : m_undoableTransaction->appendCommand(cmd);
119 20919 : cmd->redo();
120 : }
121 :
122 21 : void Transaction::pushWithoutPerforming(UndoableCommand* cmd)
123 : {
124 21 : if (m_isDummy) {
125 0 : delete cmd;
126 0 : return;
127 : }
128 :
129 21 : m_undoableTransaction->appendCommand(cmd);
130 : }
131 :
132 : //---------------------------------------------------------
133 : // TransactionManager
134 : //---------------------------------------------------------
135 :
136 2169 : TransactionManager::TransactionManager(MasterScore* masterScore)
137 2169 : : m_masterScore(masterScore)
138 : {
139 2169 : }
140 :
141 2742 : UndoStack* TransactionManager::undoStack() const
142 : {
143 2742 : return m_masterScore->undoStack();
144 : }
145 :
146 744 : void TransactionManager::transaction(const muse::TranslatableString& description, std::function<void(Transaction&)> func)
147 : {
148 744 : IF_ASSERT_FAILED(!m_currentTransaction) {
149 0 : LOGW() << "transaction already active";
150 0 : func(*m_currentTransaction);
151 0 : return;
152 : }
153 744 : beginTransaction(description);
154 744 : func(*m_currentTransaction);
155 744 : endTransaction();
156 : }
157 :
158 1267 : void TransactionManager::beginTransaction(const muse::TranslatableString& description)
159 : {
160 1267 : UndoStack* stack = undoStack();
161 :
162 1267 : if (stack->isLocked()) {
163 0 : return;
164 : }
165 :
166 1267 : if (stack->hasActiveTransaction()) {
167 3 : LOGD() << "cmd already active";
168 3 : return;
169 : }
170 :
171 1264 : MScore::setError(MsError::MS_NO_ERROR);
172 :
173 1264 : m_masterScore->cmdState().reset();
174 1264 : stack->beginTransaction(m_masterScore, description);
175 1264 : m_currentTransaction = std::unique_ptr<Transaction>(new Transaction(stack->activeTransaction()));
176 : }
177 :
178 1270 : void TransactionManager::endTransaction(bool rollback, bool layoutAllParts, bool keepRolledBackElements)
179 : {
180 1270 : UndoStack* stack = undoStack();
181 :
182 1270 : if (stack->isLocked()) {
183 4 : return;
184 : }
185 :
186 1270 : if (!stack->hasActiveTransaction()) {
187 4 : LOGW() << "no command active";
188 4 : m_masterScore->update();
189 4 : return;
190 : }
191 :
192 1266 : if (m_masterScore->readOnly() || (MScore::_error != MsError::MS_NO_ERROR && !MScore::_errorIsWarning)) {
193 1 : rollback = true;
194 : }
195 :
196 1266 : if (rollback) {
197 : /* When keepRolledBackElements is true, unwind removes the rolled-back elements from
198 : * the score but does not free them: an external owner is responsible for the deferred
199 : * deletion, since the elements are still referenced after this returns. */
200 1 : stack->activeTransaction()->unwind(!keepRolledBackElements);
201 : }
202 :
203 : // NOTE: perform update and re-layout within the transaction, so that
204 : // undoable commands generated by layout are recorded correctly.
205 1266 : m_masterScore->update(false, layoutAllParts);
206 :
207 1266 : ScoreChanges changes;
208 1266 : if (!rollback) {
209 2530 : changes = buildScoreChanges(m_masterScore->cmdState(), stack->activeTransaction()->changesInfo(),
210 1265 : stack->activeTransaction()->commands());
211 : }
212 :
213 1266 : LOGD() << "Undo stack current transaction commands count: " << stack->activeTransaction()->commands().size();
214 :
215 1266 : const bool isCurrentTransactionEmpty = stack->activeTransaction()->empty(); // nothing to undo?
216 1266 : stack->endTransaction(isCurrentTransactionEmpty);
217 1266 : m_currentTransaction.reset();
218 :
219 1266 : if (m_masterScore->dirty()) {
220 917 : m_masterScore->invalidateRepeatList(); // TODO: flag individual operations
221 : }
222 :
223 1266 : m_masterScore->cmdState().reset();
224 :
225 1266 : if (!isCurrentTransactionEmpty && !rollback) {
226 765 : m_masterScore->updateAutomation(changes);
227 765 : m_masterScore->changesChannel().send(changes);
228 : }
229 1266 : }
230 :
231 3 : void TransactionManager::reopen()
232 : {
233 3 : UndoStack* stack = undoStack();
234 :
235 3 : if (stack->isLocked()) {
236 0 : return;
237 : }
238 :
239 3 : if (stack->hasActiveTransaction()) {
240 0 : LOGD() << "cmd already active";
241 0 : return;
242 : }
243 :
244 3 : stack->reopen();
245 3 : m_currentTransaction = std::unique_ptr<Transaction>(new Transaction(stack->activeTransaction()));
246 : }
247 :
248 37575 : Transaction* TransactionManager::currentTransaction() const
249 : {
250 37575 : return m_currentTransaction.get();
251 : }
252 :
253 11270 : Transaction& TransactionManager::currentOrDummyTransaction()
254 : {
255 11270 : if (m_currentTransaction) {
256 5128 : return *m_currentTransaction;
257 : } else {
258 6142 : return Transaction::dummy();
259 : }
260 : }
261 :
262 202 : void TransactionManager::undoRedo(bool undo, EditData* editData)
263 : {
264 202 : if (m_masterScore->readOnly()) {
265 0 : return;
266 : }
267 :
268 202 : UndoStack* stack = undoStack();
269 :
270 202 : IF_ASSERT_FAILED(!stack->hasActiveTransaction()) {
271 0 : LOGW() << "cannot undo/redo while transaction is active";
272 0 : return;
273 : }
274 :
275 202 : if (undo) {
276 168 : IF_ASSERT_FAILED(stack->canUndo()) {
277 0 : LOGW() << "cannot undo";
278 0 : return;
279 : }
280 : } else {
281 34 : IF_ASSERT_FAILED(stack->canRedo()) {
282 0 : LOGW() << "cannot redo";
283 0 : return;
284 : }
285 : }
286 :
287 202 : m_masterScore->cmdState().reset();
288 :
289 : //! NOTE: the order of operations is very important here
290 : //! 1. for the undo operation, the list of changed elements is available before undo()
291 : //! 2. for the redo operation, the list of changed elements will be available after redo()
292 202 : UndoableTransaction::ChangesInfo changes;
293 202 : UndoableTransaction* transaction = nullptr;
294 :
295 202 : if (undo) {
296 168 : transaction = stack->last();
297 168 : changes = transaction->changesInfo(true);
298 168 : stack->undo(editData);
299 : } else {
300 34 : stack->redo();
301 34 : transaction = stack->last();
302 34 : changes = transaction->changesInfo(false);
303 : }
304 :
305 202 : m_masterScore->update(false);
306 202 : m_masterScore->invalidateRepeatList(); // TODO: flag individual operations
307 202 : m_masterScore->updateSelection();
308 :
309 202 : ScoreChanges result = buildScoreChanges(m_masterScore->cmdState(), changes, transaction->commands());
310 202 : m_masterScore->updateAutomation(result);
311 202 : m_masterScore->changesChannel().send(result);
312 202 : }
|