diff --git a/CMakeLists.txt b/CMakeLists.txt index fff481f1..862e2a71 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -321,6 +321,8 @@ target_sources( lib/variables/variable-line-edit.hpp lib/variables/variable-number.hpp lib/variables/variable-number.tpp + lib/variables/variable-ref.cpp + lib/variables/variable-ref.hpp lib/variables/variable-spinbox.cpp lib/variables/variable-spinbox.hpp lib/variables/variable-string.cpp diff --git a/lib/variables/variable-ref.cpp b/lib/variables/variable-ref.cpp new file mode 100644 index 00000000..5173f87a --- /dev/null +++ b/lib/variables/variable-ref.cpp @@ -0,0 +1,78 @@ +#include "variable-ref.hpp" +#include "variable.hpp" + +namespace advss { + +thread_local static VariableRefCollector *activeCollector = nullptr; + +VariableRefCollector::VariableRefCollector() : _previous(activeCollector) +{ + activeCollector = this; +} + +VariableRefCollector::~VariableRefCollector() +{ + activeCollector = _previous; +} + +void VariableRefCollector::Add(const std::string &name, int count) +{ + _counts[name] += count; +} + +void ReportVariableRef(const std::weak_ptr &variable) +{ + if (!activeCollector) { + return; + } + + auto var = variable.lock(); + if (!var) { + return; + } + + activeCollector->Add(var->Name()); +} + +static int countOccurrences(const std::string &text, const std::string &pattern) +{ + int count = 0; + for (auto pos = text.find(pattern); pos != std::string::npos; + pos = text.find(pattern, pos + pattern.length())) { + count++; + } + return count; +} + +void ReportVariableRefsInText(const std::string &text) +{ + if (!activeCollector || text.find("${") == std::string::npos) { + return; + } + + for (const auto &variable : GetVariables()) { + const auto &name = variable->Name(); + const int count = countOccurrences(text, "${" + name + "}"); + if (count > 0) { + activeCollector->Add(name, count); + } + } +} + +void VariableRef::Save(obs_data_t *obj, const char *key) const +{ + auto var = _variable.lock(); + if (!var) { + return; + } + + obs_data_set_string(obj, key, var->Name().c_str()); + ReportVariableRef(_variable); +} + +void VariableRef::Load(obs_data_t *obj, const char *key) +{ + _variable = GetWeakVariableByName(obs_data_get_string(obj, key)); +} + +} // namespace advss diff --git a/lib/variables/variable-ref.hpp b/lib/variables/variable-ref.hpp new file mode 100644 index 00000000..7af53f47 --- /dev/null +++ b/lib/variables/variable-ref.hpp @@ -0,0 +1,59 @@ +#pragma once +#include "export-symbol-helper.hpp" + +#include +#include +#include +#include + +namespace advss { + +class Variable; + +// While an instance is alive, variable references saved on the current +// thread are counted by variable name. +class VariableRefCollector { +public: + EXPORT VariableRefCollector(); + EXPORT ~VariableRefCollector(); + VariableRefCollector(const VariableRefCollector &) = delete; + VariableRefCollector &operator=(const VariableRefCollector &) = delete; + + EXPORT void Add(const std::string &name, int count = 1); + const std::unordered_map &Counts() const + { + return _counts; + } + +private: + std::unordered_map _counts; + VariableRefCollector *_previous = nullptr; +}; + +EXPORT void ReportVariableRef(const std::weak_ptr &variable); +EXPORT void ReportVariableRefsInText(const std::string &text); + +class VariableRef { +public: + VariableRef() = default; + VariableRef(const std::weak_ptr &variable) + : _variable(variable) + { + } + VariableRef &operator=(const std::weak_ptr &variable) + { + _variable = variable; + return *this; + } + operator std::weak_ptr() const { return _variable; } + std::shared_ptr lock() const { return _variable.lock(); } + bool expired() const { return _variable.expired(); } + + EXPORT void Save(obs_data_t *obj, const char *key) const; + EXPORT void Load(obs_data_t *obj, const char *key); + +private: + std::weak_ptr _variable; +}; + +} // namespace advss diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e3d2fdcb..53d66bda 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -128,6 +128,7 @@ target_sources( ${ADVSS_SOURCE_DIR}/lib/utils/item-selection-helpers.cpp ${ADVSS_SOURCE_DIR}/lib/utils/name-dialog.cpp ${ADVSS_SOURCE_DIR}/lib/utils/resizing-text-edit.cpp + ${ADVSS_SOURCE_DIR}/lib/variables/variable-ref.cpp ${ADVSS_SOURCE_DIR}/lib/variables/variable.cpp) # --- macro / macro-condition --- # diff --git a/tests/test-variable.cpp b/tests/test-variable.cpp index 1030c9ef..10442cb2 100644 --- a/tests/test-variable.cpp +++ b/tests/test-variable.cpp @@ -1,6 +1,9 @@ #include "catch.hpp" +#include #include + +#include #include TEST_CASE("Variable", "[variable]") @@ -50,3 +53,70 @@ TEST_CASE("Variable", "[variable]") variable.SetValue(123); REQUIRE(*variable.GetSecondsSinceLastChange() > 0); } + +static std::shared_ptr addVariable(const std::string &name) +{ + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", name.c_str()); + auto variable = std::make_shared(); + variable->Load(data); + advss::GetVariables().emplace_back(variable); + return variable; +} + +TEST_CASE("VariableRef", "[variable]") +{ + auto var = addVariable("var"); + addVariable("var2"); + addVariable("Var"); + + SECTION("Save reports and writes name") + { + advss::VariableRefCollector collector; + advss::VariableRef ref(var); + OBSDataAutoRelease data = obs_data_create(); + ref.Save(data, "key"); + REQUIRE(std::string(obs_data_get_string(data, "key")) == "var"); + REQUIRE(collector.Counts().at("var") == 1); + + advss::VariableRef loaded; + loaded.Load(data, "key"); + REQUIRE(loaded.lock() == var); + } + + SECTION("Expired ref is not saved or reported") + { + advss::VariableRefCollector collector; + advss::VariableRef ref; + OBSDataAutoRelease data = obs_data_create(); + ref.Save(data, "key"); + REQUIRE_FALSE(obs_data_has_user_value(data, "key")); + REQUIRE(collector.Counts().empty()); + } + + SECTION("Text references") + { + advss::VariableRefCollector collector; + advss::ReportVariableRefsInText( + "${var} ${var2} ${var} var Var"); + const auto &counts = collector.Counts(); + REQUIRE(counts.at("var") == 2); + REQUIRE(counts.at("var2") == 1); + REQUIRE(counts.find("Var") == counts.end()); + } + + SECTION("Nested collectors") + { + advss::VariableRefCollector outer; + { + advss::VariableRefCollector inner; + advss::ReportVariableRef(var); + REQUIRE(inner.Counts().at("var") == 1); + } + REQUIRE(outer.Counts().empty()); + advss::ReportVariableRef(var); + REQUIRE(outer.Counts().at("var") == 1); + } + + advss::GetVariables().clear(); +}