Add support for bindable properties to QObject

Add Q_OBJECT_BINDABLE_PROPERTY() macro that can be used to define
a bindable property inside QObject.

The macro and the class behind it creates storage for a property
that is bindable inside a QObject or QObjectPrivate. The property
only uses as much space as the data contained, ie. it has no
storage overhead, as long as no bindings are being used.

Bindings are being stored and looked up in the QBindingStorage
associated with the owning object.

Change-Id: I1dadd7bddbad6fbf10cfa791d6461574b9db82dd
Reviewed-by: Volker Hilsheimer <volker.hilsheimer@qt.io>
bb10
Lars Knoll 2020-08-25 12:27:56 +02:00
parent af149ada60
commit 3b3b190eef
4 changed files with 532 additions and 12 deletions

View File

@ -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 <typename Class, typename T, auto offset, auto Callback> QBindablePropertyData<Class, T, offset, Callback>::QBindablePropertyData()
Constructs a property with a default constructed instance of T.
*/
/*!
\fn template <typename Class, typename T, auto offset, auto Callback> explicit QBindablePropertyData<Class, T, offset, Callback>::QBindablePropertyData(const T &initialValue)
Constructs a property with the provided \a initialValue.
*/
/*!
\fn template <typename Class, typename T, auto offset, auto Callback> explicit QBindablePropertyData<Class, T, offset, Callback>::QBindablePropertyData(T &&initialValue)
Move-Constructs a property with the provided \a initialValue.
*/
/*!
\fn template <typename Class, typename T, auto offset, auto Callback> QBindablePropertyData<Class, T, offset, Callback>::QBindablePropertyData(Class *owner, const QPropertyBinding<T> &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 <typename Class, typename T, auto offset, auto Callback> QBindablePropertyData<Class, T, offset, Callback>::QBindablePropertyData(Class *owner, QPropertyBinding<T> &&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 <typename Class, typename T, auto offset, auto Callback> template <typename Functor> QBindablePropertyData<Class, T, offset, Callback>::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 <typename Class, typename T, auto offset, auto Callback> QBindablePropertyData<Class, T, offset, Callback>::~QBindablePropertyData()
Destroys the property.
*/
/*!
\fn template <typename Class, typename T, auto offset, auto Callback> T QBindablePropertyData<Class, T, offset, Callback>::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 <typename Class, typename T, auto offset, auto Callback> QBindablePropertyData<Class, T, offset, Callback>::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 <typename Class, typename T, auto offset, auto Callback> void QBindablePropertyData<Class, T, offset, Callback>::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 <typename Class, typename T, auto offset, auto Callback> void QBindablePropertyData<Class, T, offset, Callback>::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 <typename Class, typename T, auto offset, auto Callback> QPropertyBinding<T> QBindablePropertyData<Class, T, offset, Callback>::setBinding(Class *owner, const QPropertyBinding<T> &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 <typename Class, typename T, auto offset, auto Callback> template <typename Functor> QPropertyBinding<T> QBindablePropertyData<Class, T, offset, Callback>::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 <typename Class, typename T, auto offset, auto Callback> QPropertyBinding<T> QBindablePropertyData<Class, T, offset, Callback>::setBinding(Class *owner, QPropertyBinding<T> &&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 <typename Class, typename T, auto offset, auto Callback> QPropertyBinding<T> bool QBindablePropertyData<Class, T, offset, Callback>::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 <typename Class, typename T, auto offset, auto Callback> bool QBindablePropertyData<Class, T, offset, Callback>::hasBinding() const
Returns true if the property is associated with a binding; false otherwise.
*/
/*!
\fn template <typename Class, typename T, auto offset, auto Callback> QPropertyBinding<T> QBindablePropertyData<Class, T, offset, Callback>::binding() const
Returns the binding expression that is associated with this property. A
default constructed QPropertyBinding<T> will be returned if no such
association exists.
*/
/*!
\fn template <typename Class, typename T, auto offset, auto Callback> QPropertyBinding<T> QBindablePropertyData<Class, T, offset, Callback>::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 <typename Class, typename T, auto offset, auto Callback> template <typename Functor> QPropertyChangeHandler<T, Functor> QBindablePropertyData<Class, T, offset, Callback>::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 <typename Class, typename T, auto offset, auto Callback> template <typename Functor> QPropertyChangeHandler<T, Functor> QBindablePropertyData<Class, T, offset, Callback>::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 <typename T> QtPrivate::QPropertyBase &QBindablePropertyData<Class, T, offset, Callback>::propertyBase() const
\internal
*/
/*!
\class QPropertyChangeHandler

View File

@ -742,6 +742,193 @@ public:
QtPrivate::QPropertyBindingData *bindingData(QUntypedPropertyData *data, bool create);
};
template<typename Class, typename T, auto Offset, auto Signal = nullptr>
class QObjectBindableProperty : public QPropertyData<T>
{
using ThisType = QObjectBindableProperty<Class, T, Offset, Signal>;
static bool constexpr HasSignal = !std::is_same_v<decltype(Signal), std::nullptr_t>;
Class *owner()
{
char *that = reinterpret_cast<char *>(this);
return reinterpret_cast<Class *>(that - QtPrivate::detail::getOffset(Offset));
}
const Class *owner() const
{
char *that = const_cast<char *>(reinterpret_cast<const char *>(this));
return reinterpret_cast<Class *>(that - QtPrivate::detail::getOffset(Offset));
}
static void signalCallBack(QUntypedPropertyData *o)
{
QObjectBindableProperty *that = static_cast<QObjectBindableProperty *>(o);
if constexpr (HasSignal)
(that->owner()->*Signal)();
}
public:
using value_type = typename QPropertyData<T>::value_type;
using parameter_type = typename QPropertyData<T>::parameter_type;
using rvalue_ref = typename QPropertyData<T>::rvalue_ref;
using arrow_operator_result = typename QPropertyData<T>::arrow_operator_result;
QObjectBindableProperty() = default;
explicit QObjectBindableProperty(const T &initialValue) : QPropertyData<T>(initialValue) {}
explicit QObjectBindableProperty(T &&initialValue) : QPropertyData<T>(std::move(initialValue)) {}
explicit QObjectBindableProperty(const QPropertyBinding<T> &binding)
: QObjectBindableProperty()
{ setBinding(binding); }
#ifndef Q_CLANG_QDOC
template <typename Functor>
explicit QObjectBindableProperty(Functor &&f, const QPropertyBindingSourceLocation &location = QT_PROPERTY_DEFAULT_BINDING_LOCATION,
typename std::enable_if_t<std::is_invocable_r_v<T, Functor&>> * = 0)
: QObjectBindableProperty(QPropertyBinding<T>(std::forward<Functor>(f), location))
{}
#else
template <typename Functor>
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<T>) {
return value();
} else if constexpr (std::is_pointer_v<T>) {
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<T> setBinding(const QPropertyBinding<T> &newBinding)
{
QtPrivate::QPropertyBindingData *bd = qGetBindingStorage(owner())->bindingData(this, true);
QUntypedPropertyBinding oldBinding(bd->setBinding(newBinding, this, HasSignal ? &signalCallBack : nullptr));
notify(bd);
return static_cast<QPropertyBinding<T> &>(oldBinding);
}
bool setBinding(const QUntypedPropertyBinding &newBinding)
{
if (!newBinding.isNull() && newBinding.valueMetaType().id() != qMetaTypeId<T>())
return false;
setBinding(static_cast<const QPropertyBinding<T> &>(newBinding));
return true;
}
#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 {
auto *bd = qGetBindingStorage(owner())->bindingData(this);
return bd && bd->binding() != nullptr;
}
QPropertyBinding<T> binding() const
{
auto *bd = qGetBindingStorage(owner())->bindingData(this);
return static_cast<QPropertyBinding<T> &&>(QUntypedPropertyBinding(bd ? bd->binding() : nullptr));
}
QPropertyBinding<T> takeBinding()
{
return setBinding(QPropertyBinding<T>());
}
template<typename Functor>
QPropertyChangeHandler<Functor> onValueChanged(Functor f)
{
static_assert(std::is_invocable_v<Functor>, "Functor callback must be callable without any parameters");
return QPropertyChangeHandler<Functor>(*this, f);
}
template<typename Functor>
QPropertyChangeHandler<Functor> subscribe(Functor f)
{
static_assert(std::is_invocable_v<Functor>, "Functor callback must be callable without any parameters");
f();
return onValueChanged(f);
}
const QtPrivate::QPropertyBindingData &bindingData() const
{
auto *storage = const_cast<QBindingStorage *>(qGetBindingStorage(owner()));
return *storage->bindingData(const_cast<ThisType *>(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<Class, Type, Class::_qt_property_##name##_offset, __VA_ARGS__> name;
QT_END_NAMESPACE
#endif // QPROPERTY_H

View File

@ -178,6 +178,15 @@ namespace detail {
template<typename T, typename C>
struct ExtractClassFromFunctionPointer<T C::*> { 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

View File

@ -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<int> 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<QProperty<int>, 5> otherProperties = {
QProperty<int>([&]() { return instance.property + 1; }),
QProperty<int>([&]() { return instance.property + 2; }),
QProperty<int>([&]() { return instance.property + 3; }),
QProperty<int>([&]() { return instance.property + 4; }),
QProperty<int>([&]() { 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<int> injectedValue(100);
instance.property.setBinding([&injectedValue]() { return injectedValue.value(); });
auto subscriber = [&] { ++subscribedCount; };
std::array<QPropertyChangeHandler<decltype (subscriber)>, 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<int> fooData;
QProperty<int> barData;
QProperty<int> 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;