Support nested macro groups in undo / redo and import / export

This commit is contained in:
WarmUpTill
2026-10-22 18:50:36 +02:00
committed by WarmUpTill
parent 51df608edf
commit 1171714aa9
7 changed files with 503 additions and 202 deletions

View File

@@ -180,6 +180,114 @@ bool IsMacroInGroup(const Macro *macro, const Macro *group)
return false;
}
std::vector<std::shared_ptr<Macro>>
GetOutermostMacros(const std::vector<std::shared_ptr<Macro>> &macros)
{
std::vector<std::shared_ptr<Macro>> result;
for (const auto &macro : GetTopLevelMacros()) {
const bool isGiven = std::find(macros.begin(), macros.end(),
macro) != macros.end();
const bool isInGivenGroup = std::any_of(
macros.begin(), macros.end(),
[&macro](const std::shared_ptr<Macro> &group) {
return IsMacroInGroup(macro.get(), group.get());
});
if (isGiven && !isInGivenGroup) {
result.emplace_back(macro);
}
}
return result;
}
std::vector<std::shared_ptr<Macro>>
GetMacrosWithGroupContents(const std::vector<std::shared_ptr<Macro>> &macros)
{
std::vector<std::shared_ptr<Macro>> result;
for (const auto &macro : GetTopLevelMacros()) {
const bool include = std::any_of(
macros.begin(), macros.end(),
[&macro](const std::shared_ptr<Macro> &given) {
return given == macro ||
IsMacroInGroup(macro.get(), given.get());
});
if (include) {
result.emplace_back(macro);
}
}
return result;
}
static std::deque<std::shared_ptr<Macro>>::iterator
subtreeEnd(std::deque<std::shared_ptr<Macro>> &macros,
std::deque<std::shared_ptr<Macro>>::iterator it)
{
const auto remaining = std::distance(it, macros.end());
const auto size =
std::min<std::ptrdiff_t>(1 + (*it)->GroupSize(), remaining);
return std::next(it, size);
}
void InsertMacroSubtree(int index,
const std::deque<std::shared_ptr<Macro>> &subtree,
const std::shared_ptr<Macro> &parent)
{
if (subtree.empty()) {
return;
}
auto &macros = 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<Macro> &macro)
{
auto &macros = 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<Macro>
InsertGroup(const std::string &name, int index,
std::vector<std::shared_ptr<Macro>> &children,
const std::shared_ptr<Macro> &parent)
{
auto group = Macro::CreateGroup(name, children);
if (children.empty()) {
Macro::PrepareMoveToGroup(parent, group);
}
auto &macros = 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<std::shared_ptr<Macro>> 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;

View File

@@ -7,11 +7,14 @@
#include <chrono>
#include <condition_variable>
#include <deque>
#include <functional>
#include <optional>
#include <thread>
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<std::shared_ptr<Macro>> GetGroupMacroEntries(Macro *);
std::vector<std::shared_ptr<Macro>> GetGroupChildren(const Macro *group);
std::vector<std::shared_ptr<Macro>> 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<std::shared_ptr<Macro>>
GetOutermostMacros(const std::vector<std::shared_ptr<Macro>> &macros);
// Returns the given macros and the contents of the given groups, sorted by
// their position in the macro list
std::vector<std::shared_ptr<Macro>>
GetMacrosWithGroupContents(const std::vector<std::shared_ptr<Macro>> &macros);
// Inserts the macro and its group contents at the given position of the macro
// list
void InsertMacroSubtree(int index,
const std::deque<std::shared_ptr<Macro>> &subtree,
const std::shared_ptr<Macro> &parent);
// Removes the macro and its group contents from the macro list
void RemoveMacroSubtree(const std::shared_ptr<Macro> &macro);
// 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<Macro>
InsertGroup(const std::string &name, int index,
std::vector<std::shared_ptr<Macro>> &children,
const std::shared_ptr<Macro> &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<std::shared_ptr<Macro>> LoadImportedMacros(
obs_data_array_t *macros,
const std::function<bool(const std::shared_ptr<Macro> &)> &shouldImport);
EXPORT void InvalidateMacroTempVarValues();
EXPORT void ResetMacroConditionTimers(Macro *);

View File

@@ -152,29 +152,6 @@ void AdvSceneSwitcher::on_macroAdd_clicked()
RegisterMacroAddUndoRedo(name);
}
static void addGroupSubitems(std::vector<std::shared_ptr<Macro>> &macros,
const std::shared_ptr<Macro> &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<Macro> &macro)
{
if (!macro) {
@@ -196,14 +173,8 @@ void AdvSceneSwitcher::RemoveMacro(std::shared_ptr<Macro> &macro)
RegisterMacroRemoveUndoRedo(macro.get());
}
if (macro->IsGroup()) {
std::vector<std::shared_ptr<Macro>> 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<Macro> &macro)
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 &macro : 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<Macro> group;
std::vector<std::shared_ptr<Macro>> importedMacros;
auto &tempMacros = GetTemporaryMacros();
std::unique_lock<std::mutex> 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<Macro>();
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<Macro> &macro) {
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 &macro : 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);

View File

@@ -798,27 +798,7 @@ std::vector<std::shared_ptr<Macro>> MacroTree::GetCurrentMacros() const
std::vector<std::shared_ptr<Macro>> MacroTree::GetSelectedSubtreeRoots() const
{
auto selected = GetCurrentMacros();
std::vector<std::shared_ptr<Macro>> roots;
for (const auto &macro : selected) {
const bool hasSelectedAncestor = std::any_of(
selected.begin(), selected.end(),
[&macro](const std::shared_ptr<Macro> &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<Macro> &a,
const std::shared_ptr<Macro> &b) {
return treeModel->PositionOf(a.get()) <
treeModel->PositionOf(b.get());
});
return roots;
return GetOutermostMacros(GetCurrentMacros());
}
bool MacroTree::GroupsSelected() const

View File

@@ -143,11 +143,7 @@ static void deleteMacroByName(const char *jsonData)
QEvent::DeferredDelete);
} else {
auto lock = LockContext();
auto &macros = 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 &macros = 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 &macros = 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<Macro>();
group->Load(groupData);
std::vector<std::shared_ptr<Macro>> 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 &macros = 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<std::shared_ptr<Macro>> 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 &macros = 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 &macros = 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 &macros = 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);

View File

@@ -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 &macros = 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<Macro>());
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<std::shared_ptr<Macro>> LoadImportedMacros(
obs_data_array_t *array,
const std::function<bool(const std::shared_ptr<Macro> &)> &shouldImport)
{
std::deque<std::shared_ptr<Macro>> 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<Macro>();
tempMacros.emplace_back(macro);
macro->Load(obj);
RunAndClearPostLoadSteps();
loaded.emplace_back(macro);
}
tempMacros.clear();
Macro::RestoreGroupHierarchy(loaded);
std::vector<std::shared_ptr<Macro>> imported;
for (const auto &macro : 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 &macro : imported) {
macro->PostLoad();
}
RunAndClearPostLoadSteps();
return imported;
}
bool CheckMacros()
{
bool matchFound = false;

View File

@@ -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 &macros = GetTopLevelMacros();
for (const auto &macro : macros) {
if (!macro->IsGroup()) {
continue;
}
INFO("group " << macro->Name() << " in " << DescribeMacros());
const auto descendants = std::count_if(
macros.begin(), macros.end(),
[&macro](const std::shared_ptr<Macro> &m) {
return IsMacroInGroup(m.get(), macro.get());
});
REQUIRE(macro->GroupSize() == (uint32_t)descendants);
}
}
std::vector<std::shared_ptr<Macro>> macros(const Names &names)
{
std::vector<std::shared_ptr<Macro>> 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 &macro :
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<std::shared_ptr<Macro>> 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<std::shared_ptr<Macro>> 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>();
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<std::shared_ptr<Macro>> 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<Macro> &) {
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<Macro> &macro) {
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<Macro> &) { return true; });
REQUIRE(DescribeMacros() == "G1{A, B}, C");
checkGroupSizes();
}