LCOV - code coverage report
Current view: top level - src/engraving/editing/transaction - undostack.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 78.8 % 241 190
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) 2021 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              : /**
      24              :  \file
      25              :  Implementation of undo functions.
      26              : 
      27              :  The undo system requires calling startUndo() when starting a GUI command
      28              :  and calling endUndo() when ending the command. All changes to a score
      29              :  in response to a GUI command must be undoable/redoable by executing
      30              :  a sequence of low-level undo operations. This sequence is built by the code
      31              :  handling the command, by calling one or more undoOp()'s
      32              :  between startUndo() and endUndo().
      33              : */
      34              : 
      35              : #include "undostack.h"
      36              : 
      37              : #include "containers.h"
      38              : 
      39              : #include "dom/box.h"
      40              : #include "dom/select.h"
      41              : 
      42              : #include "editing/editproperty.h"
      43              : #include "editing/editstyle.h"
      44              : #include "editing/textedit.h"
      45              : #include "editing/transaction/undoablecommand.h"
      46              : 
      47              : #include "log.h"
      48              : 
      49              : #define LOG_UNDO() if constexpr (false) LOGD()
      50              : 
      51              : using namespace mu::engraving;
      52              : 
      53              : namespace mu::engraving {
      54              : extern Measure* tick2measure(int tick);
      55              : 
      56              : static const std::unordered_map<CommandType, CommandType> COMMAND_TYPE_INVERSION {
      57              :     { CommandType::AddElement, CommandType::RemoveElement },
      58              :     { CommandType::RemoveElement, CommandType::AddElement },
      59              : 
      60              :     { CommandType::AddBracket, CommandType::RemoveBracket },
      61              :     { CommandType::RemoveBracket, CommandType::AddBracket },
      62              : 
      63              :     { CommandType::AddExcerpt, CommandType::RemoveExcerpt },
      64              :     { CommandType::RemoveExcerpt, CommandType::AddExcerpt },
      65              : 
      66              :     { CommandType::AddSystemObjectStaff, CommandType::RemoveSystemObjectStaff },
      67              :     { CommandType::RemoveSystemObjectStaff, CommandType::AddSystemObjectStaff },
      68              : 
      69              :     { CommandType::InsertMeasures, CommandType::RemoveMeasures },
      70              :     { CommandType::RemoveMeasures, CommandType::InsertMeasures },
      71              : 
      72              :     { CommandType::InsertStaff, CommandType::RemoveStaff },
      73              :     { CommandType::RemoveStaff, CommandType::InsertStaff },
      74              : 
      75              :     { CommandType::InsertPart, CommandType::RemovePart },
      76              :     { CommandType::RemovePart, CommandType::InsertPart },
      77              : 
      78              :     { CommandType::Link, CommandType::Unlink },
      79              :     { CommandType::Unlink, CommandType::Link },
      80              : 
      81              :     { CommandType::AddNoteParenthesesInfo, CommandType::RemoveNoteParenthesesInfo },
      82              :     { CommandType::RemoveNoteParenthesesInfo, CommandType::AddNoteParenthesesInfo },
      83              : 
      84              :     { CommandType::ConnectSharedPart, CommandType::DisconnectSharedPart },
      85              :     { CommandType::DisconnectSharedPart, CommandType::ConnectSharedPart },
      86              : };
      87              : 
      88              : //---------------------------------------------------------
      89              : //   UndoStack
      90              : //---------------------------------------------------------
      91              : 
      92         2169 : UndoStack::UndoStack()
      93              : {
      94         2169 :     m_activeTransaction = nullptr;
      95         2169 :     m_currentIndex = 0;
      96         2169 :     m_cleanState = 0;
      97         2169 :     m_states.push_back(m_cleanState);
      98         2169 :     m_nextState = 1;
      99         2169 : }
     100              : 
     101         1944 : UndoStack::~UndoStack()
     102              : {
     103         1944 :     size_t idx = 0;
     104         2545 :     for (auto c : m_transactions) {
     105          601 :         c->cleanup(idx++ < m_currentIndex);
     106              :     }
     107         1944 :     muse::DeleteAll(m_transactions);
     108         1944 : }
     109              : 
     110         2540 : bool UndoStack::isLocked() const
     111              : {
     112         2540 :     return m_isLocked;
     113              : }
     114              : 
     115            0 : void UndoStack::setLocked(bool locked)
     116              : {
     117            0 :     m_isLocked = locked;
     118            0 : }
     119              : 
     120         1264 : void UndoStack::beginTransaction(Score* score, const TranslatableString& actionName)
     121              : {
     122         1264 :     if (m_isLocked) {
     123            0 :         return;
     124              :     }
     125              : 
     126         1264 :     IF_ASSERT_FAILED(!m_activeTransaction) {
     127            0 :         LOGE() << "Transaction already in progress";
     128            0 :         return;
     129              :     }
     130              : 
     131         1264 :     m_activeTransaction = new UndoableTransaction(score, actionName);
     132              : }
     133              : 
     134            6 : void UndoStack::remove(size_t idx)
     135              : {
     136            6 :     assert(idx <= m_currentIndex);
     137            6 :     assert(m_currentIndex != muse::nidx);
     138              :     // remove redo stack
     139           12 :     while (m_transactions.size() > m_currentIndex) {
     140            0 :         UndoableTransaction* transaction = muse::takeLast(m_transactions);
     141            0 :         m_states.pop_back();
     142            0 :         transaction->cleanup(false); // delete elements for which UndoableCommand() holds ownership
     143            0 :         delete transaction;
     144              : //            --curIdx;
     145              :     }
     146           14 :     while (m_transactions.size() > idx) {
     147            2 :         UndoableTransaction* transaction = muse::takeLast(m_transactions);
     148            2 :         m_states.pop_back();
     149            2 :         transaction->cleanup(true);
     150            2 :         delete transaction;
     151              :     }
     152            6 :     m_currentIndex = idx;
     153            6 : }
     154              : 
     155            5 : void UndoStack::mergeTransactions(size_t startIdx)
     156              : {
     157            5 :     assert(startIdx <= m_currentIndex);
     158              : 
     159            5 :     if (startIdx >= m_transactions.size()) {
     160            0 :         return;
     161              :     }
     162              : 
     163            5 :     UndoableTransaction* startTransaction = m_transactions[startIdx];
     164              : 
     165            7 :     for (size_t idx = startIdx + 1; idx < m_currentIndex; ++idx) {
     166            2 :         startTransaction->append(std::move(*m_transactions[idx]));
     167              :     }
     168            5 :     remove(startIdx + 1);   // TODO: remove from startIdx to curIdx only
     169              : }
     170              : 
     171         1266 : void UndoStack::endTransaction(bool rollback)
     172              : {
     173         1266 :     if (m_isLocked) {
     174            0 :         return;
     175              :     }
     176              : 
     177         1266 :     IF_ASSERT_FAILED(m_activeTransaction) {
     178            0 :         LOGE() << "No transaction in progress";
     179            0 :         return;
     180              :     }
     181              : 
     182         1266 :     if (rollback) {
     183          501 :         delete m_activeTransaction;
     184              :     } else {
     185              :         // remove redo stack
     186         1572 :         while (m_transactions.size() > m_currentIndex) {
     187           42 :             UndoableTransaction* transaction = muse::takeLast(m_transactions);
     188           42 :             m_states.pop_back();
     189           42 :             transaction->cleanup(false); // delete elements for which UndoableCommand() holds ownership
     190           42 :             delete transaction;
     191              :         }
     192          765 :         m_transactions.push_back(m_activeTransaction);
     193          765 :         m_states.push_back(m_nextState++);
     194          765 :         ++m_currentIndex;
     195              :     }
     196         1266 :     m_activeTransaction = nullptr;
     197              : }
     198              : 
     199            3 : void UndoStack::reopen()
     200              : {
     201            3 :     if (m_isLocked) {
     202            0 :         return;
     203              :     }
     204              : 
     205              :     LOG_UNDO() << "curIdx: " << m_currentIndex << ", size: " << m_transactions.size();
     206            3 :     assert(m_activeTransaction == nullptr);
     207            3 :     assert(m_currentIndex > 0);
     208            3 :     --m_currentIndex;
     209            3 :     m_activeTransaction = muse::takeAt(m_transactions, m_currentIndex);
     210            3 :     m_states.erase(m_states.begin() + m_currentIndex + 1);
     211            3 :     for (auto i : m_activeTransaction->commands()) {
     212              :         LOG_UNDO() << "   " << i->name();
     213              :     }
     214              : }
     215              : 
     216          194 : void UndoStack::undo(EditData* ed)
     217              : {
     218              :     LOG_UNDO() << "called";
     219              :     // Are we currently editing text?
     220          194 :     if (ed && ed->element && ed->element->isTextBase()) {
     221            0 :         TextEditData* ted = dynamic_cast<TextEditData*>(ed->getData(ed->element).get());
     222            0 :         if (ted && ted->startUndoIdx == m_currentIndex) {
     223              :             // No edits to undo, so do nothing
     224            0 :             return;
     225              :         }
     226              :     }
     227          194 :     if (m_currentIndex) {
     228          194 :         --m_currentIndex;
     229          194 :         assert(m_currentIndex < m_transactions.size());
     230          194 :         m_transactions[m_currentIndex]->undo();
     231              :     }
     232              : }
     233              : 
     234           36 : void UndoStack::redo()
     235              : {
     236              :     LOG_UNDO() << "called";
     237           36 :     if (canRedo()) {
     238           36 :         m_transactions[m_currentIndex++]->redo();
     239              :     }
     240           36 : }
     241              : 
     242              : //---------------------------------------------------------
     243              : //   UndoableTransaction
     244              : //---------------------------------------------------------
     245              : 
     246         1264 : UndoableTransaction::UndoableTransaction(Score* s, const TranslatableString& actionName)
     247         1264 :     : m_undoInputState(s->inputState()), m_actionName(actionName), m_score(s)
     248              : {
     249         1264 :     fillSelectionInfo(m_undoSelectionInfo, s->selection());
     250         1264 : }
     251              : 
     252         1146 : UndoableTransaction::~UndoableTransaction()
     253              : {
     254        18312 :     for (UndoableCommand* command : m_commands) {
     255        17166 :         delete command;
     256              :     }
     257         1146 : }
     258              : 
     259          645 : void UndoableTransaction::cleanup(bool wasDone)
     260              : {
     261        17811 :     for (UndoableCommand* command : m_commands) {
     262        17166 :         command->cleanup(wasDone);
     263              :     }
     264          645 : }
     265              : 
     266            2 : void UndoableTransaction::unwind(bool cleanUp)
     267              : {
     268              :     // Undo everything first, without freeing: commands pushed later (e.g. AddElement for a part's linked
     269              :     // clone) may still be referenced by earlier commands' undo() (e.g. Link::undo()):
     270            2 :     std::vector<UndoableCommand*> undoneCommands;
     271            2 :     undoneCommands.reserve(m_commands.size());
     272            5 :     while (!m_commands.empty()) {
     273            3 :         UndoableCommand* command = muse::takeLast(m_commands);
     274              :         LOG_UNDO() << "unwind: " << command->name();
     275            3 :         command->undo();
     276            3 :         undoneCommands.push_back(command);
     277              :     }
     278              :     // Now we can free everything:
     279            5 :     for (UndoableCommand* command : undoneCommands) {
     280            3 :         if (cleanUp) {
     281            3 :             command->cleanup(false);
     282              :         }
     283            3 :         delete command;
     284              :     }
     285            2 : }
     286              : 
     287            2 : void UndoableTransaction::appendCommands(UndoableTransaction& other)
     288              : {
     289            2 :     m_commands.insert(m_commands.end(), other.m_commands.cbegin(), other.m_commands.cend());
     290            2 :     other.m_commands.clear();
     291            2 : }
     292              : 
     293            0 : bool UndoableTransaction::hasCommandsMatchingFilter(UndoableCommandFilter f, const EngravingItem* target) const
     294              : {
     295            0 :     for (UndoableCommand* command : m_commands) {
     296            0 :         if (command->matchesFilter(f, target)) {
     297            0 :             return true;
     298              :         }
     299              :     }
     300            0 :     return false;
     301              : }
     302              : 
     303            0 : bool UndoableTransaction::hasCommandsNotMatchingFilters(const std::vector<UndoableCommandFilter>& filters,
     304              :                                                         const EngravingItem* target) const
     305              : {
     306            0 :     for (UndoableCommand* command : m_commands) {
     307            0 :         bool filtered = false;
     308            0 :         for (UndoableCommandFilter f : filters) {
     309            0 :             if (command->matchesFilter(f, target)) {
     310            0 :                 filtered = true;
     311            0 :                 break;
     312              :             }
     313              :         }
     314            0 :         if (!filtered) {
     315            0 :             return true;
     316              :         }
     317              :     }
     318            0 :     return false;
     319              : }
     320              : 
     321            3 : void UndoableTransaction::removeCommandsMatchingFilter(UndoableCommandFilter f, EngravingItem* target)
     322              : {
     323            3 :     std::vector<UndoableCommand*> acceptedList;
     324            6 :     for (UndoableCommand* command : m_commands) {
     325            3 :         if (command->matchesFilter(f, target)) {
     326            3 :             delete command;
     327              :         } else {
     328            0 :             acceptedList.push_back(command);
     329              :         }
     330              :     }
     331            3 :     m_commands = std::move(acceptedList);
     332            3 : }
     333              : 
     334          846 : bool UndoableTransaction::canRecordSelectedElement(const EngravingItem* e)
     335              : {
     336          846 :     if (e->generated()) {
     337            1 :         return false;
     338              :     }
     339              : 
     340         1468 :     return e->isNote() || (e->isChordRest() && !e->isChord())
     341          541 :            || (e->isTextBase() && !e->isInstrumentName() && !e->isHammerOnPullOffText())
     342         1468 :            || e->isFretDiagram() || e->isSoundFlag();
     343              : }
     344              : 
     345         1494 : void UndoableTransaction::fillSelectionInfo(SelectionInfo& info, const Selection& sel)
     346              : {
     347         1494 :     info.staffStart = info.staffEnd = muse::nidx;
     348         1494 :     info.startBox = info.endBox = nullptr;
     349         1494 :     info.elements.clear();
     350              : 
     351         1494 :     if (sel.isList()) {
     352         1092 :         for (EngravingItem* e : sel.elements()) {
     353          846 :             if (canRecordSelectedElement(e)) {
     354          800 :                 info.elements.push_back(e);
     355              :             } else {
     356              :                 // don't remember selection we are unable to restore
     357           46 :                 info.elements.clear();
     358           46 :                 return;
     359              :             }
     360              :         }
     361         1202 :     } else if (sel.isRange()) {
     362          142 :         info.staffStart = sel.staffStart();
     363          142 :         info.staffEnd = sel.staffEnd();
     364          142 :         info.tickStart = sel.tickStart();
     365          142 :         info.tickEnd = sel.tickEnd();
     366          142 :         info.startBox = sel.startBox();
     367          142 :         info.endBox = sel.endBox();
     368              :     }
     369              : }
     370              : 
     371           71 : void UndoableTransaction::applySelectionInfo(const SelectionInfo& info, Selection& sel)
     372              : {
     373           71 :     if (!info.elements.empty()) {
     374          108 :         for (EngravingItem* e : info.elements) {
     375           68 :             sel.add(e);
     376              :         }
     377           31 :     } else if (info.staffStart != muse::nidx) {
     378           31 :         sel.setRangeTicks(info.tickStart, info.tickEnd, info.staffStart, info.staffEnd, info.startBox, info.endBox);
     379              :     }
     380           71 : }
     381              : 
     382          194 : void UndoableTransaction::undo()
     383              : {
     384          194 :     m_redoInputState = m_score->inputState();
     385          194 :     fillSelectionInfo(m_redoSelectionInfo, m_score->selection());
     386          194 :     m_score->deselectAll();
     387              : 
     388         3360 :     for (auto it = m_commands.rbegin(); it != m_commands.rend(); ++it) {
     389              :         LOG_UNDO() << "<" << (*it)->name() << ">";
     390         3166 :         (*it)->undo();
     391              :     }
     392              : 
     393          194 :     m_score->setInputState(m_undoInputState);
     394          194 :     if (m_undoSelectionInfo.isValid()) {
     395           52 :         m_score->deselectAll();
     396           52 :         applySelectionInfo(m_undoSelectionInfo, m_score->selection());
     397              :     }
     398          194 : }
     399              : 
     400           36 : void UndoableTransaction::redo()
     401              : {
     402           36 :     m_undoInputState = m_score->inputState();
     403           36 :     fillSelectionInfo(m_undoSelectionInfo, m_score->selection());
     404           36 :     m_score->deselectAll();
     405              : 
     406          507 :     for (UndoableCommand* command : m_commands) {
     407              :         LOG_UNDO() << "<" << command->name() << ">";
     408          471 :         command->redo();
     409              :     }
     410              : 
     411           36 :     m_score->setInputState(m_redoInputState);
     412           36 :     if (m_redoSelectionInfo.isValid()) {
     413           19 :         m_score->deselectAll();
     414           19 :         applySelectionInfo(m_redoSelectionInfo, m_score->selection());
     415              :     }
     416           36 : }
     417              : 
     418            2 : void UndoableTransaction::append(UndoableTransaction&& other)
     419              : {
     420            2 :     appendCommands(other);
     421            2 :     if (m_score == other.m_score) {
     422            2 :         m_redoInputState = std::move(other.m_redoInputState);
     423            2 :         m_redoSelectionInfo = std::move(other.m_redoSelectionInfo);
     424              :     }
     425            2 : }
     426              : 
     427            0 : const InputState& UndoableTransaction::undoInputState() const
     428              : {
     429            0 :     return m_undoInputState;
     430              : }
     431              : 
     432            0 : const InputState& UndoableTransaction::redoInputState() const
     433              : {
     434            0 :     return m_redoInputState;
     435              : }
     436              : 
     437            0 : void UndoableTransaction::excludeElementFromSelectionInfo(EngravingItem* element)
     438              : {
     439            0 :     if (m_undoSelectionInfo.isValid()) {
     440            0 :         muse::remove(m_undoSelectionInfo.elements, element);
     441              :     }
     442              : 
     443            0 :     if (m_redoSelectionInfo.isValid()) {
     444            0 :         muse::remove(m_redoSelectionInfo.elements, element);
     445              :     }
     446            0 : }
     447              : 
     448            0 : const UndoableTransaction::SelectionInfo& UndoableTransaction::undoSelectionInfo() const
     449              : {
     450            0 :     return m_undoSelectionInfo;
     451              : }
     452              : 
     453            0 : const UndoableTransaction::SelectionInfo& UndoableTransaction::redoSelectionInfo() const
     454              : {
     455            0 :     return m_redoSelectionInfo;
     456              : }
     457              : 
     458         1467 : UndoableTransaction::ChangesInfo UndoableTransaction::changesInfo(bool undo) const
     459              : {
     460         1467 :     ChangesInfo result;
     461              : 
     462        24813 :     for (const UndoableCommand* command : commands()) {
     463        23346 :         CommandType type = command->type();
     464              : 
     465        23346 :         if (type == CommandType::ChangeProperty) {
     466         2310 :             auto changeProperty = static_cast<const ChangeProperty*>(command);
     467         2310 :             result.changedPropertyIdSet.insert(changeProperty->getId());
     468        21036 :         } else if (type == CommandType::ChangeStyleValues) {
     469           56 :             auto changeStyle = static_cast<const ChangeStyleValues*>(command);
     470          113 :             for (const auto& pair : changeStyle->values()) {
     471           57 :                 result.changedStyleIdSet.insert(pair.first);
     472              :             }
     473        20980 :         } else if (type == CommandType::ChangeStyle) {
     474            1 :             auto changeStyle = static_cast<const ChangeStyle*>(command);
     475            1 :             const StyleIdSet styleIds = changeStyle->changedIds();
     476            1 :             result.changedStyleIdSet.insert(styleIds.cbegin(), styleIds.cend());
     477        20980 :         } else if (type == CommandType::TextEdit) {
     478            9 :             result.isTextEditing |= static_cast<const TextEditUndoableCommand*>(command)->cursor().editing();
     479              :         }
     480              : 
     481        23346 :         if (undo) {
     482         1946 :             auto it = COMMAND_TYPE_INVERSION.find(type);
     483         1946 :             if (it != COMMAND_TYPE_INVERSION.end()) {
     484         1007 :                 type = it->second;
     485              :             }
     486              :         }
     487              : 
     488        61296 :         for (EngravingObject* object : command->objectItems()) {
     489        37950 :             if (!object) {
     490           24 :                 continue;
     491              :             }
     492              : 
     493        37926 :             result.changedObjectTypes.insert(object->type());
     494        37926 :             result.changedObjects[object].insert(type);
     495        23346 :         }
     496              :     }
     497              : 
     498         1467 :     return result;
     499            0 : }
     500              : 
     501            0 : const TranslatableString& UndoableTransaction::actionName() const
     502              : {
     503            0 :     return m_actionName;
     504              : }
     505              : }
        

Generated by: LCOV version 2.0-1