QBindable: Make ordinary Q_PROPERTYs bindable

Implements an adaptor from the notification signal of a Q_PROPERTY to
QBindable. The Q_PROPERTY does not need to be BINDABLE, but can still
be bound or used in a binding.

[ChangeLog][Core][Q_PROPERTY] Q_PROPERTYs without BINDABLE can be wrapped in QBindable to make them usable in bindings

Change-Id: Id0ca5444b93a371ba8720a38f3607925d393d98a
Reviewed-by: Fabian Kosmale <fabian.kosmale@qt.io>
This commit is contained in:
Patrick Stewart 2022-11-09 00:30:42 +00:00
parent 84d4b21f69
commit 4fb96669e3
8 changed files with 501 additions and 0 deletions

View File

@ -247,4 +247,18 @@
be called for the current value of the property, register your callback using
subscribe() instead.
\section1 Interaction with Q_PROPERTYs
A \l {The Property System}{Q_PROPERTY} that defines \c BINDABLE can be bound and
used in binding expressions. You can implement such properties using \l {QProperty},
\l {QObjectBindableProperty}, or \l {QObjectComputedProperty}.
Q_PROPERTYs without \c BINDABLE can also be bound and be used in binding expressions,
as long as they define a \c NOTIFY signal. You must wrap the property in a \l QBindable
using the \c {QBindable(QObject* obj, const char* property)} constructor. Then, the
property can be bound using \c \l QBindable::setBinding() or used in a binding
expression via \c \l QBindable::value(). You must use \c QBindable::value() in binding
expressions instead of the normal property \c READ function (or \c MEMBER) to enable
dependency tracking if the property is not \c BINDABLE.
*/

View File

