From 67396435e51e5f772ead999937a8b0e1ee714a7a Mon Sep 17 00:00:00 2001 From: reuk Date: Tue, 15 Oct 2024 14:10:58 +0100 Subject: [PATCH] DynamicObject: Add virtual functions that are called to indicate a property change --- .../containers/juce_DynamicObject.cpp | 140 +++++++++++++++++- .../juce_core/containers/juce_DynamicObject.h | 21 +-- 2 files changed, 144 insertions(+), 17 deletions(-) diff --git a/modules/juce_core/containers/juce_DynamicObject.cpp b/modules/juce_core/containers/juce_DynamicObject.cpp index a948d7ea38..eb9d0c10ac 100644 --- a/modules/juce_core/containers/juce_DynamicObject.cpp +++ b/modules/juce_core/containers/juce_DynamicObject.cpp @@ -35,19 +35,13 @@ namespace juce { -DynamicObject::DynamicObject() -{ -} +DynamicObject::DynamicObject() = default; DynamicObject::DynamicObject (const DynamicObject& other) : ReferenceCountedObject(), properties (other.properties) { } -DynamicObject::~DynamicObject() -{ -} - bool DynamicObject::hasProperty (const Identifier& propertyName) const { const var* const v = properties.getVarPointer (propertyName); @@ -62,11 +56,13 @@ const var& DynamicObject::getProperty (const Identifier& propertyName) const void DynamicObject::setProperty (const Identifier& propertyName, const var& newValue) { properties.set (propertyName, newValue); + didModifyProperty (propertyName, newValue); } void DynamicObject::removeProperty (const Identifier& propertyName) { properties.remove (propertyName); + didModifyProperty (propertyName, std::nullopt); } bool DynamicObject::hasMethod (const Identifier& methodName) const @@ -84,12 +80,16 @@ var DynamicObject::invokeMethod (Identifier method, const var::NativeFunctionArg void DynamicObject::setMethod (Identifier name, var::NativeFunction function) { - properties.set (name, var (function)); + setProperty (name, var (function)); } void DynamicObject::clear() { + auto copy = properties; properties.clear(); + + for (auto& prop : copy) + didModifyProperty (prop.name, std::nullopt); } void DynamicObject::cloneAllProperties() @@ -151,4 +151,128 @@ void DynamicObject::writeAsJSON (OutputStream& out, const JSON::FormatOptions& f out << '}'; } +//============================================================================== +//============================================================================== + +#if JUCE_UNIT_TESTS + +class DynamicObjectTests : public UnitTest +{ +public: + DynamicObjectTests() : UnitTest { "DynamicObject", UnitTestCategories::containers } {} + + void runTest() override + { + struct Action + { + Identifier key; + std::optional value; + + bool operator== (const Action& other) const + { + return other.key == key && other.value == value; + } + }; + + using Actions = std::vector; + + struct DerivedObject : public DynamicObject + { + explicit DerivedObject (Actions& a) : actions (a) {} + + void didModifyProperty (const Identifier& key, const std::optional& value) override + { + actions.push_back (Action { key, value }); + } + + Actions& actions; + }; + + Actions actions; + DerivedObject object { actions }; + + beginTest ("didModifyProperty is emitted on setProperty"); + { + expect (object.getProperties().isEmpty()); + + const Identifier key = "foo"; + const var value = 123; + object.setProperty (key, value); + + expect (actions == Actions { Action { key, value } }); + expect (object.getProperties() == NamedValueSet { { key, value } }); + } + + object.clear(); + actions.clear(); + + beginTest ("didModifyProperty is emitted on setMethod"); + { + expect (object.getProperties().isEmpty()); + + const Identifier key = "foo"; + const var::NativeFunction value = [] (const var::NativeFunctionArgs&) { return var{}; }; + object.setMethod (key, value); + + expect (actions.size() == 1); + expect (actions.back().key == key); + + expect (object.getProperties().size() == 1); + expect (object.hasMethod (key)); + } + + object.clear(); + actions.clear(); + + beginTest ("didModifyProperty is emitted on removeProperty"); + { + expect (object.getProperties().isEmpty()); + + const Identifier key = "bar"; + object.removeProperty (key); + + expect (actions == Actions { Action { key, std::nullopt } }); + expect (object.getProperties().isEmpty()); + } + + object.clear(); + actions.clear(); + + beginTest ("didModifyProperty is emitted on clear"); + { + expect (object.getProperties().isEmpty()); + + object.clear(); + + expect (actions.empty()); + + const Identifier keys[] { "foo", "bar", "baz" }; + + for (auto [index, key] : enumerate (keys, int{})) + object.setProperty (key, index); + + for (auto& key : keys) + expect (object.hasProperty (key)); + + actions.clear(); + + object.clear(); + + expect (actions.size() == std::size (keys)); + + for (auto& key : keys) + { + expect (std::find_if (actions.begin(), actions.end(), [&key] (auto& action) + { + return action.key == key && action.value == std::nullopt; + }) != actions.end()); + } + } + } +}; + +static DynamicObjectTests dynamicObjectTests; + +#endif + } // namespace juce diff --git a/modules/juce_core/containers/juce_DynamicObject.h b/modules/juce_core/containers/juce_DynamicObject.h index fc093456cd..0bac0b023a 100644 --- a/modules/juce_core/containers/juce_DynamicObject.h +++ b/modules/juce_core/containers/juce_DynamicObject.h @@ -54,7 +54,6 @@ public: //============================================================================== DynamicObject(); DynamicObject (const DynamicObject&); - ~DynamicObject() override; using Ptr = ReferenceCountedObjectPtr; @@ -76,11 +75,8 @@ public: void removeProperty (const Identifier& propertyName); //============================================================================== - /** Checks whether this object has the specified method. - - The default implementation of this just checks whether there's a property - with this name that's actually a method, but this can be overridden for - building objects with dynamic invocation. + /** Checks whether this object has a property with the given name that has a + value of type NativeFunction. */ bool hasMethod (const Identifier& methodName) const; @@ -88,9 +84,6 @@ public: The default implementation looks up the named property, and if it's a method call, then it invokes it. - - This method is virtual to allow more dynamic invocation to used for objects - where the methods may not already be set as properties. */ var invokeMethod (Identifier methodName, const var::NativeFunctionArgs& args); @@ -133,6 +126,16 @@ public: virtual void writeAsJSON (OutputStream&, const JSON::FormatOptions&); private: + /** Derived classes may override this function to take additional actions after + properties are assigned or removed. + + @param name the name of the property that changed + @param value if non-null, the value of the property after assignment + if null, indicates that the property was removed + */ + virtual void didModifyProperty ([[maybe_unused]] const Identifier& name, + [[maybe_unused]] const std::optional& value) {} + //============================================================================== NamedValueSet properties;