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 <fabian.kosmale@qt.io>
Reviewed-by: Lars Knoll <lars.knoll@qt.io>
This commit is contained in:
Ulf Hermann 2020-05-25 12:54:51 +02:00
parent 0a07f9528c
commit a64a0ce331
4 changed files with 446 additions and 10 deletions

View File

@ -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<QPropertyObserver*>(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,6 +373,7 @@ void QPropertyObserverPointer::notify(QPropertyBindingPrivate *triggeringBinding
void QPropertyObserverPointer::observeProperty(QPropertyBasePointer property)
{
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\<T\> wraps a pointer to a QProperty\<T\> and automatically
invalidates itself when the QProperty\<T\> is destroyed. It forwards all
method invocations to the wrapped property. For example:
\code
QProperty<QString> *name = new QProperty<QString>("John");
QProperty<int> age(41);
QPropertyAlias<QString> nameAlias(name);
QPropertyAlias<int> ageAlias(&age);
QPropertyAlias<QString> 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 <typename T> QPropertyAlias<T>::QPropertyAlias(QProperty<T> *property)
Constructs a property alias for the given \a property.
*/
/*!
\fn template <typename T> explicit QPropertyAlias<T>::QPropertyAlias(QPropertyAlias<T> *alias)
Constructs a property alias for the property aliased by \a alias.
*/
/*!
\fn template <typename T> T QPropertyAlias<T>::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 <typename T> QPropertyAlias<T>::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 <typename T> void QPropertyAlias<T>::setValue(const T &newValue)
Assigns \a newValue to the aliased property and removes the property's
associated binding, if present.
*/
/*!
\fn template <typename T> void QPropertyAlias<T>::setValue(T &&newValue)
\overload
Assigns \a newValue to the aliased property and removes the property's
associated binding, if present.
*/
/*!
\fn template <typename T> QPropertyAlias<T> &QPropertyAlias<T>::operator=(const T &newValue)
Assigns \a newValue to the aliased property and returns a reference to this
QPropertyAlias.
*/
/*!
\fn template <typename T> QPropertyAlias<T> &QPropertyAlias<T>::operator=(T &&newValue)
\overload
Assigns \a newValue to the aliased property and returns a reference to this
QPropertyAlias.
*/
/*!
\fn template <typename T> QPropertyAlias<T> &QPropertyAlias<T>::operator=(const QPropertyBinding<T> &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 <typename T> QPropertyBinding<T> QPropertyAlias<T>::setBinding(const QPropertyBinding<T> &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<T>.
*/
/*!
\fn template <typename T> QPropertyBinding<T> QPropertyAlias<T>::setBinding(QPropertyBinding<T> &&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<T>.
*/
/*!
\fn template <typename T> QPropertyBinding<T> bool QPropertyAlias<T>::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 <typename T> template <typename Functor> QPropertyBinding<T> 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<T>.
*/
/*!
\fn template <typename T> bool QPropertyAlias<T>::hasBinding() const
Returns true if the aliased property is associated with a binding; false
otherwise.
*/
/*!
\fn template <typename T> QPropertyBinding<T> QPropertyAlias<T>::binding() const
Returns the binding expression that is associated with the aliased property. A
default constructed QPropertyBinding<T> will be returned if no such
association exists.
*/
/*!
\fn template <typename T> QPropertyBinding<T> QPropertyAlias<T>::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 <typename T> template <typename Functor> QPropertyChangeHandler<T, Functor> QPropertyAlias<T>::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 <typename T> template <typename Functor> QPropertyChangeHandler<T, Functor> QPropertyAlias<T>::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 <typename T> bool QPropertyAlias<T>::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

View File

@ -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<typename PropertyType>
QProperty<PropertyType> *aliasedProperty() const
{
return reinterpret_cast<QProperty<PropertyType> *>(aliasedPropertyPtr);
}
private:
void setSource(QtPrivate::QPropertyBase &property);
QTaggedPointer<QPropertyObserver, ObserverTags> next;
QTaggedPointer<QPropertyObserver, ObserverTag> 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<QPropertyObserver, ObserverTags> prev;
QtPrivate::QTagPreservingPointerToPointer<QPropertyObserver, ObserverTag> 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 <typename Functor>
class QPropertyChangeHandler : public QPropertyObserver
{
@ -487,6 +493,145 @@ struct QPropertyMemberChangeHandler<PropertyMember, Callback> : public QProperty
}
};
template<typename T>
class QPropertyAlias : public QPropertyObserver
{
Q_DISABLE_COPY_MOVE(QPropertyAlias)
public:
QPropertyAlias(QProperty<T> *property)
: QPropertyObserver(property)
{
if (property)
setSource(*property);
}
QPropertyAlias(QPropertyAlias<T> *alias)
: QPropertyAlias(alias->aliasedProperty<T>())
{}
T value() const
{
if (auto *p = aliasedProperty<T>())
return p->value();
return T();
}
operator T() const { return value(); }
void setValue(T &&newValue)
{
if (auto *p = aliasedProperty<T>())
p->setValue(std::move(newValue));
}
void setValue(const T &newValue)
{
if (auto *p = aliasedProperty<T>())
p->setValue(newValue);
}
QPropertyAlias<T> &operator=(T &&newValue)
{
if (auto *p = aliasedProperty<T>())
*p = std::move(newValue);
return *this;
}
QPropertyAlias<T> &operator=(const T &newValue)
{
if (auto *p = aliasedProperty<T>())
*p = newValue;
return *this;
}
QPropertyAlias<T> &operator=(const QPropertyBinding<T> &newBinding)
{
setBinding(newBinding);
return *this;
}
QPropertyAlias<T> &operator=(QPropertyBinding<T> &&newBinding)
{
setBinding(std::move(newBinding));
return *this;
}
QPropertyBinding<T> setBinding(const QPropertyBinding<T> &newBinding)
{
if (auto *p = aliasedProperty<T>())
return p->setBinding(newBinding);
return QPropertyBinding<T>();
}
QPropertyBinding<T> setBinding(QPropertyBinding<T> &&newBinding)
{
if (auto *p = aliasedProperty<T>())
return p->setBinding(std::move(newBinding));
return QPropertyBinding<T>();
}
bool setBinding(const QUntypedPropertyBinding &newBinding)
{
if (auto *p = aliasedProperty<T>())
return p->setBinding(newBinding);
return false;
}
#ifndef Q_CLANG_QDOC
template <typename Functor>
QPropertyBinding<T> setBinding(Functor &&f,
const QPropertyBindingSourceLocation &location = QT_PROPERTY_DEFAULT_BINDING_LOCATION,
std::enable_if_t<std::is_invocable_v<Functor>> * = nullptr)
{
return setBinding(Qt::makePropertyBinding(std::forward<Functor>(f), location));
}
#else
template <typename Functor>
QPropertyBinding<T> setBinding(Functor f);
#endif
bool hasBinding() const
{
if (auto *p = aliasedProperty<T>())
return p->hasBinding();
return false;
}
QPropertyBinding<T> binding() const
{
if (auto *p = aliasedProperty<T>())
return p->binding();
return QPropertyBinding<T>();
}
QPropertyBinding<T> takeBinding()
{
if (auto *p = aliasedProperty<T>())
return p->takeBinding();
return QPropertyBinding<T>();
}
template<typename Functor>
QPropertyChangeHandler<Functor> onValueChanged(Functor f)
{
if (auto *p = aliasedProperty<T>())
return p->onValueChanged(f);
return QPropertyChangeHandler<Functor>(f);
}
template<typename Functor>
QPropertyChangeHandler<Functor> subscribe(Functor f)
{
if (auto *p = aliasedProperty<T>())
return p->subscribe(f);
return QPropertyChangeHandler<Functor>(f);
}
bool isValid() const
{
return aliasedProperty<T>() != nullptr;
}
};
QT_END_NAMESPACE

View File

@ -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);

View File

@ -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<QProperty<int>> property(new QProperty<int>);
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"