@ -5358,6 +5358,31 @@ inline bool QObjectPrivate::removeConnection(QObjectPrivate::Connection *c)
return true;
}
/*!
\internal
Used by QPropertyAdaptorSlotObject to get an existing instance for a property, if available
*/
QtPrivate::QPropertyAdaptorSlotObject *
QObjectPrivate::getPropertyAdaptorSlotObject(const QMetaProperty &property)
{
if (auto conns = connections.loadRelaxed()) {
Q_Q(QObject);
const QMetaObject *metaObject = q->metaObject();
int signal_index = methodIndexToSignalIndex(&metaObject, property.notifySignalIndex());
auto connectionList = conns->connectionsForSignal(signal_index);
for (auto c = connectionList.first.loadRelaxed(); c;
c = c->nextConnectionList.loadRelaxed()) {
if (c->isSlotObject) {
if (auto p = QtPrivate::QPropertyAdaptorSlotObject::cast(c->slotObj,
property.propertyIndex()))
return p;
}
}
}
return nullptr;
}
/*! \class QMetaObject::Connection
\inmodule QtCore
Represents a handle to a signal-slot (or signal-functor) connection.

View File

@ -179,6 +179,9 @@ public:
virtual std::string flagsForDumping() const;
QtPrivate::QPropertyAdaptorSlotObject *
getPropertyAdaptorSlotObject(const QMetaProperty &property);
public:
mutable ExtraData *extraData; // extra data set by the user
// This atomic requires acquire/release semantics in a few places,

View File

@ -361,6 +361,7 @@ namespace QtPrivate {
inline bool compare(void **a) { bool ret = false; m_impl(Compare, this, nullptr, a, &ret); return ret; }
inline void call(QObject *r, void **a) { m_impl(Call, this, r, a, nullptr); }
bool isImpl(ImplFn f) const { return m_impl == f; }
protected:
~QSlotObjectBase() {}
private:

View File

@ -8,6 +8,9 @@
#include <QScopeGuard>
#include <QtCore/qloggingcategory.h>
#include <QThread>
#include <QtCore/qmetaobject.h>
#include "qobject_p.h"
QT_BEGIN_NAMESPACE
@ -1135,6 +1138,31 @@ QString QPropertyBindingError::description() const
QObjectComputedProperty, {Qt Bindable Properties}
*/
/*!
\fn template<typename T> QBindable<T>::QBindable(QObject *obj, const char *property)
Constructs a QBindable for the \l Q_PROPERTY \a property on \a obj. The property must
have a notify signal but does not need to have \c BINDABLE in its \c Q_PROPERTY
definition, so even binding unaware \c {Q_PROPERTY}s can be bound or used in binding
expressions. You must use \c QBindable::value() in binding expressions instead of the
normal property \c READ function (or \c MEMBER) to enable dependency tracking if the
property is not \c BINDABLE. When binding using a lambda, you may prefer to capture the
QBindable by value to avoid the cost of calling this constructor in the binding
expression.
This constructor should not be used to implement \c BINDABLE for a Q_PROPERTY, as the
resulting Q_PROPERTY will not support dependency tracking. To make a property that is
usable directly without reading through a QBindable use \l QProperty or
\l QObjectBindableProperty.
\sa QProperty, QObjectBindableProperty, {Qt Bindable Properties}
*/
/*!
\fn template<typename T> QBindable<T>::QBindable(QObject *obj, const QMetaProperty &property)
See \c \l QBindable::QBindable(QObject *obj, const char *property)
*/
/*!
\fn template<typename T> QPropertyBinding<T> QBindable<T>::makeBinding(const QPropertyBindingSourceLocation &location) const
@ -2386,6 +2414,154 @@ void printMetaTypeMismatch(QMetaType actual, QMetaType expected)
*/
QBindingStatus* getBindingStatus(QtPrivate::QBindingStatusAccessToken) { return &QT_PREPEND_NAMESPACE(bindingStatus); }
namespace PropertyAdaptorSlotObjectHelpers {
void getter(const QUntypedPropertyData *d, void *value)
{
auto adaptor = static_cast<const QtPrivate::QPropertyAdaptorSlotObject *>(d);
adaptor->bindingData().registerWithCurrentlyEvaluatingBinding();
auto mt = adaptor->metaProperty().metaType();
mt.destruct(value);
mt.construct(value, adaptor->metaProperty().read(adaptor->object()).data());
}
void setter(QUntypedPropertyData *d, const void *value)
{
auto adaptor = static_cast<QtPrivate::QPropertyAdaptorSlotObject *>(d);
adaptor->bindingData().removeBinding();
adaptor->metaProperty().write(adaptor->object(),
QVariant(adaptor->metaProperty().metaType(), value));
}
QUntypedPropertyBinding getBinding(const QUntypedPropertyData *d)
{
auto adaptor = static_cast<const QtPrivate::QPropertyAdaptorSlotObject *>(d);
return QUntypedPropertyBinding(adaptor->bindingData().binding());
}
bool bindingWrapper(QMetaType type, QUntypedPropertyData *d,
QtPrivate::QPropertyBindingFunction binding, QUntypedPropertyData *temp,
void *value)
{
auto adaptor = static_cast<const QtPrivate::QPropertyAdaptorSlotObject *>(d);
type.destruct(value);
type.construct(value, adaptor->metaProperty().read(adaptor->object()).data());
if (binding.vtable->call(type, temp, binding.functor)) {
adaptor->metaProperty().write(adaptor->object(), QVariant(type, value));
return true;
}
return false;
}
QUntypedPropertyBinding setBinding(QUntypedPropertyData *d, const QUntypedPropertyBinding &binding,
QPropertyBindingWrapper wrapper)
{
auto adaptor = static_cast<QPropertyAdaptorSlotObject *>(d);
return adaptor->bindingData().setBinding(binding, d, nullptr, wrapper);
}
void setObserver(const QUntypedPropertyData *d, QPropertyObserver *observer)
{
observer->setSource(static_cast<const QPropertyAdaptorSlotObject *>(d)->bindingData());
}
}
QPropertyAdaptorSlotObject::QPropertyAdaptorSlotObject(QObject *o, const QMetaProperty &p)
: QSlotObjectBase(&impl), obj(o), metaProperty_(p)
{
}
void QPropertyAdaptorSlotObject::impl(int which, QSlotObjectBase *this_, QObject *r, void **a,
bool *ret)
{
auto self = static_cast<QPropertyAdaptorSlotObject *>(this_);
switch (which) {
case Destroy:
delete self;
break;
case Call:
if (!self->bindingData_.hasBinding())
self->bindingData_.notifyObservers(self);
break;
case Compare:
case NumOperations:
Q_UNUSED(r);
Q_UNUSED(a);
Q_UNUSED(ret);
break;
}
}
} // namespace QtPrivate end
QUntypedBindable::QUntypedBindable(QObject *obj, const QMetaProperty &metaProperty,
const QtPrivate::QBindableInterface *i)
: iface(i)
{
if (!obj)
return;
if (!metaProperty.isValid()) {
qCWarning(lcQPropertyBinding) << "QUntypedBindable: Property is not valid";
return;
}
if (metaProperty.isBindable()) {
*this = metaProperty.bindable(obj);
return;
}
if (!metaProperty.hasNotifySignal()) {
qCWarning(lcQPropertyBinding)
<< "QUntypedBindable: Property" << metaProperty.name() << "has no notify signal";
return;
}
auto metatype = iface->metaType();
if (metaProperty.metaType() != metatype) {
qCWarning(lcQPropertyBinding) << "QUntypedBindable: Property" << metaProperty.name()
<< "of type" << metaProperty.metaType().name()
<< "does not match requested type" << metatype.name();
return;
}
// Test for name pointer equality proves it's exactly the same property
if (obj->metaObject()->property(metaProperty.propertyIndex()).name() != metaProperty.name()) {
qCWarning(lcQPropertyBinding) << "QUntypedBindable: Property" << metaProperty.name()
<< "does not belong to this object";
return;
}
// Get existing binding data if it exists
auto adaptor = QObjectPrivate::get(obj)->getPropertyAdaptorSlotObject(metaProperty);
if (!adaptor) {
adaptor = new QPropertyAdaptorSlotObject(obj, metaProperty);
auto c = QObjectPrivate::connect(obj, metaProperty.notifySignalIndex(), obj, adaptor,
Qt::DirectConnection);
Q_ASSERT(c);
}
data = adaptor;
}
QUntypedBindable::QUntypedBindable(QObject *obj, const char *property,
const QtPrivate::QBindableInterface *i)
: QUntypedBindable(
obj,
[=]() -> QMetaProperty {
if (!obj)
return {};
auto propertyIndex = obj->metaObject()->indexOfProperty(property);
if (propertyIndex < 0) {
qCWarning(lcQPropertyBinding)
<< "QUntypedBindable: No property named" << property;
return {};
}
return obj->metaObject()->property(propertyIndex);
}(),
i)
{
}
QT_END_NAMESPACE

