From 1171714aa903d104252c7f9f968c2eaa39104bcb Mon Sep 17 00:00:00 2001 From: WarmUpTill <19472752+WarmUpTill@users.noreply.github.com> Date: Thu, 22 Oct 2026 18:50:36 +0200 Subject: [PATCH] Support nested macro groups in undo / redo and import / export --- lib/macro/macro-helpers.cpp | 108 +++++++++++++++ lib/macro/macro-helpers.hpp | 30 ++++ lib/macro/macro-tab.cpp | 100 ++------------ lib/macro/macro-tree.cpp | 22 +-- lib/macro/macro-undo-redo.cpp | 139 ++++++++----------- lib/macro/macro.cpp | 57 ++++++-- tests/test-macro-group.cpp | 249 ++++++++++++++++++++++++++++++++++ 7 files changed, 503 insertions(+), 202 deletions(-) diff --git a/lib/macro/macro-helpers.cpp b/lib/macro/macro-helpers.cpp index 75896808..7d235028 100644 --- a/lib/macro/macro-helpers.cpp +++ b/lib/macro/macro-helpers.cpp @@ -180,6 +180,114 @@ bool IsMacroInGroup(const Macro *macro, const Macro *group) return false; } +std::vector> +GetOutermostMacros(const std::vector> ¯os) +{ + std::vector> result; + for (const auto ¯o : GetTopLevelMacros()) { + const bool isGiven = std::find(macros.begin(), macros.end(), + macro) != macros.end(); + const bool isInGivenGroup = std::any_of( + macros.begin(), macros.end(), + [¯o](const std::shared_ptr &group) { + return IsMacroInGroup(macro.get(), group.get()); + }); + if (isGiven && !isInGivenGroup) { + result.emplace_back(macro); + } + } + return result; +} + +std::vector> +GetMacrosWithGroupContents(const std::vector> ¯os) +{ + std::vector> result; + for (const auto ¯o : GetTopLevelMacros()) { + const bool include = std::any_of( + macros.begin(), macros.end(), + [¯o](const std::shared_ptr &given) { + return given == macro || + IsMacroInGroup(macro.get(), given.get()); + }); + if (include) { + result.emplace_back(macro); + } + } + return result; +} + +static std::deque>::iterator +subtreeEnd(std::deque> ¯os, + std::deque>::iterator it) +{ + const auto remaining = std::distance(it, macros.end()); + const auto size = + std::min(1 + (*it)->GroupSize(), remaining); + return std::next(it, size); +} + +void InsertMacroSubtree(int index, + const std::deque> &subtree, + const std::shared_ptr &parent) +{ + if (subtree.empty()) { + return; + } + + auto ¯os = GetTopLevelMacros(); + Macro::PrepareMoveToGroup(parent, subtree.front()); + index = std::clamp(index, 0, (int)macros.size()); + macros.insert(macros.begin() + index, subtree.begin(), subtree.end()); +} + +void RemoveMacroSubtree(const std::shared_ptr ¯o) +{ + auto ¯os = GetTopLevelMacros(); + auto it = std::find(macros.begin(), macros.end(), macro); + if (it == macros.end()) { + return; + } + + const auto end = subtreeEnd(macros, it); + Macro::PrepareMoveToGroup(nullptr, macro); + macros.erase(it, end); +} + +std::shared_ptr +InsertGroup(const std::string &name, int index, + std::vector> &children, + const std::shared_ptr &parent) +{ + auto group = Macro::CreateGroup(name, children); + if (children.empty()) { + Macro::PrepareMoveToGroup(parent, group); + } + + auto ¯os = GetTopLevelMacros(); + index = std::clamp(index, 0, (int)macros.size()); + macros.insert(macros.begin() + index, group); + + auto previous = group; + for (const auto &child : children) { + auto start = std::find(macros.begin(), macros.end(), child); + if (start == macros.end()) { + continue; + } + const auto end = subtreeEnd(macros, start); + const std::vector> subtree(start, end); + macros.erase(start, end); + + auto position = + std::find(macros.begin(), macros.end(), previous); + position = previous == group ? std::next(position) + : subtreeEnd(macros, position); + macros.insert(position, subtree.begin(), subtree.end()); + previous = child; + } + return group; +} + std::condition_variable &GetMacroWaitCV() { static std::condition_variable cv; diff --git a/lib/macro/macro-helpers.hpp b/lib/macro/macro-helpers.hpp index 1a4aba5d..8c1d8756 100644 --- a/lib/macro/macro-helpers.hpp +++ b/lib/macro/macro-helpers.hpp @@ -7,11 +7,14 @@ #include #include #include +#include #include #include struct obs_data; typedef struct obs_data obs_data_t; +struct obs_data_array; +typedef struct obs_data_array obs_data_array_t; namespace advss { @@ -42,6 +45,27 @@ EXPORT std::vector> GetGroupMacroEntries(Macro *); std::vector> GetGroupChildren(const Macro *group); std::vector> GetGroupDescendants(const Macro *group); bool IsMacroInGroup(const Macro *macro, const Macro *group); +// Returns the given macros without the ones contained in one of the given +// groups, sorted by their position in the macro list +std::vector> +GetOutermostMacros(const std::vector> ¯os); +// Returns the given macros and the contents of the given groups, sorted by +// their position in the macro list +std::vector> +GetMacrosWithGroupContents(const std::vector> ¯os); +// Inserts the macro and its group contents at the given position of the macro +// list +void InsertMacroSubtree(int index, + const std::deque> &subtree, + const std::shared_ptr &parent); +// Removes the macro and its group contents from the macro list +void RemoveMacroSubtree(const std::shared_ptr ¯o); +// Creates a group at the given position of the macro list and moves the given +// macros and their group contents into it +std::shared_ptr +InsertGroup(const std::string &name, int index, + std::vector> &children, + const std::shared_ptr &parent); EXPORT std::condition_variable &GetMacroWaitCV(); EXPORT std::condition_variable &GetMacroTransitionCV(); @@ -85,6 +109,12 @@ void WaitForAllMacros(); EXPORT void LoadMacros(obs_data_t *obj); EXPORT void SaveMacros(obs_data_t *obj); +// Appends the exported macros to the macro list. +// Macros for which shouldImport returns false are skipped and their group +// contents are moved to the parent group of the skipped group. +std::vector> LoadImportedMacros( + obs_data_array_t *macros, + const std::function &)> &shouldImport); EXPORT void InvalidateMacroTempVarValues(); EXPORT void ResetMacroConditionTimers(Macro *); diff --git a/lib/macro/macro-tab.cpp b/lib/macro/macro-tab.cpp index 1aa6ca6f..428ddc5b 100644 --- a/lib/macro/macro-tab.cpp +++ b/lib/macro/macro-tab.cpp @@ -152,29 +152,6 @@ void AdvSceneSwitcher::on_macroAdd_clicked() RegisterMacroAddUndoRedo(name); } -static void addGroupSubitems(std::vector> ¯os, - const std::shared_ptr &group) -{ - auto subitems = GetGroupDescendants(group.get()); - - // Remove subitems which were already selected to avoid duplicates - for (const auto &subitem : subitems) { - auto it = std::find(macros.begin(), macros.end(), subitem); - if (it == macros.end()) { - continue; - } - macros.erase(it); - } - - // Add group subitems - auto it = std::find(macros.begin(), macros.end(), group); - if (it == macros.end()) { - return; - } - it = std::next(it); - macros.insert(it, subitems.begin(), subitems.end()); -} - void AdvSceneSwitcher::RemoveMacro(std::shared_ptr ¯o) { if (!macro) { @@ -196,14 +173,8 @@ void AdvSceneSwitcher::RemoveMacro(std::shared_ptr ¯o) RegisterMacroRemoveUndoRedo(macro.get()); } - if (macro->IsGroup()) { - std::vector> macros = {macro}; - addGroupSubitems(macros, macro); - for (auto macro : macros) { - ui->macroEdit->ClearSegmentWidgetCacheFor(macro.get()); - } - } else { - ui->macroEdit->ClearSegmentWidgetCacheFor(macro.get()); + for (const auto &m : GetMacrosWithGroupContents({macro})) { + ui->macroEdit->ClearSegmentWidgetCacheFor(m.get()); } // Don't cache widgets for about to be deleted macros @@ -216,7 +187,7 @@ void AdvSceneSwitcher::RemoveMacro(std::shared_ptr ¯o) void AdvSceneSwitcher::RemoveSelectedMacros() { - auto macros = GetSelectedMacros(); + auto macros = GetOutermostMacros(GetSelectedMacros()); if (macros.empty()) { return; } @@ -315,13 +286,7 @@ void AdvSceneSwitcher::RenameSelectedMacro() void AdvSceneSwitcher::ExportMacros() const { - auto selectedMacros = GetSelectedMacros(); - auto macros = selectedMacros; - for (const auto ¯o : selectedMacros) { - if (macro->IsGroup() && macro->GroupSize() > 0) { - addGroupSubitems(macros, macro); - } - } + const auto macros = GetMacrosWithGroupContents(GetSelectedMacros()); OBSDataAutoRelease data = obs_data_create(); OBSDataArrayAutoRelease macroArray = obs_data_array_create(); @@ -404,55 +369,20 @@ void AdvSceneSwitcher::ImportMacros() } OBSDataArrayAutoRelease array = obs_data_get_array(data, "macros"); - size_t count = obs_data_array_count(array); - - int groupSize = 0; - std::shared_ptr group; - std::vector> importedMacros; - auto &tempMacros = GetTemporaryMacros(); std::unique_lock lock(*GetMutex()); - for (size_t i = 0; i < count; i++) { - tempMacros.clear(); - - OBSDataAutoRelease array_obj = obs_data_array_item(array, i); - auto macro = std::make_shared(); - tempMacros.emplace_back(macro); - macro->Load(array_obj); - RunAndClearPostLoadSteps(); - - if (macroNameExists(macro->Name())) { - lock.unlock(); - bool resolved = ResolveMacroImportNameConflict(macro); - lock.lock(); - if (!resolved) { - groupSize--; - continue; + const auto importedMacros = LoadImportedMacros( + array, [this, &lock](const std::shared_ptr ¯o) { + if (!macroNameExists(macro->Name())) { + return true; } - } - - importedMacros.emplace_back(macro); - GetTopLevelMacros().emplace_back(macro); - if (groupSize > 0 && !macro->IsGroup()) { - Macro::PrepareMoveToGroup(group, macro); - groupSize--; - } - - if (macro->IsGroup()) { - group = macro; - groupSize = macro->GroupSize(); - // We are not sure if all elements will be added so we - // have to reset the group size to zero and add elements - // to the group as they come up. - macro->ResetGroupSize(); - } - } - - for (const auto ¯o : importedMacros) { - macro->PostLoad(); - } - RunAndClearPostLoadSteps(); - tempMacros.clear(); + lock.unlock(); + auto renamedMacro = macro; + const bool resolved = + ResolveMacroImportNameConflict(renamedMacro); + lock.lock(); + return resolved; + }); ui->macros->Reset(GetTopLevelMacros(), GetGlobalMacroSettings()._highlightExecuted); diff --git a/lib/macro/macro-tree.cpp b/lib/macro/macro-tree.cpp index e07668da..51561cec 100644 --- a/lib/macro/macro-tree.cpp +++ b/lib/macro/macro-tree.cpp @@ -798,27 +798,7 @@ std::vector> MacroTree::GetCurrentMacros() const std::vector> MacroTree::GetSelectedSubtreeRoots() const { - auto selected = GetCurrentMacros(); - std::vector> roots; - for (const auto ¯o : selected) { - const bool hasSelectedAncestor = std::any_of( - selected.begin(), selected.end(), - [¯o](const std::shared_ptr &other) { - return IsMacroInGroup(macro.get(), other.get()); - }); - if (!hasSelectedAncestor) { - roots.emplace_back(macro); - } - } - - auto treeModel = GetModel(); - std::sort(roots.begin(), roots.end(), - [treeModel](const std::shared_ptr &a, - const std::shared_ptr &b) { - return treeModel->PositionOf(a.get()) < - treeModel->PositionOf(b.get()); - }); - return roots; + return GetOutermostMacros(GetCurrentMacros()); } bool MacroTree::GroupsSelected() const diff --git a/lib/macro/macro-undo-redo.cpp b/lib/macro/macro-undo-redo.cpp index 7686d560..08fcebcd 100644 --- a/lib/macro/macro-undo-redo.cpp +++ b/lib/macro/macro-undo-redo.cpp @@ -143,11 +143,7 @@ static void deleteMacroByName(const char *jsonData) 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); - } + RemoveMacroSubtree(macro); } MacroSignalManager::Instance()->Remove(QString::fromStdString(name)); @@ -178,17 +174,9 @@ static void restoreMacroFromData(const char *jsonData) 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); + const auto parent = + GetWeakMacroByName(parentGroup.c_str()).lock(); + InsertMacroSubtree(index, {macro}, parent); macroName = macro->Name(); } @@ -207,8 +195,9 @@ static void restoreMacroFromData(const char *jsonData) // --------------------------------------------------------------------------- // Callback data format: -// {"group_name": "...", "group_index": N, "group_data": {...}, -// "children": [{"data": {...}}, ...]} +// {"group_name": "...", "group_index": N, "parent_group": "...", +// "group_data": {...}, "children": [{"data": {...}}, ...]} +// The children contain the contents of nested groups as well. static void deleteGroupWithChildrenFromData(const char *jsonData) { OBSDataAutoRelease data = obs_data_create_from_json(jsonData); @@ -222,7 +211,7 @@ static void deleteGroupWithChildrenFromData(const char *jsonData) return; } - const auto children = GetGroupChildren(group.get()); + const auto children = GetGroupDescendants(group.get()); auto *const window = AdvSceneSwitcher::window; if (SettingsWindowIsOpened() && window) { @@ -238,11 +227,7 @@ static void deleteGroupWithChildrenFromData(const char *jsonData) 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())); - } + RemoveMacroSubtree(group); } for (const auto &child : children) { @@ -261,21 +246,18 @@ static void restoreGroupWithChildrenFromData(const char *jsonData) } const int groupIndex = (int)obs_data_get_int(data, "group_index"); + const std::string parentGroup = + obs_data_get_string(data, "parent_group"); 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"); - } + if (!groupData) { + return; } + auto group = std::make_shared(); + group->Load(groupData); + std::vector> children; const size_t count = obs_data_array_count(childArray); for (size_t i = 0; i < count; i++) { @@ -294,22 +276,15 @@ static void restoreGroupWithChildrenFromData(const char *jsonData) children.push_back(child); } - std::string restoredGroupName; + const std::string restoredGroupName = group->Name(); { 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++; - } + std::deque> subtree = {group}; + subtree.insert(subtree.end(), children.begin(), children.end()); + Macro::RestoreGroupHierarchy(subtree); + const auto parent = + GetWeakMacroByName(parentGroup.c_str()).lock(); + InsertMacroSubtree(groupIndex, subtree, parent); } auto *const window = AdvSceneSwitcher::window; @@ -332,7 +307,8 @@ static void restoreGroupWithChildrenFromData(const char *jsonData) // --------------------------------------------------------------------------- // Callback data format for both group and ungroup: -// {"group_name": "...", "group_index": N, "children": [{"name": "..."}, ...]} +// {"group_name": "...", "group_index": N, "parent_group": "...", +// "children": [{"name": "..."}, ...]} static void removeGroupFromData(const char *jsonData) { OBSDataAutoRelease data = obs_data_create_from_json(jsonData); @@ -371,6 +347,8 @@ static void restoreGroupFromData(const char *jsonData) const std::string groupName = obs_data_get_string(data, "group_name"); const int groupIndex = (int)obs_data_get_int(data, "group_index"); + const std::string parentGroup = + obs_data_get_string(data, "parent_group"); OBSDataArrayAutoRelease childArray = obs_data_get_array(data, "children"); @@ -387,23 +365,9 @@ static void restoreGroupFromData(const char *jsonData) { 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++; - } + const auto parent = + GetWeakMacroByName(parentGroup.c_str()).lock(); + InsertGroup(groupName, groupIndex, children, parent); } auto *const window = AdvSceneSwitcher::window; @@ -804,12 +768,7 @@ static void deleteMacrosBatch(const char *jsonData) 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); - } + RemoveMacroSubtree(macro); } MacroSignalManager::Instance()->Remove( @@ -847,20 +806,13 @@ static void restoreMacrosBatch(const char *jsonData) macro->PostLoad(); RunAndClearPostLoadSteps(); - if (!parentGroup.empty()) { - const auto parent = - GetWeakMacroByName(parentGroup.c_str()) - .lock(); - if (parent) { - Macro::PrepareMoveToGroup(parent, - macro); - } - } + // The group contents are restored by the following + // entries, which will update the group size + macro->ResetGroupSize(); - auto ¯os = GetTopLevelMacros(); - const int insertIdx = - std::min(index, (int)macros.size()); - macros.insert(macros.begin() + insertIdx, macro); + const auto parent = + GetWeakMacroByName(parentGroup.c_str()).lock(); + InsertMacroSubtree(index, {macro}, parent); } addedNames.push_back(macro->Name()); @@ -1113,17 +1065,22 @@ void RegisterGroupCreateUndoRedo(const std::string &groupName) return; } + const auto parent = group->Parent(); + const std::string parentGroup = parent ? parent->Name() : ""; + const auto children = GetGroupChildren(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_string(undoData, "parent_group", parentGroup.c_str()); 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_string(redoData, "parent_group", parentGroup.c_str()); obs_data_set_array(redoData, "children", childArray); const std::string actionName = @@ -1157,17 +1114,22 @@ void RegisterGroupRemoveUndoRedo(const std::string &groupName) return; } + const auto parent = group->Parent(); + const std::string parentGroup = parent ? parent->Name() : ""; + const auto children = GetGroupChildren(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_string(undoData, "parent_group", parentGroup.c_str()); 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_string(redoData, "parent_group", parentGroup.c_str()); obs_data_set_array(redoData, "children", childArray); const std::string actionName = @@ -1202,10 +1164,13 @@ void RegisterGroupDeleteUndoRedo(Macro *macro) return; } + const auto parent = macro->Parent(); + const std::string parentGroup = parent ? parent->Name() : ""; + OBSDataAutoRelease groupData = obs_data_create(); macro->Save(groupData); - const auto children = GetGroupChildren(macro); + const auto children = GetGroupDescendants(macro); OBSDataArrayAutoRelease childArray = obs_data_array_create(); for (const auto &child : children) { OBSDataAutoRelease childData = obs_data_create(); @@ -1218,12 +1183,14 @@ void RegisterGroupDeleteUndoRedo(Macro *macro) 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_string(undoData, "parent_group", parentGroup.c_str()); 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_string(redoData, "parent_group", parentGroup.c_str()); obs_data_set_obj(redoData, "group_data", groupData); obs_data_set_array(redoData, "children", childArray); diff --git a/lib/macro/macro.cpp b/lib/macro/macro.cpp index 415dedc4..09fbcaa6 100644 --- a/lib/macro/macro.cpp +++ b/lib/macro/macro.cpp @@ -1290,17 +1290,13 @@ void Macro::SetHotkeysDesc() const void SaveMacros(obs_data_t *obj) { - obs_data_array_t *macroArray = obs_data_array_create(); + OBSDataArrayAutoRelease macroArray = obs_data_array_create(); for (const auto &m : GetTopLevelMacros()) { - obs_data_t *array_obj = obs_data_create(); - + OBSDataAutoRelease array_obj = obs_data_create(); m->Save(array_obj); obs_data_array_push_back(macroArray, array_obj); - - obs_data_release(array_obj); } obs_data_set_array(obj, "macros", macroArray); - obs_data_array_release(macroArray); } void LoadMacros(obs_data_t *obj) @@ -1308,16 +1304,15 @@ void LoadMacros(obs_data_t *obj) auto ¯os = GetTopLevelMacros(); macros.clear(); - obs_data_array_t *macroArray = obs_data_get_array(obj, "macros"); + OBSDataArrayAutoRelease macroArray = obs_data_get_array(obj, "macros"); size_t count = obs_data_array_count(macroArray); for (size_t i = 0; i < count; i++) { - obs_data_t *array_obj = obs_data_array_item(macroArray, i); + OBSDataAutoRelease array_obj = + obs_data_array_item(macroArray, i); macros.emplace_back(std::make_shared()); macros.back()->Load(array_obj); - obs_data_release(array_obj); } - obs_data_array_release(macroArray); Macro::RestoreGroupHierarchy(macros); for (const auto &m : macros) { @@ -1325,6 +1320,48 @@ void LoadMacros(obs_data_t *obj) } } +std::vector> LoadImportedMacros( + obs_data_array_t *array, + const std::function &)> &shouldImport) +{ + std::deque> loaded; + auto &tempMacros = GetTemporaryMacros(); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + tempMacros.clear(); + OBSDataAutoRelease obj = obs_data_array_item(array, i); + auto macro = std::make_shared(); + tempMacros.emplace_back(macro); + macro->Load(obj); + RunAndClearPostLoadSteps(); + loaded.emplace_back(macro); + } + tempMacros.clear(); + Macro::RestoreGroupHierarchy(loaded); + + std::vector> imported; + for (const auto ¯o : loaded) { + if (!shouldImport(macro)) { + for (const auto &child : loaded) { + if (child->Parent() == macro) { + Macro::PrepareMoveToGroup( + macro->Parent(), child); + } + } + Macro::PrepareMoveToGroup(nullptr, macro); + continue; + } + GetTopLevelMacros().emplace_back(macro); + imported.emplace_back(macro); + } + + for (const auto ¯o : imported) { + macro->PostLoad(); + } + RunAndClearPostLoadSteps(); + return imported; +} + bool CheckMacros() { bool matchFound = false; diff --git a/tests/test-macro-group.cpp b/tests/test-macro-group.cpp index 604a742f..7ce25c26 100644 --- a/tests/test-macro-group.cpp +++ b/tests/test-macro-group.cpp @@ -261,3 +261,252 @@ TEST_CASE("Execution time is propagated to all parent groups", "[macro-group]") REQUIRE(GetMacro("G2")->WasExecutedSince(start)); REQUIRE_FALSE(GetMacro("B")->WasExecutedSince(start)); } + +namespace { + +void checkGroupSizes() +{ + const auto ¯os = GetTopLevelMacros(); + for (const auto ¯o : macros) { + if (!macro->IsGroup()) { + continue; + } + INFO("group " << macro->Name() << " in " << DescribeMacros()); + const auto descendants = std::count_if( + macros.begin(), macros.end(), + [¯o](const std::shared_ptr &m) { + return IsMacroInGroup(m.get(), macro.get()); + }); + REQUIRE(macro->GroupSize() == (uint32_t)descendants); + } +} + +std::vector> macros(const Names &names) +{ + std::vector> result; + for (const auto &name : names) { + result.emplace_back(GetMacro(name)); + } + return result; +} + +int indexOf(const std::string &name) +{ + const auto &all = GetTopLevelMacros(); + for (int i = 0; i < (int)all.size(); i++) { + if (all[i]->Name() == name) { + return i; + } + } + return -1; +} + +// Mirrors AdvSceneSwitcher::ExportMacros() +OBSDataArrayAutoRelease exportMacros(const Names &selection) +{ + OBSDataArrayAutoRelease array = obs_data_array_create(); + for (const auto ¯o : + GetMacrosWithGroupContents(macros(selection))) { + OBSDataAutoRelease obj = obs_data_create(); + macro->Save(obj); + obs_data_array_push_back(array, obj); + } + return array; +} + +} // namespace + +TEST_CASE("Outermost macros", "[macro-group]") +{ + ClearMacros clear; + SetupMacros("A, G1[B, G2[C, D], E], F"); + + REQUIRE(names(GetOutermostMacros(macros({"C", "G2", "F", "A"}))) == + Names{"A", "G2", "F"}); + REQUIRE(names(GetOutermostMacros(macros({"C", "G2", "G1"}))) == + Names{"G1"}); + REQUIRE(GetOutermostMacros({}).empty()); +} + +TEST_CASE("Macros with group contents", "[macro-group]") +{ + ClearMacros clear; + SetupMacros("A, G1[B, G2[C, D], E], F"); + + REQUIRE(names(GetMacrosWithGroupContents(macros({"F", "G2", "A"}))) == + Names{"A", "G2", "C", "D", "F"}); + REQUIRE(names(GetMacrosWithGroupContents(macros({"C", "G1"}))) == + Names{"G1", "B", "G2", "C", "D", "E"}); +} + +TEST_CASE("Remove and restore macro subtrees", "[macro-group]") +{ + ClearMacros clear; + SetupMacros("A, G1[B, G2[C, D], E], F"); + + SECTION("Macro in nested group") + { + auto macro = GetMacro("C"); + const int index = indexOf("C"); + RemoveMacroSubtree(macro); + REQUIRE(DescribeMacros() == "A, G1[B, G2[D], E], F"); + checkGroupSizes(); + + InsertMacroSubtree(index, {macro}, GetMacro("G2")); + REQUIRE(DescribeMacros() == "A, G1[B, G2[C, D], E], F"); + checkGroupSizes(); + } + + SECTION("Nested group") + { + const std::deque> subtree = { + GetMacro("G2"), GetMacro("C"), GetMacro("D")}; + const int index = indexOf("G2"); + RemoveMacroSubtree(GetMacro("G2")); + REQUIRE(DescribeMacros() == "A, G1[B, E], F"); + checkGroupSizes(); + + InsertMacroSubtree(index, subtree, GetMacro("G1")); + REQUIRE(DescribeMacros() == "A, G1[B, G2[C, D], E], F"); + checkGroupSizes(); + } + + // Mirrors the group delete undo, which saves and loads the group and + // its contents + SECTION("Group from saved data") + { + auto group = GetMacro("G1"); + const int index = indexOf("G1"); + OBSDataArrayAutoRelease array = exportMacros({"G1"}); + RemoveMacroSubtree(group); + REQUIRE(DescribeMacros() == "A, F"); + + std::deque> subtree; + for (size_t i = 0; i < obs_data_array_count(array); i++) { + OBSDataAutoRelease obj = obs_data_array_item(array, i); + auto macro = std::make_shared(); + macro->Load(obj); + subtree.emplace_back(macro); + } + Macro::RestoreGroupHierarchy(subtree); + InsertMacroSubtree(index, subtree, nullptr); + REQUIRE(DescribeMacros() == "A, G1[B, G2[C, D], E], F"); + checkGroupSizes(); + } +} + +TEST_CASE("Ungroup and regroup", "[macro-group]") +{ + ClearMacros clear; + + SECTION("Nested group") + { + SetupMacros("A, G1[B, G2[C, D], E], F"); + const int index = indexOf("G2"); + auto children = GetGroupChildren(GetMacro("G2").get()); + Macro::RemoveGroup(GetMacro("G2")); + REQUIRE(DescribeMacros() == "A, G1[B, C, D, E], F"); + + InsertGroup("G2", index, children, GetMacro("G1")); + REQUIRE(DescribeMacros() == "A, G1[B, G2[C, D], E], F"); + checkGroupSizes(); + } + + SECTION("Group containing groups") + { + SetupMacros("A, G1[B, G2[C, G3[D]], E], F"); + const int index = indexOf("G1"); + auto children = GetGroupChildren(GetMacro("G1").get()); + Macro::RemoveGroup(GetMacro("G1")); + REQUIRE(DescribeMacros() == "A, B, G2[C, G3[D]], E, F"); + + InsertGroup("G1", index, children, nullptr); + REQUIRE(DescribeMacros() == "A, G1[B, G2[C, G3[D]], E], F"); + checkGroupSizes(); + } + + SECTION("Empty nested group") + { + SetupMacros("A, G1[G2[]]"); + const int index = indexOf("G2"); + std::vector> children; + Macro::RemoveGroup(GetMacro("G2")); + REQUIRE(DescribeMacros() == "A, G1[]"); + + InsertGroup("G2", index, children, GetMacro("G1")); + REQUIRE(DescribeMacros() == "A, G1[G2[]]"); + checkGroupSizes(); + } +} + +TEST_CASE("Export and import nested groups", "[macro-group]") +{ + ClearMacros clear; + SetupMacros("A, G1[B, G2{C, D}], E"); + OBSDataArrayAutoRelease array = exportMacros({"G1", "C"}); + const auto importAll = [](const std::shared_ptr &) { + return true; + }; + + SECTION("Into empty macro list") + { + GetTopLevelMacros().clear(); + const auto imported = LoadImportedMacros(array, importAll); + REQUIRE(names(imported) == Names{"G1", "B", "G2", "C", "D"}); + REQUIRE(DescribeMacros() == "G1[B, G2{C, D}]"); + checkGroupSizes(); + } + + // Mirrors AdvSceneSwitcher::ImportMacros() skipping macros for which + // the name conflict was not resolved + const auto renameOrSkip = [](const std::string &skip) { + return [skip](const std::shared_ptr ¯o) { + if (macro->Name() == skip) { + return false; + } + macro->SetName(macro->Name() + "x"); + return true; + }; + }; + + SECTION("Skip outer group") + { + LoadImportedMacros(array, renameOrSkip("G1")); + REQUIRE(DescribeMacros() == + "A, G1[B, G2{C, D}], E, Bx, G2x{Cx, Dx}"); + checkGroupSizes(); + } + + SECTION("Skip inner group") + { + LoadImportedMacros(array, renameOrSkip("G2")); + REQUIRE(DescribeMacros() == + "A, G1[B, G2{C, D}], E, G1x[Bx, Cx, Dx]"); + checkGroupSizes(); + } + + SECTION("Skip macro in nested group") + { + LoadImportedMacros(array, renameOrSkip("C")); + REQUIRE(DescribeMacros() == + "A, G1[B, G2{C, D}], E, G1x[Bx, G2x{Dx}]"); + checkGroupSizes(); + } +} + +TEST_CASE("Import single level groups", "[macro-group]") +{ + ClearMacros clear; + OBSDataAutoRelease data = obs_data_create_from_json(R"({"macros": [ + {"name": "G1", "group": true, + "groupData": {"size": 2, "collapsed": true}}, + {"name": "A"}, + {"name": "B"}, + {"name": "C"} + ]})"); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "macros"); + LoadImportedMacros(array, + [](const std::shared_ptr &) { return true; }); + REQUIRE(DescribeMacros() == "G1{A, B}, C"); + checkGroupSizes(); +}