diff --git a/src/corelib/kernel/qproperty.cpp b/src/corelib/kernel/qproperty.cpp index 3c1d682b7c..52bf9f33e6 100644 --- a/src/corelib/kernel/qproperty.cpp +++ b/src/corelib/kernel/qproperty.cpp @@ -846,6 +846,243 @@ QString QPropertyBindingError::description() const \internal */ +/*! + \class QBindablePropertyData + \inmodule QtCore + \brief The QBindablePropertyData class is a template class that enables automatic property bindings + for property data stored in QObject derived classes. + \since 6.0 + + \ingroup tools + + QBindablePropertyData is a generic container that holds an + instance of T and behaves mostly like \l QProperty. The extra template + parameters are 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 QBindablePropertyData to add binding support to code that uses Q_PROPERTY. + The getter and setter methods are easy to adapt for accessing a \l QBindablePropertyData + rather than the plain value. In order to invoke the change signal on property changes, use + QBindablePropertyData and pass the change signal as a callback. + + QBindablePropertyData is usually not used directly, instead an instance of it is created by + using the Q_BINDABLE_PROPERTY_DATA macro. + + Use the Q_BINDABLE_PROPERTY macro in the class declaration to declare the property as bindable. + + \code + class MyClass : public QObject + { + \Q_OBJECT + Q_PROPERTY(int x READ x WRITE setX NOTIFY xChanged) + public: + int x() const { return xProp; } + void setX(int x) { xProp = x; } + // declare the property as bindable. The data needs to be stored in a QBindablePropertyData instance. + // The last argument of the macro tells moc how to access that instance. + Q_BINDABLE_PROPERTY(MyClass, x, x, xProp) + + signals: + void xChanged(); + + private: + // Declare the instance of the bindable property data. + Q_BINDABLE_PROPERTY_DATA(MyClass, int, xProp, &MyClass::xChanged) + }; + \endcode +*/ + +/*! + \fn template QBindablePropertyData::QBindablePropertyData() + + Constructs a property with a default constructed instance of T. +*/ + +/*! + \fn template explicit QBindablePropertyData::QBindablePropertyData(const T &initialValue) + + Constructs a property with the provided \a initialValue. +*/ + +/*! + \fn template explicit QBindablePropertyData::QBindablePropertyData(T &&initialValue) + + Move-Constructs a property with the provided \a initialValue. +*/ + +/*! + \fn template QBindablePropertyData::QBindablePropertyData(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 QBindablePropertyData::QBindablePropertyData(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 QBindablePropertyData::QBindablePropertyData(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 QBindablePropertyData::~QBindablePropertyData() + + Destroys the property. +*/ + +/*! + \fn template T QBindablePropertyData::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 QBindablePropertyData::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 QBindablePropertyData::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 QBindablePropertyData::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 QBindablePropertyData::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 QBindablePropertyData::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 QBindablePropertyData::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 QBindablePropertyData::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 QBindablePropertyData::hasBinding() const + + Returns true if the property is associated with a binding; false otherwise. +*/ + + +/*! + \fn template QPropertyBinding QBindablePropertyData::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 QBindablePropertyData::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 QBindablePropertyData::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 QBindablePropertyData::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. +*/ + +/*! + \fn template QtPrivate::QPropertyBase &QBindablePropertyData::propertyBase() const + \internal +*/ /*! \class QPropertyChangeHandler diff --git a/src/corelib/kernel/qproperty.h b/src/corelib/kernel/qproperty.h index 4013b8c92b..14319f0382 100644 --- a/src/corelib/kernel/qproperty.h +++ b/src/corelib/kernel/qproperty.h @@ -742,6 +742,193 @@ public: QtPrivate::QPropertyBindingData *bindingData(QUntypedPropertyData *data, bool create); }; + +template +class QObjectBindableProperty : public QPropertyData +{ + using ThisType = QObjectBindableProperty; + static bool constexpr HasSignal = !std::is_same_v; + Class *owner() + { + char *that = reinterpret_cast(this); + return reinterpret_cast(that - QtPrivate::detail::getOffset(Offset)); + } + const Class *owner() const + { + char *that = const_cast(reinterpret_cast(this)); + return reinterpret_cast(that - QtPrivate::detail::getOffset(Offset)); + } + static void signalCallBack(QUntypedPropertyData *o) + { + QObjectBindableProperty *that = static_cast(o); + if constexpr (HasSignal) + (that->owner()->*Signal)(); + } +public: + using value_type = typename QPropertyData::value_type; + using parameter_type = typename QPropertyData::parameter_type; + using rvalue_ref = typename QPropertyData::rvalue_ref; + using arrow_operator_result = typename QPropertyData::arrow_operator_result; + + QObjectBindableProperty() = default; + explicit QObjectBindableProperty(const T &initialValue) : QPropertyData(initialValue) {} + explicit QObjectBindableProperty(T &&initialValue) : QPropertyData(std::move(initialValue)) {} + explicit QObjectBindableProperty(const QPropertyBinding &binding) + : QObjectBindableProperty() + { setBinding(binding); } +#ifndef Q_CLANG_QDOC + template + explicit QObjectBindableProperty(Functor &&f, const QPropertyBindingSourceLocation &location = QT_PROPERTY_DEFAULT_BINDING_LOCATION, + typename std::enable_if_t> * = 0) + : QObjectBindableProperty(QPropertyBinding(std::forward(f), location)) + {} +#else + template + explicit QProperty(Functor &&f); +#endif + + parameter_type value() const { + qGetBindingStorage(owner())->maybeUpdateBindingAndRegister(this); + return this->val; + } + + arrow_operator_result operator->() const + { + if constexpr (QTypeTraits::is_dereferenceable_v) { + return value(); + } else if constexpr (std::is_pointer_v) { + value(); + return this->val; + } else { + return; + } + } + + parameter_type operator*() const + { + return value(); + } + + operator parameter_type() const + { + return value(); + } + + void setValue(parameter_type t) { + auto *bd = qGetBindingStorage(owner())->bindingData(this); + if (bd) + bd->removeBinding(); + if (this->val == t) + return; + this->val = t; + notify(bd); + } + + void setValue(rvalue_ref t) { + auto *bd = qGetBindingStorage(owner())->bindingData(this); + if (bd) + bd->removeBinding(); + if (this->val == t) + return; + this->val = std::move(t); + notify(bd); + } + + QObjectBindableProperty &operator=(rvalue_ref newValue) + { + setValue(std::move(newValue)); + return *this; + } + + QObjectBindableProperty &operator=(parameter_type newValue) + { + setValue(newValue); + return *this; + } + + QPropertyBinding setBinding(const QPropertyBinding &newBinding) + { + QtPrivate::QPropertyBindingData *bd = qGetBindingStorage(owner())->bindingData(this, true); + QUntypedPropertyBinding oldBinding(bd->setBinding(newBinding, this, HasSignal ? &signalCallBack : nullptr)); + notify(bd); + return static_cast &>(oldBinding); + } + + bool setBinding(const QUntypedPropertyBinding &newBinding) + { + if (!newBinding.isNull() && newBinding.valueMetaType().id() != qMetaTypeId()) + return false; + setBinding(static_cast &>(newBinding)); + return true; + } + +#ifndef Q_CLANG_QDOC + template + QPropertyBinding setBinding(Functor &&f, + const QPropertyBindingSourceLocation &location = QT_PROPERTY_DEFAULT_BINDING_LOCATION, + std::enable_if_t> * = nullptr) + { + return setBinding(Qt::makePropertyBinding(std::forward(f), location)); + } +#else + template + QPropertyBinding setBinding(Functor f); +#endif + + bool hasBinding() const { + auto *bd = qGetBindingStorage(owner())->bindingData(this); + return bd && bd->binding() != nullptr; + } + + QPropertyBinding binding() const + { + auto *bd = qGetBindingStorage(owner())->bindingData(this); + return static_cast &&>(QUntypedPropertyBinding(bd ? bd->binding() : nullptr)); + } + + QPropertyBinding takeBinding() + { + return setBinding(QPropertyBinding()); + } + + template + QPropertyChangeHandler onValueChanged(Functor f) + { + static_assert(std::is_invocable_v, "Functor callback must be callable without any parameters"); + return QPropertyChangeHandler(*this, f); + } + + template + QPropertyChangeHandler subscribe(Functor f) + { + static_assert(std::is_invocable_v, "Functor callback must be callable without any parameters"); + f(); + return onValueChanged(f); + } + + const QtPrivate::QPropertyBindingData &bindingData() const + { + auto *storage = const_cast(qGetBindingStorage(owner())); + return *storage->bindingData(const_cast(this), true); + } +private: + void notify(const QtPrivate::QPropertyBindingData *binding) + { + if (binding) + binding->notifyObservers(this); + if constexpr (HasSignal) + (owner()->*Signal)(); + } +}; + +#define Q_OBJECT_BINDABLE_PROPERTY(Class, Type, name, ...) \ + static constexpr size_t _qt_property_##name##_offset() { \ + QT_WARNING_PUSH QT_WARNING_DISABLE_INVALID_OFFSETOF \ + return offsetof(Class, name); \ + QT_WARNING_POP \ + } \ + QObjectBindableProperty name; + QT_END_NAMESPACE #endif // QPROPERTY_H diff --git a/src/corelib/kernel/qpropertyprivate.h b/src/corelib/kernel/qpropertyprivate.h index adc347db84..6f0e563a28 100644 --- a/src/corelib/kernel/qpropertyprivate.h +++ b/src/corelib/kernel/qpropertyprivate.h @@ -178,6 +178,15 @@ namespace detail { template struct ExtractClassFromFunctionPointer { using Class = C; }; + + constexpr size_t getOffset(size_t o) + { + return o; + } + constexpr size_t getOffset(size_t (*offsetFn)()) + { + return offsetFn(); + } } // type erased guard functions, casts its arguments to the correct types diff --git a/tests/auto/corelib/kernel/qproperty/tst_qproperty.cpp b/tests/auto/corelib/kernel/qproperty/tst_qproperty.cpp index 340099a7e9..2136e8e212 100644 --- a/tests/auto/corelib/kernel/qproperty/tst_qproperty.cpp +++ b/tests/auto/corelib/kernel/qproperty/tst_qproperty.cpp @@ -70,6 +70,7 @@ private slots: void multipleObservers(); void propertyAlias(); void arrowAndStarOperator(); + void notifiedProperty(); void typeNoOperatorEqual(); void bindingValueReplacement(); @@ -776,6 +777,91 @@ void tst_QProperty::arrowAndStarOperator() } +struct ClassWithNotifiedProperty : public QObject +{ + QList recordedValues; + + void callback() { recordedValues << property.value(); } + int getProp() { return 0; } + + Q_OBJECT_BINDABLE_PROPERTY(ClassWithNotifiedProperty, int, property, &ClassWithNotifiedProperty::callback); +}; + +void tst_QProperty::notifiedProperty() +{ + ClassWithNotifiedProperty instance; + std::array, 5> otherProperties = { + QProperty([&]() { return instance.property + 1; }), + QProperty([&]() { return instance.property + 2; }), + QProperty([&]() { return instance.property + 3; }), + QProperty([&]() { return instance.property + 4; }), + QProperty([&]() { return instance.property + 5; }), + }; + + auto check = [&] { + const int val = instance.property.value(); + for (int i = 0; i < int(otherProperties.size()); ++i) + QCOMPARE(otherProperties[i].value(), val + i + 1); + }; + + QVERIFY(instance.recordedValues.isEmpty()); + check(); + + instance.property.setValue(42); + QCOMPARE(instance.recordedValues.count(), 1); + QCOMPARE(instance.recordedValues.at(0), 42); + instance.recordedValues.clear(); + check(); + + instance.property.setValue(42); + QVERIFY(instance.recordedValues.isEmpty()); + check(); + + int subscribedCount = 0; + QProperty injectedValue(100); + instance.property.setBinding([&injectedValue]() { return injectedValue.value(); }); + auto subscriber = [&] { ++subscribedCount; }; + std::array, 10> subscribers = { + instance.property.subscribe(subscriber), + instance.property.subscribe(subscriber), + instance.property.subscribe(subscriber), + instance.property.subscribe(subscriber), + instance.property.subscribe(subscriber), + instance.property.subscribe(subscriber), + instance.property.subscribe(subscriber), + instance.property.subscribe(subscriber), + instance.property.subscribe(subscriber), + instance.property.subscribe(subscriber) + }; + + QCOMPARE(subscribedCount, 10); + subscribedCount = 0; + + QCOMPARE(instance.property.value(), 100); + QCOMPARE(instance.recordedValues.count(), 1); + QCOMPARE(instance.recordedValues.at(0), 100); + instance.recordedValues.clear(); + check(); + QCOMPARE(subscribedCount, 0); + + injectedValue = 200; + QCOMPARE(instance.property.value(), 200); + QCOMPARE(instance.recordedValues.count(), 1); + QCOMPARE(instance.recordedValues.at(0), 200); + instance.recordedValues.clear(); + check(); + QCOMPARE(subscribedCount, 10); + subscribedCount = 0; + + injectedValue = 400; + QCOMPARE(instance.property.value(), 400); + QCOMPARE(instance.recordedValues.count(), 1); + QCOMPARE(instance.recordedValues.at(0), 400); + instance.recordedValues.clear(); + check(); + QCOMPARE(subscribedCount, 10); +} + void tst_QProperty::typeNoOperatorEqual() { struct Uncomparable @@ -908,9 +994,10 @@ public: int fooChangedCount = 0; int barChangedCount = 0; int readChangedCount = 0; - QProperty fooData; - QProperty barData; - QProperty readData; + + Q_OBJECT_BINDABLE_PROPERTY(MyQObject, int, fooData, &MyQObject::fooChanged); + Q_OBJECT_BINDABLE_PROPERTY(MyQObject, int, barData, &MyQObject::barChanged); + Q_OBJECT_BINDABLE_PROPERTY(MyQObject, int, readData, &MyQObject::readChanged); }; void tst_QProperty::testNewStuff() @@ -920,26 +1007,26 @@ void tst_QProperty::testNewStuff() QObject::connect(&object, &MyQObject::barChanged, &object, &MyQObject::barHasChanged); QObject::connect(&object, &MyQObject::readChanged, &object, &MyQObject::readHasChanged); -// QCOMPARE(object.fooChangedCount, 0); + QCOMPARE(object.fooChangedCount, 0); object.setFoo(10); -// QCOMPARE(object.fooChangedCount, 1); + QCOMPARE(object.fooChangedCount, 1); QCOMPARE(object.foo(), 10); auto f = [&object]() -> int { return object.barData; }; -// QCOMPARE(object.barChangedCount, 0); + QCOMPARE(object.barChangedCount, 0); object.setBar(42); -// QCOMPARE(object.barChangedCount, 1); -// QCOMPARE(object.fooChangedCount, 1); + QCOMPARE(object.barChangedCount, 1); + QCOMPARE(object.fooChangedCount, 1); object.fooData.setBinding(f); -// QCOMPARE(object.fooChangedCount, 2); + QCOMPARE(object.fooChangedCount, 2); QCOMPARE(object.fooData.value(), 42); object.setBar(666); -// QCOMPARE(object.fooChangedCount, 3); -// QCOMPARE(object.barChangedCount, 2); + QCOMPARE(object.fooChangedCount, 3); + QCOMPARE(object.barChangedCount, 2); QCOMPARE(object.fooData.value(), 666); -// QCOMPARE(object.fooChangedCount, 3); + QCOMPARE(object.fooChangedCount, 3); auto f2 = [&object]() -> int { return object.barData / 2;