View File

@ -597,6 +597,60 @@ enum Reason { InvalidInterface, NonBindableInterface, ReadOnlyInterface };
Q_CORE_EXPORT void printUnsuitableBindableWarning(QAnyStringView prefix, Reason reason);
Q_CORE_EXPORT void printMetaTypeMismatch(QMetaType actual, QMetaType expected);
}
namespace PropertyAdaptorSlotObjectHelpers {
Q_CORE_EXPORT void getter(const QUntypedPropertyData *d, void *value);
Q_CORE_EXPORT void setter(QUntypedPropertyData *d, const void *value);
Q_CORE_EXPORT QUntypedPropertyBinding getBinding(const QUntypedPropertyData *d);
Q_CORE_EXPORT bool bindingWrapper(QMetaType type, QUntypedPropertyData *d,
QtPrivate::QPropertyBindingFunction binding,
QUntypedPropertyData *temp, void *value);
Q_CORE_EXPORT QUntypedPropertyBinding setBinding(QUntypedPropertyData *d,
const QUntypedPropertyBinding &binding,
QPropertyBindingWrapper wrapper);
Q_CORE_EXPORT void setObserver(const QUntypedPropertyData *d, QPropertyObserver *observer);
template<typename T>
bool bindingWrapper(QMetaType type, QUntypedPropertyData *d,
QtPrivate::QPropertyBindingFunction binding)
{
struct Data : QPropertyData<T>
{
void *data() { return &this->val; }
} temp;
return bindingWrapper(type, d, binding, &temp, temp.data());
}
template<typename T>
QUntypedPropertyBinding setBinding(QUntypedPropertyData *d, const QUntypedPropertyBinding &binding)
{
return setBinding(d, binding, &bindingWrapper<T>);
}
template<typename T>
QUntypedPropertyBinding makeBinding(const QUntypedPropertyData *d,
const QPropertyBindingSourceLocation &location)
{
return Qt::makePropertyBinding(
[d]() -> T {
T r;
getter(d, &r);
return r;
},
location);
}
template<class T>
inline constexpr QBindableInterface iface = {
&getter,
&setter,
&getBinding,
&setBinding<T>,
&makeBinding<T>,
&setObserver,
&QMetaType::fromType<T>,
};
}
}
class QUntypedBindable
@ -609,6 +663,9 @@ protected:
: data(d), iface(i)
{}
Q_CORE_EXPORT QUntypedBindable(QObject* obj, const QMetaProperty &property, const QtPrivate::QBindableInterface *i);
Q_CORE_EXPORT QUntypedBindable(QObject* obj, const char* property, const QtPrivate::QBindableInterface *i);
public:
constexpr QUntypedBindable() = default;
template<typename Property>
@ -745,6 +802,12 @@ public:
}
}
QBindable(QObject *obj, const QMetaProperty &property)
: QUntypedBindable(obj, property, &QtPrivate::PropertyAdaptorSlotObjectHelpers::iface<T>) {}
QBindable(QObject *obj, const char *property)
: QUntypedBindable(obj, property, &QtPrivate::PropertyAdaptorSlotObjectHelpers::iface<T>) {}
QPropertyBinding<T> makeBinding(const QPropertyBindingSourceLocation &location = QT_PROPERTY_DEFAULT_BINDING_LOCATION) const
{
return static_cast<QPropertyBinding<T> &&>(QUntypedBindable::makeBinding(location));

View File

@ -18,8 +18,10 @@
#include <private/qglobal_p.h>
#include <qproperty.h>
#include <qmetaobject.h>
#include <qscopedpointer.h>
#include <qscopedvaluerollback.h>
#include <qvariant.h>
#include <vector>
#include <QtCore/QVarLengthArray>
@ -898,6 +900,37 @@ QPropertyBindingPrivate *QBindingObserverPtr::binding() const noexcept { return
QPropertyObserver *QBindingObserverPtr::operator->() { return d; }
namespace QtPrivate {
class QPropertyAdaptorSlotObject : public QUntypedPropertyData, public QSlotObjectBase
{
QPropertyBindingData bindingData_;
QObject *obj;
QMetaProperty metaProperty_;
static void impl(int which, QSlotObjectBase *this_, QObject *r, void **a, bool *ret);
QPropertyAdaptorSlotObject(QObject *o, const QMetaProperty& p);
public:
static QPropertyAdaptorSlotObject *cast(QSlotObjectBase *ptr, int propertyIndex)
{
if (ptr->isImpl(&QPropertyAdaptorSlotObject::impl)) {
auto p = static_cast<QPropertyAdaptorSlotObject *>(ptr);
if (p->metaProperty_.propertyIndex() == propertyIndex)
return p;
}
return nullptr;
}
inline const QPropertyBindingData &bindingData() const { return bindingData_; }
inline QPropertyBindingData &bindingData() { return bindingData_; }
inline QObject *object() const { return obj; }
inline const QMetaProperty &metaProperty() const { return metaProperty_; }
friend class QT_PREPEND_NAMESPACE(QUntypedBindable);
};
}
QT_END_NAMESPACE
#endif // QPROPERTY_P_H

View File

@ -99,6 +99,8 @@ private slots:
void scheduleNotify();
void notifyAfterAllDepsGone();
void propertyAdaptorBinding();
};
void tst_QProperty::functorBinding()
@ -1663,6 +1665,190 @@ void tst_QProperty::noFakeDependencies()
QCOMPARE(old, bindingFunctionCalled);
}
class PropertyAdaptorTester : public QObject
{
Q_OBJECT
Q_PROPERTY(int foo READ foo WRITE setFoo NOTIFY fooChanged)
Q_PROPERTY(int foo1 READ foo WRITE setFoo)
signals:
void fooChanged(int newFoo);
public slots:
void fooHasChanged() { fooChangedCount++; }
public:
int foo() const { return fooData; }
void setFoo(int i)
{
if (i != fooData) {
fooData = i;
fooChanged(fooData);
}
}
public:
int fooData = 0;
int fooChangedCount = 0;
};
void tst_QProperty::propertyAdaptorBinding()
{
QProperty<int> source { 5 };
QProperty<int> dest1 { 99 };
QProperty<int> dest2 { 98 };
// Check binding of non BINDABLE property
PropertyAdaptorTester object;
QObject::connect(&object, &PropertyAdaptorTester::fooChanged, &object,
&PropertyAdaptorTester::fooHasChanged);
QBindable<int> binding(&object, "foo");
binding.setBinding([&]() { return source + 1; });
QCOMPARE(object.foo(), 6);
QCOMPARE(object.fooChangedCount, 1);
struct MyBindable : QBindable<int> {
using QBindable<int>::QBindable;
QtPrivate::QPropertyAdaptorSlotObject* data() {
return static_cast<QtPrivate::QPropertyAdaptorSlotObject*>(QUntypedBindable::data);
}
} dataBinding(&object, "foo");
QPropertyBindingDataPointer data{&dataBinding.data()->bindingData()};
QCOMPARE(data.observerCount(), 0);
dest1.setBinding(binding.makeBinding());
QCOMPARE(data.observerCount(), 1);
dest2.setBinding([=]() { return binding.value() + 1; });
binding = {};
QCOMPARE(data.observerCount(), 2);
// Check addNotifer
{
int local_foo = 0;
auto notifier = QBindable<int>(&object, "foo").addNotifier([&]() { local_foo++; });
QCOMPARE(data.observerCount(), 3);
QCOMPARE(object.foo(), 6);
QCOMPARE(dest1.value(), 6);
QCOMPARE(dest2.value(), 7);
QCOMPARE(local_foo, 0);
QCOMPARE(object.fooChangedCount, 1);
source = 7;
QCOMPARE(object.foo(), 8);
QCOMPARE(dest1.value(), 8);
QCOMPARE(dest2.value(), 9);
QCOMPARE(local_foo, 1);
QCOMPARE(object.fooChangedCount, 2);
}
QCOMPARE(data.observerCount(), 2);
// Check a new QBindable object can override the existing binding
QBindable<int>(&object, "foo").setValue(10);
QCOMPARE(object.foo(), 10);
QCOMPARE(dest1.value(), 10);
QCOMPARE(dest2.value(), 11);
QCOMPARE(object.fooChangedCount, 3);
source.setValue(99);
QCOMPARE(object.foo(), 10);
QCOMPARE(dest1.value(), 10);
QCOMPARE(dest2.value(), 11);
QCOMPARE(object.fooChangedCount, 3);
object.setFoo(12);
QCOMPARE(object.foo(), 12);
QCOMPARE(dest1.value(), 12);
QCOMPARE(dest2.value(), 13);
QCOMPARE(object.fooChangedCount, 4);
// Check binding multiple notifiers
QProperty<int> source2 { 20 };
source.setValue(21);
binding = QBindable<int>(&object, "foo");
binding.setBinding([&]() { return source + source2; });
QCOMPARE(object.foo(), 41);
QCOMPARE(dest1.value(), 41);
QCOMPARE(object.fooChangedCount, 5);
source.setValue(22);
QCOMPARE(object.foo(), 42);
QCOMPARE(dest1.value(), 42);
QCOMPARE(object.fooChangedCount, 6);
source2.setValue(21);
QCOMPARE(object.foo(), 43);
QCOMPARE(dest1.value(), 43);
QCOMPARE(object.fooChangedCount, 7);
// Check update group
Qt::beginPropertyUpdateGroup();
source.setValue(23);
source2.setValue(22);
QCOMPARE(object.foo(), 43);
QCOMPARE(dest1.value(), 43);
QCOMPARE(object.fooChangedCount, 7);
Qt::endPropertyUpdateGroup();
QCOMPARE(object.foo(), 45);
QCOMPARE(dest1.value(), 45);
QCOMPARE(object.fooChangedCount, 8);
PropertyAdaptorTester object2;
PropertyAdaptorTester object3;
// Check multiple observers
QBindable<int> binding2(&object2, "foo");
QBindable<int> binding3(&object3, "foo");
binding.setBinding([=]() { return binding2.value(); });
binding3.setBinding([=]() { return binding.value(); });
QCOMPARE(object.foo(), 0);
QCOMPARE(object2.foo(), 0);
QCOMPARE(object3.foo(), 0);
QCOMPARE(dest1.value(), 0);
object2.setFoo(1);
QCOMPARE(object.foo(), 1);
QCOMPARE(object2.foo(), 1);
QCOMPARE(object3.foo(), 1);
QCOMPARE(dest1.value(), 1);
// Check interoperation with BINDABLE properties
MyQObject bindableObject;
bindableObject.fooData.setBinding([]() { return 5; });
QVERIFY(bindableObject.fooData.hasBinding());
QVERIFY(!bindableObject.barData.hasBinding());
QVERIFY(QBindable<int>(&bindableObject, "foo").hasBinding());
QBindable<int> bindableBar(&bindableObject, "bar");
QVERIFY(!bindableBar.hasBinding());
bindableBar.setBinding([]() { return 6; });
QVERIFY(bindableBar.hasBinding());
QVERIFY(bindableObject.barData.hasBinding());
// Check bad arguments
#ifndef QT_NO_DEBUG
QTest::ignoreMessage(QtMsgType::QtWarningMsg, "QUntypedBindable: Property is not valid");
#endif
QVERIFY(!QBindable<int>(&object, QMetaProperty{}).isValid());
#ifndef QT_NO_DEBUG
QTest::ignoreMessage(QtMsgType::QtWarningMsg, "QUntypedBindable: Property foo1 has no notify signal");
#endif
QVERIFY(!QBindable<int>(&object, "foo1").isValid());
#ifndef QT_NO_DEBUG
QTest::ignoreMessage(QtMsgType::QtWarningMsg, "QUntypedBindable: Property foo of type int does not match requested type bool");
#endif
QVERIFY(!QBindable<bool>(&object, "foo").isValid());
#ifndef QT_NO_DEBUG
QTest::ignoreMessage(QtMsgType::QtWarningMsg,
"QUntypedBindable: Property foo does not belong to this object");
#endif
QObject qobj;
QVERIFY(!QBindable<int>(
&qobj,
object.metaObject()->property(object.metaObject()->indexOfProperty("foo")))
.isValid());
#ifndef QT_NO_DEBUG
QTest::ignoreMessage(QtMsgType::QtWarningMsg, "QUntypedBindable: No property named fizz");
QTest::ignoreMessage(QtMsgType::QtWarningMsg, "QUntypedBindable: Property is not valid");
#endif
QVERIFY(!QBindable<int>(&object, "fizz").isValid());
}
#if QT_CONFIG(thread)
struct ThreadSafetyTester : public QObject
{