From 69b0494ee9f9284e0de8dc06c1205fe61e2250f7 Mon Sep 17 00:00:00 2001 From: WarmUpTill <19472752+WarmUpTill@users.noreply.github.com> Date: Wed, 12 Aug 2026 11:48:53 +0200 Subject: [PATCH] Add undo / redo support for macros --- CMakeLists.txt | 2 + data/locale/en-US.ini | 15 + forms/advanced-scene-switcher.ui | 7 + lib/general.cpp | 9 + lib/macro/macro-action-edit.cpp | 33 + lib/macro/macro-action-loop.cpp | 1 + lib/macro/macro-action-macro.cpp | 1 + lib/macro/macro-condition-edit.cpp | 25 + lib/macro/macro-edit.cpp | 214 ++++- lib/macro/macro-edit.hpp | 10 +- lib/macro/macro-tab.cpp | 22 +- lib/macro/macro-tree.cpp | 4 + lib/macro/macro-undo-redo.cpp | 1394 ++++++++++++++++++++++++++++ lib/macro/macro-undo-redo.hpp | 38 + lib/macro/macro.cpp | 5 + lib/macro/macro.hpp | 4 + tests/stubs/macro-edit.cpp | 4 +- 17 files changed, 1768 insertions(+), 20 deletions(-) create mode 100644 lib/macro/macro-undo-redo.cpp create mode 100644 lib/macro/macro-undo-redo.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index df1e47c3..7dc00a3a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -173,6 +173,8 @@ target_sources( lib/macro/macro-signals.hpp lib/macro/macro-tab.cpp lib/macro/macro-tree.cpp + lib/macro/macro-undo-redo.cpp + lib/macro/macro-undo-redo.hpp lib/macro/macro-tree.hpp lib/macro/macro-websocket-trigger.cpp lib/macro/macro.cpp diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index 16ea5f3c..94ff2a47 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -47,6 +47,7 @@ AdvSceneSwitcher.generalTab.generalBehavior.hideLegacyTabs="Hide tabs which can AdvSceneSwitcher.generalTab.generalBehavior.alwaysShowMacroSearch="Always show macro search" AdvSceneSwitcher.generalTab.generalBehavior.alwaysShowFeatureTabs="Always show all feature tabs" AdvSceneSwitcher.generalTab.generalBehavior.alwaysShowFeatureTabs.tooltip="Show tabs that are normally hidden until first used in a macro (e.g. Action Queues, Variables, Twitch, Websockets)." +AdvSceneSwitcher.generalTab.generalBehavior.disableUndoRedo="Disable undo/redo for macro changes" AdvSceneSwitcher.generalTab.generalBehavior.settingsLock="Lock settings dialog with password" AdvSceneSwitcher.generalTab.generalBehavior.settingsLock.title="Settings Locked" AdvSceneSwitcher.generalTab.generalBehavior.settingsLock.passwordLabel="Enter password:" @@ -198,6 +199,20 @@ AdvSceneSwitcher.macroTab.groupNameExists="The name \"%1\" is already used by a AdvSceneSwitcher.macroTab.removeSingleMacroPopup.text="Are you sure you want to remove \"%1\"?" AdvSceneSwitcher.macroTab.removeMultipleMacrosPopup.text="Are you sure you want to remove %1 macros/groups?" AdvSceneSwitcher.macroTab.removeGroupPopup.text="Are you sure you want to remove \"%1\" group and all its elements?" +AdvSceneSwitcher.undo.renameMacro="Rename Macro '%1'" +AdvSceneSwitcher.undo.modifyMacro="Modify Macro '%1'" +AdvSceneSwitcher.undo.addMacro="Add Macro '%1'" +AdvSceneSwitcher.undo.removeMacro="Remove Macro '%1'" +AdvSceneSwitcher.undo.importMacros="Import Macros" +AdvSceneSwitcher.undo.addSegment="Add %1 in '%2'" +AdvSceneSwitcher.undo.removeSegment="Remove %1 in '%2'" +AdvSceneSwitcher.undo.changeSegmentType="Change %1 Type in '%2'" +AdvSceneSwitcher.undo.groupMacros="Group Macros '%1'" +AdvSceneSwitcher.undo.ungroupMacros="Ungroup Macros '%1'" +AdvSceneSwitcher.undo.removeMacroGroup="Remove Macro Group '%1'" +AdvSceneSwitcher.undo.segment.action="Action" +AdvSceneSwitcher.undo.segment.elseAction="Else Action" +AdvSceneSwitcher.undo.segment.condition="Condition" AdvSceneSwitcher.macroTab.contextMenuAdd="Add" AdvSceneSwitcher.macroTab.copy="Duplicate Macro" AdvSceneSwitcher.macroTab.group="Group Selected Macros" diff --git a/forms/advanced-scene-switcher.ui b/forms/advanced-scene-switcher.ui index aa5acd43..a1ea130a 100644 --- a/forms/advanced-scene-switcher.ui +++ b/forms/advanced-scene-switcher.ui @@ -265,6 +265,13 @@ + + + + AdvSceneSwitcher.generalTab.generalBehavior.disableUndoRedo + + + diff --git a/lib/general.cpp b/lib/general.cpp index dccd8c94..aad09668 100644 --- a/lib/general.cpp +++ b/lib/general.cpp @@ -7,6 +7,7 @@ #include "macro-helpers.hpp" #include "macro-search.hpp" #include "macro-settings.hpp" +#include "macro-undo-redo.hpp" #include "macro.hpp" #include "path-helpers.hpp" #include "source-helpers.hpp" @@ -1028,6 +1029,14 @@ void AdvSceneSwitcher::SetupGeneralTab() ui->suppressCrashRecoveryDialog->setChecked(GetSuppressCrashDialog()); ui->hideLegacyTabs->setChecked(switcher->hideLegacyTabs); ui->settingsLock->setChecked(switcher->settingsLockEnabled); + ui->disableUndoRedo->setChecked(!GetUndoRedoEnabled()); + connect(ui->disableUndoRedo, &QCheckBox::stateChanged, this, + [this](int state) { + if (loading) { + return; + } + SetUndoRedoEnabled(!state); + }); populatePriorityFunctionList(ui->priorityList); populateThreadPriorityList(ui->threadPriority); diff --git a/lib/macro/macro-action-edit.cpp b/lib/macro/macro-action-edit.cpp index 8f31feb6..d5616bc0 100644 --- a/lib/macro/macro-action-edit.cpp +++ b/lib/macro/macro-action-edit.cpp @@ -1,4 +1,5 @@ #include "macro-action-edit.hpp" +#include "macro-undo-redo.hpp" #include "advanced-scene-switcher.hpp" #include "macro-helpers.hpp" #include "macro-settings.hpp" @@ -111,9 +112,31 @@ void MacroActionEdit::ActionSelectionChanged(const QString &text) return; } + Macro *const parentMacro = + (*_entryData)->GetMacro()->GetNestedParentMacro(); + OBSDataAutoRelease parentBeforeData; + if (parentMacro) { + parentBeforeData = obs_data_create(); + parentMacro->Save(parentBeforeData); + } + + const std::string oldId = (*_entryData)->GetId(); + OBSDataAutoRelease oldData = obs_data_create(); + (*_entryData)->Save(oldData); + HeaderInfoChanged(""); auto idx = _entryData->get()->GetIndex(); auto macro = _entryData->get()->GetMacro(); + + const auto *rawPtr = (*_entryData).get(); + const auto &elseActions = macro->ElseActions(); + const bool isElse = std::any_of(elseActions.begin(), elseActions.end(), + [rawPtr](const auto &a) { + return a.get() == rawPtr; + }); + const auto segmentType = isElse ? MacroEdit::SegmentType::ELSE_ACTION + : MacroEdit::SegmentType::ACTION; + { auto lock = LockContext(); _entryData->reset(); @@ -122,6 +145,16 @@ void MacroActionEdit::ActionSelectionChanged(const QString &text) (*_entryData)->PostLoad(); RunAndClearPostLoadSteps(); } + + OBSDataAutoRelease newData = obs_data_create(); + (*_entryData)->Save(newData); + if (parentMacro) { + RegisterMacroModifyUndoRedo(parentMacro, parentBeforeData); + } else { + RegisterSegmentTypeChangeUndoRedo(macro, segmentType, idx, + oldId, oldData, 0, id, + newData, 0); + } auto widget = MacroActionFactory::CreateWidget(id, this, *_entryData); QWidget::connect(widget, SIGNAL(HeaderInfoChanged(const QString &)), this, SLOT(HeaderInfoChanged(const QString &))); diff --git a/lib/macro/macro-action-loop.cpp b/lib/macro/macro-action-loop.cpp index e69f7310..4274e81a 100644 --- a/lib/macro/macro-action-loop.cpp +++ b/lib/macro/macro-action-loop.cpp @@ -83,6 +83,7 @@ bool MacroActionLoop::PostLoad() MacroAction::PostLoad(); _loopMacro->PostLoad(); _loopMacro->SetActionTriggerMode(Macro::ActionTriggerMode::ALWAYS); + _loopMacro->SetNestedParentMacro(GetMacro()); return true; } diff --git a/lib/macro/macro-action-macro.cpp b/lib/macro/macro-action-macro.cpp index cf4b8112..0ee65fa7 100644 --- a/lib/macro/macro-action-macro.cpp +++ b/lib/macro/macro-action-macro.cpp @@ -281,6 +281,7 @@ bool MacroActionMacro::PostLoad() MacroAction::PostLoad(); _runOptions.macro.PostLoad(); _nestedMacro->PostLoad(); + _nestedMacro->SetNestedParentMacro(GetMacro()); return true; } diff --git a/lib/macro/macro-condition-edit.cpp b/lib/macro/macro-condition-edit.cpp index 885871fd..61222df7 100644 --- a/lib/macro/macro-condition-edit.cpp +++ b/lib/macro/macro-condition-edit.cpp @@ -1,4 +1,5 @@ #include "macro-condition-edit.hpp" +#include "macro-undo-redo.hpp" #include "advanced-scene-switcher.hpp" #include "condition-logic.hpp" @@ -250,6 +251,19 @@ void MacroConditionEdit::ConditionSelectionChanged(const QString &text) return; } + Macro *const parentMacro = + (*_entryData)->GetMacro()->GetNestedParentMacro(); + OBSDataAutoRelease parentBeforeData; + if (parentMacro) { + parentBeforeData = obs_data_create(); + parentMacro->Save(parentBeforeData); + } + + const std::string oldId = (*_entryData)->GetId(); + const int oldLogic = (int)(*_entryData)->GetLogicType(); + OBSDataAutoRelease oldData = obs_data_create(); + (*_entryData)->Save(oldData); + auto temp = DurationModifier(); _dur->SetValue(temp); HeaderInfoChanged(""); @@ -267,6 +281,17 @@ void MacroConditionEdit::ConditionSelectionChanged(const QString &text) (*_entryData)->PostLoad(); RunAndClearPostLoadSteps(); } + + OBSDataAutoRelease newData = obs_data_create(); + (*_entryData)->Save(newData); + if (parentMacro) { + RegisterMacroModifyUndoRedo(parentMacro, parentBeforeData); + } else { + RegisterSegmentTypeChangeUndoRedo( + macro, MacroEdit::SegmentType::CONDITION, idx, oldId, + oldData, oldLogic, id, newData, + (int)(*_entryData)->GetLogicType()); + } auto widget = MacroConditionFactory::CreateWidget(id, this, *_entryData); QWidget::connect(widget, SIGNAL(HeaderInfoChanged(const QString &)), diff --git a/lib/macro/macro-edit.cpp b/lib/macro/macro-edit.cpp index 4f5e8f4e..f20ecb64 100644 --- a/lib/macro/macro-edit.cpp +++ b/lib/macro/macro-edit.cpp @@ -1,6 +1,7 @@ #include "macro-edit.hpp" #include "condition-logic.hpp" +#include "macro-undo-redo.hpp" #include "cursor-shape-changer.hpp" #include "macro-action-edit.hpp" #include "macro-action-macro.hpp" @@ -257,6 +258,92 @@ std::shared_ptr MacroEdit::GetMacro() const return _currentMacro; } +void MacroEdit::InsertSegmentWidget(SegmentType type, int idx) +{ + auto macro = _currentMacro; + if (!macro) { + return; + } + + switch (type) { + case SegmentType::ACTION: { + if (idx < 0 || idx > (int)macro->Actions().size()) { + break; + } + ui->actionsList->Insert( + idx, new MacroActionEdit(this, ¯o->Actions()[idx])); + SetActionData(*macro); + ui->actionsList->SetHelpMsgVisible(false); + break; + } + case SegmentType::ELSE_ACTION: { + if (idx < 0 || idx > (int)macro->ElseActions().size()) { + break; + } + ui->elseActionsList->Insert( + idx, + new MacroActionEdit(this, ¯o->ElseActions()[idx])); + SetElseActionData(*macro); + ui->elseActionsList->SetHelpMsgVisible(false); + break; + } + case SegmentType::CONDITION: { + if (idx < 0 || idx > (int)macro->Conditions().size()) { + break; + } + ui->conditionsList->Insert( + idx, + new MacroConditionEdit(this, ¯o->Conditions()[idx], + idx == 0)); + if (idx == 0 && macro->Conditions().size() > 1) { + static_cast( + ui->conditionsList->WidgetAt(1)) + ->SetRootNode(false); + } + SetConditionData(*macro); + ui->conditionsList->SetHelpMsgVisible(false); + break; + } + } +} + +void MacroEdit::RemoveSegmentWidget(SegmentType type, int idx) +{ + auto macro = _currentMacro; + if (!macro) { + return; + } + + switch (type) { + case SegmentType::ACTION: + if (idx >= 0 && + idx < ui->actionsList->ContentLayout()->count()) { + ui->actionsList->Remove(idx); + SetActionData(*macro); + } + break; + case SegmentType::ELSE_ACTION: + if (idx >= 0 && + idx < ui->elseActionsList->ContentLayout()->count()) { + ui->elseActionsList->Remove(idx); + SetElseActionData(*macro); + } + break; + case SegmentType::CONDITION: + if (idx >= 0 && + idx < ui->conditionsList->ContentLayout()->count()) { + ui->conditionsList->Remove(idx); + if (idx == 0 && !macro->Conditions().empty()) { + static_cast( + ui->conditionsList->WidgetAt(0)) + ->SetRootNode(true); + } + SetConditionData(*macro); + } + break; + } +} + void MacroEdit::ClearSegmentWidgetCacheFor(Macro *macro) const { ui->conditionsList->ClearWidgetsFromCacheFor(macro); @@ -295,7 +382,7 @@ isValidMacroSegmentIdx(const std::deque> &list, return (idx > 0 || (unsigned)idx < list.size()); } -void MacroEdit::SetupMacroSegmentSelection(MacroSection type, int idx) +void MacroEdit::SetupMacroSegmentSelection(SegmentType type, int idx) { auto macro = _currentMacro; if (!macro) { @@ -308,7 +395,7 @@ void MacroEdit::SetupMacroSegmentSelection(MacroSection type, int idx) std::deque> segments; switch (type) { - case MacroEdit::MacroSection::CONDITIONS: + case MacroEdit::SegmentType::CONDITION: setList = ui->conditionsList; setIdx = ¤tConditionIdx; segments = {macro->Conditions().begin(), @@ -319,7 +406,7 @@ void MacroEdit::SetupMacroSegmentSelection(MacroSection type, int idx) resetIdx1 = ¤tActionIdx; resetIdx2 = ¤tElseActionIdx; break; - case MacroEdit::MacroSection::ACTIONS: + case MacroEdit::SegmentType::ACTION: setList = ui->actionsList; setIdx = ¤tActionIdx; segments = {macro->Actions().begin(), macro->Actions().end()}; @@ -329,7 +416,7 @@ void MacroEdit::SetupMacroSegmentSelection(MacroSection type, int idx) resetIdx1 = ¤tConditionIdx; resetIdx2 = ¤tElseActionIdx; break; - case MacroEdit::MacroSection::ELSE_ACTIONS: + case MacroEdit::SegmentType::ELSE_ACTION: setList = ui->elseActionsList; setIdx = ¤tElseActionIdx; segments = {macro->ElseActions().begin(), @@ -965,7 +1052,7 @@ void MacroEdit::UpMacroSegmentHotkey() int conditionSize = macro->Conditions().size(); if (currentActionIdx == -1 && currentConditionIdx == -1) { - if (lastInteracted == MacroSection::CONDITIONS) { + if (lastInteracted == SegmentType::CONDITION) { if (conditionSize == 0) { MacroActionSelectionChanged(0); } else { @@ -1021,7 +1108,7 @@ void MacroEdit::DownMacroSegmentHotkey() int conditionSize = macro->Conditions().size(); if (currentActionIdx == -1 && currentConditionIdx == -1) { - if (lastInteracted == MacroSection::CONDITIONS) { + if (lastInteracted == SegmentType::CONDITION) { if (conditionSize == 0) { MacroActionSelectionChanged(0); } else { @@ -1084,6 +1171,13 @@ void MacroEdit::AddMacroAction(Macro *macro, int idx, const std::string &id, return; } + Macro *const parentMacro = macro->GetNestedParentMacro(); + OBSDataAutoRelease parentBeforeData; + if (parentMacro) { + parentBeforeData = obs_data_create(); + parentMacro->Save(parentBeforeData); + } + { auto lock = LockContext(); macro->Actions().emplace(macro->Actions().begin() + idx, @@ -1098,6 +1192,11 @@ void MacroEdit::AddMacroAction(Macro *macro, int idx, const std::string &id, idx, new MacroActionEdit(this, ¯o->Actions()[idx])); SetActionData(*macro); } + if (parentMacro) { + RegisterMacroModifyUndoRedo(parentMacro, parentBeforeData); + } else { + RegisterSegmentAddUndoRedo(macro, SegmentType::ACTION, idx); + } HighlightAction(idx); ui->actionsList->SetHelpMsgVisible(false); emit(MacroSegmentOrderChanged()); @@ -1163,6 +1262,24 @@ void MacroEdit::RemoveMacroAction(int idx) return; } + Macro *const parentMacro = macro->GetNestedParentMacro(); + OBSDataAutoRelease parentBeforeData; + if (parentMacro) { + parentBeforeData = obs_data_create(); + parentMacro->Save(parentBeforeData); + } else { + OBSDataAutoRelease segData = obs_data_create(); + std::string segId; + { + auto lock = LockContext(); + macro->Actions().at(idx)->Save(segData); + segId = macro->Actions().at(idx)->GetId(); + } + RegisterSegmentRemoveUndoRedo(macro.get(), SegmentType::ACTION, + idx, segId, segData, + (int)Logic::Type::ROOT_NONE); + } + { auto lock = LockContext(); ui->actionsList->Remove(idx); @@ -1172,8 +1289,11 @@ void MacroEdit::RemoveMacroAction(int idx) macro->UpdateActionIndices(); SetActionData(*macro); } + if (parentMacro) { + RegisterMacroModifyUndoRedo(parentMacro, parentBeforeData); + } MacroActionSelectionChanged(-1); - lastInteracted = MacroSection::ACTIONS; + lastInteracted = SegmentType::ACTION; emit(MacroSegmentOrderChanged()); } @@ -1353,7 +1473,7 @@ void MacroEdit::MoveMacroActionDown(int idx) void MacroEdit::MacroElseActionSelectionChanged(int idx) { - SetupMacroSegmentSelection(MacroSection::ELSE_ACTIONS, idx); + SetupMacroSegmentSelection(SegmentType::ELSE_ACTION, idx); } void MacroEdit::MacroElseActionReorder(int to, int from) @@ -1390,6 +1510,13 @@ void MacroEdit::AddMacroElseAction(Macro *macro, int idx, const std::string &id, return; } + Macro *const parentMacro = macro->GetNestedParentMacro(); + OBSDataAutoRelease parentBeforeData; + if (parentMacro) { + parentBeforeData = obs_data_create(); + parentMacro->Save(parentBeforeData); + } + { auto lock = LockContext(); macro->ElseActions().emplace(macro->ElseActions().begin() + idx, @@ -1406,6 +1533,12 @@ void MacroEdit::AddMacroElseAction(Macro *macro, int idx, const std::string &id, new MacroActionEdit(this, ¯o->ElseActions()[idx])); SetElseActionData(*macro); } + if (parentMacro) { + RegisterMacroModifyUndoRedo(parentMacro, parentBeforeData); + } else { + RegisterSegmentAddUndoRedo(macro, SegmentType::ELSE_ACTION, + idx); + } HighlightElseAction(idx); ui->elseActionsList->SetHelpMsgVisible(false); emit(MacroSegmentOrderChanged()); @@ -1455,6 +1588,25 @@ void MacroEdit::RemoveMacroElseAction(int idx) return; } + Macro *const parentMacro = macro->GetNestedParentMacro(); + OBSDataAutoRelease parentBeforeData; + if (parentMacro) { + parentBeforeData = obs_data_create(); + parentMacro->Save(parentBeforeData); + } else { + OBSDataAutoRelease segData = obs_data_create(); + std::string segId; + { + auto lock = LockContext(); + macro->ElseActions().at(idx)->Save(segData); + segId = macro->ElseActions().at(idx)->GetId(); + } + RegisterSegmentRemoveUndoRedo(macro.get(), + SegmentType::ELSE_ACTION, idx, + segId, segData, + (int)Logic::Type::ROOT_NONE); + } + { auto lock = LockContext(); ui->elseActionsList->Remove(idx); @@ -1464,8 +1616,11 @@ void MacroEdit::RemoveMacroElseAction(int idx) macro->UpdateElseActionIndices(); SetElseActionData(*macro); } + if (parentMacro) { + RegisterMacroModifyUndoRedo(parentMacro, parentBeforeData); + } MacroElseActionSelectionChanged(-1); - lastInteracted = MacroSection::ELSE_ACTIONS; + lastInteracted = SegmentType::ELSE_ACTION; emit(MacroSegmentOrderChanged()); } @@ -1526,7 +1681,7 @@ void MacroEdit::MoveMacroElseActionDown(int idx) void MacroEdit::MacroActionSelectionChanged(int idx) { - SetupMacroSegmentSelection(MacroSection::ACTIONS, idx); + SetupMacroSegmentSelection(SegmentType::ACTION, idx); } void MacroEdit::MacroActionReorder(int to, int from) @@ -1599,6 +1754,13 @@ void MacroEdit::AddMacroCondition(Macro *macro, int idx, const std::string &id, return; } + Macro *const parentMacro = macro->GetNestedParentMacro(); + OBSDataAutoRelease parentBeforeData; + if (parentMacro) { + parentBeforeData = obs_data_create(); + parentMacro->Save(parentBeforeData); + } + { auto lock = LockContext(); auto cond = macro->Conditions().emplace( @@ -1617,6 +1779,11 @@ void MacroEdit::AddMacroCondition(Macro *macro, int idx, const std::string &id, idx == 0)); SetConditionData(*macro); } + if (parentMacro) { + RegisterMacroModifyUndoRedo(parentMacro, parentBeforeData); + } else { + RegisterSegmentAddUndoRedo(macro, SegmentType::CONDITION, idx); + } HighlightCondition(idx); ui->conditionsList->SetHelpMsgVisible(false); emit(MacroSegmentOrderChanged()); @@ -1652,6 +1819,26 @@ void MacroEdit::RemoveMacroCondition(int idx) return; } + Macro *const parentMacro = macro->GetNestedParentMacro(); + OBSDataAutoRelease parentBeforeData; + if (parentMacro) { + parentBeforeData = obs_data_create(); + parentMacro->Save(parentBeforeData); + } else { + OBSDataAutoRelease segData = obs_data_create(); + std::string segId; + int logic; + { + auto lock = LockContext(); + macro->Conditions().at(idx)->Save(segData); + segId = macro->Conditions().at(idx)->GetId(); + logic = (int)macro->Conditions().at(idx)->GetLogicType(); + } + RegisterSegmentRemoveUndoRedo(macro.get(), + SegmentType::CONDITION, idx, + segId, segData, logic); + } + { auto lock = LockContext(); ui->conditionsList->Remove(idx); @@ -1666,8 +1853,11 @@ void MacroEdit::RemoveMacroCondition(int idx) } SetConditionData(*macro); } + if (parentMacro) { + RegisterMacroModifyUndoRedo(parentMacro, parentBeforeData); + } MacroConditionSelectionChanged(-1); - lastInteracted = MacroSection::CONDITIONS; + lastInteracted = SegmentType::CONDITION; emit(MacroSegmentOrderChanged()); } @@ -1792,7 +1982,7 @@ void MacroEdit::MoveMacroConditionDown(int idx) void MacroEdit::MacroConditionSelectionChanged(int idx) { - SetupMacroSegmentSelection(MacroSection::CONDITIONS, idx); + SetupMacroSegmentSelection(SegmentType::CONDITION, idx); } void MacroEdit::MacroConditionReorder(int to, int from) diff --git a/lib/macro/macro-edit.hpp b/lib/macro/macro-edit.hpp index 8b2ff6b3..79db7393 100644 --- a/lib/macro/macro-edit.hpp +++ b/lib/macro/macro-edit.hpp @@ -12,11 +12,15 @@ class MacroEdit : public QWidget { Q_OBJECT public: + enum class SegmentType { CONDITION, ACTION, ELSE_ACTION }; + MacroEdit(QWidget *parent, QStringList helpMsg = {}); void SetMacro(const std::shared_ptr &); std::shared_ptr GetMacro() const; void ClearSegmentWidgetCacheFor(Macro *) const; void SetControlsDisabled(bool disable) const; + void InsertSegmentWidget(SegmentType type, int idx); + void RemoveSegmentWidget(SegmentType type, int idx); void HighlightAction(int idx, QColor color = QColor(Qt::green)) const; void HighlightElseAction(int idx, QColor color = QColor(Qt::green)) const; @@ -112,14 +116,12 @@ protected: bool eventFilter(QObject *obj, QEvent *event) override; private: - enum class MacroSection { CONDITIONS, ACTIONS, ELSE_ACTIONS }; - void PopulateMacroActions(Macro &m, uint32_t afterIdx = 0); void PopulateMacroElseActions(Macro &m, uint32_t afterIdx = 0); void PopulateMacroConditions(Macro &m, uint32_t afterIdx = 0); void ScrollAndFocusNewSegment(MacroSegmentList *list, int newIdx, int *currentIdx); - void SetupMacroSegmentSelection(MacroSection type, int idx); + void SetupMacroSegmentSelection(SegmentType type, int idx); void SetupContextMenu(const QPoint &pos, const std::function &remove, @@ -131,7 +133,7 @@ private: void RunSegmentHighlightChecks(); bool ElseSectionIsVisible() const; - MacroSection lastInteracted = MacroSection::CONDITIONS; + SegmentType lastInteracted = SegmentType::CONDITION; int currentConditionIdx = -1; int currentActionIdx = -1; int currentElseActionIdx = -1; diff --git a/lib/macro/macro-tab.cpp b/lib/macro/macro-tab.cpp index 98ed78c1..a7710bfe 100644 --- a/lib/macro/macro-tab.cpp +++ b/lib/macro/macro-tab.cpp @@ -7,6 +7,7 @@ #include "macro-settings.hpp" #include "macro-signals.hpp" #include "macro-tree.hpp" +#include "macro-undo-redo.hpp" #include "macro.hpp" #include "math-helpers.hpp" #include "name-dialog.hpp" @@ -122,6 +123,7 @@ void AdvSceneSwitcher::on_macroAdd_clicked() ui->macros->Add(newMacro); MacroSignalManager::Instance()->Add( QString::fromStdString(name)); + RegisterMacroAddUndoRedo(name); return; } @@ -131,6 +133,7 @@ void AdvSceneSwitcher::on_macroAdd_clicked() ui->macros->AddToGroup(newMacro, selectedMacro); MacroSignalManager::Instance()->Add( QString::fromStdString(name)); + RegisterMacroAddUndoRedo(name); return; } @@ -139,12 +142,14 @@ void AdvSceneSwitcher::on_macroAdd_clicked() ui->macros->Add(newMacro, selectedMacro); MacroSignalManager::Instance()->Add( QString::fromStdString(name)); + RegisterMacroAddUndoRedo(name); return; } Macro::PrepareMoveToGroup(selectedMacroGroup, newMacro); ui->macros->Add(newMacro, selectedMacro); MacroSignalManager::Instance()->Add(QString::fromStdString(name)); + RegisterMacroAddUndoRedo(name); } static void addGroupSubitems(std::vector> ¯os, @@ -185,6 +190,12 @@ void AdvSceneSwitcher::RemoveMacro(std::shared_ptr ¯o) } } + if (macro->IsGroup()) { + RegisterGroupDeleteUndoRedo(macro.get()); + } else { + RegisterMacroRemoveUndoRedo(macro.get()); + } + if (macro->IsGroup()) { std::vector> macros = {macro}; addGroupSubitems(macros, macro); @@ -241,13 +252,16 @@ void AdvSceneSwitcher::RemoveSelectedMacros() void AdvSceneSwitcher::RenameMacro(std::shared_ptr ¯o, const QString &name) { - auto oldName = QString::fromStdString(macro->Name()); + const std::string oldName = macro->Name(); + const std::string newName = name.toStdString(); { auto lock = LockContext(); - macro->SetName(name.toStdString()); + macro->SetName(newName); } - MacroSignalManager::Instance()->Rename(oldName, name); + RegisterMacroRenameUndoRedo(oldName, newName); + MacroSignalManager::Instance()->Rename(QString::fromStdString(oldName), + name); } void AdvSceneSwitcher::on_macroRemove_clicked() @@ -443,6 +457,7 @@ void AdvSceneSwitcher::ImportMacros() ui->macros->Reset(GetTopLevelMacros(), GetGlobalMacroSettings()._highlightExecuted); disableAddButtonHighlight(); + RegisterMacrosImportUndoRedo(importedMacros); } void AdvSceneSwitcher::on_macroName_editingFinished() @@ -814,6 +829,7 @@ void AdvSceneSwitcher::CopyMacro() ui->macros->Add(newMacro, macro); disableAddButtonHighlight(); MacroSignalManager::Instance()->Add(QString::fromStdString(name)); + RegisterMacroAddUndoRedo(name); } void AdvSceneSwitcher::PauseSelectedMacros() diff --git a/lib/macro/macro-tree.cpp b/lib/macro/macro-tree.cpp index 0b5c2e44..25203448 100644 --- a/lib/macro/macro-tree.cpp +++ b/lib/macro/macro-tree.cpp @@ -1,4 +1,5 @@ #include "macro-tree.hpp" +#include "macro-undo-redo.hpp" #include "macro-helpers.hpp" #include "macro-search.hpp" @@ -824,6 +825,8 @@ void MacroTreeModel::GroupSelectedItems(QModelIndexList &indices) Reset(_macros); assert(IsInValidState()); + + RegisterGroupCreateUndoRedo(name.toStdString()); } void MacroTreeModel::UngroupSelectedGroups(QModelIndexList &indices) @@ -837,6 +840,7 @@ void MacroTreeModel::UngroupSelectedGroups(QModelIndexList &indices) std::shared_ptr item = _macros[ModelIndexToMacroIndex( indices[i].row(), _macros)]; if (item->IsGroup()) { + RegisterGroupRemoveUndoRedo(item->Name()); Macro::RemoveGroup(item); } } diff --git a/lib/macro/macro-undo-redo.cpp b/lib/macro/macro-undo-redo.cpp new file mode 100644 index 00000000..672ae320 --- /dev/null +++ b/lib/macro/macro-undo-redo.cpp @@ -0,0 +1,1394 @@ +#include "macro-undo-redo.hpp" + +#include "advanced-scene-switcher.hpp" +#include "condition-logic.hpp" +#include "macro-action-factory.hpp" +#include "macro-condition-factory.hpp" +#include "macro-edit.hpp" +#include "macro-helpers.hpp" +#include "macro-segment-list.hpp" +#include "macro-settings.hpp" +#include "macro-signals.hpp" +#include "macro.hpp" +#include "obs-module-helper.hpp" +#include "plugin-state-helpers.hpp" +#include "sync-helpers.hpp" + +#include +#include + +#include +#include + +namespace advss { + +using SegmentType = MacroEdit::SegmentType; + +static bool undoRedoDisabled = false; + +bool GetUndoRedoEnabled() +{ + return !undoRedoDisabled; +} + +void SetUndoRedoEnabled(bool enabled) +{ + undoRedoDisabled = !enabled; +} + +static bool UndoRedoEnabled() +{ + return !undoRedoDisabled; +} + +[[maybe_unused]] static bool setupUndoRedoPersistence = []() { + AddSaveStep([](obs_data_t *obj) { + obs_data_set_bool(obj, "disableUndoRedo", undoRedoDisabled); + }); + AddLoadStep([](obs_data_t *obj) { + undoRedoDisabled = obs_data_get_bool(obj, "disableUndoRedo"); + }); + return true; +}(); + +// --------------------------------------------------------------------------- +// Internal helpers +// --------------------------------------------------------------------------- + +enum class SegmentOp { ADD, REMOVE }; + +static const char *segmentTypeName(SegmentType type) +{ + switch (type) { + case SegmentType::ACTION: + return obs_module_text("AdvSceneSwitcher.undo.segment.action"); + case SegmentType::ELSE_ACTION: + return obs_module_text( + "AdvSceneSwitcher.undo.segment.elseAction"); + case SegmentType::CONDITION: + return obs_module_text( + "AdvSceneSwitcher.undo.segment.condition"); + } + return ""; +} + +static std::string fmtUndoName(const char *key, const std::string &arg1) +{ + return QString(obs_module_text(key)) + .arg(QString::fromStdString(arg1)) + .toStdString(); +} + +static std::string fmtUndoName(const char *key, const char *arg1, + const std::string &arg2) +{ + return QString(obs_module_text(key)) + .arg(arg1) + .arg(QString::fromStdString(arg2)) + .toStdString(); +} + +static void updateMacroEditSegment(AdvSceneSwitcher *window, + const std::string ¯oName, SegmentOp op, + SegmentType type, int idx) +{ + if (!SettingsWindowIsOpened()) { + return; + } + auto *const macroEdit = window->ui->macroEdit; + const auto current = macroEdit->GetMacro(); + if (!current || current->Name() != macroName) { + return; + } + + // std::deque invalidates all element addresses on any insert or erase. + // Re-point every existing widget before inserting/removing one, so none + // hold stale pointers. + macroEdit->SetActionData(*current); + macroEdit->SetElseActionData(*current); + macroEdit->SetConditionData(*current); + + if (op == SegmentOp::ADD) { + macroEdit->InsertSegmentWidget(type, idx); + } else { + macroEdit->RemoveSegmentWidget(type, idx); + } +} + +// --------------------------------------------------------------------------- +// Macro-level undo/redo callbacks +// --------------------------------------------------------------------------- + +// Callback data format for deletion: {"name": "MacroName"} +static void deleteMacroByName(const char *jsonData) +{ + OBSDataAutoRelease data = obs_data_create_from_json(jsonData); + if (!data) { + return; + } + + const std::string name = obs_data_get_string(data, "name"); + const auto macro = GetWeakMacroByName(name.c_str()).lock(); + if (!macro) { + return; + } + + auto *const window = AdvSceneSwitcher::window; + if (SettingsWindowIsOpened() && window) { + window->ui->macroEdit->ClearSegmentWidgetCacheFor(macro.get()); + MacroSegmentList::SetCachingEnabled(false); + window->ui->macros->Remove(macro); + MacroSegmentList::SetCachingEnabled(true); + QCoreApplication::sendPostedEvents(nullptr, + QEvent::DeferredDelete); + } else { + auto lock = LockContext(); + auto ¯os = GetTopLevelMacros(); + const auto it = std::find(macros.begin(), macros.end(), macro); + if (it != macros.end()) { + macros.erase(it); + } + } + + MacroSignalManager::Instance()->Remove(QString::fromStdString(name)); +} + +// Callback data format for restoration: +// {"index": N, "parent_group": "GroupName", "macro": {...}} +static void restoreMacroFromData(const char *jsonData) +{ + OBSDataAutoRelease data = obs_data_create_from_json(jsonData); + if (!data) { + return; + } + + const int index = (int)obs_data_get_int(data, "index"); + const std::string parentGroup = + obs_data_get_string(data, "parent_group"); + OBSDataAutoRelease macroData = obs_data_get_obj(data, "macro"); + if (!macroData) { + return; + } + + auto macro = std::make_shared(); + std::string macroName; + { + auto lock = LockContext(); + macro->Load(macroData); + macro->PostLoad(); + RunAndClearPostLoadSteps(); + + if (!parentGroup.empty()) { + const auto parent = + GetWeakMacroByName(parentGroup.c_str()).lock(); + if (parent) { + Macro::PrepareMoveToGroup(parent, macro); + } + } + + auto ¯os = GetTopLevelMacros(); + const int insertIdx = std::min(index, (int)macros.size()); + macros.insert(macros.begin() + insertIdx, macro); + macroName = macro->Name(); + } + + auto *const window = AdvSceneSwitcher::window; + if (SettingsWindowIsOpened() && window) { + window->ui->macros->Reset( + GetTopLevelMacros(), + GetGlobalMacroSettings()._highlightExecuted); + } + + MacroSignalManager::Instance()->Add(QString::fromStdString(macroName)); +} + +// --------------------------------------------------------------------------- +// Group-delete undo/redo callbacks +// --------------------------------------------------------------------------- + +// Callback data format: +// {"group_name": "...", "group_index": N, "group_data": {...}, +// "children": [{"data": {...}}, ...]} +static void deleteGroupWithChildrenFromData(const char *jsonData) +{ + OBSDataAutoRelease data = obs_data_create_from_json(jsonData); + if (!data) { + return; + } + + const std::string groupName = obs_data_get_string(data, "group_name"); + const auto group = GetWeakMacroByName(groupName.c_str()).lock(); + if (!group || !group->IsGroup()) { + return; + } + + const auto children = GetGroupMacroEntries(group.get()); + + auto *const window = AdvSceneSwitcher::window; + if (SettingsWindowIsOpened() && window) { + window->ui->macroEdit->ClearSegmentWidgetCacheFor(group.get()); + for (const auto &child : children) { + window->ui->macroEdit->ClearSegmentWidgetCacheFor( + child.get()); + } + MacroSegmentList::SetCachingEnabled(false); + window->ui->macros->Remove(group); + MacroSegmentList::SetCachingEnabled(true); + QCoreApplication::sendPostedEvents(nullptr, + QEvent::DeferredDelete); + } else { + auto lock = LockContext(); + auto ¯os = GetTopLevelMacros(); + const auto it = std::find(macros.begin(), macros.end(), group); + if (it != macros.end()) { + macros.erase(it, std::next(it, 1 + group->GroupSize())); + } + } + + for (const auto &child : children) { + MacroSignalManager::Instance()->Remove( + QString::fromStdString(child->Name())); + } + MacroSignalManager::Instance()->Remove( + QString::fromStdString(groupName)); +} + +static void restoreGroupWithChildrenFromData(const char *jsonData) +{ + OBSDataAutoRelease data = obs_data_create_from_json(jsonData); + if (!data) { + return; + } + + const int groupIndex = (int)obs_data_get_int(data, "group_index"); + OBSDataAutoRelease groupData = obs_data_get_obj(data, "group_data"); + OBSDataArrayAutoRelease childArray = + obs_data_get_array(data, "children"); + + std::string groupName; + bool collapsed = false; + if (groupData) { + groupName = obs_data_get_string(groupData, "name"); + OBSDataAutoRelease gd = + obs_data_get_obj(groupData, "groupData"); + if (gd) { + collapsed = obs_data_get_bool(gd, "collapsed"); + } + } + + std::vector> children; + const size_t count = obs_data_array_count(childArray); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease entry = obs_data_array_item(childArray, i); + OBSDataAutoRelease childData = obs_data_get_obj(entry, "data"); + if (!childData) { + continue; + } + auto child = std::make_shared(); + { + auto lock = LockContext(); + child->Load(childData); + child->PostLoad(); + RunAndClearPostLoadSteps(); + } + children.push_back(child); + } + + std::string restoredGroupName; + { + auto lock = LockContext(); + auto group = Macro::CreateGroup(groupName, children); + group->SetCollapsed(collapsed); + restoredGroupName = group->Name(); + + auto ¯os = GetTopLevelMacros(); + const int insertIdx = std::min(groupIndex, (int)macros.size()); + macros.insert(macros.begin() + insertIdx, group); + int offset = 1; + for (const auto &child : children) { + macros.insert(macros.begin() + insertIdx + offset, + child); + offset++; + } + } + + auto *const window = AdvSceneSwitcher::window; + if (SettingsWindowIsOpened() && window) { + window->ui->macros->Reset( + GetTopLevelMacros(), + GetGlobalMacroSettings()._highlightExecuted); + } + + MacroSignalManager::Instance()->Add( + QString::fromStdString(restoredGroupName)); + for (const auto &child : children) { + MacroSignalManager::Instance()->Add( + QString::fromStdString(child->Name())); + } +} + +// --------------------------------------------------------------------------- +// Group-level undo/redo callbacks +// --------------------------------------------------------------------------- + +// Callback data format for both group and ungroup: +// {"group_name": "...", "group_index": N, "children": [{"name": "..."}, ...]} +static void removeGroupFromData(const char *jsonData) +{ + OBSDataAutoRelease data = obs_data_create_from_json(jsonData); + if (!data) { + return; + } + + const std::string groupName = obs_data_get_string(data, "group_name"); + const auto group = GetWeakMacroByName(groupName.c_str()).lock(); + if (!group || !group->IsGroup()) { + return; + } + + { + auto lock = LockContext(); + Macro::RemoveGroup(group); + } + + auto *const window = AdvSceneSwitcher::window; + if (SettingsWindowIsOpened() && window) { + window->ui->macros->Reset( + GetTopLevelMacros(), + GetGlobalMacroSettings()._highlightExecuted); + } + + MacroSignalManager::Instance()->Remove( + QString::fromStdString(groupName)); +} + +static void restoreGroupFromData(const char *jsonData) +{ + OBSDataAutoRelease data = obs_data_create_from_json(jsonData); + if (!data) { + return; + } + + const std::string groupName = obs_data_get_string(data, "group_name"); + const int groupIndex = (int)obs_data_get_int(data, "group_index"); + OBSDataArrayAutoRelease childArray = + obs_data_get_array(data, "children"); + + std::vector> children; + const size_t count = obs_data_array_count(childArray); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease item = obs_data_array_item(childArray, i); + const std::string name = obs_data_get_string(item, "name"); + const auto macro = GetWeakMacroByName(name.c_str()).lock(); + if (macro) { + children.push_back(macro); + } + } + + { + auto lock = LockContext(); + auto group = Macro::CreateGroup(groupName, children); + + auto ¯os = GetTopLevelMacros(); + const int insertIdx = std::min(groupIndex, (int)macros.size()); + macros.insert(macros.begin() + insertIdx, group); + + int offset = 1; + for (const auto &child : children) { + const auto it = + std::find(macros.begin(), macros.end(), child); + if (it != macros.end()) { + macros.erase(it); + } + macros.insert(macros.begin() + insertIdx + offset, + child); + offset++; + } + } + + auto *const window = AdvSceneSwitcher::window; + if (SettingsWindowIsOpened() && window) { + window->ui->macros->Reset( + GetTopLevelMacros(), + GetGlobalMacroSettings()._highlightExecuted); + } + + MacroSignalManager::Instance()->Add(QString::fromStdString(groupName)); +} + +// --------------------------------------------------------------------------- +// Segment-level undo/redo callbacks +// --------------------------------------------------------------------------- + +// Callback data format for segment removal: +// {"macro": "Name", "type": N, "index": N} +static void removeSegmentFromData(const char *jsonData) +{ + OBSDataAutoRelease data = obs_data_create_from_json(jsonData); + if (!data) { + return; + } + + const std::string macroName = obs_data_get_string(data, "macro"); + const int type = (int)obs_data_get_int(data, "type"); + const int index = (int)obs_data_get_int(data, "index"); + + const auto macro = GetWeakMacroByName(macroName.c_str()).lock(); + if (!macro) { + return; + } + + { + auto lock = LockContext(); + switch ((SegmentType)type) { + case SegmentType::ACTION: + if (index >= 0 && + index < (int)macro->Actions().size()) { + macro->Actions().erase( + macro->Actions().begin() + index); + SetMacroAbortWait(true); + GetMacroWaitCV().notify_all(); + macro->UpdateActionIndices(); + } + break; + case SegmentType::ELSE_ACTION: + if (index >= 0 && + index < (int)macro->ElseActions().size()) { + macro->ElseActions().erase( + macro->ElseActions().begin() + index); + SetMacroAbortWait(true); + GetMacroWaitCV().notify_all(); + macro->UpdateElseActionIndices(); + } + break; + case SegmentType::CONDITION: + if (index >= 0 && + index < (int)macro->Conditions().size()) { + macro->Conditions().erase( + macro->Conditions().begin() + index); + macro->UpdateConditionIndices(); + if (index == 0 && + !macro->Conditions().empty()) { + macro->Conditions().at(0)->SetLogicType( + Logic::Type::ROOT_NONE); + } + } + break; + } + } + + if (auto *window = AdvSceneSwitcher::window) { + updateMacroEditSegment(window, macroName, SegmentOp::REMOVE, + (SegmentType)type, index); + } +} + +// Callback data format for segment addition: +// {"macro": "Name", "type": N, "index": N, "id": "...", "logic": N, +// "segment": {...}} +static void addSegmentFromData(const char *jsonData) +{ + OBSDataAutoRelease data = obs_data_create_from_json(jsonData); + if (!data) { + return; + } + + const std::string macroName = obs_data_get_string(data, "macro"); + const int type = (int)obs_data_get_int(data, "type"); + const int index = (int)obs_data_get_int(data, "index"); + const std::string id = obs_data_get_string(data, "id"); + const int logic = (int)obs_data_get_int(data, "logic"); + OBSDataAutoRelease segData = obs_data_get_obj(data, "segment"); + + const auto macro = GetWeakMacroByName(macroName.c_str()).lock(); + if (!macro) { + return; + } + + { + auto lock = LockContext(); + switch ((SegmentType)type) { + case SegmentType::ACTION: { + if (index < 0 || index > (int)macro->Actions().size()) { + break; + } + macro->Actions().emplace( + macro->Actions().begin() + index, + MacroActionFactory::Create(id, macro.get())); + if (segData) { + macro->Actions().at(index)->Load(segData); + } + macro->Actions().at(index)->PostLoad(); + RunAndClearPostLoadSteps(); + macro->UpdateActionIndices(); + break; + } + case SegmentType::ELSE_ACTION: { + if (index < 0 || + index > (int)macro->ElseActions().size()) { + break; + } + macro->ElseActions().emplace( + macro->ElseActions().begin() + index, + MacroActionFactory::Create(id, macro.get())); + if (segData) { + macro->ElseActions().at(index)->Load(segData); + } + macro->ElseActions().at(index)->PostLoad(); + RunAndClearPostLoadSteps(); + macro->UpdateElseActionIndices(); + break; + } + case SegmentType::CONDITION: { + if (index < 0 || + index > (int)macro->Conditions().size()) { + break; + } + macro->Conditions().emplace( + macro->Conditions().begin() + index, + MacroConditionFactory::Create(id, macro.get())); + if (segData) { + macro->Conditions().at(index)->Load(segData); + } + macro->Conditions().at(index)->PostLoad(); + RunAndClearPostLoadSteps(); + macro->Conditions().at(index)->SetLogicType( + (Logic::Type)logic); + macro->UpdateConditionIndices(); + break; + } + } + } + + if (auto *window = AdvSceneSwitcher::window) { + updateMacroEditSegment(window, macroName, SegmentOp::ADD, + (SegmentType)type, index); + } +} + +// --------------------------------------------------------------------------- +// Segment type-change undo/redo callback +// --------------------------------------------------------------------------- + +// Same data format as addSegmentFromData: +// {"macro": "Name", "type": N, "index": N, "id": "...", "logic": N, +// "segment": {...}} +static void replaceSegmentFromData(const char *jsonData) +{ + OBSDataAutoRelease data = obs_data_create_from_json(jsonData); + if (!data) { + return; + } + + const std::string macroName = obs_data_get_string(data, "macro"); + const int type = (int)obs_data_get_int(data, "type"); + const int index = (int)obs_data_get_int(data, "index"); + const std::string id = obs_data_get_string(data, "id"); + const int logic = (int)obs_data_get_int(data, "logic"); + OBSDataAutoRelease segData = obs_data_get_obj(data, "segment"); + + const auto macro = GetWeakMacroByName(macroName.c_str()).lock(); + if (!macro) { + return; + } + + { + auto lock = LockContext(); + switch ((SegmentType)type) { + case SegmentType::ACTION: { + if (index < 0 || + index >= (int)macro->Actions().size()) { + break; + } + macro->Actions().erase(macro->Actions().begin() + + index); + macro->Actions().emplace( + macro->Actions().begin() + index, + MacroActionFactory::Create(id, macro.get())); + if (segData) { + macro->Actions().at(index)->Load(segData); + } + macro->Actions().at(index)->PostLoad(); + RunAndClearPostLoadSteps(); + macro->UpdateActionIndices(); + break; + } + case SegmentType::ELSE_ACTION: { + if (index < 0 || + index >= (int)macro->ElseActions().size()) { + break; + } + macro->ElseActions().erase( + macro->ElseActions().begin() + index); + macro->ElseActions().emplace( + macro->ElseActions().begin() + index, + MacroActionFactory::Create(id, macro.get())); + if (segData) { + macro->ElseActions().at(index)->Load(segData); + } + macro->ElseActions().at(index)->PostLoad(); + RunAndClearPostLoadSteps(); + macro->UpdateElseActionIndices(); + break; + } + case SegmentType::CONDITION: { + if (index < 0 || + index >= (int)macro->Conditions().size()) { + break; + } + macro->Conditions().erase(macro->Conditions().begin() + + index); + macro->Conditions().emplace( + macro->Conditions().begin() + index, + MacroConditionFactory::Create(id, macro.get())); + if (segData) { + macro->Conditions().at(index)->Load(segData); + } + macro->Conditions().at(index)->PostLoad(); + RunAndClearPostLoadSteps(); + macro->Conditions().at(index)->SetLogicType( + (Logic::Type)logic); + macro->UpdateConditionIndices(); + break; + } + } + } + + if (auto *window = AdvSceneSwitcher::window) { + updateMacroEditSegment(window, macroName, SegmentOp::REMOVE, + (SegmentType)type, index); + updateMacroEditSegment(window, macroName, SegmentOp::ADD, + (SegmentType)type, index); + } +} + +// --------------------------------------------------------------------------- +// Macro state restore callback +// --------------------------------------------------------------------------- + +// Callback data format: {"name": "MacroName", "macro": {...}} +static void restoreMacroStateFromData(const char *jsonData) +{ + OBSDataAutoRelease data = obs_data_create_from_json(jsonData); + if (!data) { + return; + } + + const std::string macroName = obs_data_get_string(data, "name"); + OBSDataAutoRelease macroData = obs_data_get_obj(data, "macro"); + if (!macroData) { + return; + } + + const auto macro = GetWeakMacroByName(macroName.c_str()).lock(); + if (!macro) { + return; + } + + auto *const window = AdvSceneSwitcher::window; + const bool uiVisible = SettingsWindowIsOpened() && window; + auto *const macroEdit = uiVisible ? window->ui->macroEdit : nullptr; + const bool isCurrent = macroEdit && macroEdit->GetMacro() && + macroEdit->GetMacro()->Name() == macroName; + + // Clear widgets while data is still valid; also saves splitter positions. + if (isCurrent) { + MacroSegmentList::SetCachingEnabled(false); + macroEdit->SetMacro({}); + } + + const QList savedCondSplitter = + macro->GetActionConditionSplitterPosition(); + const QList savedElseSplitter = + macro->GetElseActionSplitterPosition(); + + // Flush deferred-delete widgets before the deques are cleared. + QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (isCurrent) { + MacroSegmentList::SetCachingEnabled(true); + } + + { + auto lock = LockContext(); + macro->Conditions().clear(); + macro->Actions().clear(); + macro->ElseActions().clear(); + macro->Load(macroData); + macro->PostLoad(); + RunAndClearPostLoadSteps(); + } + + if (isCurrent) { + if (!savedCondSplitter.isEmpty()) { + macro->SetActionConditionSplitterPosition( + savedCondSplitter); + } + if (!savedElseSplitter.isEmpty()) { + macro->SetElseActionSplitterPosition(savedElseSplitter); + } + macroEdit->SetMacro(macro); + } +} + +// --------------------------------------------------------------------------- +// Rename undo/redo callback +// --------------------------------------------------------------------------- + +// Callback data format: {"old_name": "...", "new_name": "..."} +// Used for both undo (new->old) and redo (old->new) by swapping order in +// obs_frontend_add_undo_redo_action. +static void renameMacroFromData(const char *jsonData) +{ + OBSDataAutoRelease data = obs_data_create_from_json(jsonData); + if (!data) { + return; + } + + const std::string oldName = obs_data_get_string(data, "old_name"); + const std::string newName = obs_data_get_string(data, "new_name"); + + const auto macro = GetWeakMacroByName(oldName.c_str()).lock(); + if (!macro) { + return; + } + + { + auto lock = LockContext(); + macro->SetName(newName); + } + + MacroSignalManager::Instance()->Rename(QString::fromStdString(oldName), + QString::fromStdString(newName)); + + auto *const window = AdvSceneSwitcher::window; + if (SettingsWindowIsOpened() && window) { + window->ui->macros->Reset( + GetTopLevelMacros(), + GetGlobalMacroSettings()._highlightExecuted); + } +} + +// --------------------------------------------------------------------------- +// Batch macro undo/redo callbacks (for import) +// --------------------------------------------------------------------------- + +// Callback data format: {"names": [{"name": "..."}, ...]} +static void deleteMacrosBatch(const char *jsonData) +{ + OBSDataAutoRelease data = obs_data_create_from_json(jsonData); + if (!data) { + return; + } + OBSDataArrayAutoRelease names = obs_data_get_array(data, "names"); + const int count = (int)obs_data_array_count(names); + + auto *const window = AdvSceneSwitcher::window; + const bool uiVisible = SettingsWindowIsOpened() && window; + + for (int i = count - 1; i >= 0; i--) { + OBSDataAutoRelease item = obs_data_array_item(names, i); + const std::string name = obs_data_get_string(item, "name"); + const auto macro = GetWeakMacroByName(name.c_str()).lock(); + if (!macro) { + continue; + } + + if (uiVisible) { + window->ui->macroEdit->ClearSegmentWidgetCacheFor( + macro.get()); + MacroSegmentList::SetCachingEnabled(false); + window->ui->macros->Remove(macro); + MacroSegmentList::SetCachingEnabled(true); + QCoreApplication::sendPostedEvents( + nullptr, QEvent::DeferredDelete); + } else { + auto lock = LockContext(); + auto ¯os = GetTopLevelMacros(); + const auto it = + std::find(macros.begin(), macros.end(), macro); + if (it != macros.end()) { + macros.erase(it); + } + } + + MacroSignalManager::Instance()->Remove( + QString::fromStdString(name)); + } +} + +// Callback data format: +// {"macros": [{"index": N, "parent_group": "...", "macro": {...}}, ...]} +static void restoreMacrosBatch(const char *jsonData) +{ + OBSDataAutoRelease data = obs_data_create_from_json(jsonData); + if (!data) { + return; + } + OBSDataArrayAutoRelease macrosArray = + obs_data_get_array(data, "macros"); + const size_t count = obs_data_array_count(macrosArray); + + std::vector addedNames; + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease entry = obs_data_array_item(macrosArray, i); + const int index = (int)obs_data_get_int(entry, "index"); + const std::string parentGroup = + obs_data_get_string(entry, "parent_group"); + OBSDataAutoRelease macroData = obs_data_get_obj(entry, "macro"); + if (!macroData) { + continue; + } + + auto macro = std::make_shared(); + { + auto lock = LockContext(); + macro->Load(macroData); + macro->PostLoad(); + RunAndClearPostLoadSteps(); + + if (!parentGroup.empty()) { + const auto parent = + GetWeakMacroByName(parentGroup.c_str()) + .lock(); + if (parent) { + Macro::PrepareMoveToGroup(parent, + macro); + } + } + + auto ¯os = GetTopLevelMacros(); + const int insertIdx = + std::min(index, (int)macros.size()); + macros.insert(macros.begin() + insertIdx, macro); + } + + addedNames.push_back(macro->Name()); + } + + auto *const window = AdvSceneSwitcher::window; + if (SettingsWindowIsOpened() && window) { + window->ui->macros->Reset( + GetTopLevelMacros(), + GetGlobalMacroSettings()._highlightExecuted); + } + + for (const auto &name : addedNames) { + MacroSignalManager::Instance()->Add( + QString::fromStdString(name)); + } +} + +// --------------------------------------------------------------------------- +// Public registration functions +// --------------------------------------------------------------------------- + +void RegisterMacroAddUndoRedo(const std::string ¯oName) +{ + if (!UndoRedoEnabled()) { + return; + } + const auto macro = GetWeakMacroByName(macroName.c_str()).lock(); + if (!macro || macro->IsGroup()) { + return; + } + + const auto ¯os = GetTopLevelMacros(); + int index = -1; + for (int i = 0; i < (int)macros.size(); i++) { + if (macros[i]->Name() == macroName) { + index = i; + break; + } + } + if (index < 0) { + return; + } + + std::string parentGroup; + if (macro->Parent()) { + parentGroup = macro->Parent()->Name(); + } + + OBSDataAutoRelease macroData = obs_data_create(); + macro->Save(macroData); + + OBSDataAutoRelease undoData = obs_data_create(); + obs_data_set_string(undoData, "name", macroName.c_str()); + + OBSDataAutoRelease redoData = obs_data_create(); + obs_data_set_int(redoData, "index", index); + obs_data_set_string(redoData, "parent_group", parentGroup.c_str()); + obs_data_set_obj(redoData, "macro", macroData); + + const std::string actionName = + fmtUndoName("AdvSceneSwitcher.undo.addMacro", macroName); + obs_frontend_add_undo_redo_action(actionName.c_str(), + &deleteMacroByName, + &restoreMacroFromData, + obs_data_get_json(undoData), + obs_data_get_json(redoData), false); +} + +bool RegisterMacroRemoveUndoRedo(Macro *macro) +{ + if (!UndoRedoEnabled()) { + return false; + } + if (!macro || macro->IsGroup()) { + return false; + } + + const std::string macroName = macro->Name(); + + const auto ¯os = GetTopLevelMacros(); + int index = -1; + for (int i = 0; i < (int)macros.size(); i++) { + if (macros[i].get() == macro) { + index = i; + break; + } + } + if (index < 0) { + return false; + } + + std::string parentGroup; + if (macro->Parent()) { + parentGroup = macro->Parent()->Name(); + } + + OBSDataAutoRelease macroData = obs_data_create(); + macro->Save(macroData); + + OBSDataAutoRelease undoData = obs_data_create(); + obs_data_set_int(undoData, "index", index); + obs_data_set_string(undoData, "parent_group", parentGroup.c_str()); + obs_data_set_obj(undoData, "macro", macroData); + + OBSDataAutoRelease redoData = obs_data_create(); + obs_data_set_string(redoData, "name", macroName.c_str()); + + const std::string actionName = + fmtUndoName("AdvSceneSwitcher.undo.removeMacro", macroName); + obs_frontend_add_undo_redo_action(actionName.c_str(), + &restoreMacroFromData, + &deleteMacroByName, + obs_data_get_json(undoData), + obs_data_get_json(redoData), false); + return true; +} + +void RegisterSegmentAddUndoRedo(Macro *macro, SegmentType type, int index) +{ + if (!UndoRedoEnabled()) { + return; + } + if (!macro) { + return; + } + + const std::string macroName = macro->Name(); + std::string id; + int logic = (int)Logic::Type::ROOT_NONE; + OBSDataAutoRelease segData = obs_data_create(); + + { + auto lock = LockContext(); + switch (type) { + case SegmentType::ACTION: + if (index < (int)macro->Actions().size()) { + macro->Actions().at(index)->Save(segData); + id = macro->Actions().at(index)->GetId(); + } + break; + case SegmentType::ELSE_ACTION: + if (index < (int)macro->ElseActions().size()) { + macro->ElseActions().at(index)->Save(segData); + id = macro->ElseActions().at(index)->GetId(); + } + break; + case SegmentType::CONDITION: + if (index < (int)macro->Conditions().size()) { + macro->Conditions().at(index)->Save(segData); + id = macro->Conditions().at(index)->GetId(); + logic = (int)macro->Conditions() + .at(index) + ->GetLogicType(); + } + break; + } + } + + OBSDataAutoRelease undoData = obs_data_create(); + obs_data_set_string(undoData, "macro", macroName.c_str()); + obs_data_set_int(undoData, "type", (int)type); + obs_data_set_int(undoData, "index", index); + + OBSDataAutoRelease redoData = obs_data_create(); + obs_data_set_string(redoData, "macro", macroName.c_str()); + obs_data_set_int(redoData, "type", (int)type); + obs_data_set_int(redoData, "index", index); + obs_data_set_string(redoData, "id", id.c_str()); + obs_data_set_int(redoData, "logic", logic); + obs_data_set_obj(redoData, "segment", segData); + + const std::string actionName = + fmtUndoName("AdvSceneSwitcher.undo.addSegment", + segmentTypeName(type), macroName); + obs_frontend_add_undo_redo_action(actionName.c_str(), + &removeSegmentFromData, + &addSegmentFromData, + obs_data_get_json(undoData), + obs_data_get_json(redoData), false); +} + +void RegisterSegmentRemoveUndoRedo(Macro *macro, SegmentType type, int index, + const std::string &segmentId, + obs_data_t *segmentData, int logic) +{ + if (!UndoRedoEnabled()) { + return; + } + if (!macro) { + return; + } + + const std::string macroName = macro->Name(); + + OBSDataAutoRelease undoData = obs_data_create(); + obs_data_set_string(undoData, "macro", macroName.c_str()); + obs_data_set_int(undoData, "type", (int)type); + obs_data_set_int(undoData, "index", index); + obs_data_set_string(undoData, "id", segmentId.c_str()); + obs_data_set_int(undoData, "logic", logic); + obs_data_set_obj(undoData, "segment", segmentData); + + OBSDataAutoRelease redoData = obs_data_create(); + obs_data_set_string(redoData, "macro", macroName.c_str()); + obs_data_set_int(redoData, "type", (int)type); + obs_data_set_int(redoData, "index", index); + + const std::string actionName = + fmtUndoName("AdvSceneSwitcher.undo.removeSegment", + segmentTypeName(type), macroName); + obs_frontend_add_undo_redo_action(actionName.c_str(), + &addSegmentFromData, + &removeSegmentFromData, + obs_data_get_json(undoData), + obs_data_get_json(redoData), false); +} + +static OBSDataArrayAutoRelease +buildChildArray(const std::vector> &children) +{ + OBSDataArrayAutoRelease arr = obs_data_array_create(); + for (const auto &child : children) { + OBSDataAutoRelease item = obs_data_create(); + obs_data_set_string(item, "name", child->Name().c_str()); + obs_data_array_push_back(arr, item); + } + return arr; +} + +void RegisterGroupCreateUndoRedo(const std::string &groupName) +{ + if (!UndoRedoEnabled()) { + return; + } + const auto group = GetWeakMacroByName(groupName.c_str()).lock(); + if (!group || !group->IsGroup()) { + return; + } + + const auto ¯os = GetTopLevelMacros(); + int groupIndex = -1; + for (int i = 0; i < (int)macros.size(); i++) { + if (macros[i].get() == group.get()) { + groupIndex = i; + break; + } + } + if (groupIndex < 0) { + return; + } + + const auto children = GetGroupMacroEntries(group.get()); + OBSDataArrayAutoRelease childArray = buildChildArray(children); + + OBSDataAutoRelease undoData = obs_data_create(); + obs_data_set_string(undoData, "group_name", groupName.c_str()); + obs_data_set_int(undoData, "group_index", groupIndex); + obs_data_set_array(undoData, "children", childArray); + + OBSDataAutoRelease redoData = obs_data_create(); + obs_data_set_string(redoData, "group_name", groupName.c_str()); + obs_data_set_int(redoData, "group_index", groupIndex); + obs_data_set_array(redoData, "children", childArray); + + const std::string actionName = + fmtUndoName("AdvSceneSwitcher.undo.groupMacros", groupName); + obs_frontend_add_undo_redo_action(actionName.c_str(), + &removeGroupFromData, + &restoreGroupFromData, + obs_data_get_json(undoData), + obs_data_get_json(redoData), false); +} + +void RegisterGroupRemoveUndoRedo(const std::string &groupName) +{ + if (!UndoRedoEnabled()) { + return; + } + const auto group = GetWeakMacroByName(groupName.c_str()).lock(); + if (!group || !group->IsGroup()) { + return; + } + + const auto ¯os = GetTopLevelMacros(); + int groupIndex = -1; + for (int i = 0; i < (int)macros.size(); i++) { + if (macros[i].get() == group.get()) { + groupIndex = i; + break; + } + } + if (groupIndex < 0) { + return; + } + + const auto children = GetGroupMacroEntries(group.get()); + OBSDataArrayAutoRelease childArray = buildChildArray(children); + + OBSDataAutoRelease undoData = obs_data_create(); + obs_data_set_string(undoData, "group_name", groupName.c_str()); + obs_data_set_int(undoData, "group_index", groupIndex); + obs_data_set_array(undoData, "children", childArray); + + OBSDataAutoRelease redoData = obs_data_create(); + obs_data_set_string(redoData, "group_name", groupName.c_str()); + obs_data_set_int(redoData, "group_index", groupIndex); + obs_data_set_array(redoData, "children", childArray); + + const std::string actionName = + fmtUndoName("AdvSceneSwitcher.undo.ungroupMacros", groupName); + obs_frontend_add_undo_redo_action(actionName.c_str(), + &restoreGroupFromData, + &removeGroupFromData, + obs_data_get_json(undoData), + obs_data_get_json(redoData), false); +} + +void RegisterGroupDeleteUndoRedo(Macro *macro) +{ + if (!UndoRedoEnabled()) { + return; + } + if (!macro || !macro->IsGroup()) { + return; + } + + const std::string groupName = macro->Name(); + + const auto ¯os = GetTopLevelMacros(); + int groupIndex = -1; + for (int i = 0; i < (int)macros.size(); i++) { + if (macros[i].get() == macro) { + groupIndex = i; + break; + } + } + if (groupIndex < 0) { + return; + } + + OBSDataAutoRelease groupData = obs_data_create(); + macro->Save(groupData); + + const auto children = GetGroupMacroEntries(macro); + OBSDataArrayAutoRelease childArray = obs_data_array_create(); + for (const auto &child : children) { + OBSDataAutoRelease childData = obs_data_create(); + child->Save(childData); + OBSDataAutoRelease entry = obs_data_create(); + obs_data_set_obj(entry, "data", childData); + obs_data_array_push_back(childArray, entry); + } + + OBSDataAutoRelease undoData = obs_data_create(); + obs_data_set_string(undoData, "group_name", groupName.c_str()); + obs_data_set_int(undoData, "group_index", groupIndex); + obs_data_set_obj(undoData, "group_data", groupData); + obs_data_set_array(undoData, "children", childArray); + + OBSDataAutoRelease redoData = obs_data_create(); + obs_data_set_string(redoData, "group_name", groupName.c_str()); + obs_data_set_int(redoData, "group_index", groupIndex); + obs_data_set_obj(redoData, "group_data", groupData); + obs_data_set_array(redoData, "children", childArray); + + const std::string actionName = fmtUndoName( + "AdvSceneSwitcher.undo.removeMacroGroup", groupName); + obs_frontend_add_undo_redo_action(actionName.c_str(), + &restoreGroupWithChildrenFromData, + &deleteGroupWithChildrenFromData, + obs_data_get_json(undoData), + obs_data_get_json(redoData), false); +} + +void RegisterMacroRenameUndoRedo(const std::string &oldName, + const std::string &newName) +{ + if (!UndoRedoEnabled()) { + return; + } + OBSDataAutoRelease undoData = obs_data_create(); + obs_data_set_string(undoData, "old_name", newName.c_str()); + obs_data_set_string(undoData, "new_name", oldName.c_str()); + + OBSDataAutoRelease redoData = obs_data_create(); + obs_data_set_string(redoData, "old_name", oldName.c_str()); + obs_data_set_string(redoData, "new_name", newName.c_str()); + + const std::string actionName = + fmtUndoName("AdvSceneSwitcher.undo.renameMacro", oldName); + obs_frontend_add_undo_redo_action(actionName.c_str(), + &renameMacroFromData, + &renameMacroFromData, + obs_data_get_json(undoData), + obs_data_get_json(redoData), false); +} + +void RegisterSegmentTypeChangeUndoRedo(Macro *macro, SegmentType type, + int index, const std::string &oldId, + obs_data_t *oldData, int oldLogic, + const std::string &newId, + obs_data_t *newData, int newLogic) +{ + if (!UndoRedoEnabled()) { + return; + } + if (!macro) { + return; + } + + const std::string macroName = macro->Name(); + + OBSDataAutoRelease undoData = obs_data_create(); + obs_data_set_string(undoData, "macro", macroName.c_str()); + obs_data_set_int(undoData, "type", (int)type); + obs_data_set_int(undoData, "index", index); + obs_data_set_string(undoData, "id", oldId.c_str()); + obs_data_set_int(undoData, "logic", oldLogic); + obs_data_set_obj(undoData, "segment", oldData); + + OBSDataAutoRelease redoData = obs_data_create(); + obs_data_set_string(redoData, "macro", macroName.c_str()); + obs_data_set_int(redoData, "type", (int)type); + obs_data_set_int(redoData, "index", index); + obs_data_set_string(redoData, "id", newId.c_str()); + obs_data_set_int(redoData, "logic", newLogic); + obs_data_set_obj(redoData, "segment", newData); + + const std::string actionName = + fmtUndoName("AdvSceneSwitcher.undo.changeSegmentType", + segmentTypeName(type), macroName); + obs_frontend_add_undo_redo_action(actionName.c_str(), + &replaceSegmentFromData, + &replaceSegmentFromData, + obs_data_get_json(undoData), + obs_data_get_json(redoData), false); +} + +void RegisterMacroModifyUndoRedo(Macro *parentMacro, obs_data_t *beforeData) +{ + if (!UndoRedoEnabled()) { + return; + } + if (!parentMacro || !beforeData) { + return; + } + + const std::string macroName = parentMacro->Name(); + + OBSDataAutoRelease afterMacroData = obs_data_create(); + parentMacro->Save(afterMacroData); + + OBSDataAutoRelease undoData = obs_data_create(); + obs_data_set_string(undoData, "name", macroName.c_str()); + obs_data_set_obj(undoData, "macro", beforeData); + + OBSDataAutoRelease redoData = obs_data_create(); + obs_data_set_string(redoData, "name", macroName.c_str()); + obs_data_set_obj(redoData, "macro", afterMacroData); + + const std::string actionName = + fmtUndoName("AdvSceneSwitcher.undo.modifyMacro", macroName); + obs_frontend_add_undo_redo_action(actionName.c_str(), + &restoreMacroStateFromData, + &restoreMacroStateFromData, + obs_data_get_json(undoData), + obs_data_get_json(redoData), false); +} + +void RegisterMacrosImportUndoRedo( + const std::vector> &importedMacros) +{ + if (!UndoRedoEnabled()) { + return; + } + if (importedMacros.empty()) { + return; + } + + OBSDataArrayAutoRelease namesArray = obs_data_array_create(); + OBSDataArrayAutoRelease macrosArray = obs_data_array_create(); + + const auto &allMacros = GetTopLevelMacros(); + for (const auto ¯o : importedMacros) { + const std::string macroName = macro->Name(); + + OBSDataAutoRelease nameItem = obs_data_create(); + obs_data_set_string(nameItem, "name", macroName.c_str()); + obs_data_array_push_back(namesArray, nameItem); + + int index = -1; + for (int i = 0; i < (int)allMacros.size(); i++) { + if (allMacros[i].get() == macro.get()) { + index = i; + break; + } + } + if (index < 0) { + continue; + } + + std::string parentGroup; + if (macro->Parent()) { + parentGroup = macro->Parent()->Name(); + } + + OBSDataAutoRelease macroData = obs_data_create(); + macro->Save(macroData); + + OBSDataAutoRelease entry = obs_data_create(); + obs_data_set_int(entry, "index", index); + obs_data_set_string(entry, "parent_group", parentGroup.c_str()); + obs_data_set_obj(entry, "macro", macroData); + obs_data_array_push_back(macrosArray, entry); + } + + OBSDataAutoRelease undoData = obs_data_create(); + obs_data_set_array(undoData, "names", namesArray); + + OBSDataAutoRelease redoData = obs_data_create(); + obs_data_set_array(redoData, "macros", macrosArray); + + obs_frontend_add_undo_redo_action( + obs_module_text("AdvSceneSwitcher.undo.importMacros"), + &deleteMacrosBatch, &restoreMacrosBatch, + obs_data_get_json(undoData), obs_data_get_json(redoData), + false); +} + +} // namespace advss diff --git a/lib/macro/macro-undo-redo.hpp b/lib/macro/macro-undo-redo.hpp new file mode 100644 index 00000000..c2dfaa0c --- /dev/null +++ b/lib/macro/macro-undo-redo.hpp @@ -0,0 +1,38 @@ +#pragma once +#include "macro-edit.hpp" + +#include +#include +#include +#include + +namespace advss { + +class Macro; + +bool GetUndoRedoEnabled(); +void SetUndoRedoEnabled(bool enabled); + +void RegisterMacroAddUndoRedo(const std::string ¯oName); +bool RegisterMacroRemoveUndoRedo(Macro *macro); +void RegisterSegmentAddUndoRedo(Macro *macro, MacroEdit::SegmentType type, + int index); +void RegisterSegmentTypeChangeUndoRedo(Macro *macro, + MacroEdit::SegmentType type, int index, + const std::string &oldId, + obs_data_t *oldData, int oldLogic, + const std::string &newId, + obs_data_t *newData, int newLogic); +void RegisterSegmentRemoveUndoRedo(Macro *macro, MacroEdit::SegmentType type, + int index, const std::string &segmentId, + obs_data_t *segmentData, int logic); +void RegisterMacroRenameUndoRedo(const std::string &oldName, + const std::string &newName); +void RegisterGroupCreateUndoRedo(const std::string &groupName); +void RegisterGroupRemoveUndoRedo(const std::string &groupName); +void RegisterGroupDeleteUndoRedo(Macro *macro); +void RegisterMacroModifyUndoRedo(Macro *parentMacro, obs_data_t *beforeData); +void RegisterMacrosImportUndoRedo( + const std::vector> &importedMacros); + +} // namespace advss diff --git a/lib/macro/macro.cpp b/lib/macro/macro.cpp index e49b84d1..cde7a113 100644 --- a/lib/macro/macro.cpp +++ b/lib/macro/macro.cpp @@ -797,6 +797,11 @@ std::shared_ptr Macro::Parent() const return _parent.lock(); } +void Macro::SetNestedParentMacro(Macro *parent) +{ + _nestedParentMacro = parent; +} + bool Macro::Save(obs_data_t *obj, bool saveForCopy) const { if (!saveForCopy) { diff --git a/lib/macro/macro.hpp b/lib/macro/macro.hpp index 30601a88..ce6155b4 100644 --- a/lib/macro/macro.hpp +++ b/lib/macro/macro.hpp @@ -130,6 +130,9 @@ public: void SetParent(std::shared_ptr m) { _parent = m; } std::shared_ptr Parent() const; + void SetNestedParentMacro(Macro *parent); + Macro *GetNestedParentMacro() const { return _nestedParentMacro; } + // Saving and loading bool Save(obs_data_t *obj, bool saveForCopy = false) const; bool Load(obs_data_t *obj); @@ -187,6 +190,7 @@ private: std::deque> _elseActions; std::weak_ptr _parent; + Macro *_nestedParentMacro = nullptr; uint32_t _groupSize = 0; bool _isGroup = false; bool _isCollapsed = false; diff --git a/tests/stubs/macro-edit.cpp b/tests/stubs/macro-edit.cpp index 629040b3..7ea6e98c 100644 --- a/tests/stubs/macro-edit.cpp +++ b/tests/stubs/macro-edit.cpp @@ -110,7 +110,9 @@ void MacroEdit::HighlightControls() const {} void MacroEdit::PopulateMacroActions(Macro &, uint32_t) {} void MacroEdit::PopulateMacroElseActions(Macro &, uint32_t) {} void MacroEdit::PopulateMacroConditions(Macro &, uint32_t) {} -void MacroEdit::SetupMacroSegmentSelection(MacroSection, int) {} +void MacroEdit::InsertSegmentWidget(SegmentType, int) {} +void MacroEdit::RemoveSegmentWidget(SegmentType, int) {} +void MacroEdit::SetupMacroSegmentSelection(SegmentType, int) {} void MacroEdit::SetupContextMenu(const QPoint &, const std::function &, const std::function &,