LCOV - code coverage report
Current view: top level - src/engraving/editing/transaction - transaction.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 89.3 % 150 134
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
       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 : }
        

Generated by: LCOV version 2.0-1