From a64a0ce3317afa1ff633d1d08904c967dbc6ecad Mon Sep 17 00:00:00 2001 From: Ulf Hermann Date: Mon, 25 May 2020 12:54:51 +0200 Subject: [PATCH] Add a QPropertyAlias A property alias is the equivalent of the "alias" keyword in QML. It provides the same API as QProperty, but redirects any access to the QProperty it was initialized with. When the original property is destroyed the binding becomes invalid and ignores any further acccess. Task-number: QTBUG-84370 Change-Id: I0aef8d50e73a2aa9e7703d51194d4c5480573578 Reviewed-by: Fabian Kosmale Reviewed-by: Lars Knoll --- src/corelib/kernel/qproperty.cpp | 249 +++++++++++++++++- src/corelib/kernel/qproperty.h | 159 ++++++++++- src/corelib/kernel/qproperty_p.h | 1 + .../kernel/qproperty/tst_qproperty.cpp | 47 ++++ 4 files changed, 446 insertions(+), 10 deletions(-) diff --git a/src/corelib/kernel/qproperty.cpp b/src/corelib/kernel/qproperty.cpp index 4ed4af4c8e..c3e7711f94 100644 --- a/src/corelib/kernel/qproperty.cpp +++ b/src/corelib/kernel/qproperty.cpp @@ -254,6 +254,12 @@ QPropertyObserver::QPropertyObserver(void (*callback)(QPropertyObserver *, void d.setChangeHandler(callback); } +QPropertyObserver::QPropertyObserver(void *aliasedPropertyPtr) +{ + QPropertyObserverPointer d{this}; + d.setAliasedProperty(aliasedPropertyPtr); +} + void QPropertyObserver::setSource(QPropertyBase &property) { QPropertyObserverPointer d{this}; @@ -303,6 +309,8 @@ QPropertyObserver &QPropertyObserver::operator=(QPropertyObserver &&other) void QPropertyObserverPointer::unlink() { + if (ptr->next.tag() & QPropertyObserver::ObserverNotifiesAlias) + ptr->aliasedPropertyPtr = 0; if (ptr->next) ptr->next->prev = ptr->prev; if (ptr->prev) @@ -317,6 +325,12 @@ void QPropertyObserverPointer::setChangeHandler(void (*changeHandler)(QPropertyO ptr->next.setTag(QPropertyObserver::ObserverNotifiesChangeHandler); } +void QPropertyObserverPointer::setAliasedProperty(void *propertyPtr) +{ + ptr->aliasedPropertyPtr = quintptr(propertyPtr); + ptr->next.setTag(QPropertyObserver::ObserverNotifiesAlias); +} + void QPropertyObserverPointer::setBindingToMarkDirty(QPropertyBindingPrivate *binding) { ptr->bindingToMarkDirty = binding; @@ -331,7 +345,8 @@ void QPropertyObserverPointer::notify(QPropertyBindingPrivate *triggeringBinding auto observer = const_cast(ptr); while (observer) { auto * const next = observer->next.data(); - if (observer->next.tag() == QPropertyObserver::ObserverNotifiesChangeHandler) { + switch (observer->next.tag()) { + case QPropertyObserver::ObserverNotifiesChangeHandler: if (!knownIfPropertyChanged && triggeringBinding) { knownIfPropertyChanged = true; @@ -344,9 +359,13 @@ void QPropertyObserverPointer::notify(QPropertyBindingPrivate *triggeringBinding handlerToCall(observer, propertyDataPtr); observer->changeHandler = handlerToCall; } - } else { + break; + case QPropertyObserver::ObserverNotifiesBinding: if (observer->bindingToMarkDirty) observer->bindingToMarkDirty->markDirtyAndNotifyObservers(); + break; + case QPropertyObserver::ObserverNotifiesAlias: + break; } observer = next; } @@ -354,7 +373,8 @@ void QPropertyObserverPointer::notify(QPropertyBindingPrivate *triggeringBinding void QPropertyObserverPointer::observeProperty(QPropertyBasePointer property) { - unlink(); + if (ptr->prev) + unlink(); property.addObserver(ptr); } @@ -697,4 +717,227 @@ QPropertyBindingSourceLocation QPropertyBindingError::location() const A handler instance can be transferred between C++ scopes using move semantics. */ +/*! + \class QPropertyAlias + \inmodule QtCore + \brief The QPropertyAlias class is a safe alias for a QProperty with same template parameter. + + \ingroup tools + + QPropertyAlias\ wraps a pointer to a QProperty\ and automatically + invalidates itself when the QProperty\ is destroyed. It forwards all + method invocations to the wrapped property. For example: + + \code + QProperty *name = new QProperty("John"); + QProperty age(41); + + QPropertyAlias nameAlias(name); + QPropertyAlias ageAlias(&age); + + QPropertyAlias fullname; + fullname.setBinding([&]() { return nameAlias.value() + " age:" + QString::number(ageAlias.value()); }); + + qDebug() << fullname.value(); // Prints "Smith age: 41" + + *name = "Emma"; // Marks binding expression as dirty + + qDebug() << fullname.value(); // Re-evaluates the binding expression and prints "Emma age: 41" + + // Birthday is coming up + ageAlias.setValue(age.value() + 1); // Writes the age property through the alias + + qDebug() << fullname.value(); // Re-evaluates the binding expression and prints "Emma age: 42" + + delete name; // Leaves the alias in an invalid, but accessible state + nameAlias.setValue("Eve"); // Ignored: nameAlias carries a default-constructed QString now + + ageAlias.setValue(92); + qDebug() << fullname.value(); // Re-evaluates the binding expression and prints " age: 92" + \endcode +*/ + +/*! + \fn template QPropertyAlias::QPropertyAlias(QProperty *property) + + Constructs a property alias for the given \a property. +*/ + +/*! + \fn template explicit QPropertyAlias::QPropertyAlias(QPropertyAlias *alias) + + Constructs a property alias for the property aliased by \a alias. +*/ + +/*! + \fn template T QPropertyAlias::value() const + + Returns the value of the aliased property. This may evaluate a binding + expression that is tied to the property, before returning the value. +*/ + +/*! + \fn template QPropertyAlias::operator T() const + + Returns the value of the aliased property. This may evaluate a binding + expression that is tied to the property, before returning the value. +*/ + +/*! + \fn template void QPropertyAlias::setValue(const T &newValue) + + Assigns \a newValue to the aliased property and removes the property's + associated binding, if present. +*/ + +/*! + \fn template void QPropertyAlias::setValue(T &&newValue) + \overload + + Assigns \a newValue to the aliased property and removes the property's + associated binding, if present. +*/ + +/*! + \fn template QPropertyAlias &QPropertyAlias::operator=(const T &newValue) + + Assigns \a newValue to the aliased property and returns a reference to this + QPropertyAlias. +*/ + +/*! + \fn template QPropertyAlias &QPropertyAlias::operator=(T &&newValue) + \overload + + Assigns \a newValue to the aliased property and returns a reference to this + QPropertyAlias. +*/ + +/*! + \fn template QPropertyAlias &QPropertyAlias::operator=(const QPropertyBinding &newBinding) + \overload + + Associates the value of the aliased property with the provided \a newBinding + expression and returns a reference to this alias. The first time the + property value is read, either from the property itself or from any alias, 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. +*/ + +/*! + \fn template QPropertyBinding QPropertyAlias::setBinding(const QPropertyBinding &newBinding) + + Associates the value of the aliased property with the provided \a newBinding + expression and returns any previous binding the associated with the aliased + property. The first time the property value is read, either from the property + itself or from any alias, 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. + + Returns any previous binding associated with the property, or a + default-constructed QPropertyBinding. +*/ + +/*! + \fn template QPropertyBinding QPropertyAlias::setBinding(QPropertyBinding &&newBinding) + \overload + + Associates the value of the aliased property with the provided \a newBinding + expression and returns any previous binding the associated with the aliased + property. The first time the property value is read, either from the property + itself or from any alias, 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. + + Returns any previous binding associated with the property, or a + default-constructed QPropertyBinding. +*/ + +/*! + \fn template QPropertyBinding bool QPropertyAlias::setBinding(const QUntypedPropertyBinding &newBinding) + \overload + + Associates the value of the aliased property with the provided \a newBinding + expression. The first time the property value is read, either from the + property itself or from any alias, 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. + + Returns true if the type of this property is the same as the type the binding + function returns; false otherwise. +*/ + +/*! + \fn template template QPropertyBinding setBinding(Functor f) + \overload + + Associates the value of the aliased property with the provided functor \a f + expression. The first time the property value is read, either from the + property itself or from any alias, 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. + + Returns any previous binding associated with the property, or a + default-constructed QPropertyBinding. +*/ + +/*! + \fn template bool QPropertyAlias::hasBinding() const + + Returns true if the aliased property is associated with a binding; false + otherwise. +*/ + +/*! + \fn template QPropertyBinding QPropertyAlias::binding() const + + Returns the binding expression that is associated with the aliased property. A + default constructed QPropertyBinding will be returned if no such + association exists. +*/ + +/*! + \fn template QPropertyBinding QPropertyAlias::takeBinding() + + Disassociates the binding expression from the aliased 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 QPropertyAlias::onValueChanged(Functor f) + + Registers the given functor \a f as a callback that shall be called whenever + the value of the aliased 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 QPropertyAlias::subscribe(Functor f) + + Subscribes the given functor \a f as a callback that is called immediately and whenever + the value of the aliased 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 bool QPropertyAlias::isValid() const + + Returns true if the aliased property still exists; false otherwise. + + If the aliased property doesn't exist, all other method calls are ignored. +*/ + QT_END_NAMESPACE diff --git a/src/corelib/kernel/qproperty.h b/src/corelib/kernel/qproperty.h index 938de61fd5..76c41f356c 100644 --- a/src/corelib/kernel/qproperty.h +++ b/src/corelib/kernel/qproperty.h @@ -378,10 +378,10 @@ class Q_CORE_EXPORT QPropertyObserver public: // Internal enum ObserverTag { - ObserverNotifiesBinding = 0x0, - ObserverNotifiesChangeHandler = 0x1, + ObserverNotifiesBinding, + ObserverNotifiesChangeHandler, + ObserverNotifiesAlias, }; - Q_DECLARE_FLAGS(ObserverTags, ObserverTag) QPropertyObserver(); QPropertyObserver(QPropertyObserver &&other); @@ -394,18 +394,26 @@ public: protected: QPropertyObserver(void (*callback)(QPropertyObserver*, void *)); + QPropertyObserver(void *aliasedPropertyPtr); + + template + QProperty *aliasedProperty() const + { + return reinterpret_cast *>(aliasedPropertyPtr); + } private: void setSource(QtPrivate::QPropertyBase &property); - QTaggedPointer next; + QTaggedPointer next; // prev is a pointer to the "next" element within the previous node, or to the "firstObserverPtr" if it is the // first node. - QtPrivate::QTagPreservingPointerToPointer prev; + QtPrivate::QTagPreservingPointerToPointer prev; union { QPropertyBindingPrivate *bindingToMarkDirty = nullptr; void (*changeHandler)(QPropertyObserver*, void *); + quintptr aliasedPropertyPtr; }; QPropertyObserver(const QPropertyObserver &) = delete; @@ -416,8 +424,6 @@ private: friend class QPropertyBindingPrivate; }; -Q_DECLARE_OPERATORS_FOR_FLAGS(QPropertyObserver::ObserverTags) - template class QPropertyChangeHandler : public QPropertyObserver { @@ -487,6 +493,145 @@ struct QPropertyMemberChangeHandler : public QProperty } }; +template +class QPropertyAlias : public QPropertyObserver +{ + Q_DISABLE_COPY_MOVE(QPropertyAlias) +public: + QPropertyAlias(QProperty *property) + : QPropertyObserver(property) + { + if (property) + setSource(*property); + } + + QPropertyAlias(QPropertyAlias *alias) + : QPropertyAlias(alias->aliasedProperty()) + {} + + T value() const + { + if (auto *p = aliasedProperty()) + return p->value(); + return T(); + } + + operator T() const { return value(); } + + void setValue(T &&newValue) + { + if (auto *p = aliasedProperty()) + p->setValue(std::move(newValue)); + } + + void setValue(const T &newValue) + { + if (auto *p = aliasedProperty()) + p->setValue(newValue); + } + + QPropertyAlias &operator=(T &&newValue) + { + if (auto *p = aliasedProperty()) + *p = std::move(newValue); + return *this; + } + + QPropertyAlias &operator=(const T &newValue) + { + if (auto *p = aliasedProperty()) + *p = newValue; + return *this; + } + + QPropertyAlias &operator=(const QPropertyBinding &newBinding) + { + setBinding(newBinding); + return *this; + } + + QPropertyAlias &operator=(QPropertyBinding &&newBinding) + { + setBinding(std::move(newBinding)); + return *this; + } + + QPropertyBinding setBinding(const QPropertyBinding &newBinding) + { + if (auto *p = aliasedProperty()) + return p->setBinding(newBinding); + return QPropertyBinding(); + } + + QPropertyBinding setBinding(QPropertyBinding &&newBinding) + { + if (auto *p = aliasedProperty()) + return p->setBinding(std::move(newBinding)); + return QPropertyBinding(); + } + + bool setBinding(const QUntypedPropertyBinding &newBinding) + { + if (auto *p = aliasedProperty()) + return p->setBinding(newBinding); + return false; + } + +#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 + { + if (auto *p = aliasedProperty()) + return p->hasBinding(); + return false; + } + + QPropertyBinding binding() const + { + if (auto *p = aliasedProperty()) + return p->binding(); + return QPropertyBinding(); + } + + QPropertyBinding takeBinding() + { + if (auto *p = aliasedProperty()) + return p->takeBinding(); + return QPropertyBinding(); + } + + template + QPropertyChangeHandler onValueChanged(Functor f) + { + if (auto *p = aliasedProperty()) + return p->onValueChanged(f); + return QPropertyChangeHandler(f); + } + + template + QPropertyChangeHandler subscribe(Functor f) + { + if (auto *p = aliasedProperty()) + return p->subscribe(f); + return QPropertyChangeHandler(f); + } + + bool isValid() const + { + return aliasedProperty() != nullptr; + } +}; QT_END_NAMESPACE diff --git a/src/corelib/kernel/qproperty_p.h b/src/corelib/kernel/qproperty_p.h index 6638785ccf..e5365f36fc 100644 --- a/src/corelib/kernel/qproperty_p.h +++ b/src/corelib/kernel/qproperty_p.h @@ -88,6 +88,7 @@ struct QPropertyObserverPointer void setBindingToMarkDirty(QPropertyBindingPrivate *binding); void setChangeHandler(void (*changeHandler)(QPropertyObserver *, void *)); + void setAliasedProperty(void *propertyPtr); void notify(QPropertyBindingPrivate *triggeringBinding, void *propertyDataPtr); void observeProperty(QPropertyBasePointer property); diff --git a/tests/auto/corelib/kernel/qproperty/tst_qproperty.cpp b/tests/auto/corelib/kernel/qproperty/tst_qproperty.cpp index a8846e0987..15adcdffd6 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 staticChangeHandler(); void setBindingFunctor(); void multipleObservers(); + void propertyAlias(); }; void tst_QProperty::functorBinding() @@ -722,6 +723,52 @@ void tst_QProperty::multipleObservers() QCOMPARE(property.value(), 22); } +void tst_QProperty::propertyAlias() +{ + QScopedPointer> property(new QProperty); + property->setValue(5); + QPropertyAlias alias(property.get()); + QVERIFY(alias.isValid()); + QCOMPARE(alias.value(), 5); + + int value1 = 1; + auto changeHandler = alias.onValueChanged([&]() { value1 = alias.value(); }); + QCOMPARE(value1, 1); + + int value2 = 2; + auto subscribeHandler = alias.subscribe([&]() { value2 = alias.value(); }); + QCOMPARE(value2, 5); + + alias.setValue(6); + QVERIFY(alias.isValid()); + QCOMPARE(alias.value(), 6); + QCOMPARE(value1, 6); + QCOMPARE(value2, 6); + + alias.setBinding([]() { return 12; }); + QCOMPARE(value1, 12); + QCOMPARE(value2, 12); + QCOMPARE(alias.value(), 12); + + alias.setValue(22); + QCOMPARE(value1, 22); + QCOMPARE(value2, 22); + QCOMPARE(alias.value(), 22); + + property.reset(); + + QVERIFY(!alias.isValid()); + QCOMPARE(alias.value(), int()); + QCOMPARE(value1, 22); + QCOMPARE(value2, 22); + + // Does not crash + alias.setValue(25); + QCOMPARE(alias.value(), int()); + QCOMPARE(value1, 22); + QCOMPARE(value2, 22); +} + QTEST_MAIN(tst_QProperty); #include "tst_qproperty.moc"