aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/qml/jsruntime/qv4engine.cpp5
-rw-r--r--src/qml/jsruntime/qv4engine_p.h2
-rw-r--r--src/qml/qml/qqmlobjectcreator.cpp6
-rw-r--r--tests/auto/qml/debugger/qqmlpreview/data/inplace_animation.qml30
-rw-r--r--tests/auto/qml/debugger/qqmlpreview/tst_qqmlpreview.cpp25
-rw-r--r--tests/auto/qml/debugger/qqmlpreviewdiff/data/LoopingAnimationDurationLiteral.qml20
-rw-r--r--tests/auto/qml/debugger/qqmlpreviewdiff/data/LoopingAnimationDurationLiteralOld.qml20
-rw-r--r--tests/auto/qml/debugger/qqmlpreviewdiff/data/LoopingAnimationDurationOld.qml20
-rw-r--r--tests/auto/qml/debugger/qqmlpreviewdiff/data/LoopingAnimationDurationScript.qml20
-rw-r--r--tests/auto/qml/debugger/qqmlpreviewdiff/tst_qqmlpreviewobjectpatch.cpp57
10 files changed, 203 insertions, 2 deletions
diff --git a/src/qml/jsruntime/qv4engine.cpp b/src/qml/jsruntime/qv4engine.cpp
index e984288ff3..d8dfd80459 100644
--- a/src/qml/jsruntime/qv4engine.cpp
+++ b/src/qml/jsruntime/qv4engine.cpp
@@ -946,6 +946,11 @@ void ExecutionEngine::setPreviewing(bool enabled)
hasPreview.storeRelease(enabled);
}
+bool ExecutionEngine::isPreviewing()
+{
+ return hasPreview.loadAcquire();
+}
+
#endif // QT_CONFIG(qml_debug)
void ExecutionEngine::initRootContext()
diff --git a/src/qml/jsruntime/qv4engine_p.h b/src/qml/jsruntime/qv4engine_p.h
index 67d0e847aa..69162701c8 100644
--- a/src/qml/jsruntime/qv4engine_p.h
+++ b/src/qml/jsruntime/qv4engine_p.h
@@ -549,6 +549,7 @@ public:
void setDebugger(Debugging::Debugger *) {}
void setProfiler(Profiling::Profiler *) {}
static void setPreviewing(bool) {}
+ static bool isPreviewing() { return false; }
#else
QV4::Debugging::Debugger *debugger() const { return m_debugger.data(); }
QV4::Profiling::Profiler *profiler() const { return m_profiler.data(); }
@@ -556,6 +557,7 @@ public:
void setDebugger(Debugging::Debugger *debugger);
void setProfiler(Profiling::Profiler *profiler);
static void setPreviewing(bool enabled);
+ static bool isPreviewing();
#endif // QT_CONFIG(qml_debug)
// We don't want to #include <private/qv4stackframe_p.h> here, but we still want
diff --git a/src/qml/qml/qqmlobjectcreator.cpp b/src/qml/qml/qqmlobjectcreator.cpp
index 0c646efb5a..36d693c01a 100644
--- a/src/qml/qml/qqmlobjectcreator.cpp
+++ b/src/qml/qml/qqmlobjectcreator.cpp
@@ -1693,7 +1693,9 @@ bool QQmlObjectCreator::finalize(QQmlInstantiationInterrupt &interrupt)
captured)?
If the answer to all of those questions is "no", it is safe to remove the binding, as there is no
way for it to change its value afterwards from that point on.
+ The QML preview, however, needs to re-evaluate the binding if its code is changed.
*/
+ const bool removeConstantBindings = !QV4::ExecutionEngine::isPreviewing();
while (!sharedState->allCreatedBindings.empty()) {
QQmlAbstractBinding::Ptr b = sharedState->allCreatedBindings.back();
@@ -1707,7 +1709,7 @@ bool QQmlObjectCreator::finalize(QQmlInstantiationInterrupt &interrupt)
data->clearPendingBindingBit(b->targetPropertyIndex().coreIndex());
b->setEnabled(true, QQmlPropertyData::BypassInterceptor |
QQmlPropertyData::DontRemoveBinding);
- if (b->kind() == QQmlAbstractBinding::QmlBinding) {
+ if (removeConstantBindings && b->kind() == QQmlAbstractBinding::QmlBinding) {
QQmlBinding *binding = static_cast<QQmlBinding*>(b.data());
if (!binding->hasError() && !binding->hasDependencies()
&& !binding->hasUnresolvedNames()) {
@@ -1741,7 +1743,7 @@ bool QQmlObjectCreator::finalize(QQmlInstantiationInterrupt &interrupt)
sharedState->allQPropertyBindings.pop_front();
// If the binding was actually not set, it's deleted now.
- if (success && bindingPrivateRefCount > 1) {
+ if (removeConstantBindings && success && bindingPrivateRefCount > 1) {
if (auto priv = QPropertyBindingPrivate::get(qmlBinding); priv->isQmlBinding()) {
const auto qmlBindingBase = static_cast<const QQmlPropertyBindingBase *>(priv);
if (qmlBindingBase->bindingKind()
diff --git a/tests/auto/qml/debugger/qqmlpreview/data/inplace_animation.qml b/tests/auto/qml/debugger/qqmlpreview/data/inplace_animation.qml
new file mode 100644
index 0000000000..31c8e9fdb4
--- /dev/null
+++ b/tests/auto/qml/debugger/qqmlpreview/data/inplace_animation.qml
@@ -0,0 +1,30 @@
+// Copyright (C) 2026 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+import QtQuick
+
+Item {
+ width: 200
+ height: 100
+
+ Rectangle {
+ width: 20
+ height: 20
+
+ NumberAnimation on x {
+ id: animation
+ from: 0
+ to: 180
+ duration: Math.abs(1000)
+ loops: Animation.Infinite
+ }
+ }
+
+ Timer {
+ repeat: true
+ interval: 100
+ running: true
+ onTriggered: console.log("animation duration=" + animation.duration
+ + " running=" + animation.running)
+ }
+}
diff --git a/tests/auto/qml/debugger/qqmlpreview/tst_qqmlpreview.cpp b/tests/auto/qml/debugger/qqmlpreview/tst_qqmlpreview.cpp
index 9fd7510ce0..ab8d929152 100644
--- a/tests/auto/qml/debugger/qqmlpreview/tst_qqmlpreview.cpp
+++ b/tests/auto/qml/debugger/qqmlpreview/tst_qqmlpreview.cpp
@@ -79,6 +79,7 @@ private slots:
void inPlaceUpdateConstant();
void inPlaceUpdateColor();
void inPlaceUpdateBindingChange();
+ void inPlaceUpdateConstantScriptBinding();
void inPlaceUpdatePropertyAdd();
void inPlaceUpdateBrokenFile();
void rerunAfterInPlaceUpdate();
@@ -868,6 +869,30 @@ void tst_QQmlPreview::inPlaceUpdateBindingChange()
QVERIFY(m_serviceErrors.isEmpty());
}
+// In-place update of a script binding without dependencies. Such a binding is usually dropped after
+// its first evaluation. Then there is nothing left to re-evaluate when its code changes
+// (QTBUG-150561).
+void tst_QQmlPreview::inPlaceUpdateConstantScriptBinding()
+{
+ const QString file("inplace_animation.qml");
+ QCOMPARE(startQmlProcess(file), ConnectSuccess);
+ QVERIFY(m_client);
+ QTRY_COMPARE(m_client->state(), QQmlDebugClient::Enabled);
+
+ enableInPlaceUpdates();
+ verifyProcessOutputContains("animation duration=1000 running=true");
+
+ QByteArray contents = readAndModify(file, {{"Math.abs(1000)", "Math.abs(100)"}});
+ serveFile(testFile(file), contents);
+ m_client->triggerLoad(testFileUrl(file));
+
+ verifyProcessOutputContains("animation duration=100 running=true");
+
+ m_process->stop();
+ QTRY_COMPARE(m_client->state(), QQmlDebugClient::NotConnected);
+ QVERIFY(m_serviceErrors.isEmpty());
+}
+
// In-place update: add a new property. This is a structural change that
// requires reattach.
void tst_QQmlPreview::inPlaceUpdatePropertyAdd()
diff --git a/tests/auto/qml/debugger/qqmlpreviewdiff/data/LoopingAnimationDurationLiteral.qml b/tests/auto/qml/debugger/qqmlpreviewdiff/data/LoopingAnimationDurationLiteral.qml
new file mode 100644
index 0000000000..e8000dd018
--- /dev/null
+++ b/tests/auto/qml/debugger/qqmlpreviewdiff/data/LoopingAnimationDurationLiteral.qml
@@ -0,0 +1,20 @@
+// Copyright (C) 2026 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+import QtQuick
+
+Item {
+ property alias animation: animation
+
+ Rectangle {
+ width: 20
+ height: 20
+
+ NumberAnimation on x {
+ id: animation
+ from: 0
+ to: 180
+ duration: 100
+ loops: Animation.Infinite
+ }
+ }
+}
diff --git a/tests/auto/qml/debugger/qqmlpreviewdiff/data/LoopingAnimationDurationLiteralOld.qml b/tests/auto/qml/debugger/qqmlpreviewdiff/data/LoopingAnimationDurationLiteralOld.qml
new file mode 100644
index 0000000000..3a81586bbf
--- /dev/null
+++ b/tests/auto/qml/debugger/qqmlpreviewdiff/data/LoopingAnimationDurationLiteralOld.qml
@@ -0,0 +1,20 @@
+// Copyright (C) 2026 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+import QtQuick
+
+Item {
+ property alias animation: animation
+
+ Rectangle {
+ width: 20
+ height: 20
+
+ NumberAnimation on x {
+ id: animation
+ from: 0
+ to: 180
+ duration: 1000
+ loops: Animation.Infinite
+ }
+ }
+}
diff --git a/tests/auto/qml/debugger/qqmlpreviewdiff/data/LoopingAnimationDurationOld.qml b/tests/auto/qml/debugger/qqmlpreviewdiff/data/LoopingAnimationDurationOld.qml
new file mode 100644
index 0000000000..a9edcbfee1
--- /dev/null
+++ b/tests/auto/qml/debugger/qqmlpreviewdiff/data/LoopingAnimationDurationOld.qml
@@ -0,0 +1,20 @@
+// Copyright (C) 2026 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+import QtQuick
+
+Item {
+ property alias animation: animation
+
+ Rectangle {
+ width: 20
+ height: 20
+
+ NumberAnimation on x {
+ id: animation
+ from: 0
+ to: 180
+ duration: Math.abs(1000)
+ loops: Animation.Infinite
+ }
+ }
+}
diff --git a/tests/auto/qml/debugger/qqmlpreviewdiff/data/LoopingAnimationDurationScript.qml b/tests/auto/qml/debugger/qqmlpreviewdiff/data/LoopingAnimationDurationScript.qml
new file mode 100644
index 0000000000..3c87151199
--- /dev/null
+++ b/tests/auto/qml/debugger/qqmlpreviewdiff/data/LoopingAnimationDurationScript.qml
@@ -0,0 +1,20 @@
+// Copyright (C) 2026 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+import QtQuick
+
+Item {
+ property alias animation: animation
+
+ Rectangle {
+ width: 20
+ height: 20
+
+ NumberAnimation on x {
+ id: animation
+ from: 0
+ to: 180
+ duration: Math.abs(100)
+ loops: Animation.Infinite
+ }
+ }
+}
diff --git a/tests/auto/qml/debugger/qqmlpreviewdiff/tst_qqmlpreviewobjectpatch.cpp b/tests/auto/qml/debugger/qqmlpreviewdiff/tst_qqmlpreviewobjectpatch.cpp
index e8981df6eb..b11abecbdd 100644
--- a/tests/auto/qml/debugger/qqmlpreviewdiff/tst_qqmlpreviewobjectpatch.cpp
+++ b/tests/auto/qml/debugger/qqmlpreviewdiff/tst_qqmlpreviewobjectpatch.cpp
@@ -26,6 +26,7 @@
#include <QtQuick/qquickitem.h>
#include <QtQuick/qquickwindow.h>
#include <private/qquickitem_p.h>
+#include <private/qquickanimation_p.h>
#include <QtGui/qfont.h>
#include <QtGui/qcolor.h>
@@ -36,6 +37,7 @@
#include <functional>
using namespace QV4::CompiledData;
+using namespace Qt::StringLiterals;
// A single observable value of a reloaded widget: how to read it, and what it should be
// before the reload, after the reload, and after mutating the source property (to prove the
@@ -207,6 +209,11 @@ private slots:
// Granular live-object update tests
void granularConstantUpdate();
void granularConstantUpdatePreservesUserOverride();
+
+ // A running looping animation picks up an edited duration on its next loop, just like when
+ // the duration is changed at run time (QTBUG-150561).
+ void inPlacePatchLoopingAnimationDuration_data();
+ void inPlacePatchLoopingAnimationDuration();
void granularPropertyAdditionWithStash();
void granularPropertyRemovalWithStash();
void updateObjectsFunctionAdd();
@@ -597,6 +604,56 @@ void tst_QQmlPreviewObjectPatch::granularConstantUpdatePreservesUserOverride()
QCOMPARE(object->property("count").toInt(), 99); // preserved
}
+void tst_QQmlPreviewObjectPatch::inPlacePatchLoopingAnimationDuration_data()
+{
+ QTest::addColumn<QString>("oldFile");
+ QTest::addColumn<QString>("newFile");
+
+ QTest::newRow("script") << u"LoopingAnimationDurationOld.qml"_s
+ << u"LoopingAnimationDurationScript.qml"_s;
+ QTest::newRow("literal") << u"LoopingAnimationDurationLiteralOld.qml"_s
+ << u"LoopingAnimationDurationLiteral.qml"_s;
+}
+
+void tst_QQmlPreviewObjectPatch::inPlacePatchLoopingAnimationDuration()
+{
+ QFETCH(QString, oldFile);
+ QFETCH(QString, newFile);
+
+ QQmlComponent oldComponent(&engine, testFileUrl(oldFile));
+ QVERIFY2(oldComponent.isReady(), qPrintable(oldComponent.errorString()));
+ QScopedPointer<QObject> object(oldComponent.create());
+ QVERIFY(object);
+
+ auto *animation = object->property("animation").value<QQuickPropertyAnimation *>();
+ QVERIFY(animation);
+ // QQuickAbstractAnimation::duration() is the duration of the running animation job.
+ QQuickAbstractAnimation *running = animation;
+ QVERIFY(animation->isRunning());
+ QCOMPARE(animation->duration(), 1000);
+ QCOMPARE(running->duration(), 1000);
+
+ QQmlComponent newComponent(&engine, testFileUrl(newFile));
+ QVERIFY2(newComponent.isReady(), qPrintable(newComponent.errorString()));
+
+ const auto oldExecUnit = QQmlComponentPrivate::get(&oldComponent)->compilationUnit();
+ const auto newExecUnit = QQmlComponentPrivate::get(&newComponent)->compilationUnit();
+ QVERIFY(oldExecUnit && newExecUnit);
+
+ auto objects = objectsForCompilationUnit(&engine, oldExecUnit);
+ QCOMPARE(updateObjects(objects, oldExecUnit, newExecUnit),
+ QQmlPreview::PatchResult::PatchedInPlace);
+
+ QCOMPARE(animation->duration(), 100);
+ QVERIFY(animation->isRunning());
+
+ // The next loop starts with the new duration, and the animation keeps going.
+ QTRY_COMPARE_WITH_TIMEOUT(running->duration(), 100, 5s);
+ QVERIFY(animation->isRunning());
+ const int currentTime = running->currentTime();
+ QTRY_VERIFY(running->currentTime() != currentTime);
+}
+
void tst_QQmlPreviewObjectPatch::granularPropertyAdditionWithStash()
{
QQmlComponent oldComponent(&engine, testFileUrl("GranularPropertyAddOld.qml"));