diff options
| author | Fabian Kosmale <fabian.kosmale@qt.io> | 2025-10-17 15:58:27 +0200 |
|---|---|---|
| committer | Fabian Kosmale <fabian.kosmale@qt.io> | 2025-11-06 22:06:16 +0100 |
| commit | de20cc1785e654093329cef65564f25e8e7569ad (patch) | |
| tree | 61064196634075e3cf862e6859dad9683a0372a6 | |
| parent | eeb59642cf58fd1d7615acca74424f7a8c14655e (diff) | |
Binding: Fix "value changed" detection and restore logic
We need to keep in mind that the value might have changed compared to
what we initially had when writing to a property.
In a future dev only change, we might rather make Binding a
(conditional) interceptor. That would avoid the read and comparison, as
we could track invalidations more directly. However, for picking back to
stable branches, we want a more minimal change in any case.
This requires us to do a similar conversion as QQmlProperty::write does.
Moreover, we need to take additional care for QQmlListProperty: There,
we would so far store a QQmlListReference, referencing whatever the
Binding wrote to the property. Instead, we need to create a snapshot of
the list. Moreover, we can't just store a QList of QObjects, as the
objects might get garbage collected. Instead, put them into a
QQmlListWrapper (in "owning" mode).
Amends 6a8478829747289cdcce2a6e9628b31cfd865f15
Pick-to: 6.8
Fixes: QTBUG-140858
Change-Id: I5d0ec4ee5f1a20a6ee4b5f1696f561b2638bd07e
Reviewed-by: Ulf Hermann <ulf.hermann@qt.io>
(cherry picked from commit 1bfc96ba0e15e843a082d0695c5db3a3a9ae4fc9)
Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
| -rw-r--r-- | src/qmlmeta/types/qqmlbind.cpp | 47 | ||||
| -rw-r--r-- | tests/auto/qml/qqmlbinding/data/restoreNotConfusedByTypeConversion.qml | 44 | ||||
| -rw-r--r-- | tests/auto/qml/qqmlbinding/tst_qqmlbinding.cpp | 42 |
3 files changed, 132 insertions, 1 deletions
diff --git a/src/qmlmeta/types/qqmlbind.cpp b/src/qmlmeta/types/qqmlbind.cpp index 579bc7e095..e66217528d 100644 --- a/src/qmlmeta/types/qqmlbind.cpp +++ b/src/qmlmeta/types/qqmlbind.cpp @@ -15,12 +15,15 @@ #include <private/qv4qmlcontext_p.h> #include <private/qv4resolvedtypereference_p.h> #include <private/qv4runtime_p.h> +#include <private/qv4qobjectwrapper_p.h> #include <QtQml/qqmlcontext.h> #include <QtQml/qqmlengine.h> #include <QtQml/qqmlinfo.h> #include <QtQml/qqmlproperty.h> #include <QtQml/qqmlpropertymap.h> +#include <QtQml/private/qqmllist_p.h> +#include <QtQml/private/qqmllistwrapper_p.h> #include <QtCore/private/qobject_p.h> @@ -1253,7 +1256,42 @@ bool QQmlBindPrivate::isCurrent(QQmlBindEntry *entry) const } case QQmlBindEntryKind::Variant: { const QV4::PersistentValue &v4Value = entry->current.v4Value; - return v4Value.engine()->toVariant(*v4Value.valueRef(), entry->prop.propertyMetaType()) + QMetaType propMetaType = entry->prop.propertyMetaType(); + /* + * We currently really want to use toVariant here, and not metaTypeFromJS. + * metaTypeFromJS can't do all conversions, and would leave us with an + * empty QVariant if it fails; whereas toVariant tries very hard to give + * us a QVariant representation, even though its type might not match the + * property's meta-type. In case of an actual mismatch, we use + * convertToWriteTargetType to mirror the conversions that writing to a + * property would have done; with the exception of list properties, where + * we directly go through a specialized code path using + * QQmlPropertyPrivate::convertToQQmlListProperty + */ + QVariant valueAsVariant = v4Value.engine()->toVariant(*v4Value.valueRef(), + propMetaType); + if (propMetaType.flags() & QMetaType::IsQmlList) { + // our expected value is not necessarily of the correct type, so we copy the steps done by + // QQmlPropertyPrivate::write to bring them into the correct format + QList<QObject *> expectedObjectList; + QQmlListProperty<QObject> expectedAsListProp(nullptr, &expectedObjectList); + QQmlPropertyPrivate::convertToQQmlListProperty(&expectedAsListProp, propMetaType, valueAsVariant); + + QVariant actualValue = entry->prop.read(); + // reading a QQmlListProperty property yields a QQmlListReference + QQmlListReference* listReference = get_if<QQmlListReference>(&actualValue); + Q_ASSERT(listReference); + auto actualObjectList = QQmlListReferencePrivate::get(listReference)->property.toList<QList<QObject *>>(); + return std::equal(expectedObjectList.constBegin(), expectedObjectList.constEnd(), + actualObjectList.constBegin(), actualObjectList.constEnd()); + + } else if (QMetaType varMetaType = valueAsVariant.metaType(); varMetaType != propMetaType) { + QVariant converted = QQmlPropertyPrivate::convertToWriteTargetType( + valueAsVariant, propMetaType); + if (converted.isValid()) + valueAsVariant = std::move(converted); + } + return valueAsVariant == entry->prop.read(); } case QQmlBindEntryKind::Binding: @@ -1329,6 +1367,13 @@ void QQmlBindPrivate::preEvalEntry(QQmlBindEntry *entry) auto retVal = vmemo->vmeProperty(propData.coreIndex()); entry->previousKind = entry->previous.set( QV4::PersistentValue(vmemo->engine, retVal), entry->previousKind); + } else if (entry->prop.propertyMetaType().flags() & QMetaType::IsQmlList) { + using namespace QV4; + ExecutionEngine *v4 = qmlEngine(q_func())->handle(); + entry->previousKind = entry->previous.setVariant( + QV4::PersistentValue(v4, QV4::QmlListWrapper::createOwned(v4, entry->prop)), + entry->previousKind); + } else { // nope, use the meta object to get a QVariant entry->previousKind = entry->previous.set( diff --git a/tests/auto/qml/qqmlbinding/data/restoreNotConfusedByTypeConversion.qml b/tests/auto/qml/qqmlbinding/data/restoreNotConfusedByTypeConversion.qml new file mode 100644 index 0000000000..b573c80350 --- /dev/null +++ b/tests/auto/qml/qqmlbinding/data/restoreNotConfusedByTypeConversion.qml @@ -0,0 +1,44 @@ +import QtQuick + +Item { + id: root + + property bool isRectBlue: false + property bool shouldHaveListItem: false + property bool shouldHaveListInt: false + property bool hasListItem: myList.length > 0 + property bool hasListInt: myIntList.length > 0 + property alias color: rect.color + + property list<int> myIntList + + Binding on myIntList { + when: shouldHaveListInt + value: 42 + } + + property list<Item> myList + + Binding on myList { + when: root.shouldHaveListItem + value: rect + } + + width: 640 + height: 480 + + Rectangle { + id: rect + anchors { + top: parent.top + left: parent.left + right: parent.horizontalCenter + bottom: parent.bottom + } + + Binding on color { + when: root.isRectBlue + value: "blue" + } + } +} diff --git a/tests/auto/qml/qqmlbinding/tst_qqmlbinding.cpp b/tests/auto/qml/qqmlbinding/tst_qqmlbinding.cpp index 11905c8210..360886906b 100644 --- a/tests/auto/qml/qqmlbinding/tst_qqmlbinding.cpp +++ b/tests/auto/qml/qqmlbinding/tst_qqmlbinding.cpp @@ -54,6 +54,7 @@ private slots: void qQmlPropertyToPropertyBinding(); void qQmlPropertyToPropertyBindingReverse(); void delayedBindingDestruction(); + void restoreNotConfusedByTypeConversion(); void deleteStashedObject(); void multiValueTypeBinding(); @@ -893,6 +894,47 @@ void tst_qqmlbinding::delayedBindingDestruction() verifyDelegate(QLatin1String("foo")); } +void tst_qqmlbinding::restoreNotConfusedByTypeConversion() +{ + QQmlEngine engine; + QQmlComponent component(&engine, testFileUrl("restoreNotConfusedByTypeConversion.qml")); + QVERIFY2(component.isReady(), qPrintable(component.errorString())); + QScopedPointer<QObject> object(component.create()); + QVERIFY(object); + { + // toggling the binding twice yields the blue color + // even if "blue" and QColor::fromString("blue") are different values + object->setProperty("isRectBlue", true); + QCOMPARE(object->property("color").value<QColor>(), QColor::fromString("blue")); + object->setProperty("isRectBlue", false); + QCOMPARE_NE(object->property("color").value<QColor>(), QColor::fromString("blue")); + // toggling the binding twice yields the blue color + // even if "blue" and QColor::fromString("blue") are different values + object->setProperty("isRectBlue", true); + QCOMPARE(object->property("color").value<QColor>(), QColor::fromString("blue")); + } + { + // now do the same with an Item list property, exercising another code path + QVERIFY(!object->property("hasListItem").toBool()); // initially empty + object->setProperty("shouldHaveListItem", true); + QVERIFY(object->property("hasListItem").toBool()); + object->setProperty("shouldHaveListItem", false); + QVERIFY(!object->property("hasListItem").toBool()); + object->setProperty("shouldHaveListItem", true); + QVERIFY(object->property("hasListItem").toBool()); + } + { + // now do the same with a value list property, exercising yet another code path + QVERIFY(!object->property("hasListInt").toBool()); // initially empty + object->setProperty("shouldHaveListInt", true); + QVERIFY(object->property("hasListInt").toBool()); + object->setProperty("shouldHaveListInt", false); + QVERIFY(!object->property("hasListInt").toBool()); + object->setProperty("shouldHaveListInt", true); + QVERIFY(object->property("hasListInt").toBool()); + } +} + void tst_qqmlbinding::deleteStashedObject() { QQmlEngine engine; |
