diff --git a/lib/macro/macro-helpers.cpp b/lib/macro/macro-helpers.cpp index 57ca7729..75896808 100644 --- a/lib/macro/macro-helpers.cpp +++ b/lib/macro/macro-helpers.cpp @@ -3,6 +3,8 @@ #include "macro-action-macro.hpp" #include "plugin-state-helpers.hpp" +#include + namespace advss { static std::atomic_bool abortMacroWait = {false}; @@ -124,25 +126,58 @@ bool IsGroupMacro(Macro *macro) std::vector> GetGroupMacroEntries(Macro *macro) { - if (!macro || !macro->IsGroup()) { + auto entries = GetGroupDescendants(macro); + entries.erase(std::remove_if(entries.begin(), entries.end(), + [](const std::shared_ptr &entry) { + return entry->IsGroup(); + }), + entries.end()); + return entries; +} + +std::vector> GetGroupChildren(const Macro *group) +{ + if (!group || !group->IsGroup()) { return {}; } - std::vector> entries; - entries.reserve(macro->GroupSize()); + std::vector> children; + for (const auto ¯o : GetTopLevelMacros()) { + if (macro->Parent().get() == group) { + children.emplace_back(macro); + } + } + return children; +} - const auto ¯os = 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> GetGroupDescendants(const Macro *group) +{ + if (!group || !group->IsGroup()) { + return {}; } - return entries; + std::vector> descendants; + descendants.reserve(group->GroupSize()); + for (const auto ¯o : 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() diff --git a/lib/macro/macro-helpers.hpp b/lib/macro/macro-helpers.hpp index ae7da46a..1a4aba5d 100644 --- a/lib/macro/macro-helpers.hpp +++ b/lib/macro/macro-helpers.hpp @@ -37,7 +37,11 @@ EXPORT std::optional>> GetMacroConditions(Macro *); EXPORT bool IsGroupMacro(Macro *); +// Returns all non-group macros within the group including nested groups 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); EXPORT std::condition_variable &GetMacroWaitCV(); EXPORT std::condition_variable &GetMacroTransitionCV(); diff --git a/lib/macro/macro-tab.cpp b/lib/macro/macro-tab.cpp index 353dfe40..5399cb3f 100644 --- a/lib/macro/macro-tab.cpp +++ b/lib/macro/macro-tab.cpp @@ -155,7 +155,7 @@ void AdvSceneSwitcher::on_macroAdd_clicked() static void addGroupSubitems(std::vector> ¯os, const std::shared_ptr &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) { diff --git a/lib/macro/macro-undo-redo.cpp b/lib/macro/macro-undo-redo.cpp index 672ae320..7686d560 100644 --- a/lib/macro/macro-undo-redo.cpp +++ b/lib/macro/macro-undo-redo.cpp @@ -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(); diff --git a/lib/macro/macro.cpp b/lib/macro/macro.cpp index d5f60320..415dedc4 100644 --- a/lib/macro/macro.cpp +++ b/lib/macro/macro.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include #undef max @@ -62,11 +63,23 @@ Macro::CreateGroup(const std::string &name, { auto group = std::make_shared(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 &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 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 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 group, int count) +{ + for (; group; group = group->Parent()) { + assert((int)group->_groupSize + count >= 0); + group->_groupSize += count; + } +} + +void Macro::RestoreGroupHierarchy( + const std::deque> ¯os) +{ + struct OpenGroup { + std::shared_ptr group; + uint32_t remaining; + }; + std::vector openGroups; + + for (size_t i = 0; i < macros.size(); i++) { + const auto ¯o = 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 ¯o : macros) { + macro->_groupSize = 0; + } + for (const auto ¯o : 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::max()) { @@ -1250,42 +1319,10 @@ void LoadMacros(obs_data_t *obj) } obs_data_array_release(macroArray); - int groupCount = 0; - std::shared_ptr group; - std::vector> 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() diff --git a/lib/macro/macro.hpp b/lib/macro/macro.hpp index ead1aa95..cd15f6dd 100644 --- a/lib/macro/macro.hpp +++ b/lib/macro/macro.hpp @@ -121,7 +121,10 @@ public: std::shared_ptr item); static void PrepareMoveToGroup(std::shared_ptr group, std::shared_ptr item); + static void + RestoreGroupHierarchy(const std::deque> &); 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> _actions; std::deque> _elseActions; + static void AddToGroupSizes(std::shared_ptr group, int count); + std::weak_ptr _parent; Macro *_nestedParentMacro = nullptr; bool _isNested = false; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8340b14b..b339839c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -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 diff --git a/tests/macro-test-helpers.cpp b/tests/macro-test-helpers.cpp new file mode 100644 index 00000000..8690a110 --- /dev/null +++ b/tests/macro-test-helpers.cpp @@ -0,0 +1,122 @@ +#include "macro-test-helpers.hpp" + +#include +#include + +#include +#include +#include + +namespace advss::test { + +namespace { + +class LayoutParser { +public: + explicit LayoutParser(const std::string &layout) : _text(layout) {} + + std::deque> Parse() + { + std::deque> result; + ParseList(result); + return result; + } + +private: + std::vector> + ParseList(std::deque> &out) + { + std::vector> 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 + ParseItem(std::deque> &out) + { + std::string name; + while (_pos < _text.size() && + std::isalnum(static_cast(_text[_pos]))) { + name += _text[_pos++]; + } + + if (_pos >= _text.size() || + (_text[_pos] != '[' && _text[_pos] != '{')) { + auto macro = std::make_shared(name); + out.emplace_back(macro); + return macro; + } + + const bool collapsed = _text[_pos] == '{'; + _pos++; + std::deque> 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 ¯o : 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 GetMacro(const std::string &name) +{ + return GetWeakMacroByName(name.c_str()).lock(); +} + +} // namespace advss::test diff --git a/tests/macro-test-helpers.hpp b/tests/macro-test-helpers.hpp new file mode 100644 index 00000000..e1587e17 --- /dev/null +++ b/tests/macro-test-helpers.hpp @@ -0,0 +1,21 @@ +#pragma once +#include +#include + +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 GetMacro(const std::string &name); + +} // namespace advss::test diff --git a/tests/test-macro-group.cpp b/tests/test-macro-group.cpp new file mode 100644 index 00000000..604a742f --- /dev/null +++ b/tests/test-macro-group.cpp @@ -0,0 +1,263 @@ +#include "catch.hpp" +#include "macro-test-helpers.hpp" + +#include +#include + +#include + +namespace { + +using namespace advss; +using namespace advss::test; + +using Names = std::vector; + +Names names(const std::vector> ¯os) +{ + Names result; + for (const auto ¯o : 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> 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)); +} diff --git a/tests/test-macro-tree.cpp b/tests/test-macro-tree.cpp index 736f59d1..8ecef1ec 100644 --- a/tests/test-macro-tree.cpp +++ b/tests/test-macro-tree.cpp @@ -1,4 +1,5 @@ #include "catch.hpp" +#include "macro-test-helpers.hpp" #include "ui-test-helpers.hpp" #include @@ -16,7 +17,6 @@ #include #include -#include #include namespace { @@ -28,109 +28,6 @@ using Names = std::vector; 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> Parse() - { - std::deque> result; - ParseList(result); - return result; - } - -private: - std::vector> - ParseList(std::deque> &out) - { - std::vector> 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 - ParseItem(std::deque> &out) - { - std::string name; - while (_pos < _text.size() && - std::isalnum(static_cast(_text[_pos]))) { - name += _text[_pos++]; - } - - if (_pos >= _text.size() || - (_text[_pos] != '[' && _text[_pos] != '{')) { - auto macro = std::make_shared(name); - out.emplace_back(macro); - return macro; - } - - const bool collapsed = _text[_pos] == '{'; - _pos++; - std::deque> 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 ¯o : 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 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 ¯os = 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 openGroups; for (const auto ¯o : 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(), [¯o](const std::shared_ptr &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"});