aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFabian Kosmale <fabian.kosmale@qt.io>2025-10-17 15:58:27 +0200
committerFabian Kosmale <fabian.kosmale@qt.io>2025-11-06 22:06:16 +0100
commitde20cc1785e654093329cef65564f25e8e7569ad (patch)
tree61064196634075e3cf862e6859dad9683a0372a6
parenteeb59642cf58fd1d7615acca74424f7a8c14655e (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.cpp47
-rw-r--r--tests/auto/qml/qqmlbinding/data/restoreNotConfusedByTypeConversion.qml44
-rw-r--r--tests/auto/qml/qqmlbinding/tst_qqmlbinding.cpp42
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;