Support nested macro groups in data model

This commit is contained in:
WarmUpTill
2026-10-21 21:12:43 +02:00
committed by WarmUpTill
parent 60e5475642
commit 1a542f8092
11 changed files with 639 additions and 252 deletions

View File

@@ -3,6 +3,8 @@
#include "macro-action-macro.hpp"
#include "plugin-state-helpers.hpp"
#include <algorithm>
namespace advss {
static std::atomic_bool abortMacroWait = {false};
@@ -124,25 +126,58 @@ bool IsGroupMacro(Macro *macro)
std::vector<std::shared_ptr<Macro>> GetGroupMacroEntries(Macro *macro)
{
if (!macro || !macro->IsGroup()) {
auto entries = GetGroupDescendants(macro);
entries.erase(std::remove_if(entries.begin(), entries.end(),
[](const std::shared_ptr<Macro> &entry) {
return entry->IsGroup();
}),
entries.end());
return entries;
}
std::vector<std::shared_ptr<Macro>> GetGroupChildren(const Macro *group)
{
if (!group || !group->IsGroup()) {
return {};
}
std::vector<std::shared_ptr<Macro>> entries;
entries.reserve(macro->GroupSize());
std::vector<std::shared_ptr<Macro>> children;
for (const auto &macro : GetTopLevelMacros()) {
if (macro->Parent().get() == group) {
children.emplace_back(macro);
}
}
return children;
}
const auto &macros = GetTopLevelMacros();
for (auto it = macros.begin(); it < macros.end(); it++) {
if ((*it)->Name() != macro->Name()) {
continue;
}
for (uint32_t i = 1; i <= macro->GroupSize(); i++) {
entries.emplace_back(*std::next(it, i));
}
break;
std::vector<std::shared_ptr<Macro>> GetGroupDescendants(const Macro *group)
{
if (!group || !group->IsGroup()) {
return {};
}
return entries;
std::vector<std::shared_ptr<Macro>> descendants;
descendants.reserve(group->GroupSize());
for (const auto &macro : GetTopLevelMacros()) {
if (IsMacroInGroup(macro.get(), group)) {
descendants.emplace_back(macro);
}
}
return descendants;
}
bool IsMacroInGroup(const Macro *macro, const Macro *group)
{
if (!macro || !group) {
return false;
}
for (auto parent = macro->Parent(); parent; parent = parent->Parent()) {
if (parent.get() == group) {
return true;
}
}
return false;
}
std::condition_variable &GetMacroWaitCV()

View File

@@ -37,7 +37,11 @@ EXPORT std::optional<std::deque<std::shared_ptr<MacroCondition>>>
GetMacroConditions(Macro *);
EXPORT bool IsGroupMacro(Macro *);
// Returns all non-group macros within the group including nested groups
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);
EXPORT std::condition_variable &GetMacroWaitCV();
EXPORT std::condition_variable &GetMacroTransitionCV();

View File

@@ -155,7 +155,7 @@ void AdvSceneSwitcher::on_macroAdd_clicked()
static void addGroupSubitems(std::vector<std::shared_ptr<Macro>> &macros,
const std::shared_ptr<Macro> &group)
{
auto subitems = GetGroupMacroEntries(group.get());
auto subitems = GetGroupDescendants(group.get());
// Remove subitems which were already selected to avoid duplicates
for (const auto &subitem : subitems) {

View File

@@ -222,7 +222,7 @@ static void deleteGroupWithChildrenFromData(const char *jsonData)
return;
}
const auto children = GetGroupMacroEntries(group.get());
const auto children = GetGroupChildren(group.get());
auto *const window = AdvSceneSwitcher::window;
if (SettingsWindowIsOpened() && window) {
@@ -1113,7 +1113,7 @@ void RegisterGroupCreateUndoRedo(const std::string &groupName)
return;
}
const auto children = GetGroupMacroEntries(group.get());
const auto children = GetGroupChildren(group.get());
OBSDataArrayAutoRelease childArray = buildChildArray(children);
OBSDataAutoRelease undoData = obs_data_create();
@@ -1157,7 +1157,7 @@ void RegisterGroupRemoveUndoRedo(const std::string &groupName)
return;
}
const auto children = GetGroupMacroEntries(group.get());
const auto children = GetGroupChildren(group.get());
OBSDataArrayAutoRelease childArray = buildChildArray(children);
OBSDataAutoRelease undoData = obs_data_create();
@@ -1205,7 +1205,7 @@ void RegisterGroupDeleteUndoRedo(Macro *macro)
OBSDataAutoRelease groupData = obs_data_create();
macro->Save(groupData);
const auto children = GetGroupMacroEntries(macro);
const auto children = GetGroupChildren(macro);
OBSDataArrayAutoRelease childArray = obs_data_array_create();
for (const auto &child : children) {
OBSDataAutoRelease childData = obs_data_create();

View File

@@ -14,6 +14,7 @@
#include <QAction>
#include <QMainWindow>
#include <algorithm>
#include <chrono>
#include <limits>
#undef max
@@ -62,11 +63,23 @@ Macro::CreateGroup(const std::string &name,
{
auto group = std::make_shared<Macro>(name);
group->_registerHotkeys = false;
for (auto &c : children) {
c->SetParent(group);
}
group->_isGroup = true;
group->_groupSize = children.size();
if (!children.empty()) {
const auto parent = children.front()->Parent();
const bool haveSameParent = std::all_of(
children.begin(), children.end(),
[&parent](const std::shared_ptr<Macro> &child) {
return child->Parent() == parent;
});
if (haveSameParent) {
PrepareMoveToGroup(parent, group);
}
}
for (auto &child : children) {
PrepareMoveToGroup(group, child);
}
return group;
}
@@ -78,11 +91,11 @@ void Macro::RemoveGroup(std::shared_ptr<Macro> group)
return;
}
auto size = group->GroupSize();
for (uint32_t i = 1; i <= size; i++) {
auto m = std::next(it, i);
(*m)->SetParent(nullptr);
const auto parent = group->Parent();
for (const auto &child : GetGroupChildren(group.get())) {
child->SetParent(parent);
}
AddToGroupSizes(parent, -1);
macros.erase(it);
}
@@ -106,15 +119,72 @@ void Macro::PrepareMoveToGroup(std::shared_ptr<Macro> group,
return;
}
// Potentially remove from old group
auto oldGroup = item->Parent();
if (oldGroup) {
oldGroup->_groupSize--;
if (group &&
(group == item || IsMacroInGroup(group.get(), item.get()))) {
blog(LOG_WARNING, "cannot move \"%s\" into itself",
item->Name().c_str());
return;
}
const int count = 1 + (int)item->_groupSize;
AddToGroupSizes(item->Parent(), -count);
item->SetParent(group);
if (group) {
group->_groupSize++;
AddToGroupSizes(group, count);
}
void Macro::AddToGroupSizes(std::shared_ptr<Macro> group, int count)
{
for (; group; group = group->Parent()) {
assert((int)group->_groupSize + count >= 0);
group->_groupSize += count;
}
}
void Macro::RestoreGroupHierarchy(
const std::deque<std::shared_ptr<Macro>> &macros)
{
struct OpenGroup {
std::shared_ptr<Macro> group;
uint32_t remaining;
};
std::vector<OpenGroup> openGroups;
for (size_t i = 0; i < macros.size(); i++) {
const auto &macro = macros[i];
while (!openGroups.empty() &&
openGroups.back().remaining == 0) {
openGroups.pop_back();
}
macro->SetParent(openGroups.empty() ? nullptr
: openGroups.back().group);
for (auto &openGroup : openGroups) {
openGroup.remaining--;
}
if (!macro->IsGroup()) {
continue;
}
const auto maxSize = openGroups.empty()
? (uint32_t)(macros.size() - i - 1)
: openGroups.back().remaining;
auto size = macro->_groupSize;
if (size > maxSize) {
blog(LOG_WARNING,
"invalid group size detected for \"%s\" - "
"reducing size from %u to %u",
macro->Name().c_str(), size, maxSize);
size = maxSize;
}
openGroups.push_back({macro, size});
}
for (const auto &macro : macros) {
macro->_groupSize = 0;
}
for (const auto &macro : macros) {
AddToGroupSizes(macro->Parent(), 1);
}
}
@@ -339,8 +409,7 @@ bool Macro::PerformActions(bool match, bool forceParallel, bool ignorePause)
}
_lastExecutionTime = std::chrono::high_resolution_clock::now();
auto group = _parent.lock();
if (group) {
for (auto group = _parent.lock(); group; group = group->Parent()) {
group->_lastExecutionTime = _lastExecutionTime;
}
if (_runCount != std::numeric_limits<int>::max()) {
@@ -1250,42 +1319,10 @@ void LoadMacros(obs_data_t *obj)
}
obs_data_array_release(macroArray);
int groupCount = 0;
std::shared_ptr<Macro> group;
std::vector<std::shared_ptr<Macro>> invalidGroups;
Macro::RestoreGroupHierarchy(macros);
for (const auto &m : macros) {
if (groupCount && m->IsGroup()) {
blog(LOG_ERROR,
"nested group detected - will delete \"%s\"",
m->Name().c_str());
invalidGroups.emplace_back(m);
continue;
}
if (groupCount) {
m->SetParent(group);
groupCount--;
}
if (m->IsGroup()) {
groupCount = m->GroupSize();
group = m;
}
m->PostLoad();
}
if (groupCount) {
blog(LOG_ERROR,
"invalid group size detected - will delete \"%s\"",
group->Name().c_str());
invalidGroups.emplace_back(group);
}
for (auto &m : invalidGroups) {
auto it = std::find(macros.begin(), macros.end(), m);
if (it == macros.end()) {
continue;
}
macros.erase(it);
}
}
bool CheckMacros()

View File

@@ -121,7 +121,10 @@ public:
std::shared_ptr<Macro> item);
static void PrepareMoveToGroup(std::shared_ptr<Macro> group,
std::shared_ptr<Macro> item);
static void
RestoreGroupHierarchy(const std::deque<std::shared_ptr<Macro>> &);
bool IsGroup() const { return _isGroup; }
// Number of all macros and groups nested within this group
uint32_t GroupSize() const { return _groupSize; }
void ResetGroupSize() { _groupSize = 0; }
bool IsSubitem() const { return !_parent.expired(); }
@@ -191,6 +194,8 @@ private:
std::deque<std::shared_ptr<MacroAction>> _actions;
std::deque<std::shared_ptr<MacroAction>> _elseActions;
static void AddToGroupSizes(std::shared_ptr<Macro> group, int count);
std::weak_ptr<Macro> _parent;
Macro *_nestedParentMacro = nullptr;
bool _isNested = false;

View File

@@ -137,6 +137,9 @@ target_sources(
${PROJECT_NAME}
PRIVATE test-macro.cpp
test-macro-condition.cpp
test-macro-group.cpp
macro-test-helpers.cpp
macro-test-helpers.hpp
stubs/path-helpers.cpp
stubs/ui-helpers.cpp
stubs/plugin-state-helpers.cpp

View File

@@ -0,0 +1,122 @@
#include "macro-test-helpers.hpp"
#include <macro.hpp>
#include <macro-helpers.hpp>
#include <cctype>
#include <deque>
#include <vector>
namespace advss::test {
namespace {
class LayoutParser {
public:
explicit LayoutParser(const std::string &layout) : _text(layout) {}
std::deque<std::shared_ptr<Macro>> Parse()
{
std::deque<std::shared_ptr<Macro>> result;
ParseList(result);
return result;
}
private:
std::vector<std::shared_ptr<Macro>>
ParseList(std::deque<std::shared_ptr<Macro>> &out)
{
std::vector<std::shared_ptr<Macro>> items;
SkipSpaces();
while (_pos < _text.size() && _text[_pos] != ']' &&
_text[_pos] != '}') {
items.emplace_back(ParseItem(out));
SkipSpaces();
if (_pos < _text.size() && _text[_pos] == ',') {
_pos++;
}
SkipSpaces();
}
return items;
}
std::shared_ptr<Macro>
ParseItem(std::deque<std::shared_ptr<Macro>> &out)
{
std::string name;
while (_pos < _text.size() &&
std::isalnum(static_cast<unsigned char>(_text[_pos]))) {
name += _text[_pos++];
}
if (_pos >= _text.size() ||
(_text[_pos] != '[' && _text[_pos] != '{')) {
auto macro = std::make_shared<Macro>(name);
out.emplace_back(macro);
return macro;
}
const bool collapsed = _text[_pos] == '{';
_pos++;
std::deque<std::shared_ptr<Macro>> descendants;
auto children = ParseList(descendants);
_pos++;
auto group = Macro::CreateGroup(name, children);
group->SetCollapsed(collapsed);
out.emplace_back(group);
out.insert(out.end(), descendants.begin(), descendants.end());
return group;
}
void SkipSpaces()
{
while (_pos < _text.size() && _text[_pos] == ' ') {
_pos++;
}
}
std::string _text;
size_t _pos = 0;
};
std::string describeChildren(const Macro *parent)
{
std::string result;
for (const auto &macro : GetTopLevelMacros()) {
if (macro->Parent().get() != parent) {
continue;
}
if (!result.empty()) {
result += ", ";
}
result += macro->Name();
if (!macro->IsGroup()) {
continue;
}
const bool collapsed = macro->IsCollapsed();
result += collapsed ? "{" : "[";
result += describeChildren(macro.get());
result += collapsed ? "}" : "]";
}
return result;
}
} // namespace
void SetupMacros(const std::string &layout)
{
GetTopLevelMacros() = LayoutParser(layout).Parse();
}
std::string DescribeMacros()
{
return describeChildren(nullptr);
}
std::shared_ptr<Macro> GetMacro(const std::string &name)
{
return GetWeakMacroByName(name.c_str()).lock();
}
} // namespace advss::test

View File

@@ -0,0 +1,21 @@
#pragma once
#include <memory>
#include <string>
namespace advss {
class Macro;
}
namespace advss::test {
// Replaces all macros with the given layout.
// Layout syntax: "A, G1[B, C], G2{D}" where [] is an expanded and {} a
// collapsed group.
void SetupMacros(const std::string &layout);
// Describes the current macros using the same syntax as SetupMacros()
std::string DescribeMacros();
std::shared_ptr<Macro> GetMacro(const std::string &name);
} // namespace advss::test

263
tests/test-macro-group.cpp Normal file
View File

@@ -0,0 +1,263 @@
#include "catch.hpp"
#include "macro-test-helpers.hpp"
#include <macro.hpp>
#include <macro-helpers.hpp>
#include <obs.hpp>
namespace {
using namespace advss;
using namespace advss::test;
using Names = std::vector<std::string>;
Names names(const std::vector<std::shared_ptr<Macro>> &macros)
{
Names result;
for (const auto &macro : macros) {
result.emplace_back(macro->Name());
}
return result;
}
uint32_t groupSize(const std::string &name)
{
auto group = GetMacro(name);
REQUIRE(group);
REQUIRE(group->IsGroup());
return group->GroupSize();
}
void loadFromJson(const char *json)
{
OBSDataAutoRelease data = obs_data_create_from_json(json);
REQUIRE(data);
LoadMacros(data);
}
void saveAndLoad()
{
OBSDataAutoRelease data = obs_data_create();
SaveMacros(data);
GetTopLevelMacros().clear();
LoadMacros(data);
}
struct ClearMacros {
~ClearMacros() { GetTopLevelMacros().clear(); }
};
} // namespace
TEST_CASE("Nested group sizes", "[macro-group]")
{
ClearMacros clear;
SetupMacros("A, G1[B, G2[C, D], E], F");
REQUIRE(DescribeMacros() == "A, G1[B, G2[C, D], E], F");
REQUIRE(groupSize("G1") == 5);
REQUIRE(groupSize("G2") == 2);
REQUIRE(GetMacro("C")->Parent() == GetMacro("G2"));
REQUIRE(GetMacro("G2")->Parent() == GetMacro("G1"));
REQUIRE_FALSE(GetMacro("A")->Parent());
}
TEST_CASE("Group helpers", "[macro-group]")
{
ClearMacros clear;
SetupMacros("A, G1[B, G2[C, D], E, G3[]], F");
auto g1 = GetMacro("G1").get();
auto g2 = GetMacro("G2").get();
REQUIRE(names(GetGroupChildren(g1)) == Names{"B", "G2", "E", "G3"});
REQUIRE(names(GetGroupDescendants(g1)) ==
Names{"B", "G2", "C", "D", "E", "G3"});
REQUIRE(names(GetGroupMacroEntries(g1)) == Names{"B", "C", "D", "E"});
REQUIRE(names(GetGroupMacroEntries(g2)) == Names{"C", "D"});
REQUIRE(GetGroupMacroEntries(GetMacro("G3").get()).empty());
REQUIRE(GetGroupChildren(GetMacro("A").get()).empty());
REQUIRE(IsMacroInGroup(GetMacro("C").get(), g1));
REQUIRE(IsMacroInGroup(GetMacro("C").get(), g2));
REQUIRE_FALSE(IsMacroInGroup(GetMacro("B").get(), g2));
REQUIRE_FALSE(IsMacroInGroup(g1, g1));
REQUIRE_FALSE(IsMacroInGroup(GetMacro("A").get(), g1));
}
TEST_CASE("Move macros between nested groups", "[macro-group]")
{
ClearMacros clear;
SetupMacros("A, G1[B, G2[C, D], E], F");
SECTION("Group with children to top level")
{
Macro::PrepareMoveToGroup(nullptr, GetMacro("G2"));
REQUIRE(groupSize("G1") == 2);
REQUIRE(groupSize("G2") == 2);
REQUIRE_FALSE(GetMacro("G2")->Parent());
}
SECTION("Macro into nested group")
{
Macro::PrepareMoveToGroup(GetMacro("G2"), GetMacro("A"));
REQUIRE(groupSize("G1") == 6);
REQUIRE(groupSize("G2") == 3);
REQUIRE(GetMacro("A")->Parent() == GetMacro("G2"));
}
SECTION("Macro from nested group to outer group")
{
Macro::PrepareMoveToGroup(GetMacro("G1"), GetMacro("C"));
REQUIRE(groupSize("G1") == 5);
REQUIRE(groupSize("G2") == 1);
}
SECTION("Group into itself is rejected")
{
Macro::PrepareMoveToGroup(GetMacro("G1"), GetMacro("G1"));
REQUIRE_FALSE(GetMacro("G1")->Parent());
REQUIRE(groupSize("G1") == 5);
}
SECTION("Group into own descendant is rejected")
{
Macro::PrepareMoveToGroup(GetMacro("G2"), GetMacro("G1"));
REQUIRE_FALSE(GetMacro("G1")->Parent());
REQUIRE(groupSize("G1") == 5);
REQUIRE(groupSize("G2") == 2);
}
}
TEST_CASE("Create group from nested macros", "[macro-group]")
{
ClearMacros clear;
SetupMacros("A, G1[B, C, D]");
std::vector<std::shared_ptr<Macro>> children = {GetMacro("B"),
GetMacro("C")};
auto group = Macro::CreateGroup("G2", children);
REQUIRE(group->Parent() == GetMacro("G1"));
REQUIRE(group->GroupSize() == 2);
REQUIRE(groupSize("G1") == 4);
REQUIRE(GetMacro("B")->Parent() == group);
}
TEST_CASE("Remove nested group", "[macro-group]")
{
ClearMacros clear;
SetupMacros("A, G1[B, G2[C, D], E], F");
SECTION("Inner group")
{
Macro::RemoveGroup(GetMacro("G2"));
REQUIRE(DescribeMacros() == "A, G1[B, C, D, E], F");
REQUIRE(groupSize("G1") == 4);
}
SECTION("Outer group")
{
Macro::RemoveGroup(GetMacro("G1"));
REQUIRE(DescribeMacros() == "A, B, G2[C, D], E, F");
REQUIRE(groupSize("G2") == 2);
}
}
TEST_CASE("Save and load nested groups", "[macro-group]")
{
ClearMacros clear;
SetupMacros("A, G1{B, G2[C, G3{D}], E, G4[]}, F");
saveAndLoad();
REQUIRE(DescribeMacros() == "A, G1{B, G2[C, G3{D}], E, G4[]}, F");
REQUIRE(groupSize("G1") == 7);
REQUIRE(groupSize("G2") == 3);
REQUIRE(groupSize("G3") == 1);
REQUIRE(groupSize("G4") == 0);
}
TEST_CASE("Saved group size is number of descendants", "[macro-group]")
{
ClearMacros clear;
SetupMacros("G1[A, G2[B, C]]");
OBSDataAutoRelease data = obs_data_create();
SaveMacros(data);
OBSDataArrayAutoRelease array = obs_data_get_array(data, "macros");
REQUIRE(obs_data_array_count(array) == 5);
OBSDataAutoRelease g1 = obs_data_array_item(array, 0);
OBSDataAutoRelease g1Data = obs_data_get_obj(g1, "groupData");
REQUIRE(obs_data_get_int(g1Data, "size") == 4);
OBSDataAutoRelease g2 = obs_data_array_item(array, 2);
OBSDataAutoRelease g2Data = obs_data_get_obj(g2, "groupData");
REQUIRE(obs_data_get_int(g2Data, "size") == 2);
}
TEST_CASE("Load single level groups", "[macro-group]")
{
ClearMacros clear;
loadFromJson(R"({"macros": [
{"name": "A"},
{"name": "G1", "group": true,
"groupData": {"size": 2, "collapsed": true}},
{"name": "B"},
{"name": "C"},
{"name": "D"}
]})");
REQUIRE(DescribeMacros() == "A, G1{B, C}, D");
REQUIRE(groupSize("G1") == 2);
}
TEST_CASE("Load groups with invalid sizes", "[macro-group]")
{
ClearMacros clear;
SECTION("Group size exceeds number of macros")
{
loadFromJson(R"({"macros": [
{"name": "A"},
{"name": "G1", "group": true, "groupData": {"size": 5}},
{"name": "B"},
{"name": "C"}
]})");
REQUIRE(DescribeMacros() == "A, G1[B, C]");
REQUIRE(groupSize("G1") == 2);
}
SECTION("Nested group exceeds parent group")
{
loadFromJson(R"({"macros": [
{"name": "G1", "group": true, "groupData": {"size": 2}},
{"name": "G2", "group": true, "groupData": {"size": 3}},
{"name": "A"},
{"name": "B"},
{"name": "C"}
]})");
REQUIRE(DescribeMacros() == "G1[G2[A]], B, C");
REQUIRE(groupSize("G1") == 2);
REQUIRE(groupSize("G2") == 1);
}
SECTION("Empty group at the end")
{
loadFromJson(R"({"macros": [
{"name": "A"},
{"name": "G1", "group": true, "groupData": {"size": 1}}
]})");
REQUIRE(DescribeMacros() == "A, G1[]");
}
}
TEST_CASE("Execution time is propagated to all parent groups", "[macro-group]")
{
ClearMacros clear;
SetupMacros("G1[G2[A]], B");
const auto start = std::chrono::high_resolution_clock::now();
GetMacro("A")->PerformActions(true);
REQUIRE(GetMacro("G1")->WasExecutedSince(start));
REQUIRE(GetMacro("G2")->WasExecutedSince(start));
REQUIRE_FALSE(GetMacro("B")->WasExecutedSince(start));
}

