From 36f69229253480116de72a7ef46c3bdde1e805e5 Mon Sep 17 00:00:00 2001 From: Simon Hausmann Date: Wed, 29 Apr 2020 12:41:36 +0200 Subject: [PATCH] Implement support for QProperty with a static observer A common pattern in Qt Quick will be QProperty members that are connected to a callback that needs to perform something when the value changes, for example emitting a compatibility signal or marking scene graph node data dirty. To make such a pattern more efficient, a new QNotifiedProperty type is introduced that offers the same API as QProperty, with two changes: (1) The template instantiation not only takes the property type as parameter but also a callback pointer-to-member. (2) Since that member itself cannot be called without an instance and to avoid storing an instance pointer permanently, the API for setBinding and setValue are adjusted to also take the instance pointer. For the former it gets stored in the binding, for the latter it is used to invoke the callback after setting the new value. Change-Id: I85cc1d1d1c0472164c4ae87808cfdc0d0b1475e1 Reviewed-by: Ulf Hermann --- src/corelib/kernel/qproperty.cpp | 230 +++++++++++++++++- src/corelib/kernel/qproperty.h | 146 ++++++++++- src/corelib/kernel/qpropertybinding.cpp | 2 + src/corelib/kernel/qpropertybinding_p.h | 4 + src/corelib/kernel/qpropertyprivate.h | 4 +- .../kernel/qproperty/tst_qproperty.cpp | 38 +++ 6 files changed, 420 insertions(+), 4 deletions(-) diff --git a/src/corelib/kernel/qproperty.cpp b/src/corelib/kernel/qproperty.cpp index c3e7711f94b..a1b1d7b127f 100644 --- a/src/corelib/kernel/qproperty.cpp +++ b/src/corelib/kernel/qproperty.cpp @@ -92,7 +92,10 @@ QPropertyBase::~QPropertyBase() binding->unlinkAndDeref(); } -QUntypedPropertyBinding QPropertyBase::setBinding(const QUntypedPropertyBinding &binding, void *propertyDataPtr) +QUntypedPropertyBinding QPropertyBase::setBinding(const QUntypedPropertyBinding &binding, + void *propertyDataPtr, + void *staticObserver, + void (*staticObserverCallback)(void*)) { QPropertyBindingPrivatePtr oldBinding; QPropertyBindingPrivatePtr newBinding = binding.d; @@ -119,6 +122,7 @@ QUntypedPropertyBinding QPropertyBase::setBinding(const QUntypedPropertyBinding newBinding->setProperty(propertyDataPtr); if (observer) newBinding->prependObserver(observer); + newBinding->setStaticObserver(staticObserver, staticObserverCallback); } else if (observer) { d.setObservers(observer.ptr); } else { @@ -703,6 +707,230 @@ QPropertyBindingSourceLocation QPropertyBindingError::location() const goes out of scope, the callback is unsubscribed. */ +/*! + \class QNotifiedProperty + \inmodule QtCore + \brief The QNotifiedProperty class is a template class that enables automatic property bindings + and invokes a callback function on the surrounding class when the value changes. + + \ingroup tools + + QNotifiedProperty\ is a generic container that holds an + instance of T and behaves mostly like \l QProperty. The extra template + parameter is used to identify the surrounding class and a member function of + that class. The member function will be called whenever the value held by the + property changes. + + You can use QNotifiedProperty to port code that uses Q_PROPERTY. The getter + and setter are trivial to adapt for accessing a \l QProperty rather than the + plain value. In order to invoke the change signal on property changes, use + QNotifiedProperty and pass the change signal as callback. + + \code + class MyClass : public QObject + { + \Q_OBJECT + // Replacing: Q_PROPERTY(int x READ x WRITE setX NOTIFY xChanged) + public: + int x() const { return xProp; } + void setX(int x) { xProp = x; } + + signals: + void xChanged(); + + private: + // Now you can set bindings on xProp and use it in other bindings. + QNotifiedProperty xProp; + }; + \endcode +*/ + +/*! + \fn template QNotifiedProperty::QNotifiedProperty() + + Constructs a property with a default constructed instance of T. +*/ + +/*! + \fn template explicit QNotifiedProperty::QNotifiedProperty(const T &initialValue) + + Constructs a property with the provided \a initialValue. +*/ + +/*! + \fn template explicit QNotifiedProperty::QNotifiedProperty(T &&initialValue) + + Move-Constructs a property with the provided \a initialValue. +*/ + +/*! + \fn template QNotifiedProperty::QNotifiedProperty(Class *owner, const QPropertyBinding &binding) + + Constructs a property that is tied to the provided \a binding expression. The + first time the property value is read, the binding is evaluated. Whenever a + dependency of the binding changes, the binding will be re-evaluated the next + time the value of this property is read. When the property value changes \a + owner is notified via the Callback function. +*/ + +/*! + \fn template QNotifiedProperty::QNotifiedProperty(Class *owner, QPropertyBinding &&binding) + + Constructs a property that is tied to the provided \a binding expression. The + first time the property value is read, the binding is evaluated. Whenever a + dependency of the binding changes, the binding will be re-evaluated the next + time the value of this property is read. When the property value changes \a + owner is notified via the Callback function. +*/ + + +/*! + \fn template template QNotifiedProperty::QNotifiedProperty(Class *owner, Functor &&f) + + Constructs a property that is tied to the provided binding expression \a f. The + first time the property value is read, the binding is evaluated. Whenever a + dependency of the binding changes, the binding will be re-evaluated the next + time the value of this property is read. When the property value changes \a + owner is notified via the Callback function. +*/ + +/*! + \fn template QNotifiedProperty::~QNotifiedProperty() + + Destroys the property. +*/ + +/*! + \fn template T QNotifiedProperty::value() const + + Returns the value of the property. This may evaluate a binding expression that + is tied to this property, before returning the value. +*/ + +/*! + \fn template QNotifiedProperty::operator T() const + + Returns the value of the property. This may evaluate a binding expression that + is tied to this property, before returning the value. +*/ + +/*! + \fn template void QNotifiedProperty::setValue(Class *owner, const T &newValue) + + Assigns \a newValue to this property and removes the property's associated + binding, if present. If the property value changes as a result, calls the + Callback function on \a owner. +*/ + +/*! + \fn template void QNotifiedProperty::setValue(Class *owner, T &&newValue) + \overload + + Assigns \a newValue to this property and removes the property's associated + binding, if present. If the property value changes as a result, calls the + Callback function on \a owner. +*/ + +/*! + \fn template QPropertyBinding QNotifiedProperty::setBinding(Class *owner, const QPropertyBinding &newBinding) + + Associates the value of this property with the provided \a newBinding + expression and returns the previously associated binding. The first time the + property value is read, the binding is evaluated. Whenever a dependency of the + binding changes, the binding will be re-evaluated the next time the value of + this property is read. When the property value changes \a owner is notified + via the Callback function. +*/ + +/*! + \fn template template QPropertyBinding QNotifiedProperty::setBinding(Class *owner, Functor f) + \overload + + Associates the value of this property with the provided functor \a f and + returns the previously associated binding. The first time the property value + is read, the binding is evaluated by invoking the call operator () of \a f. + Whenever a dependency of the binding changes, the binding will be re-evaluated + the next time the value of this property is read. When the property value + changes \a owner is notified via the Callback function. +*/ + +/*! + \fn template QPropertyBinding QNotifiedProperty::setBinding(Class *owner, QPropertyBinding &&newBinding) + \overload + + Associates the value of this property with the provided \a newBinding + expression and returns the previously associated binding. The first time the + property value is read, the binding is evaluated. Whenever a dependency of the + binding changes, the binding will be re-evaluated the next time the value of + this property is read. When the property value changes \a owner is notified + via the Callback function. +*/ + +/*! + \fn template QPropertyBinding bool QNotifiedProperty::setBinding(Class *owner, const QUntypedPropertyBinding &newBinding) + \overload + + Associates the value of this property with the provided \a newBinding + expression. The first time the property value is read, the binding is evaluated. + Whenever a dependency of the binding changes, the binding will be re-evaluated + the next time the value of this property is read. When the property value + changes \a owner is notified via the Callback function. + + Returns true if the type of this property is the same as the type the binding + function returns; false otherwise. +*/ + +/*! + \fn template bool QNotifiedProperty::hasBinding() const + + Returns true if the property is associated with a binding; false otherwise. +*/ + + +/*! + \fn template QPropertyBinding QNotifiedProperty::binding() const + + Returns the binding expression that is associated with this property. A + default constructed QPropertyBinding will be returned if no such + association exists. +*/ + +/*! + \fn template QPropertyBinding QNotifiedProperty::takeBinding() + + Disassociates the binding expression from this property and returns it. After + calling this function, the value of the property will only change if you + assign a new value to it, or when a new binding is set. +*/ + +/*! + \fn template template QPropertyChangeHandler QNotifiedProperty::onValueChanged(Functor f) + + Registers the given functor \a f as a callback that shall be called whenever + the value of the property changes. + + The callback \a f is expected to be a type that has a plain call operator () without any + parameters. This means that you can provide a C++ lambda expression, an std::function + or even a custom struct with a call operator. + + The returned property change handler object keeps track of the registration. When it + goes out of scope, the callback is de-registered. +*/ + +/*! + \fn template template QPropertyChangeHandler QNotifiedProperty::subscribe(Functor f) + + Subscribes the given functor \a f as a callback that is called immediately and whenever + the value of the property changes in the future. + + The callback \a f is expected to be a type that has a plain call operator () without any + parameters. This means that you can provide a C++ lambda expression, an std::function + or even a custom struct with a call operator. + + The returned property change handler object keeps track of the subscription. When it + goes out of scope, the callback is unsubscribed. +*/ + /*! \class QPropertyChangeHandler \inmodule QtCore diff --git a/src/corelib/kernel/qproperty.h b/src/corelib/kernel/qproperty.h index 76c41f356c6..e2a6172d844 100644 --- a/src/corelib/kernel/qproperty.h +++ b/src/corelib/kernel/qproperty.h @@ -85,6 +85,7 @@ struct Q_CORE_EXPORT QPropertyBindingSourceLocation template class QPropertyChangeHandler; template class QProperty; +template class QNotifiedProperty; class QPropertyBindingErrorPrivate; @@ -178,12 +179,15 @@ public: : QUntypedPropertyBinding(property.d.priv.binding()) {} -private: + template + QPropertyBinding(const QNotifiedProperty &property) + : QUntypedPropertyBinding(property.d.priv.binding()) + {} + // Internal explicit QPropertyBinding(const QUntypedPropertyBinding &binding) : QUntypedPropertyBinding(binding) {} - friend class QProperty; }; namespace QtPrivate { @@ -370,6 +374,140 @@ namespace Qt { } } +template +class QNotifiedProperty +{ +public: + using value_type = T; + + QNotifiedProperty() = default; + + explicit QNotifiedProperty(const T &initialValue) : d(initialValue) {} + explicit QNotifiedProperty(T &&initialValue) : d(std::move(initialValue)) {} + + QNotifiedProperty(Class *owner, const QPropertyBinding &binding) + : QNotifiedProperty() + { setBinding(owner, binding); } + QNotifiedProperty(Class *owner, QPropertyBinding &&binding) + : QNotifiedProperty() + { setBinding(owner, std::move(binding)); } + +#ifndef Q_CLANG_QDOC + template + explicit QNotifiedProperty(Class *owner, Functor &&f, const QPropertyBindingSourceLocation &location = QT_PROPERTY_DEFAULT_BINDING_LOCATION, + typename std::enable_if_t> * = 0) + : QNotifiedProperty(QPropertyBinding(owner, std::forward(f), location)) + {} +#else + template + explicit QProperty(Class *owner, Functor &&f); +#endif + + ~QNotifiedProperty() = default; + + T value() const + { + if (d.priv.hasBinding()) + d.priv.evaluateIfDirty(); + d.priv.registerWithCurrentlyEvaluatingBinding(); + return d.getValue(); + } + + operator T() const + { + return value(); + } + + void setValue(Class *owner, T &&newValue) + { + if (d.setValueAndReturnTrueIfChanged(std::move(newValue))) + notify(owner); + d.priv.removeBinding(); + } + + void setValue(Class *owner, const T &newValue) + { + if (d.setValueAndReturnTrueIfChanged(newValue)) + notify(owner); + d.priv.removeBinding(); + } + + QPropertyBinding setBinding(Class *owner, const QPropertyBinding &newBinding) + { + QPropertyBinding oldBinding(d.priv.setBinding(newBinding, &d, owner, [](void *o) { + (reinterpret_cast(o)->*Callback)(); + })); + notify(owner); + return oldBinding; + } + + QPropertyBinding setBinding(Class *owner, QPropertyBinding &&newBinding) + { + QPropertyBinding b(std::move(newBinding)); + QPropertyBinding oldBinding(d.priv.setBinding(b, &d, owner, [](void *o) { + (reinterpret_cast(o)->*Callback)(); + })); + notify(owner); + return oldBinding; + } + + bool setBinding(Class *owner, const QUntypedPropertyBinding &newBinding) + { + if (newBinding.valueMetaType().id() != qMetaTypeId()) + return false; + d.priv.setBinding(newBinding, &d, owner, [](void *o) { + (reinterpret_cast(o)->*Callback)(); + }); + notify(owner); + return true; + } + +#ifndef Q_CLANG_QDOC + template + QPropertyBinding setBinding(Class *owner, Functor &&f, + const QPropertyBindingSourceLocation &location = QT_PROPERTY_DEFAULT_BINDING_LOCATION, + std::enable_if_t> * = nullptr) + { + return setBinding(owner, Qt::makePropertyBinding(std::forward(f), location)); + } +#else + template + QPropertyBinding setBinding(Class *owner, Functor f); +#endif + + bool hasBinding() const { return d.priv.hasBinding(); } + + QPropertyBinding binding() const + { + return QPropertyBinding(*this); + } + + QPropertyBinding takeBinding() + { + return QPropertyBinding(d.priv.setBinding(QUntypedPropertyBinding(), &d)); + } + + template + QPropertyChangeHandler onValueChanged(Functor f); + template + QPropertyChangeHandler subscribe(Functor f); + +private: + void notify(Class *owner) + { + d.priv.notifyObservers(&d); + (owner->*Callback)(); + } + + Q_DISABLE_COPY_MOVE(QNotifiedProperty) + + friend class QPropertyBinding; + friend class QPropertyObserver; + // Mutable because querying for the value may require evalating the binding expression, calling + // non-const functions on QPropertyBase. + mutable QtPrivate::QPropertyValueStorage d; +}; + struct QPropertyObserverPrivate; struct QPropertyObserverPointer; @@ -392,6 +530,10 @@ public: void setSource(const QProperty &property) { setSource(property.d.priv); } + template + void setSource(const QNotifiedProperty &property) + { setSource(property.d.priv); } + protected: QPropertyObserver(void (*callback)(QPropertyObserver*, void *)); QPropertyObserver(void *aliasedPropertyPtr); diff --git a/src/corelib/kernel/qpropertybinding.cpp b/src/corelib/kernel/qpropertybinding.cpp index 29b1a4a69a5..55f2fe913d1 100644 --- a/src/corelib/kernel/qpropertybinding.cpp +++ b/src/corelib/kernel/qpropertybinding.cpp @@ -63,6 +63,8 @@ void QPropertyBindingPrivate::unlinkAndDeref() void QPropertyBindingPrivate::markDirtyAndNotifyObservers() { dirty = true; + if (staticObserver) + staticObserverCallback(staticObserver); if (firstObserver) firstObserver.notify(this, propertyDataPtr); } diff --git a/src/corelib/kernel/qpropertybinding_p.h b/src/corelib/kernel/qpropertybinding_p.h index 7c4166592bf..a2c733abd94 100644 --- a/src/corelib/kernel/qpropertybinding_p.h +++ b/src/corelib/kernel/qpropertybinding_p.h @@ -71,6 +71,9 @@ private: QPropertyObserverPointer firstObserver; std::array inlineDependencyObservers; QScopedPointer> heapObservers; + // ### merge with inline dependency observer storage + void *staticObserver = nullptr; + void (*staticObserverCallback)(void*) = nullptr; void *propertyDataPtr = nullptr; @@ -96,6 +99,7 @@ public: void setDirty(bool d) { dirty = d; } void setProperty(void *propertyPtr) { propertyDataPtr = propertyPtr; } + void setStaticObserver(void *observer, void (*callback)(void*)) { staticObserver = observer; staticObserverCallback = callback; } void prependObserver(QPropertyObserverPointer observer) { observer.ptr->prev = const_cast(&firstObserver.ptr); firstObserver = observer; diff --git a/src/corelib/kernel/qpropertyprivate.h b/src/corelib/kernel/qpropertyprivate.h index 4b0b09d9dbd..7a2c6a5b774 100644 --- a/src/corelib/kernel/qpropertyprivate.h +++ b/src/corelib/kernel/qpropertyprivate.h @@ -82,7 +82,9 @@ public: bool hasBinding() const { return d_ptr & BindingBit; } - QUntypedPropertyBinding setBinding(const QUntypedPropertyBinding &newBinding, void *propertyDataPtr); + QUntypedPropertyBinding setBinding(const QUntypedPropertyBinding &newBinding, + void *propertyDataPtr, void *staticObserver = nullptr, + void (*staticObserverCallback)(void*) = nullptr); QPropertyBindingPrivate *binding(); void evaluateIfDirty(); diff --git a/tests/auto/corelib/kernel/qproperty/tst_qproperty.cpp b/tests/auto/corelib/kernel/qproperty/tst_qproperty.cpp index 15adcdffd67..394438ab88b 100644 --- a/tests/auto/corelib/kernel/qproperty/tst_qproperty.cpp +++ b/tests/auto/corelib/kernel/qproperty/tst_qproperty.cpp @@ -71,6 +71,7 @@ private slots: void setBindingFunctor(); void multipleObservers(); void propertyAlias(); + void notifiedProperty(); }; void tst_QProperty::functorBinding() @@ -769,6 +770,43 @@ void tst_QProperty::propertyAlias() QCOMPARE(value2, 22); } +struct ClassWithNotifiedProperty +{ + QVector recordedValues; + + void callback() { recordedValues << property.value(); } + + QNotifiedProperty property; +}; + +void tst_QProperty::notifiedProperty() +{ + ClassWithNotifiedProperty instance; + QVERIFY(instance.recordedValues.isEmpty()); + + instance.property.setValue(&instance, 42); + QCOMPARE(instance.recordedValues.count(), 1); + QCOMPARE(instance.recordedValues.at(0), 42); + instance.recordedValues.clear(); + + instance.property.setValue(&instance, 42); + QVERIFY(instance.recordedValues.isEmpty()); + + QProperty injectedValue(100); + instance.property.setBinding(&instance, [&injectedValue]() { return injectedValue.value(); }); + + QCOMPARE(instance.property.value(), 100); + QCOMPARE(instance.recordedValues.count(), 1); + QCOMPARE(instance.recordedValues.at(0), 100); + instance.recordedValues.clear(); + + injectedValue = 200; + QCOMPARE(instance.property.value(), 200); + QCOMPARE(instance.recordedValues.count(), 1); + QCOMPARE(instance.recordedValues.at(0), 200); + instance.recordedValues.clear(); +} + QTEST_MAIN(tst_QProperty); #include "tst_qproperty.moc"