QtQml: Long live Qt.labs.synchronizer!
[ChangeLog][QtQml] The new Synchronizer element allows you to synchronize values between two or more properties without breaking their bindings. This is useful for connecting user-editable controls to backend values. Fixes: QTBUG-21558 Change-Id: I01c32d7a39f1efc89975d8494ad698444c803fd4 Reviewed-by: Fabian Kosmale <fabian.kosmale@qt.io>
This commit is contained in:
parent
2a794e97e3
commit
f52428e600
|
@ -1,6 +1,8 @@
|
|||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
add_subdirectory(synchronizer)
|
||||
|
||||
if(QT_FEATURE_settings)
|
||||
add_subdirectory(settings)
|
||||
endif()
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
qt_internal_add_qml_module(LabsSynchronizer
|
||||
URI "Qt.labs.synchronizer"
|
||||
VERSION "${PROJECT_VERSION}"
|
||||
DEPENDENCIES
|
||||
QtQml/auto
|
||||
SOURCES
|
||||
qqmlsynchronizer.cpp qqmlsynchronizer_p.h
|
||||
PUBLIC_LIBRARIES
|
||||
Qt::Core
|
||||
LIBRARIES
|
||||
Qt::QmlPrivate
|
||||
PRIVATE_MODULE_INTERFACE
|
||||
Qt::QmlPrivate
|
||||
)
|
|
@ -0,0 +1,876 @@
|
|||
// Copyright (C) 2025 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
||||
|
||||
#include "qqmlsynchronizer_p.h"
|
||||
|
||||
#include <private/qobject_p.h>
|
||||
#include <private/qqmlproperty_p.h>
|
||||
#include <private/qqmlvaluetype_p.h>
|
||||
#include <private/qv4scopedvalue_p.h>
|
||||
|
||||
#include <QtQml/qqmlinfo.h>
|
||||
#include <QtCore/qcompare.h>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
|
||||
struct QQmlSynchronizerProperty
|
||||
{
|
||||
QQmlSynchronizerProperty() = default;
|
||||
QQmlSynchronizerProperty(
|
||||
QObject *object, const QQmlPropertyData *core,
|
||||
const QQmlPropertyData *valueTypeData = nullptr)
|
||||
: m_object(object)
|
||||
, m_core(core)
|
||||
, m_valueTypeData(valueTypeData)
|
||||
{}
|
||||
|
||||
bool isValid() const { return m_object != nullptr && m_core != nullptr; }
|
||||
|
||||
QVariant read() const;
|
||||
void write(QVariant &&value) const;
|
||||
|
||||
QObject *object() const { return m_object; }
|
||||
const QQmlPropertyData *core() const { return m_core; }
|
||||
const QQmlPropertyData *valueTypeData() const { return m_valueTypeData; }
|
||||
|
||||
QString name() const
|
||||
{
|
||||
if (!m_core)
|
||||
return QString();
|
||||
|
||||
const QString coreName = m_core->name(m_object);
|
||||
|
||||
const QQmlPropertyData *vt = valueTypeData();
|
||||
if (!vt)
|
||||
return coreName;
|
||||
|
||||
const QMetaObject *vtMetaObject = QQmlMetaType::metaObjectForValueType(m_core->propType());
|
||||
Q_ASSERT(vtMetaObject);
|
||||
const char *vtName = vtMetaObject->property(vt->coreIndex()).name();
|
||||
return coreName + QLatin1Char('.') + QString::fromUtf8(vtName);
|
||||
|
||||
}
|
||||
|
||||
int notifyIndex() const { return m_core->notifyIndex(); }
|
||||
|
||||
void clear()
|
||||
{
|
||||
m_object = nullptr;
|
||||
m_core = nullptr;
|
||||
m_valueTypeData = nullptr;
|
||||
}
|
||||
|
||||
QVariant coerce(const QVariant &source, QQmlSynchronizer *q) const;
|
||||
|
||||
private:
|
||||
QPointer<QObject> m_object;
|
||||
const QQmlPropertyData *m_core = nullptr;
|
||||
const QQmlPropertyData *m_valueTypeData = nullptr;
|
||||
|
||||
friend bool comparesEqual(
|
||||
const QQmlSynchronizerProperty &a, const QQmlSynchronizerProperty &b) noexcept
|
||||
{
|
||||
return a.m_core == b.m_core && a.m_valueTypeData == b.m_valueTypeData
|
||||
&& a.m_object == b.m_object;
|
||||
}
|
||||
Q_DECLARE_EQUALITY_COMPARABLE(QQmlSynchronizerProperty)
|
||||
|
||||
friend size_t qHash(const QQmlSynchronizerProperty &v, size_t seed = 0)
|
||||
{
|
||||
// We'd better not use m_object here since it may turn to nullptr spontaneously.
|
||||
// This results in a weaker hash, but we can live with it.
|
||||
return qHashMulti(seed, v.m_core, v.m_valueTypeData);
|
||||
}
|
||||
|
||||
|
||||
QMetaType metaType() const
|
||||
{
|
||||
return m_valueTypeData
|
||||
? m_valueTypeData->propType()
|
||||
: m_core->propType();
|
||||
}
|
||||
};
|
||||
|
||||
class QQmlSynchronizerSlotObject : public QtPrivate::QSlotObjectBase
|
||||
{
|
||||
public:
|
||||
QQmlSynchronizerSlotObject(
|
||||
QQmlSynchronizer *receiver, const QQmlSynchronizerProperty &property)
|
||||
: QtPrivate::QSlotObjectBase(&impl)
|
||||
, m_property(property)
|
||||
, m_connection(createConnection(receiver))
|
||||
{
|
||||
}
|
||||
|
||||
~QQmlSynchronizerSlotObject()
|
||||
{
|
||||
QObject::disconnect(m_connection);
|
||||
}
|
||||
|
||||
bool contains(const QQmlSynchronizerProperty &p) const { return m_property == p; }
|
||||
void write(QVariant value) const { m_property.write(std::move(value)); }
|
||||
QVariant coerce(const QVariant &source, QQmlSynchronizer *q) const
|
||||
{
|
||||
return m_property.coerce(source, q);
|
||||
}
|
||||
QQmlSynchronizerProperty property() const { return m_property; }
|
||||
|
||||
static void impl(int which, QtPrivate::QSlotObjectBase *self, QObject *r, void **a, bool *ret);
|
||||
void operator()(QQmlSynchronizer *receiver)
|
||||
{
|
||||
impl(QtPrivate::QSlotObjectBase::Call, this, receiver, nullptr, nullptr);
|
||||
}
|
||||
|
||||
void addref() { ref(); }
|
||||
void release() { destroyIfLastRef(); }
|
||||
|
||||
private:
|
||||
QMetaObject::Connection createConnection(QQmlSynchronizer *receiver)
|
||||
{
|
||||
Q_ASSERT(receiver);
|
||||
QObject *object = m_property.object();
|
||||
Q_ASSERT(object);
|
||||
const int notifyIndex = m_property.notifyIndex();
|
||||
Q_ASSERT(notifyIndex != -1);
|
||||
|
||||
return QObjectPrivate::connectImpl(
|
||||
object, notifyIndex, receiver, nullptr, this, Qt::AutoConnection, nullptr,
|
||||
object->metaObject());
|
||||
}
|
||||
|
||||
|
||||
QQmlSynchronizerProperty m_property;
|
||||
QMetaObject::Connection m_connection;
|
||||
};
|
||||
|
||||
class QQmlSynchronizerHandler
|
||||
{
|
||||
public:
|
||||
QQmlSynchronizerHandler(
|
||||
QQmlSynchronizerPrivate *receiver, const QQmlSynchronizerProperty &property)
|
||||
: m_property(property)
|
||||
, m_synchronizer(receiver)
|
||||
{}
|
||||
|
||||
bool contains(const QQmlSynchronizerProperty &p) const { return m_property == p; }
|
||||
void write(QVariant value) const { m_property.write(std::move(value)); }
|
||||
QVariant coerce(const QVariant &source, QQmlSynchronizer *q) const
|
||||
{
|
||||
return m_property.coerce(source, q);
|
||||
}
|
||||
QQmlSynchronizerProperty property() const { return m_property; }
|
||||
|
||||
void operator()() const;
|
||||
|
||||
protected:
|
||||
QPropertyChangeHandler<QQmlSynchronizerHandler> createChangeHandler()
|
||||
{
|
||||
const QQmlPropertyData *core = m_property.core();
|
||||
Q_ASSERT(core && core->isValid());
|
||||
QObject *object = m_property.object();
|
||||
Q_ASSERT(object);
|
||||
|
||||
QUntypedBindable bindable;
|
||||
void *argv[1] { &bindable };
|
||||
core->doMetacall<QMetaObject::BindableProperty>(object, core->coreIndex(), argv);
|
||||
Q_ASSERT(bindable.isValid());
|
||||
|
||||
return bindable.onValueChanged(*this);
|
||||
}
|
||||
|
||||
private:
|
||||
QQmlSynchronizerProperty m_property;
|
||||
QQmlSynchronizerPrivate *m_synchronizer = nullptr;
|
||||
};
|
||||
|
||||
class QQmlSynchronizerChangeHandler : public QQmlSynchronizerHandler
|
||||
{
|
||||
public:
|
||||
QQmlSynchronizerChangeHandler(
|
||||
QQmlSynchronizerPrivate *receiver, const QQmlSynchronizerProperty &property)
|
||||
: QQmlSynchronizerHandler(receiver, property)
|
||||
, changeHandler(createChangeHandler())
|
||||
{}
|
||||
|
||||
private:
|
||||
QPropertyChangeHandler<QQmlSynchronizerHandler> changeHandler;
|
||||
};
|
||||
|
||||
class QQmlSynchronizerPrivate : public QObjectPrivate
|
||||
{
|
||||
Q_DECLARE_PUBLIC(QQmlSynchronizer)
|
||||
public:
|
||||
enum Result
|
||||
{
|
||||
Origin,
|
||||
Accepted,
|
||||
Bounced,
|
||||
Ignored
|
||||
};
|
||||
|
||||
struct State
|
||||
{
|
||||
QVariant value;
|
||||
QHash<QQmlSynchronizerProperty, Result> results;
|
||||
};
|
||||
|
||||
struct OwnedTarget
|
||||
{
|
||||
QPointer<QObject> object;
|
||||
std::unique_ptr<const QQmlPropertyData> core;
|
||||
std::unique_ptr<const QQmlPropertyData> auxiliary;
|
||||
};
|
||||
|
||||
static QQmlSynchronizerPrivate *get(QQmlSynchronizer *q) { return q->d_func(); }
|
||||
|
||||
std::vector<QQmlRefPointer<QQmlSynchronizerSlotObject>> slotObjects;
|
||||
std::vector<QQmlSynchronizerChangeHandler> changeHandlers;
|
||||
|
||||
// Target set by QQmlPropertyValueSource
|
||||
// This can't change, but we have to hold on to the QQmlPropertyData.
|
||||
OwnedTarget target;
|
||||
|
||||
// Target set using sourceObject/sourceProperty properties
|
||||
// The string is the primary source of truth for sourceProperty.
|
||||
// The QQmlPropertyData are only used if the object does not have a property cache.
|
||||
QString sourceProperty;
|
||||
OwnedTarget sourceObjectProperty;
|
||||
|
||||
// Target set using targetObject/targetProperty properties
|
||||
// The string is the primary source of truth for targetProperty.
|
||||
// The QQmlPropertyData are only used if the object does not have a property cache.
|
||||
QString targetProperty;
|
||||
OwnedTarget targetObjectProperty;
|
||||
|
||||
State *currentState = nullptr;
|
||||
|
||||
bool isComponentFinalized = false;
|
||||
|
||||
void createConnection(QQmlSynchronizer *q, const QQmlSynchronizerProperty &property);
|
||||
|
||||
void disconnectObjectProperty(const QString &property, OwnedTarget *objectProperty);
|
||||
QQmlSynchronizerProperty connectObjectProperty(
|
||||
const QString &property, OwnedTarget *objectProperty, QQmlSynchronizer *q);
|
||||
|
||||
QQmlSynchronizerProperty connectTarget(QQmlSynchronizer *q);
|
||||
|
||||
void initialize(QQmlSynchronizer *q);
|
||||
|
||||
void synchronize(const QQmlSynchronizerProperty &property);
|
||||
};
|
||||
|
||||
/*!
|
||||
\qmlmodule Qt.labs.synchronizer
|
||||
\title Qt Labs Synchronizer QML Types
|
||||
\ingroup qmlmodules
|
||||
\brief Synchronizer synchronizes values between two or more properties.
|
||||
|
||||
To use this module, import the module with the following line:
|
||||
|
||||
\qml
|
||||
import Qt.labs.synchronizer
|
||||
\endqml
|
||||
*/
|
||||
|
||||
/*!
|
||||
\qmltype Synchronizer
|
||||
\inqmlmodule Qt.labs.synchronizer
|
||||
\brief Synchronizes values between two or more properties.
|
||||
|
||||
A Synchronizer object binds two or more properties together so that a change
|
||||
to any one of them automatically updates all others. While doing so, none of
|
||||
the bindings on any of the properties are broken. You can use Synchronizer if
|
||||
the direction of data flow between two properties is not pre-determined. For
|
||||
example, a TextInput may be initialized with a model value but should also
|
||||
update the model value when editing is finished.
|
||||
|
||||
\note The input elements provided by Qt Quick and Qt Quick Controls solve this
|
||||
problem by providing user interaction signals separate from value change
|
||||
signals and hiding the value assignments in C++ code. You \e{don't} need
|
||||
Synchronizer for their internals. However, it may still be useful when
|
||||
connecting a control to a model.
|
||||
|
||||
Consider the following example.
|
||||
|
||||
\section1 Without Synchronizer
|
||||
|
||||
\qml
|
||||
// MyCustomTextInput.qml
|
||||
Item {
|
||||
property string text
|
||||
function append(characters: string) { text += characters }
|
||||
[...]
|
||||
}
|
||||
\endqml
|
||||
|
||||
You may be inclined to populate the \c text property from a model and update
|
||||
the model when the \c textChanged signal is received.
|
||||
|
||||
\qml
|
||||
// Does not work!
|
||||
Item {
|
||||
id: root
|
||||
property string model: "lorem ipsum"
|
||||
MyCustomTextInput {
|
||||
text: root.model
|
||||
onTextChanged: root.model = text
|
||||
}
|
||||
}
|
||||
\endqml
|
||||
|
||||
This does \e not work. When the \c append function is called, the
|
||||
\c text property is modified, and the binding that updates it from the
|
||||
\c model property is broken. The next time the \c model is updated
|
||||
independently \c text is not updated anymore.
|
||||
|
||||
To solve this, you can omit the binding altogether and use only signals
|
||||
for updating both properties. This way you would need to give up the
|
||||
convenience of bindings.
|
||||
|
||||
Or, you can use Synchronizer.
|
||||
|
||||
\section1 With Synchronizer
|
||||
|
||||
\qml
|
||||
Item {
|
||||
id: root
|
||||
property string model: "lorem ipsum"
|
||||
MyCustomTextInput {
|
||||
Synchronizer on text {
|
||||
property alias source: root.model
|
||||
}
|
||||
}
|
||||
}
|
||||
\endqml
|
||||
|
||||
Synchronizer makes sure that whenever either the model or
|
||||
the text change, the other one is updated.
|
||||
|
||||
You can specify properties to be synchronized in several ways:
|
||||
\list
|
||||
\li Using the \c on syntax
|
||||
\li Populating the \c sourceObject and \c sourceProperty properties
|
||||
\li Populating the \c targetObject and \c targetProperty properties
|
||||
\li Creating aliases in the scope of the synchronizer
|
||||
\endlist
|
||||
|
||||
The following example synchronizes four different properties,
|
||||
exercising all the different options:
|
||||
|
||||
\qml
|
||||
Item {
|
||||
id: root
|
||||
property string model: "lorem ipsum"
|
||||
|
||||
MyCustomTextInput {
|
||||
Synchronizer on text {
|
||||
sourceObject: other
|
||||
sourceProperty: "text"
|
||||
|
||||
targetObject: root.children[0]
|
||||
targetProperty: "objectName"
|
||||
|
||||
property alias source: root.model
|
||||
property alias another: root.objectName
|
||||
}
|
||||
}
|
||||
|
||||
MyCustomTextInput {
|
||||
id: other
|
||||
}
|
||||
}
|
||||
\endqml
|
||||
|
||||
Optionally, Synchronizer will perform an initial synchronization:
|
||||
\list
|
||||
\li If one of the aliases is called \c{source}, then it will be used to
|
||||
initialize the other properties.
|
||||
\li Otherwise, if the values assigned to \l{sourceObject} and
|
||||
\l{sourceProperty} denote a property, that property will be used
|
||||
as source for initial synchronization.
|
||||
\li Otherwise, if the \c on syntax is used, the property on which the
|
||||
Synchronizer is created that way is used as source for initial
|
||||
synchronization.
|
||||
\li Otherwise no initial synchronization is performed. Only when one of
|
||||
the properties changes the others will be updated.
|
||||
\endlist
|
||||
|
||||
Synchronizer automatically \e{de-bounces}. While it is synchronizing
|
||||
using a given value as the source, it does not accept further updates from
|
||||
one of the properties expected to be the target of the update. Such
|
||||
behavior would otherwise easily lead to infinite update loops.
|
||||
Synchronizer uses the \l{valueBounced} signal to notify about this
|
||||
condition. Furthermore, it detects properties that silently
|
||||
refuse an update and emits the \l{valueIgnored} signal for them.
|
||||
Silence, in this context, is determined by the lack of a change signal
|
||||
after calling the setter for the given property.
|
||||
|
||||
If the properties to be synchronized are of different types, the usual
|
||||
QML type coercions are applied.
|
||||
*/
|
||||
|
||||
QQmlSynchronizer::QQmlSynchronizer(QObject *parent)
|
||||
: QObject(*(new QQmlSynchronizerPrivate), parent)
|
||||
{
|
||||
}
|
||||
|
||||
void QQmlSynchronizer::setTarget(const QQmlProperty &target)
|
||||
{
|
||||
Q_D(QQmlSynchronizer);
|
||||
|
||||
// Should be set before component completion
|
||||
Q_ASSERT(!d->isComponentFinalized);
|
||||
|
||||
QQmlPropertyPrivate *p = QQmlPropertyPrivate::get(target);
|
||||
d->target.object = p->object;
|
||||
d->target.core = std::make_unique<QQmlPropertyData>(p->core);
|
||||
d->target.auxiliary = p->valueTypeData.isValid()
|
||||
? std::make_unique<QQmlPropertyData>(p->valueTypeData)
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
void QQmlSynchronizer::componentFinalized()
|
||||
{
|
||||
Q_D(QQmlSynchronizer);
|
||||
|
||||
d->isComponentFinalized = true;
|
||||
d->initialize(this);
|
||||
}
|
||||
|
||||
/*!
|
||||
\qmlproperty QtObject Qt.labs.synchronizer::Synchronizer::sourceObject
|
||||
|
||||
This property holds the \l{sourceObject} part of the
|
||||
\l{sourceObject}/\l{sourceProperty} pair that together can specify one of
|
||||
the properties Synchronizer will synchronize.
|
||||
*/
|
||||
QObject *QQmlSynchronizer::sourceObject() const
|
||||
{
|
||||
Q_D(const QQmlSynchronizer);
|
||||
return d->sourceObjectProperty.object;
|
||||
}
|
||||
|
||||
void QQmlSynchronizer::setSourceObject(QObject *object)
|
||||
{
|
||||
Q_D(QQmlSynchronizer);
|
||||
if (object == d->sourceObjectProperty.object)
|
||||
return;
|
||||
|
||||
if (d->isComponentFinalized)
|
||||
d->disconnectObjectProperty(d->sourceProperty, &d->sourceObjectProperty);
|
||||
|
||||
d->sourceObjectProperty.object = object;
|
||||
emit sourceObjectChanged();
|
||||
|
||||
if (d->isComponentFinalized)
|
||||
d->connectObjectProperty(d->sourceProperty, &d->sourceObjectProperty, this);
|
||||
}
|
||||
|
||||
/*!
|
||||
\qmlproperty string Qt.labs.synchronizer::Synchronizer::sourceProperty
|
||||
|
||||
This sourceProperty holds the \l{sourceProperty} part of the
|
||||
\l{sourceObject}/\l{sourceProperty} pair that together can specify one of
|
||||
the properties Synchronizer will synchronize.
|
||||
*/
|
||||
QString QQmlSynchronizer::sourceProperty() const
|
||||
{
|
||||
Q_D(const QQmlSynchronizer);
|
||||
return d->sourceProperty;
|
||||
}
|
||||
|
||||
void QQmlSynchronizer::setSourceProperty(const QString &property)
|
||||
{
|
||||
Q_D(QQmlSynchronizer);
|
||||
if (property == d->sourceProperty)
|
||||
return;
|
||||
|
||||
if (d->isComponentFinalized)
|
||||
d->disconnectObjectProperty(d->sourceProperty, &d->sourceObjectProperty);
|
||||
|
||||
d->sourceProperty = property;
|
||||
emit sourcePropertyChanged();
|
||||
|
||||
if (d->isComponentFinalized)
|
||||
d->connectObjectProperty(d->sourceProperty, &d->sourceObjectProperty, this);
|
||||
}
|
||||
|
||||
/*!
|
||||
\qmlproperty QtObject Qt.labs.synchronizer::Synchronizer::targetObject
|
||||
|
||||
This property holds the \l{targetObject} part of the
|
||||
\l{targetObject}/\l{targetProperty} pair that together can specify one of
|
||||
the properties Synchronizer will synchronize.
|
||||
*/
|
||||
QObject *QQmlSynchronizer::targetObject() const
|
||||
{
|
||||
Q_D(const QQmlSynchronizer);
|
||||
return d->targetObjectProperty.object;
|
||||
}
|
||||
|
||||
void QQmlSynchronizer::setTargetObject(QObject *object)
|
||||
{
|
||||
Q_D(QQmlSynchronizer);
|
||||
if (object == d->targetObjectProperty.object)
|
||||
return;
|
||||
|
||||
if (d->isComponentFinalized)
|
||||
d->disconnectObjectProperty(d->targetProperty, &d->targetObjectProperty);
|
||||
|
||||
d->targetObjectProperty.object = object;
|
||||
emit targetObjectChanged();
|
||||
|
||||
if (d->isComponentFinalized)
|
||||
d->connectObjectProperty(d->targetProperty, &d->targetObjectProperty, this);
|
||||
}
|
||||
|
||||
/*!
|
||||
\qmlproperty string Qt.labs.synchronizer::Synchronizer::targetProperty
|
||||
|
||||
This targetProperty holds the \l{targetProperty} part of the
|
||||
\l{targetObject}/\l{targetProperty} pair that together can specify one of
|
||||
the properties Synchronizer will synchronize.
|
||||
*/
|
||||
QString QQmlSynchronizer::targetProperty() const
|
||||
{
|
||||
Q_D(const QQmlSynchronizer);
|
||||
return d->targetProperty;
|
||||
}
|
||||
|
||||
void QQmlSynchronizer::setTargetProperty(const QString &property)
|
||||
{
|
||||
Q_D(QQmlSynchronizer);
|
||||
if (property == d->targetProperty)
|
||||
return;
|
||||
|
||||
if (d->isComponentFinalized)
|
||||
d->disconnectObjectProperty(d->targetProperty, &d->targetObjectProperty);
|
||||
|
||||
d->targetProperty = property;
|
||||
emit targetPropertyChanged();
|
||||
|
||||
if (d->isComponentFinalized)
|
||||
d->connectObjectProperty(d->targetProperty, &d->targetObjectProperty, this);
|
||||
}
|
||||
|
||||
/*!
|
||||
\qmlsignal Qt.labs.synchronizer::Synchronizer::valueBounced(QtObject object, string property)
|
||||
|
||||
This signal is emitted if the \a{property} of the \a{object} refused
|
||||
an attempt to set its value as part of the synchronization and produced a
|
||||
different value in response. Such \e{bounced} values are ignored and
|
||||
\e{don't} trigger another round of synchronization.
|
||||
*/
|
||||
|
||||
/*!
|
||||
\qmlsignal Qt.labs.synchronizer::Synchronizer::valueIgnored(QtObject object, string property)
|
||||
|
||||
This signal is emitted if \a{property} of the \a{object} did not
|
||||
respond to an attempt to set its value as part of the synchronization.
|
||||
*/
|
||||
|
||||
|
||||
void QQmlSynchronizerPrivate::createConnection(
|
||||
QQmlSynchronizer *q, const QQmlSynchronizerProperty &property)
|
||||
{
|
||||
if (property.core()->notifiesViaBindable()) {
|
||||
changeHandlers.push_back(QQmlSynchronizerChangeHandler(this, property));
|
||||
} else if (const int notifyIndex = property.notifyIndex(); notifyIndex != -1) {
|
||||
slotObjects.push_back(QQmlRefPointer(
|
||||
new QQmlSynchronizerSlotObject(q, property),
|
||||
QQmlRefPointer<QQmlSynchronizerSlotObject>::AddRef));
|
||||
}
|
||||
}
|
||||
|
||||
void QQmlSynchronizerPrivate::disconnectObjectProperty(
|
||||
const QString &property, OwnedTarget *objectProperty)
|
||||
{
|
||||
QObject *object = objectProperty->object;
|
||||
if (!object || property.isEmpty())
|
||||
return;
|
||||
|
||||
QQmlPropertyData localCore;
|
||||
const QQmlPropertyData *coreData = objectProperty->core
|
||||
? objectProperty->core.get()
|
||||
: QQmlPropertyCache::property(object, property, {}, &localCore);
|
||||
if (!coreData || coreData->isFunction())
|
||||
return;
|
||||
|
||||
const QQmlSynchronizerProperty synchronizerProperty = QQmlSynchronizerProperty(object, coreData);
|
||||
const auto slot = std::find_if(slotObjects.begin(), slotObjects.end(), [&](const auto &slot) {
|
||||
return slot->contains(synchronizerProperty);
|
||||
});
|
||||
|
||||
if (slot == slotObjects.end()) {
|
||||
const auto handler
|
||||
= std::find_if(changeHandlers.begin(), changeHandlers.end(), [&](const auto &handler) {
|
||||
return handler.contains(synchronizerProperty);
|
||||
});
|
||||
|
||||
Q_ASSERT(handler != changeHandlers.end());
|
||||
changeHandlers.erase(handler);
|
||||
} else {
|
||||
slotObjects.erase(slot);
|
||||
}
|
||||
|
||||
objectProperty->core.reset();
|
||||
objectProperty->auxiliary.reset();
|
||||
}
|
||||
|
||||
QQmlSynchronizerProperty QQmlSynchronizerPrivate::connectObjectProperty(
|
||||
const QString &property, OwnedTarget *objectProperty, QQmlSynchronizer *q)
|
||||
{
|
||||
QObject *object = objectProperty->object;
|
||||
if (!object || property.isEmpty())
|
||||
return QQmlSynchronizerProperty();
|
||||
|
||||
QQmlPropertyData localCore;
|
||||
const QQmlPropertyData *coreData = QQmlPropertyCache::property(object, property, {}, &localCore);
|
||||
if (!coreData) {
|
||||
qmlWarning(q) << "Target object has no property called " << property;
|
||||
return QQmlSynchronizerProperty();
|
||||
}
|
||||
|
||||
if (coreData->isFunction()) {
|
||||
qmlWarning(q) << "Member " << property << " of target object is a function";
|
||||
return QQmlSynchronizerProperty();
|
||||
}
|
||||
|
||||
if (coreData == &localCore) {
|
||||
objectProperty->core = std::make_unique<QQmlPropertyData>(std::move(localCore));
|
||||
coreData = objectProperty->core.get();
|
||||
}
|
||||
|
||||
const QQmlSynchronizerProperty synchronizerProperty(object, coreData);
|
||||
createConnection(q, synchronizerProperty);
|
||||
return synchronizerProperty;
|
||||
}
|
||||
|
||||
QQmlSynchronizerProperty QQmlSynchronizerPrivate::connectTarget(QQmlSynchronizer *q)
|
||||
{
|
||||
QObject *object = target.object;
|
||||
if (!object)
|
||||
return QQmlSynchronizerProperty();
|
||||
|
||||
const QQmlPropertyData *core = target.core.get();
|
||||
Q_ASSERT(core->isValid());
|
||||
|
||||
if (const QQmlPropertyData *valueTypeData = target.auxiliary.get()) {
|
||||
const QQmlSynchronizerProperty property(object, core, valueTypeData);
|
||||
createConnection(q, property);
|
||||
return property;
|
||||
}
|
||||
|
||||
const QQmlSynchronizerProperty property(object, core);
|
||||
createConnection(q, property);
|
||||
return property;
|
||||
}
|
||||
|
||||
void QQmlSynchronizerPrivate::initialize(QQmlSynchronizer *q)
|
||||
{
|
||||
changeHandlers.clear();
|
||||
slotObjects.clear();
|
||||
|
||||
QQmlSynchronizerProperty initializationSource = connectTarget(q);
|
||||
if (QQmlSynchronizerProperty source = connectObjectProperty(
|
||||
sourceProperty, &sourceObjectProperty, q); source.isValid()) {
|
||||
initializationSource = source;
|
||||
}
|
||||
connectObjectProperty(targetProperty, &targetObjectProperty, q);
|
||||
|
||||
const QQmlPropertyCache::ConstPtr propertyCache = QQmlData::ensurePropertyCache(q);
|
||||
Q_ASSERT(propertyCache);
|
||||
|
||||
const int propertyCount = propertyCache->propertyCount();
|
||||
const int propertyOffset = QQmlSynchronizer::staticMetaObject.propertyCount();
|
||||
|
||||
bool foundSource = false;
|
||||
for (int i = propertyOffset; i < propertyCount; ++i) {
|
||||
const QQmlPropertyData *property = propertyCache->property(i);
|
||||
if (!property->isAlias())
|
||||
continue;
|
||||
|
||||
const QQmlSynchronizerProperty synchronizerProperty(q, property);
|
||||
createConnection(q, synchronizerProperty);
|
||||
|
||||
if (!foundSource && property->name(q) == QLatin1String("source")) {
|
||||
initializationSource = synchronizerProperty;
|
||||
foundSource = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (initializationSource.isValid())
|
||||
synchronize(initializationSource);
|
||||
}
|
||||
|
||||
void QQmlSynchronizerPrivate::synchronize(const QQmlSynchronizerProperty &property)
|
||||
{
|
||||
const QVariant value = property.read();
|
||||
|
||||
if (currentState) {
|
||||
// There can be interesting logic attached to the target property that causes it
|
||||
// to change multiple times in a row. We only count the last change.
|
||||
currentState->results[property] = (value == currentState->value) ? Accepted : Bounced;
|
||||
return;
|
||||
}
|
||||
|
||||
Q_Q(QQmlSynchronizer);
|
||||
|
||||
State state;
|
||||
state.results[property] = Origin;
|
||||
const auto guard = QScopedValueRollback(currentState, &state);
|
||||
|
||||
for (const auto &slotObject : slotObjects) {
|
||||
if (slotObject->contains(property))
|
||||
continue;
|
||||
state.results[slotObject->property()] = Ignored;
|
||||
state.value = slotObject->coerce(value, q);
|
||||
slotObject->write(state.value);
|
||||
}
|
||||
|
||||
for (const QQmlSynchronizerChangeHandler &changeHandler : changeHandlers) {
|
||||
if (changeHandler.contains(property))
|
||||
continue;
|
||||
state.results[changeHandler.property()] = Ignored;
|
||||
state.value = changeHandler.coerce(value, q);
|
||||
changeHandler.write(state.value);
|
||||
}
|
||||
|
||||
|
||||
for (auto it = state.results.constBegin(), end = state.results.constEnd(); it != end; ++it) {
|
||||
switch (*it) {
|
||||
case Origin:
|
||||
case Accepted:
|
||||
break;
|
||||
case Bounced: {
|
||||
const QQmlSynchronizerProperty &key = it.key();
|
||||
emit q->valueBounced(key.object(), key.name());
|
||||
break;
|
||||
}
|
||||
case Ignored: {
|
||||
const QQmlSynchronizerProperty &key = it.key();
|
||||
emit q->valueIgnored(key.object(), key.name());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void QQmlSynchronizerSlotObject::impl(
|
||||
int which, QSlotObjectBase *self, QObject *r, void **a, bool *ret)
|
||||
{
|
||||
Q_UNUSED(a);
|
||||
QQmlSynchronizerSlotObject *synchronizerSlotObject
|
||||
= static_cast<QQmlSynchronizerSlotObject *>(self);
|
||||
switch (which) {
|
||||
case Destroy:
|
||||
delete synchronizerSlotObject;
|
||||
break;
|
||||
case Call: {
|
||||
QQmlSynchronizer *q = static_cast<QQmlSynchronizer *>(r);
|
||||
QQmlSynchronizerPrivate::get(q)->synchronize(synchronizerSlotObject->m_property);
|
||||
break;
|
||||
}
|
||||
case Compare:
|
||||
if (ret)
|
||||
*ret = false;
|
||||
break;
|
||||
case NumOperations:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static QVariant doReadProperty(QObject *object, const QQmlPropertyData *property)
|
||||
{
|
||||
const QMetaType metaType = property->propType();
|
||||
if (metaType == QMetaType::fromType<QVariant>()) {
|
||||
QVariant content;
|
||||
property->readProperty(object, &content);
|
||||
return content;
|
||||
}
|
||||
|
||||
QVariant content(metaType);
|
||||
property->readProperty(object, content.data());
|
||||
return content;
|
||||
}
|
||||
|
||||
QVariant QQmlSynchronizerProperty::read() const
|
||||
{
|
||||
Q_ASSERT(m_object);
|
||||
Q_ASSERT(m_core);
|
||||
|
||||
QVariant coreContent = doReadProperty(m_object.data(), m_core);
|
||||
|
||||
if (!m_valueTypeData)
|
||||
return coreContent;
|
||||
|
||||
if (QQmlGadgetPtrWrapper *wrapper
|
||||
= QQmlGadgetPtrWrapper::instance(qmlEngine(m_object), coreContent.metaType())) {
|
||||
return doReadProperty(wrapper, m_valueTypeData);
|
||||
}
|
||||
|
||||
QQmlGadgetPtrWrapper wrapper(QQmlMetaType::valueType(coreContent.metaType()));
|
||||
return doReadProperty(&wrapper, m_valueTypeData);
|
||||
}
|
||||
|
||||
void QQmlSynchronizerProperty::write(QVariant &&value) const
|
||||
{
|
||||
Q_ASSERT(value.metaType() == metaType());
|
||||
|
||||
if (!m_object) // Was disconnected in some way or other
|
||||
return;
|
||||
|
||||
if (!m_valueTypeData) {
|
||||
m_core->writeProperty(m_object, value.data(), QQmlPropertyData::DontRemoveBinding);
|
||||
return;
|
||||
}
|
||||
|
||||
QVariant coreContent = doReadProperty(m_object, m_core);
|
||||
|
||||
if (QQmlGadgetPtrWrapper *wrapper
|
||||
= QQmlGadgetPtrWrapper::instance(qmlEngine(m_object), coreContent.metaType())) {
|
||||
m_valueTypeData->writeProperty(wrapper, value.data(), QQmlPropertyData::DontRemoveBinding);
|
||||
m_core->writeProperty(m_object, coreContent.data(), QQmlPropertyData::DontRemoveBinding);
|
||||
return;
|
||||
}
|
||||
|
||||
QQmlGadgetPtrWrapper wrapper(QQmlMetaType::valueType(coreContent.metaType()));
|
||||
m_valueTypeData->writeProperty(&wrapper, value.data(), QQmlPropertyData::DontRemoveBinding);
|
||||
m_core->writeProperty(m_object, coreContent.data(), QQmlPropertyData::DontRemoveBinding);
|
||||
}
|
||||
|
||||
QVariant QQmlSynchronizerProperty::coerce(const QVariant &source, QQmlSynchronizer *q) const
|
||||
{
|
||||
Q_ASSERT(m_core);
|
||||
|
||||
const QMetaType targetMetaType = m_valueTypeData
|
||||
? m_valueTypeData->propType()
|
||||
: m_core->propType();
|
||||
const QMetaType sourceMetaType = source.metaType();
|
||||
if (targetMetaType == sourceMetaType)
|
||||
return source;
|
||||
|
||||
QVariant target(targetMetaType);
|
||||
|
||||
QQmlData *ddata = QQmlData::get(q);
|
||||
if (ddata && !ddata->jsWrapper.isNullOrUndefined()) {
|
||||
QV4::Scope scope(ddata->jsWrapper.engine());
|
||||
QV4::ScopedValue scoped(scope, scope.engine->fromData(sourceMetaType, source.constData()));
|
||||
if (QV4::ExecutionEngine::metaTypeFromJS(scoped, targetMetaType, target.data()))
|
||||
return target;
|
||||
}
|
||||
|
||||
if (QMetaType::convert(sourceMetaType, source.constData(), targetMetaType, target.data()))
|
||||
return target;
|
||||
|
||||
qmlWarning(q) << "Cannot convert from " << sourceMetaType.name() << " to " << targetMetaType.name();
|
||||
return target;
|
||||
}
|
||||
|
||||
void QQmlSynchronizerHandler::operator()() const
|
||||
{
|
||||
m_synchronizer->synchronize(m_property);
|
||||
}
|
||||
|
||||
QT_END_NAMESPACE
|
|
@ -0,0 +1,75 @@
|
|||
// Copyright (C) 2025 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
||||
|
||||
#ifndef QQMLSYNCHRONIZER_P_H
|
||||
#define QQMLSYNCHRONIZER_P_H
|
||||
//
|
||||
// W A R N I N G
|
||||
// -------------
|
||||
//
|
||||
// This file is not part of the Qt API. It exists purely as an
|
||||
// implementation detail. This header file may change from version to
|
||||
// version without notice, or even be removed.
|
||||
//
|
||||
// We mean it.
|
||||
//
|
||||
|
||||
#include <QtQml/private/qqmlfinalizer_p.h>
|
||||
#include <QtQml/qqmlpropertyvaluesource.h>
|
||||
#include <QtQmlMeta/qtqmlmetaexports.h>
|
||||
#include <QtQmlIntegration/qqmlintegration.h>
|
||||
#include <QtCore/qobject.h>
|
||||
|
||||
#include <QtLabsSynchronizer/qtlabssynchronizerexports.h>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
|
||||
class QQmlSynchronizerPrivate;
|
||||
class Q_LABSSYNCHRONIZER_EXPORT QQmlSynchronizer
|
||||
: public QObject, public QQmlPropertyValueSource, public QQmlFinalizerHook
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_DECLARE_PRIVATE(QQmlSynchronizer)
|
||||
Q_INTERFACES(QQmlPropertyValueSource QQmlFinalizerHook)
|
||||
Q_PROPERTY(QObject *sourceObject READ sourceObject WRITE setSourceObject
|
||||
NOTIFY sourceObjectChanged FINAL)
|
||||
Q_PROPERTY(QString sourceProperty READ sourceProperty WRITE setSourceProperty
|
||||
NOTIFY sourcePropertyChanged FINAL)
|
||||
Q_PROPERTY(QObject *targetObject READ targetObject WRITE setTargetObject
|
||||
NOTIFY targetObjectChanged FINAL)
|
||||
Q_PROPERTY(QString targetProperty READ targetProperty WRITE setTargetProperty
|
||||
NOTIFY targetPropertyChanged FINAL)
|
||||
QML_NAMED_ELEMENT(Synchronizer)
|
||||
QML_ADDED_IN_VERSION(6, 10)
|
||||
public:
|
||||
QQmlSynchronizer(QObject *parent = nullptr);
|
||||
|
||||
QObject *sourceObject() const;
|
||||
void setSourceObject(QObject *object);
|
||||
|
||||
QString sourceProperty() const;
|
||||
void setSourceProperty(const QString &property);
|
||||
|
||||
QObject *targetObject() const;
|
||||
void setTargetObject(QObject *object);
|
||||
|
||||
QString targetProperty() const;
|
||||
void setTargetProperty(const QString &property);
|
||||
|
||||
Q_SIGNALS:
|
||||
void sourceObjectChanged();
|
||||
void sourcePropertyChanged();
|
||||
void targetObjectChanged();
|
||||
void targetPropertyChanged();
|
||||
|
||||
void valueBounced(QObject *object, const QString &property);
|
||||
void valueIgnored(QObject *object, const QString &property);
|
||||
|
||||
protected:
|
||||
void setTarget(const QQmlProperty &target) final;
|
||||
void componentFinalized() final;
|
||||
};
|
||||
|
||||
QT_END_NAMESPACE
|
||||
|
||||
#endif // QQMLSYNCHRONIZER_P_H
|
|
@ -130,6 +130,7 @@ if(QT_FEATURE_private_tests)
|
|||
add_subdirectory(qqmlbinding)
|
||||
add_subdirectory(qqmlchangeset)
|
||||
add_subdirectory(qqmlconnections)
|
||||
add_subdirectory(qqmlsynchronizer)
|
||||
add_subdirectory(qqmllistcompositor)
|
||||
add_subdirectory(qqmllistmodel)
|
||||
add_subdirectory(qqmllistmodelworkerscript)
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
# Copyright (C) 2025 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
#####################################################################
|
||||
## tst_qqmlsynchronizer Test:
|
||||
#####################################################################
|
||||
|
||||
if(NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT)
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(tst_qqmlsynchronizer LANGUAGES CXX)
|
||||
find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST)
|
||||
endif()
|
||||
|
||||
# Collect test data
|
||||
file(GLOB_RECURSE test_data_glob
|
||||
RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
data/*)
|
||||
list(APPEND test_data ${test_data_glob})
|
||||
|
||||
qt_internal_add_test(tst_qqmlsynchronizer
|
||||
SOURCES
|
||||
tst_qqmlsynchronizer.cpp
|
||||
LIBRARIES
|
||||
Qt::Qml
|
||||
Qt::QmlMeta
|
||||
Qt::QuickTestUtilsPrivate
|
||||
Qt::QmlModelsPrivate
|
||||
TESTDATA ${test_data}
|
||||
)
|
||||
|
||||
## Scopes:
|
||||
#####################################################################
|
||||
|
||||
qt_internal_extend_target(tst_qqmlsynchronizer CONDITION ANDROID OR IOS
|
||||
DEFINES
|
||||
QT_QMLTEST_DATADIR=":/data"
|
||||
)
|
||||
|
||||
qt_internal_extend_target(tst_qqmlsynchronizer CONDITION NOT ANDROID AND NOT IOS
|
||||
DEFINES
|
||||
QT_QMLTEST_DATADIR="${CMAKE_CURRENT_SOURCE_DIR}/data"
|
||||
)
|
|
@ -0,0 +1,43 @@
|
|||
// Copyright (C) 2025 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
|
||||
import QtQml
|
||||
import Qt.labs.synchronizer
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
objectName: "bar"
|
||||
property string unbindable: "a"
|
||||
property int count: 12
|
||||
|
||||
property int countBounces: 0
|
||||
property int countIgnores: 0
|
||||
|
||||
property QtObject other: QtObject {
|
||||
id: other
|
||||
objectName: "foo"
|
||||
property string unbindable: "b"
|
||||
property double count: 13.5
|
||||
|
||||
onCountChanged: count = 13.5 // reject any changes to count
|
||||
}
|
||||
|
||||
property Synchronizer s1: Synchronizer {
|
||||
sourceObject: root
|
||||
sourceProperty: "objectName"
|
||||
property alias target: other.objectName
|
||||
}
|
||||
|
||||
property Synchronizer s2: Synchronizer {
|
||||
sourceObject: root
|
||||
sourceProperty: "unbindable"
|
||||
property alias target: other.unbindable
|
||||
}
|
||||
|
||||
property Synchronizer s3: Synchronizer {
|
||||
sourceObject: root
|
||||
sourceProperty: "count"
|
||||
property alias target: other.count
|
||||
onValueBounced: ++root.countBounces
|
||||
onValueIgnored: ++root.countIgnores
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
// Copyright (C) 2025 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
|
||||
import QtQml
|
||||
import Qt.labs.synchronizer
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
objectName: "bar"
|
||||
property string unbindable: "a"
|
||||
property int count: 12
|
||||
|
||||
property int countBounces: 0
|
||||
property int countIgnores: 0
|
||||
|
||||
property QtObject other: QtObject {
|
||||
id: other
|
||||
objectName: "foo"
|
||||
property string unbindable: "b"
|
||||
property double count: 13.5
|
||||
|
||||
onCountChanged: count = 13.5 // reject any changes to count
|
||||
}
|
||||
|
||||
property Synchronizer s1: Synchronizer {
|
||||
targetObject: root
|
||||
targetProperty: "objectName"
|
||||
property alias source: other.objectName
|
||||
}
|
||||
|
||||
property Synchronizer s2: Synchronizer {
|
||||
targetObject: root
|
||||
targetProperty: "unbindable"
|
||||
property alias source: other.unbindable
|
||||
}
|
||||
|
||||
property Synchronizer s3: Synchronizer {
|
||||
targetObject: root
|
||||
targetProperty: "count"
|
||||
property alias source: other.count
|
||||
onValueBounced: ++root.countBounces
|
||||
onValueIgnored: ++root.countIgnores
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
// Copyright (C) 2025 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
|
||||
import QtQml
|
||||
import Qt.labs.synchronizer
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
objectName: "bar"
|
||||
property string unbindable: "a"
|
||||
property int count: 12
|
||||
|
||||
property int countBounces: 0
|
||||
property int countIgnores: 0
|
||||
|
||||
property QtObject other: QtObject {
|
||||
id: other
|
||||
objectName: "foo"
|
||||
property string unbindable: "b"
|
||||
property double count: 13.5
|
||||
}
|
||||
|
||||
Synchronizer on count {
|
||||
property alias target: other.count
|
||||
onValueBounced: ++root.countBounces
|
||||
onValueIgnored: ++root.countIgnores
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
// Copyright (C) 2025 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
|
||||
import QtQml
|
||||
import Qt.labs.synchronizer
|
||||
import Test
|
||||
|
||||
DelegateModel {
|
||||
id: root
|
||||
|
||||
property ListModel listModel: ListModel {
|
||||
ListElement {
|
||||
a: "a"
|
||||
b: "b"
|
||||
}
|
||||
}
|
||||
|
||||
model: listModel
|
||||
|
||||
delegate: QtObject {
|
||||
objectName: "foo"
|
||||
|
||||
required property QtObject model
|
||||
|
||||
Synchronizer on objectName {
|
||||
targetObject: model
|
||||
targetProperty: "a"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
// Copyright (C) 2025 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
|
||||
import QtQml
|
||||
import Qt.labs.synchronizer
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
objectName: "bar"
|
||||
property string unbindable: "a"
|
||||
property int count: 12
|
||||
|
||||
property int countBounces: 0
|
||||
|
||||
property QtObject other: QtObject {
|
||||
id: other
|
||||
objectName: "foo"
|
||||
property string unbindable: "b"
|
||||
property double count: 13.5
|
||||
|
||||
onCountChanged: count = 13.5 // reject any changes to count
|
||||
}
|
||||
|
||||
Synchronizer on objectName {
|
||||
property alias target: other.objectName
|
||||
}
|
||||
|
||||
Synchronizer on unbindable {
|
||||
property alias target: other.unbindable
|
||||
}
|
||||
|
||||
Synchronizer on count {
|
||||
property alias target: other.count
|
||||
onValueBounced: ++root.countBounces
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
// Copyright (C) 2025 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
|
||||
import QtQml
|
||||
import Qt.labs.synchronizer
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
objectName: "bar"
|
||||
property string unbindable: "a"
|
||||
property int count: 12
|
||||
|
||||
property int countBounces: 0
|
||||
|
||||
property QtObject other: QtObject {
|
||||
id: other
|
||||
objectName: "foo"
|
||||
property string unbindable: "b"
|
||||
property double count: 13.5
|
||||
|
||||
onCountChanged: count = 13.5 // reject any changes to count
|
||||
}
|
||||
|
||||
Synchronizer on objectName {
|
||||
sourceObject: other
|
||||
sourceProperty: "objectName"
|
||||
}
|
||||
|
||||
Synchronizer on unbindable {
|
||||
sourceObject: other
|
||||
sourceProperty: "unbindable"
|
||||
}
|
||||
|
||||
Synchronizer on count {
|
||||
sourceObject: other
|
||||
sourceProperty: "count"
|
||||
onValueBounced: ++root.countBounces
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
// Copyright (C) 2025 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
|
||||
import QtQml
|
||||
import Qt.labs.synchronizer
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
objectName: "bar"
|
||||
property string unbindable: "a"
|
||||
property int count: 12
|
||||
|
||||
property int countBounces: 0
|
||||
|
||||
property QtObject other: QtObject {
|
||||
id: other
|
||||
objectName: "foo"
|
||||
property string unbindable: "b"
|
||||
property double count: 13.5
|
||||
|
||||
onCountChanged: count = 13.5 // reject any changes to count
|
||||
}
|
||||
|
||||
Synchronizer on objectName {
|
||||
targetObject: other
|
||||
targetProperty: "objectName"
|
||||
}
|
||||
|
||||
Synchronizer on unbindable {
|
||||
targetObject: other
|
||||
targetProperty: "unbindable"
|
||||
}
|
||||
|
||||
Synchronizer on count {
|
||||
targetObject: other
|
||||
targetProperty: "count"
|
||||
onValueBounced: ++root.countBounces
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
// Copyright (C) 2025 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
|
||||
import QtQml
|
||||
import Qt.labs.synchronizer
|
||||
import Test
|
||||
|
||||
BindablePoint {
|
||||
id: root
|
||||
objectName: "11"
|
||||
property int count: 12
|
||||
|
||||
property alias aa: root.point.x
|
||||
|
||||
property QtObject other: QtObject {
|
||||
id: other
|
||||
objectName: "foo"
|
||||
property double count: 13.5
|
||||
property point point: ({x: 103, y: 104})
|
||||
}
|
||||
|
||||
Synchronizer on count {
|
||||
property alias target: other.point.x
|
||||
}
|
||||
|
||||
Synchronizer on objectName {
|
||||
property alias target: other.point.y
|
||||
}
|
||||
|
||||
property Synchronizer s1: Synchronizer {
|
||||
targetObject: other
|
||||
targetProperty: "count"
|
||||
property alias source: root.point.x
|
||||
}
|
||||
|
||||
property Synchronizer s2: Synchronizer {
|
||||
property alias source: root.point.y
|
||||
property alias target: other.objectName
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
// Copyright (C) 2025 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
|
||||
import QtQml
|
||||
import Qt.labs.synchronizer
|
||||
import Test
|
||||
|
||||
BindablePoint {
|
||||
id: root
|
||||
objectName: "bar"
|
||||
property string unbindable: "a"
|
||||
property int count: 12
|
||||
|
||||
property int countBounces: 0
|
||||
property int countIgnores: 0
|
||||
|
||||
function doThings() {}
|
||||
|
||||
property QtObject other: QtObject {
|
||||
id: other
|
||||
objectName: "foo"
|
||||
property string unbindable: "b"
|
||||
property double count: 13.5
|
||||
}
|
||||
|
||||
property Synchronizer mismatchedType: Synchronizer {
|
||||
targetObject: root
|
||||
targetProperty: "point"
|
||||
property alias source: root.other
|
||||
}
|
||||
|
||||
property Synchronizer missingProperty: Synchronizer {
|
||||
targetObject: root
|
||||
targetProperty: "doesNotExist"
|
||||
property alias source: other.unbindable
|
||||
}
|
||||
|
||||
property Synchronizer onFunction: Synchronizer {
|
||||
targetObject: root
|
||||
targetProperty: "doThings"
|
||||
property alias source: other.count
|
||||
}
|
||||
}
|
|
@ -0,0 +1,253 @@
|
|||
// Copyright (C) 2025 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
|
||||
|
||||
#include <QtQuickTestUtils/private/qmlutils_p.h>
|
||||
#include <QtQmlModels/private/qqmldelegatemodel_p.h>
|
||||
|
||||
#include <QtQml/qqmlcomponent.h>
|
||||
#include <QtQml/qqmlengine.h>
|
||||
|
||||
#include <QtTest/qtest.h>
|
||||
|
||||
#include <QtCore/qproperty.h>
|
||||
|
||||
class tst_qqmlsynchronizer : public QQmlDataTest
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
tst_qqmlsynchronizer();
|
||||
|
||||
private slots:
|
||||
void basics_data();
|
||||
void basics();
|
||||
|
||||
void ignored();
|
||||
void valueTypes();
|
||||
void modelObject();
|
||||
void warnings();
|
||||
};
|
||||
|
||||
class BindablePoint : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
Q_PROPERTY(QPointF point BINDABLE bindablePoint READ default WRITE default)
|
||||
public:
|
||||
BindablePoint(QObject *parent = nullptr) : QObject(parent), m_point(QPointF(101, 102)) {}
|
||||
|
||||
QBindable<QPointF> bindablePoint() { return QBindable<QPointF>(&m_point); }
|
||||
|
||||
private:
|
||||
QProperty<QPointF> m_point;
|
||||
};
|
||||
|
||||
tst_qqmlsynchronizer::tst_qqmlsynchronizer()
|
||||
: QQmlDataTest(QT_QMLTEST_DATADIR)
|
||||
{
|
||||
qmlRegisterTypesAndRevisions<BindablePoint>("Test", 1);
|
||||
}
|
||||
|
||||
void tst_qqmlsynchronizer::basics_data()
|
||||
{
|
||||
QTest::addColumn<QUrl>("url");
|
||||
QTest::addColumn<bool>("initializesOther");
|
||||
QTest::addRow("onVsAlias") << testFileUrl("onVsAlias.qml") << true;
|
||||
QTest::addRow("onVsTargetProperty") << testFileUrl("onVsTargetProperty.qml") << true;
|
||||
QTest::addRow("aliasVsTargetProperty") << testFileUrl("aliasVsTargetProperty.qml") << false;
|
||||
QTest::addRow("onVsSourceProperty") << testFileUrl("onVsSourceProperty.qml") << false;
|
||||
QTest::addRow("aliasVsSourceProperty") << testFileUrl("aliasVsSourceProperty.qml") << true;
|
||||
}
|
||||
|
||||
void tst_qqmlsynchronizer::basics()
|
||||
{
|
||||
QFETCH(QUrl, url);
|
||||
QFETCH(bool, initializesOther);
|
||||
|
||||
QQmlEngine engine;
|
||||
QQmlComponent c(&engine, url);
|
||||
QVERIFY2(c.isReady(), qPrintable(c.errorString()));
|
||||
QScopedPointer<QObject> o(c.create());
|
||||
QVERIFY(!o.isNull());
|
||||
|
||||
QObject *other = o->property("other").value<QObject *>();
|
||||
QVERIFY(other);
|
||||
|
||||
QCOMPARE(o->objectName(), initializesOther ? "bar" : "foo");
|
||||
QCOMPARE(other->objectName(), initializesOther ? "bar" : "foo");
|
||||
|
||||
o->setObjectName("baz");
|
||||
QCOMPARE(o->objectName(), "baz");
|
||||
QCOMPARE(other->objectName(), "baz");
|
||||
|
||||
other->setObjectName("foo");
|
||||
QCOMPARE(o->objectName(), "foo");
|
||||
QCOMPARE(other->objectName(), "foo");
|
||||
|
||||
QCOMPARE(o->property("unbindable").toString(), initializesOther ? "a" : "b");
|
||||
QCOMPARE(other->property("unbindable").toString(), initializesOther ? "a" : "b");
|
||||
|
||||
o->setProperty("unbindable", QStringLiteral("c"));
|
||||
|
||||
QCOMPARE(o->property("unbindable").toString(), "c");
|
||||
QCOMPARE(other->property("unbindable").toString(), "c");
|
||||
|
||||
other->setProperty("unbindable", QStringLiteral("d"));
|
||||
|
||||
QCOMPARE(o->property("unbindable").toString(), "d");
|
||||
QCOMPARE(other->property("unbindable").toString(), "d");
|
||||
|
||||
QCOMPARE(o->property("count").toInt(), initializesOther ? 12 : 13);
|
||||
QCOMPARE(other->property("count").toDouble(), 13.5);
|
||||
int countBounces = initializesOther ? 1 : 0;
|
||||
QCOMPARE(o->property("countBounces").toInt(), countBounces);
|
||||
QCOMPARE(o->property("countIgnores").toInt(), 0);
|
||||
|
||||
o->setProperty("count", 17);
|
||||
++countBounces;
|
||||
QCOMPARE(o->property("count").toInt(), 17);
|
||||
QCOMPARE(other->property("count").toDouble(), 13.5);
|
||||
QCOMPARE(o->property("countBounces").toInt(), countBounces);
|
||||
QCOMPARE(o->property("countIgnores").toInt(), 0);
|
||||
|
||||
other->setProperty("count", 18);
|
||||
QCOMPARE(o->property("count").toInt(), 13);
|
||||
QCOMPARE(other->property("count").toDouble(), 13.5);
|
||||
QCOMPARE(o->property("countBounces").toInt(), countBounces);
|
||||
QCOMPARE(o->property("countIgnores").toInt(), 0);
|
||||
}
|
||||
|
||||
void tst_qqmlsynchronizer::ignored()
|
||||
{
|
||||
QQmlEngine engine;
|
||||
QQmlComponent c(&engine, testFileUrl("ignored.qml"));
|
||||
QVERIFY2(c.isReady(), qPrintable(c.errorString()));
|
||||
|
||||
QScopedPointer<QObject> o(c.create());
|
||||
QVERIFY(!o.isNull());
|
||||
|
||||
QObject *other = o->property("other").value<QObject *>();
|
||||
QVERIFY(other);
|
||||
|
||||
QCOMPARE(o->property("count").toInt(), 12);
|
||||
QCOMPARE(other->property("count").toDouble(), 12);
|
||||
|
||||
QCOMPARE(o->property("countBounces").toInt(), 0);
|
||||
QCOMPARE(o->property("countIgnores").toInt(), 0);
|
||||
|
||||
other->setProperty("count", 12.4);
|
||||
QCOMPARE(o->property("count").toInt(), 12);
|
||||
QCOMPARE(o->property("countBounces").toInt(), 0);
|
||||
QCOMPARE(o->property("countIgnores").toInt(), 1);
|
||||
}
|
||||
|
||||
void tst_qqmlsynchronizer::valueTypes()
|
||||
{
|
||||
QQmlEngine engine;
|
||||
QQmlComponent c(&engine, testFileUrl("valueTypes.qml"));
|
||||
QVERIFY2(c.isReady(), qPrintable(c.errorString()));
|
||||
|
||||
QScopedPointer<QObject> o(c.create());
|
||||
QVERIFY(!o.isNull());
|
||||
|
||||
QObject *other = o->property("other").value<QObject *>();
|
||||
QVERIFY(other);
|
||||
|
||||
QCOMPARE(o->property("point").value<QPointF>(), QPointF(101, 102));
|
||||
QCOMPARE(other->property("point").value<QPointF>(), QPointF(12, 11));
|
||||
QCOMPARE(o->objectName(), "11");
|
||||
QCOMPARE(other->objectName(), "102");
|
||||
QCOMPARE(other->property("count"), 101);
|
||||
QCOMPARE(o->property("count"), 12);
|
||||
|
||||
// Since point can only be summarily signaled, this updates the other synchronizer, too.
|
||||
o->setProperty("count", 77);
|
||||
QCOMPARE(other->property("point").value<QPointF>(), QPointF(77, 11));
|
||||
QCOMPARE(o->objectName(), "11");
|
||||
|
||||
other->setProperty("point", QPointF(97, 98));
|
||||
QCOMPARE(o->property("count"), 97);
|
||||
QCOMPARE(o->objectName(), "98");
|
||||
|
||||
o->setObjectName("54");
|
||||
QCOMPARE(other->property("point").value<QPointF>(), QPointF(97, 54));
|
||||
|
||||
o->setProperty("point", QPointF(33.25, 34.8));
|
||||
QCOMPARE(other->property("count").toDouble(), 33.25);
|
||||
QCOMPARE(other->objectName(), "34.8");
|
||||
other->setProperty("count", 55.5);
|
||||
QCOMPARE(o->property("point").value<QPointF>(), QPointF(55.5, 34.8));
|
||||
QCOMPARE(other->objectName(), "34.8");
|
||||
other->setObjectName("77.2");
|
||||
QCOMPARE(o->property("point").value<QPointF>(), QPointF(55.5, 77.2));
|
||||
}
|
||||
|
||||
void tst_qqmlsynchronizer::modelObject()
|
||||
{
|
||||
QQmlEngine engine;
|
||||
QQmlComponent c(&engine, testFileUrl("modelObject.qml"));
|
||||
QVERIFY2(c.isReady(), qPrintable(c.errorString()));
|
||||
|
||||
QScopedPointer<QObject> o(c.create());
|
||||
QVERIFY(!o.isNull());
|
||||
|
||||
QQmlDelegateModel *delegateModel = qobject_cast<QQmlDelegateModel *>(o.data());
|
||||
QVERIFY(delegateModel);
|
||||
|
||||
QObject *delegate = delegateModel->object(0);
|
||||
QVERIFY(delegate);
|
||||
|
||||
QAbstractListModel *model = delegateModel->property("listModel").value<QAbstractListModel *>();
|
||||
QVERIFY(model);
|
||||
|
||||
const auto roleNames = model->roleNames();
|
||||
int role = -1;
|
||||
for (auto it = roleNames.begin(), end = roleNames.end(); it != end; ++it) {
|
||||
if (it.value() == "a") {
|
||||
role = it.key();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const QModelIndex modelIndex = model->index(0);
|
||||
|
||||
QCOMPARE(delegate->objectName(), "foo");
|
||||
QCOMPARE(model->data(modelIndex, role), "foo");
|
||||
|
||||
model->setData(modelIndex, "c", role);
|
||||
QCOMPARE(model->data(modelIndex, role), "c");
|
||||
QCOMPARE(delegate->objectName(), "c");
|
||||
|
||||
delegate->setObjectName("d");
|
||||
QCOMPARE(model->data(modelIndex, role), "d");
|
||||
}
|
||||
|
||||
void tst_qqmlsynchronizer::warnings()
|
||||
{
|
||||
QQmlEngine engine;
|
||||
const QUrl url = testFileUrl("warnings.qml");
|
||||
QQmlComponent c(&engine, url);
|
||||
QVERIFY2(c.isReady(), qPrintable(c.errorString()));
|
||||
|
||||
QTest::ignoreMessage(
|
||||
QtWarningMsg,
|
||||
qPrintable(url.toString()
|
||||
+ ":31:44: QML Synchronizer: Target object has no property called doesNotExist"));
|
||||
QTest::ignoreMessage(
|
||||
QtWarningMsg,
|
||||
qPrintable(url.toString()
|
||||
+ ":37:39: QML Synchronizer: Member doThings of target object is a function"));
|
||||
|
||||
QScopedPointer<QObject> o(c.create());
|
||||
QVERIFY(!o.isNull());
|
||||
|
||||
QTest::ignoreMessage(
|
||||
QtWarningMsg,
|
||||
qPrintable(url.toString()
|
||||
+ ":25:43: QML Synchronizer: Cannot convert from QPointF to QObject*"));
|
||||
|
||||
o->setProperty("point", QPointF(13, 14));
|
||||
}
|
||||
|
||||
QTEST_MAIN(tst_qqmlsynchronizer)
|
||||
|
||||
#include "tst_qqmlsynchronizer.moc"
|
Loading…
Reference in New Issue