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:
Ulf Hermann 2025-02-07 13:45:03 +01:00
parent 2a794e97e3
commit f52428e600
16 changed files with 1600 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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"
}
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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"