View File

@@ -1,4 +1,5 @@
#include "catch.hpp"
#include "macro-test-helpers.hpp"
#include "ui-test-helpers.hpp"
#include <macro.hpp>
@@ -16,7 +17,6 @@
#include <QVBoxLayout>
#include <algorithm>
#include <cctype>
#include <memory>
namespace {
@@ -28,109 +28,6 @@ using Names = std::vector<std::string>;
const std::string newGroupName = "AdvSceneSwitcher.macroTab.defaultGroupName";
// Layout syntax: "A, G1[B, C], G2{D}" where [] is an expanded and {} a
// collapsed group
class LayoutParser {
public:
explicit LayoutParser(const std::string &layout) : _text(layout) {}
std::deque<std::shared_ptr<Macro>> Parse()
{
std::deque<std::shared_ptr<Macro>> result;
ParseList(result);
return result;
}
private:
std::vector<std::shared_ptr<Macro>>
ParseList(std::deque<std::shared_ptr<Macro>> &out)
{
std::vector<std::shared_ptr<Macro>> items;
SkipSpaces();
while (_pos < _text.size() && _text[_pos] != ']' &&
_text[_pos] != '}') {
items.emplace_back(ParseItem(out));
SkipSpaces();
if (_pos < _text.size() && _text[_pos] == ',') {
_pos++;
}
SkipSpaces();
}
return items;
}
std::shared_ptr<Macro>
ParseItem(std::deque<std::shared_ptr<Macro>> &out)
{
std::string name;
while (_pos < _text.size() &&
std::isalnum(static_cast<unsigned char>(_text[_pos]))) {
name += _text[_pos++];
}
if (_pos >= _text.size() ||
(_text[_pos] != '[' && _text[_pos] != '{')) {
auto macro = std::make_shared<Macro>(name);
out.emplace_back(macro);
return macro;
}
const bool collapsed = _text[_pos] == '{';
_pos++;
std::deque<std::shared_ptr<Macro>> descendants;
auto children = ParseList(descendants);
_pos++;
auto group = Macro::CreateGroup(name, children);
group->SetCollapsed(collapsed);
out.emplace_back(group);
out.insert(out.end(), descendants.begin(), descendants.end());
return group;
}
void SkipSpaces()
{
while (_pos < _text.size() && _text[_pos] == ' ') {
_pos++;
}
}
std::string _text;
size_t _pos = 0;
};
std::string describeChildren(const Macro *parent)
{
std::string result;
for (const auto &macro : GetTopLevelMacros()) {
if (macro->Parent().get() != parent) {
continue;
}
if (!result.empty()) {
result += ", ";
}
result += macro->Name();
if (!macro->IsGroup()) {
continue;
}
const bool collapsed = macro->IsCollapsed();
result += collapsed ? "{" : "[";
result += describeChildren(macro.get());
result += collapsed ? "}" : "]";
}
return result;
}
std::string describeMacros()
{
return describeChildren(nullptr);
}
std::shared_ptr<Macro> getMacro(const std::string &name)
{
return GetWeakMacroByName(name.c_str()).lock();
}
bool isHiddenByCollapsedAncestor(const Macro *macro)
{
for (auto parent = macro->Parent(); parent; parent = parent->Parent()) {
@@ -148,8 +45,7 @@ public:
_tree(new MacroTree(_window.get()))
{
GetMacroSearchSettings() = {};
auto &macros = GetTopLevelMacros();
macros = LayoutParser(layout).Parse();
SetupMacros(layout);
// Same settings as in forms/advanced-scene-switcher.ui
_tree->setDragEnabled(true);
@@ -394,7 +290,7 @@ private:
std::vector<Macro *> openGroups;
for (const auto &macro : macros) {
INFO("macro \"" << macro->Name() << "\" in "
<< describeMacros());
<< DescribeMacros());
const auto parent = macro->Parent().get();
while (!openGroups.empty() &&
openGroups.back() != parent) {
@@ -413,20 +309,21 @@ private:
continue;
}
INFO("group \"" << macro->Name() << "\" in "
<< describeMacros());
const auto children = std::count_if(
<< DescribeMacros());
const auto descendants = std::count_if(
macros.begin(), macros.end(),
[&macro](const std::shared_ptr<Macro> &m) {
return m->Parent() == macro;
return IsMacroInGroup(m.get(),
macro.get());
});
REQUIRE(macro->GroupSize() == (uint32_t)children);
REQUIRE(macro->GroupSize() == (uint32_t)descendants);
}
}
void CheckRowWidget(const std::string &name) const
{
INFO("row \"" << name << "\" in " << describeMacros());
auto macro = getMacro(name);
INFO("row \"" << name << "\" in " << DescribeMacros());
auto macro = GetMacro(name);
REQUIRE(macro);
auto widget = RowWidget(name);
REQUIRE(widget);
@@ -518,12 +415,12 @@ TEST_CASE("Macro tree expand and collapse", "[macro-tree]")
SECTION("Expand checkbox")
{
f.ClickCheckBox(f.ExpandCheckBox("G1"));
REQUIRE(getMacro("G1")->IsCollapsed());
REQUIRE(GetMacro("G1")->IsCollapsed());
f.CheckInvariants();
REQUIRE(f.VisibleRows() == Names{"A", "G1", "D", "G2"});
f.ClickCheckBox(f.ExpandCheckBox("G2"));
REQUIRE_FALSE(getMacro("G2")->IsCollapsed());
REQUIRE_FALSE(GetMacro("G2")->IsCollapsed());
f.CheckInvariants();
REQUIRE(f.VisibleRows() == Names{"A", "G1", "D", "G2", "E"});
@@ -536,11 +433,11 @@ TEST_CASE("Macro tree expand and collapse", "[macro-tree]")
SECTION("Double click")
{
DoubleClick(f.Viewport(), f.RowCenter("G2"));
REQUIRE_FALSE(getMacro("G2")->IsCollapsed());
REQUIRE_FALSE(GetMacro("G2")->IsCollapsed());
f.CheckInvariants();
DoubleClick(f.Viewport(), f.RowCenter("G1"));
REQUIRE(getMacro("G1")->IsCollapsed());
REQUIRE(GetMacro("G1")->IsCollapsed());
f.CheckInvariants();
}
@@ -573,43 +470,43 @@ TEST_CASE("Macro tree add", "[macro-tree]")
{
f.Tree()->Add(newMacro);
f.CheckInvariants();
REQUIRE(describeMacros() == "A, G1[B], C, G2{D}, X");
REQUIRE(DescribeMacros() == "A, G1[B], C, G2{D}, X");
REQUIRE(f.Tree()->GetCurrentMacro() == newMacro);
}
SECTION("After top level macro")
{
f.Tree()->Add(newMacro, getMacro("A"));
f.Tree()->Add(newMacro, GetMacro("A"));
f.CheckInvariants();
REQUIRE(describeMacros() == "A, X, G1[B], C, G2{D}");
REQUIRE(DescribeMacros() == "A, X, G1[B], C, G2{D}");
}
SECTION("After macro following collapsed group")
{
f.Tree()->Add(newMacro, getMacro("C"));
f.Tree()->Add(newMacro, GetMacro("C"));
f.CheckInvariants();
REQUIRE(describeMacros() == "A, G1[B], C, X, G2{D}");
REQUIRE(DescribeMacros() == "A, G1[B], C, X, G2{D}");
}
// Mirrors AdvSceneSwitcher::on_macroAdd_clicked() with a group selected
SECTION("Into group")
{
auto group = getMacro("G2");
auto group = GetMacro("G2");
f.Tree()->ExpandGroup(group);
Macro::PrepareMoveToGroup(group, newMacro);
f.Tree()->AddToGroup(newMacro, group);
f.CheckInvariants();
REQUIRE(describeMacros() == "A, G1[B], C, G2[X, D]");
REQUIRE(DescribeMacros() == "A, G1[B], C, G2[X, D]");
}
// Mirrors AdvSceneSwitcher::on_macroAdd_clicked() with a subitem
// selected
SECTION("After subitem")
{
Macro::PrepareMoveToGroup(getMacro("G1"), newMacro);
f.Tree()->Add(newMacro, getMacro("B"));
Macro::PrepareMoveToGroup(GetMacro("G1"), newMacro);
f.Tree()->Add(newMacro, GetMacro("B"));
f.CheckInvariants();
REQUIRE(describeMacros() == "A, G1[B, X], C, G2{D}");
REQUIRE(DescribeMacros() == "A, G1[B, X], C, G2{D}");
}
}
@@ -619,40 +516,40 @@ TEST_CASE("Macro tree remove", "[macro-tree]")
SECTION("Subitem")
{
f.Tree()->Remove(getMacro("B"));
f.Tree()->Remove(GetMacro("B"));
f.CheckInvariants();
REQUIRE(describeMacros() == "A, G1[C], D, G2{E, F}, H");
REQUIRE(DescribeMacros() == "A, G1[C], D, G2{E, F}, H");
f.Tree()->Remove(getMacro("C"));
f.Tree()->Remove(GetMacro("C"));
f.CheckInvariants();
REQUIRE(describeMacros() == "A, G1[], D, G2{E, F}, H");
REQUIRE(DescribeMacros() == "A, G1[], D, G2{E, F}, H");
}
SECTION("Expanded group")
{
f.Tree()->Remove(getMacro("G1"));
f.Tree()->Remove(GetMacro("G1"));
f.CheckInvariants();
REQUIRE(describeMacros() == "A, D, G2{E, F}, H");
REQUIRE(DescribeMacros() == "A, D, G2{E, F}, H");
}
SECTION("Collapsed group")
{
f.Tree()->Remove(getMacro("G2"));
f.Tree()->Remove(GetMacro("G2"));
f.CheckInvariants();
REQUIRE(describeMacros() == "A, G1[B, C], D, H");
REQUIRE(DescribeMacros() == "A, G1[B, C], D, H");
}
SECTION("Macro after collapsed group")
{
f.Tree()->Remove(getMacro("H"));
f.Tree()->Remove(GetMacro("H"));
f.CheckInvariants();
REQUIRE(describeMacros() == "A, G1[B, C], D, G2{E, F}");
REQUIRE(DescribeMacros() == "A, G1[B, C], D, G2{E, F}");
}
SECTION("Everything")
{
for (const auto &name : {"H", "A", "G2", "B", "G1", "D"}) {
f.Tree()->Remove(getMacro(name));
f.Tree()->Remove(GetMacro(name));
f.CheckInvariants();
}
REQUIRE(GetTopLevelMacros().empty());
@@ -663,7 +560,7 @@ TEST_CASE("Macro tree remove", "[macro-tree]")
f.ClickRow("D");
f.Tree()->Remove(f.Tree()->GetCurrentMacro());
f.CheckInvariants();
REQUIRE(describeMacros() == "A, G1[B, C], G2{E, F}, H");
REQUIRE(DescribeMacros() == "A, G1[B, C], G2{E, F}, H");
REQUIRE(f.Tree()->SelectionEmpty());
}
}
@@ -672,80 +569,80 @@ TEST_CASE("Macro tree up and down", "[macro-tree]")
{
TreeFixture f("A, B, G1[C, D], E, G2{F}, H");
auto up = [&](const char *name) {
f.Tree()->Up(getMacro(name));
f.Tree()->Up(GetMacro(name));
f.CheckInvariants();
};
auto down = [&](const char *name) {
f.Tree()->Down(getMacro(name));
f.Tree()->Down(GetMacro(name));
f.CheckInvariants();
};
SECTION("Top level")
{
up("B");
REQUIRE(describeMacros() == "B, A, G1[C, D], E, G2{F}, H");
REQUIRE(DescribeMacros() == "B, A, G1[C, D], E, G2{F}, H");
down("B");
REQUIRE(describeMacros() == "A, B, G1[C, D], E, G2{F}, H");
REQUIRE(DescribeMacros() == "A, B, G1[C, D], E, G2{F}, H");
}
SECTION("First and last")
{
up("A");
down("H");
REQUIRE(describeMacros() == "A, B, G1[C, D], E, G2{F}, H");
REQUIRE(DescribeMacros() == "A, B, G1[C, D], E, G2{F}, H");
}
SECTION("Within group")
{
up("D");
REQUIRE(describeMacros() == "A, B, G1[D, C], E, G2{F}, H");
REQUIRE(DescribeMacros() == "A, B, G1[D, C], E, G2{F}, H");
up("D");
REQUIRE(describeMacros() == "A, B, G1[D, C], E, G2{F}, H");
REQUIRE(DescribeMacros() == "A, B, G1[D, C], E, G2{F}, H");
down("D");
down("D");
REQUIRE(describeMacros() == "A, B, G1[C, D], E, G2{F}, H");
REQUIRE(DescribeMacros() == "A, B, G1[C, D], E, G2{F}, H");
}
SECTION("Macro past expanded group")
{
down("B");
REQUIRE(describeMacros() == "A, G1[C, D], B, E, G2{F}, H");
REQUIRE(DescribeMacros() == "A, G1[C, D], B, E, G2{F}, H");
up("E");
up("E");
REQUIRE(describeMacros() == "A, E, G1[C, D], B, G2{F}, H");
REQUIRE(DescribeMacros() == "A, E, G1[C, D], B, G2{F}, H");
}
SECTION("Macro past collapsed group")
{
down("E");
REQUIRE(describeMacros() == "A, B, G1[C, D], G2{F}, E, H");
REQUIRE(DescribeMacros() == "A, B, G1[C, D], G2{F}, E, H");
up("E");
up("H");
REQUIRE(describeMacros() == "A, B, G1[C, D], E, H, G2{F}");
REQUIRE(DescribeMacros() == "A, B, G1[C, D], E, H, G2{F}");
}
SECTION("Expanded group")
{
down("G1");
REQUIRE(describeMacros() == "A, B, E, G1[C, D], G2{F}, H");
REQUIRE(DescribeMacros() == "A, B, E, G1[C, D], G2{F}, H");
down("G1");
REQUIRE(describeMacros() == "A, B, E, G2{F}, G1[C, D], H");
REQUIRE(DescribeMacros() == "A, B, E, G2{F}, G1[C, D], H");
up("G1");
up("G1");
up("G1");
REQUIRE(describeMacros() == "A, G1[C, D], B, E, G2{F}, H");
REQUIRE(DescribeMacros() == "A, G1[C, D], B, E, G2{F}, H");
}
SECTION("Collapsed group")
{
up("G2");
REQUIRE(describeMacros() == "A, B, G1[C, D], G2{F}, E, H");
REQUIRE(DescribeMacros() == "A, B, G1[C, D], G2{F}, E, H");
up("G2");
REQUIRE(describeMacros() == "A, B, G2{F}, G1[C, D], E, H");
REQUIRE(DescribeMacros() == "A, B, G2{F}, G1[C, D], E, H");
down("G2");
down("G2");
down("G2");
REQUIRE(describeMacros() == "A, B, G1[C, D], E, H, G2{F}");
REQUIRE(DescribeMacros() == "A, B, G1[C, D], E, H, G2{F}");
}
}
@@ -757,7 +654,7 @@ TEST_CASE("Macro tree group and ungroup", "[macro-tree]")
f.SelectRows({"B", "C"});
f.Tree()->GroupSelectedItems();
f.CheckInvariants();
REQUIRE(describeMacros() == "A, " + newGroupName + "[B, C], D");
REQUIRE(DescribeMacros() == "A, " + newGroupName + "[B, C], D");
}
SECTION("Group non adjacent macros")
@@ -766,7 +663,7 @@ TEST_CASE("Macro tree group and ungroup", "[macro-tree]")
f.SelectRows({"D", "B"});
f.Tree()->GroupSelectedItems();
f.CheckInvariants();
REQUIRE(describeMacros() == "A, " + newGroupName + "[B, D], C");
REQUIRE(DescribeMacros() == "A, " + newGroupName + "[B, D], C");
}
SECTION("Group after collapsed group")
@@ -775,7 +672,7 @@ TEST_CASE("Macro tree group and ungroup", "[macro-tree]")
f.SelectRows({"B", "C"});
f.Tree()->GroupSelectedItems();
f.CheckInvariants();
REQUIRE(describeMacros() ==
REQUIRE(DescribeMacros() ==
"G1{X}, A, " + newGroupName + "[B, C]");
}
@@ -785,7 +682,7 @@ TEST_CASE("Macro tree group and ungroup", "[macro-tree]")
f.SelectRows({"G1"});
f.Tree()->UngroupSelectedGroups();
f.CheckInvariants();
REQUIRE(describeMacros() == "A, B, C, D");
REQUIRE(DescribeMacros() == "A, B, C, D");
}
SECTION("Ungroup collapsed group")
@@ -794,7 +691,7 @@ TEST_CASE("Macro tree group and ungroup", "[macro-tree]")
f.SelectRows({"G1"});
f.Tree()->UngroupSelectedGroups();
f.CheckInvariants();
REQUIRE(describeMacros() == "A, B, C, D");
REQUIRE(DescribeMacros() == "A, B, C, D");
REQUIRE(f.VisibleRows() == Names{"A", "B", "C", "D"});
}
@@ -804,7 +701,7 @@ TEST_CASE("Macro tree group and ungroup", "[macro-tree]")
f.SelectRows({"G1", "G2"});
f.Tree()->UngroupSelectedGroups();
f.CheckInvariants();
REQUIRE(describeMacros() == "A, B, C, D");
REQUIRE(DescribeMacros() == "A, B, C, D");
}
}
@@ -821,7 +718,7 @@ TEST_CASE("Macro tree drag and drop", "[macro-tree]")
f.IgnoreModelSignalErrors();
f.Drag(f.RowCenter("A"), f.RowBottom("C"));
f.CheckInvariants();
REQUIRE(describeMacros() == "B, C, A, D");
REQUIRE(DescribeMacros() == "B, C, A, D");
}
SECTION("Macro above macro")
@@ -830,7 +727,7 @@ TEST_CASE("Macro tree drag and drop", "[macro-tree]")
f.IgnoreModelSignalErrors();
f.Drag(f.RowCenter("D"), f.RowTop("A"));
f.CheckInvariants();
REQUIRE(describeMacros() == "D, A, B, C");
REQUIRE(DescribeMacros() == "D, A, B, C");
}
SECTION("Macro below last row")
@@ -840,7 +737,7 @@ TEST_CASE("Macro tree drag and drop", "[macro-tree]")
f.Drag(f.RowCenter("A"),
QPoint(40, f.Viewport()->height() - 5));
f.CheckInvariants();
REQUIRE(describeMacros() == "B, C, A");
REQUIRE(DescribeMacros() == "B, C, A");
}
SECTION("Macro onto expanded group")
@@ -849,7 +746,7 @@ TEST_CASE("Macro tree drag and drop", "[macro-tree]")
f.IgnoreModelSignalErrors();
f.Drag(f.RowCenter("A"), f.RowCenter("G1"));
f.CheckInvariants();
REQUIRE(describeMacros() == "G1[A, B, C], D");
REQUIRE(DescribeMacros() == "G1[A, B, C], D");
}
SECTION("Macro between subitems")
@@ -858,7 +755,7 @@ TEST_CASE("Macro tree drag and drop", "[macro-tree]")
f.IgnoreModelSignalErrors();
f.Drag(f.RowCenter("D"), f.RowTop("C"));
f.CheckInvariants();
REQUIRE(describeMacros() == "A, G1[B, D, C]");
REQUIRE(DescribeMacros() == "A, G1[B, D, C]");
}
SECTION("Subitem out of group")
@@ -867,7 +764,7 @@ TEST_CASE("Macro tree drag and drop", "[macro-tree]")
f.IgnoreModelSignalErrors();
f.Drag(f.RowCenter("B"), f.RowBottom("D"));
f.CheckInvariants();
REQUIRE(describeMacros() == "A, G1[C], D, B");
REQUIRE(DescribeMacros() == "A, G1[C], D, B");
}
SECTION("Subitem to top")
@@ -876,7 +773,7 @@ TEST_CASE("Macro tree drag and drop", "[macro-tree]")
f.IgnoreModelSignalErrors();
f.Drag(f.RowCenter("C"), f.RowTop("A"));
f.CheckInvariants();
REQUIRE(describeMacros() == "C, A, G1[B], D");
REQUIRE(DescribeMacros() == "C, A, G1[B], D");
}
SECTION("Macro past collapsed group")
@@ -885,7 +782,7 @@ TEST_CASE("Macro tree drag and drop", "[macro-tree]")
f.IgnoreModelSignalErrors();
f.Drag(f.RowCenter("D"), f.RowTop("A"));
f.CheckInvariants();
REQUIRE(describeMacros() == "D, A, G1{B, C}");
REQUIRE(DescribeMacros() == "D, A, G1{B, C}");
}
SECTION("Expanded group")
@@ -894,7 +791,7 @@ TEST_CASE("Macro tree drag and drop", "[macro-tree]")
f.IgnoreModelSignalErrors();
f.Drag(f.RowCenter("G1"), f.RowBottom("D"));
f.CheckInvariants();
REQUIRE(describeMacros() == "A, D, G1[B, C], E");
REQUIRE(DescribeMacros() == "A, D, G1[B, C], E");
}
SECTION("Collapsed group")
@@ -903,7 +800,7 @@ TEST_CASE("Macro tree drag and drop", "[macro-tree]")
f.IgnoreModelSignalErrors();
f.Drag(f.RowCenter("G1"), f.RowBottom("D"));
f.CheckInvariants();
REQUIRE(describeMacros() == "A, D, G1{B, C}, E");
REQUIRE(DescribeMacros() == "A, D, G1{B, C}, E");
}
SECTION("Group onto group is rejected")
@@ -912,7 +809,7 @@ TEST_CASE("Macro tree drag and drop", "[macro-tree]")
f.IgnoreModelSignalErrors();
f.Drag(f.RowCenter("G2"), f.RowCenter("G1"));
f.CheckInvariants();
REQUIRE(describeMacros() == "G1[A], G2[B]");
REQUIRE(DescribeMacros() == "G1[A], G2[B]");
}
SECTION("Multiple macros")
@@ -922,7 +819,7 @@ TEST_CASE("Macro tree drag and drop", "[macro-tree]")
f.SelectRows({"A", "C"});
f.Drag(f.RowCenter("C"), f.RowBottom("D"));
f.CheckInvariants();
REQUIRE(describeMacros() == "B, D, A, C");
REQUIRE(DescribeMacros() == "B, D, A, C");
}
}
@@ -934,7 +831,7 @@ TEST_CASE("Macro tree drop onto collapsed group", "[macro-tree][!shouldfail]")
f.IgnoreModelSignalErrors();
f.Drag(f.RowCenter("D"), f.RowCenter("G1"));
f.CheckInvariants();
REQUIRE(describeMacros() == "A, G1{D, B, C}");
REQUIRE(DescribeMacros() == "A, G1{D, B, C}");
}
TEST_CASE("Macro tree search", "[macro-tree]")
@@ -967,9 +864,9 @@ TEST_CASE("Macro tree search", "[macro-tree]")
TreeFixture f("A1, G1[B1, C2], D2, E2");
f.SetupSearch();
f.Search("2");
f.Tree()->Remove(getMacro("D2"));
f.Tree()->Remove(GetMacro("D2"));
f.CheckInvariants();
f.Tree()->Up(getMacro("E2"));
f.Tree()->Up(GetMacro("E2"));
f.CheckInvariants();
f.ClickCheckBox(f.ExpandCheckBox("G1"));
f.CheckInvariants();
@@ -991,9 +888,9 @@ TEST_CASE("Macro tree running state", "[macro-tree]")
REQUIRE(groupCheckBox->checkState() == Qt::Checked);
f.ClickCheckBox(groupCheckBox);
REQUIRE(getMacro("B")->Paused());
REQUIRE(getMacro("C")->Paused());
REQUIRE_FALSE(getMacro("A")->Paused());
REQUIRE(GetMacro("B")->Paused());
REQUIRE(GetMacro("C")->Paused());
REQUIRE_FALSE(GetMacro("A")->Paused());
REQUIRE(groupCheckBox->checkState() == Qt::Unchecked);
f.CheckInvariants();
}
@@ -1002,7 +899,7 @@ TEST_CASE("Macro tree running state", "[macro-tree]")
{
TreeFixture f("A, G1[B, C]");
f.ClickCheckBox(f.RunningCheckBox("B"));
REQUIRE(getMacro("B")->Paused());
REQUIRE(GetMacro("B")->Paused());
REQUIRE(f.RunningCheckBox("G1")->checkState() ==
Qt::PartiallyChecked);
f.CheckInvariants();
@@ -1011,7 +908,7 @@ TEST_CASE("Macro tree running state", "[macro-tree]")
SECTION("Update after backend change")
{
TreeFixture f("G0{X}, A, B");
getMacro("B")->SetPaused(true);
GetMacro("B")->SetPaused(true);
f.Tree()->UpdateRunningStates();
f.CheckInvariants();
}
@@ -1024,14 +921,14 @@ TEST_CASE("Macro tree running state after collapsed group",
{
TreeFixture f("G0{X}, A, G1[B, C]");
f.ClickCheckBox(f.RunningCheckBox("B"));
REQUIRE(getMacro("B")->Paused());
REQUIRE(GetMacro("B")->Paused());
REQUIRE(f.RunningCheckBox("G1")->checkState() == Qt::PartiallyChecked);
}
TEST_CASE("Macro tree rename", "[macro-tree]")
{
TreeFixture f("A, G1{B}, C");
getMacro("C")->SetName("Z");
GetMacro("C")->SetName("Z");
emit MacroSignalManager::Instance()->Rename("C", "Z");
f.CheckInvariants();
REQUIRE(f.VisibleRows() == Names{"A", "G1", "Z"});