From 42e403a51da3539cc5062fde13884678dba55b5a Mon Sep 17 00:00:00 2001 From: Shawn Rutledge Date: Thu, 4 Jul 2019 13:28:51 +0200 Subject: Notify QQItem::mouseUngrabEvent() when an Event Handler steals grab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We already took care of cases when the handler is a child of the Item, grab transitions involving Flickable etc.; but the bug is about a simpler case when the handler is in the parent of the item that has the grab, and steals from it. Amends 38a016c7b1337d83d77879f45b4a2e6fec11d049 Fixes: QTBUG-71218 Fixes: QTBUG-75025 Change-Id: Id1d6d370e0db75c59ec7dce4a8e545701c501827 Reviewed-by: Jan Arve Sæther --- src/quick/items/qquickevents.cpp | 10 +++-- .../data/dragParentOfMPTA.qml | 50 ++++++++++++++++++++++ .../tst_multipointtoucharea_interop.cpp | 43 +++++++++++++++++++ 3 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 tests/auto/quick/pointerhandlers/multipointtoucharea_interop/data/dragParentOfMPTA.qml diff --git a/src/quick/items/qquickevents.cpp b/src/quick/items/qquickevents.cpp index 28b217b7b3..8303c3fed1 100644 --- a/src/quick/items/qquickevents.cpp +++ b/src/quick/items/qquickevents.cpp @@ -913,10 +913,14 @@ void QQuickEventPoint::setGrabberPointerHandler(QQuickPointerHandler *grabber, b passiveGrabber->onGrabChanged(grabber, OverrideGrabPassive, this); } } - if (oldGrabberHandler) + if (oldGrabberHandler) { oldGrabberHandler->onGrabChanged(oldGrabberHandler, (grabber ? CancelGrabExclusive : UngrabExclusive), this); - else if (oldGrabberItem && pointerEvent()->asPointerTouchEvent()) - oldGrabberItem->touchUngrabEvent(); + } else if (oldGrabberItem) { + if (pointerEvent()->asPointerTouchEvent()) + oldGrabberItem->touchUngrabEvent(); + else if (pointerEvent()->asPointerMouseEvent()) + oldGrabberItem->mouseUngrabEvent(); + } // touchUngrabEvent() can result in the grabber being set to null (MPTA does that, for example). // So set it again to ensure that final state is what we want. m_exclusiveGrabber = QPointer(grabber); diff --git a/tests/auto/quick/pointerhandlers/multipointtoucharea_interop/data/dragParentOfMPTA.qml b/tests/auto/quick/pointerhandlers/multipointtoucharea_interop/data/dragParentOfMPTA.qml new file mode 100644 index 0000000000..dc7e5f6411 --- /dev/null +++ b/tests/auto/quick/pointerhandlers/multipointtoucharea_interop/data/dragParentOfMPTA.qml @@ -0,0 +1,50 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.12 + +Item { + width: 640 + height: 480 + property alias touchpointPressed: tp1.pressed + + Rectangle { + color: tp1.pressed ? "lightsteelblue" : drag.active ? "tomato" : "wheat" + width: 180 + height: 180 + + DragHandler { id: drag } + + MultiPointTouchArea { + anchors.fill: parent + touchPoints: [ + TouchPoint { id: tp1 } + ] + } + } +} diff --git a/tests/auto/quick/pointerhandlers/multipointtoucharea_interop/tst_multipointtoucharea_interop.cpp b/tests/auto/quick/pointerhandlers/multipointtoucharea_interop/tst_multipointtoucharea_interop.cpp index bf582b820b..cd18580ccf 100644 --- a/tests/auto/quick/pointerhandlers/multipointtoucharea_interop/tst_multipointtoucharea_interop.cpp +++ b/tests/auto/quick/pointerhandlers/multipointtoucharea_interop/tst_multipointtoucharea_interop.cpp @@ -56,6 +56,7 @@ private slots: void touchDrag(); void touchesThenPinch(); void unloadHandlerWithPassiveGrab(); + void dragHandlerInParentStealingGrabFromItem(); private: void createView(QScopedPointer &window, const char *fileName); @@ -301,6 +302,48 @@ void tst_MptaInterop::unloadHandlerWithPassiveGrab() QTest::mouseRelease(window, Qt::LeftButton, 0, point); // QTBUG-73819: don't crash } +void tst_MptaInterop::dragHandlerInParentStealingGrabFromItem() // QTBUG-75025 +{ + const int dragThreshold = QGuiApplication::styleHints()->startDragDistance(); + QScopedPointer windowPtr; + createView(windowPtr, "dragParentOfMPTA.qml"); + QQuickView * window = windowPtr.data(); + auto pointerEvent = QQuickWindowPrivate::get(window)->pointerEventInstance(QQuickPointerDevice::genericMouseDevice()); + + QPointer handler = window->rootObject()->findChild(); + QVERIFY(handler); + QQuickMultiPointTouchArea *mpta = window->rootObject()->findChild(); + QVERIFY(mpta); + + // In QTBUG-75025 there is a QQ Controls Button; the MPTA here stands in for that, + // simply as an Item that grabs the mouse. The bug has nothing to do with MPTA specifically. + + QPoint point(20, 20); + QTest::mousePress(window, Qt::LeftButton, Qt::NoModifier, point); + QCOMPARE(window->mouseGrabberItem(), mpta); + QCOMPARE(window->rootObject()->property("touchpointPressed").toBool(), true); + + // Start dragging + // DragHandler keeps monitoring, due to its passive grab, + // and eventually steals the exclusive grab from MPTA + int dragStoleGrab = 0; + for (int i = 0; i < 4; ++i) { + point += QPoint(dragThreshold / 2, 0); + QTest::mouseMove(window, point); + if (!dragStoleGrab && pointerEvent->point(0)->exclusiveGrabber() == handler) + dragStoleGrab = i; + } + if (dragStoleGrab) + qCDebug(lcPointerTests, "DragHandler stole the grab after %d events", dragStoleGrab); + QVERIFY(dragStoleGrab > 1); + QCOMPARE(handler->active(), true); + QCOMPARE(window->rootObject()->property("touchpointPressed").toBool(), false); + + QTest::mouseRelease(window, Qt::LeftButton, Qt::NoModifier, point); + QCOMPARE(handler->active(), false); + QCOMPARE(window->rootObject()->property("touchpointPressed").toBool(), false); +} + QTEST_MAIN(tst_MptaInterop) #include "tst_multipointtoucharea_interop.moc" -- cgit v1.2.3 From 9b01e2e5d4b38533f02ba9ba907505e8c341cd0a Mon Sep 17 00:00:00 2001 From: Shawn Rutledge Date: Wed, 3 Jul 2019 17:17:53 +0200 Subject: TapHandler: wait until after tapped is emitted to reset point.position MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We don't want it to hold its position indefinitely after the button is released. But in practice, reset() gets called again anyway in QQuickSinglePointHandler::handlePointerEventImpl(), _after_ handleEventPoint(), which means after tapped() is emitted. Having the point hold its position that much longer is convenient for applications and more consistent with the state expressed by the release event. Also amend the documentation. Partially reverts 17237efaefabe924599abe00e92d8b54032d7915 [ChangeLog][Event Handlers][Important Behavior Changes] TapHandler.point now holds the release position while the tapped() signal is emitted. Fixes: QTBUG-76871 Task-number: QTBUG-64847 Change-Id: I621a2eba4507a498788e9384344e8b4b7da32403 Reviewed-by: Jan Arve Sæther --- src/quick/handlers/qquickhandlerpoint.cpp | 15 +++++---------- src/quick/handlers/qquicktaphandler.cpp | 9 --------- .../pointerhandlers/qquicktaphandler/data/Button.qml | 2 ++ .../qquicktaphandler/tst_qquicktaphandler.cpp | 8 ++++++++ 4 files changed, 15 insertions(+), 19 deletions(-) diff --git a/src/quick/handlers/qquickhandlerpoint.cpp b/src/quick/handlers/qquickhandlerpoint.cpp index de21537f27..f3d92cf200 100644 --- a/src/quick/handlers/qquickhandlerpoint.cpp +++ b/src/quick/handlers/qquickhandlerpoint.cpp @@ -51,13 +51,14 @@ Q_DECLARE_LOGGING_CATEGORY(DBG_TOUCH_TARGET) A QML representation of a QQuickEventPoint. - It's possible to make bindings to properties of a \l SinglePointHandler's - current point. For example: + It's possible to make bindings to properties of a handler's current + \l {SinglePointHandler::point}{point} or + \l {MultiPointHandler::centroid}{centroid}. For example: \snippet pointerHandlers/dragHandlerNullTarget.qml 0 The point is kept up-to-date when the DragHandler is actively responding to - an EventPoint; but when the point is released, or the current point is + an EventPoint; but after the point is released, or when the current point is being handled by a different handler, \c position.x and \c position.y are 0. \note This is practically identical to QtQuick::EventPoint; however an @@ -68,7 +69,7 @@ Q_DECLARE_LOGGING_CATEGORY(DBG_TOUCH_TARGET) handler is handling. HandlerPoint is a Q_GADGET that the handler owns. This allows you to make lifetime bindings to its properties. - \sa SinglePointHandler::point + \sa SinglePointHandler::point, MultiPointHandler::centroid */ QQuickHandlerPoint::QQuickHandlerPoint() @@ -106,12 +107,6 @@ void QQuickHandlerPoint::reset(const QQuickEventPoint *point) m_scenePressPosition = point->scenePosition(); m_pressedButtons = event->buttons(); break; - case QQuickEventPoint::Released: - if (event->buttons() == Qt::NoButton) { - reset(); - return; - } - break; default: break; } diff --git a/src/quick/handlers/qquicktaphandler.cpp b/src/quick/handlers/qquicktaphandler.cpp index 73c559c998..61f97868e8 100644 --- a/src/quick/handlers/qquicktaphandler.cpp +++ b/src/quick/handlers/qquicktaphandler.cpp @@ -391,9 +391,6 @@ void QQuickTapHandler::updateTimeHeld() from the release event about the point that was tapped: \snippet pointerHandlers/tapHandlerOnTapped.qml 0 - - \note At the time this signal is emitted, \l point has been reset - (all coordinates are \c 0). */ /*! @@ -405,9 +402,6 @@ void QQuickTapHandler::updateTimeHeld() it can be tapped again; but if the time until the next tap is less, \l tapCount will increase. The \c eventPoint signal parameter contains information from the release event about the point that was tapped. - - \note At the time this signal is emitted, \l point has been reset - (all coordinates are \c 0). */ /*! @@ -421,9 +415,6 @@ void QQuickTapHandler::updateTimeHeld() \l singleTapped, \l tapped, and \l tapCountChanged. The \c eventPoint signal parameter contains information from the release event about the point that was tapped. - - \note At the time this signal is emitted, \l point has been reset - (all coordinates are \c 0). */ /*! diff --git a/tests/auto/quick/pointerhandlers/qquicktaphandler/data/Button.qml b/tests/auto/quick/pointerhandlers/qquicktaphandler/data/Button.qml index 221e7df139..042b730799 100644 --- a/tests/auto/quick/pointerhandlers/qquicktaphandler/data/Button.qml +++ b/tests/auto/quick/pointerhandlers/qquicktaphandler/data/Button.qml @@ -34,6 +34,7 @@ Rectangle { property alias pressed: tap.pressed property bool checked: false property alias gesturePolicy: tap.gesturePolicy + property point tappedPosition: Qt.point(0, 0) signal tapped signal canceled @@ -51,6 +52,7 @@ Rectangle { longPressThreshold: 100 // CI can be insanely slow, so don't demand a timely release to generate onTapped onTapped: { tapFlash.start() + root.tappedPosition = point.scenePosition root.tapped() } onCanceled: root.canceled() diff --git a/tests/auto/quick/pointerhandlers/qquicktaphandler/tst_qquicktaphandler.cpp b/tests/auto/quick/pointerhandlers/qquicktaphandler/tst_qquicktaphandler.cpp index 33cea69147..e77ea97518 100644 --- a/tests/auto/quick/pointerhandlers/qquicktaphandler/tst_qquicktaphandler.cpp +++ b/tests/auto/quick/pointerhandlers/qquicktaphandler/tst_qquicktaphandler.cpp @@ -107,6 +107,8 @@ void tst_TapHandler::touchGesturePolicyDragThreshold() QQuickItem *buttonDragThreshold = window->rootObject()->findChild("DragThreshold"); QVERIFY(buttonDragThreshold); + QQuickTapHandler *tapHandler = buttonDragThreshold->findChild(); + QVERIFY(tapHandler); QSignalSpy dragThresholdTappedSpy(buttonDragThreshold, SIGNAL(tapped())); // DragThreshold button stays pressed while touchpoint stays within dragThreshold, emits tapped on release @@ -122,6 +124,8 @@ void tst_TapHandler::touchGesturePolicyDragThreshold() QQuickTouchUtils::flush(window); QTRY_VERIFY(!buttonDragThreshold->property("pressed").toBool()); QCOMPARE(dragThresholdTappedSpy.count(), 1); + QCOMPARE(buttonDragThreshold->property("tappedPosition").toPoint(), p1); + QCOMPARE(tapHandler->point().position(), QPointF()); // DragThreshold button is no longer pressed if touchpoint goes beyond dragThreshold dragThresholdTappedSpy.clear(); @@ -152,6 +156,8 @@ void tst_TapHandler::mouseGesturePolicyDragThreshold() QQuickItem *buttonDragThreshold = window->rootObject()->findChild("DragThreshold"); QVERIFY(buttonDragThreshold); + QQuickTapHandler *tapHandler = buttonDragThreshold->findChild(); + QVERIFY(tapHandler); QSignalSpy dragThresholdTappedSpy(buttonDragThreshold, SIGNAL(tapped())); // DragThreshold button stays pressed while mouse stays within dragThreshold, emits tapped on release @@ -164,6 +170,8 @@ void tst_TapHandler::mouseGesturePolicyDragThreshold() QTest::mouseRelease(window, Qt::LeftButton, Qt::NoModifier, p1); QTRY_VERIFY(!buttonDragThreshold->property("pressed").toBool()); QTRY_COMPARE(dragThresholdTappedSpy.count(), 1); + QCOMPARE(buttonDragThreshold->property("tappedPosition").toPoint(), p1); + QCOMPARE(tapHandler->point().position(), QPointF()); // DragThreshold button is no longer pressed if mouse goes beyond dragThreshold dragThresholdTappedSpy.clear(); -- cgit v1.2.3 From 1982d1b1aa55ae44a1a775a5745e5c2f11001398 Mon Sep 17 00:00:00 2001 From: Shawn Rutledge Date: Thu, 4 Jul 2019 11:44:52 +0200 Subject: Move Event Handler acceptedButtons check back up to QQPDeviceHandler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverts what's left of e53510944169ac9f6753e0d14e1b24a24ff7bd9a (amends 73258eca7ab7e3981d9f4aaa5484020cb67854a0): MultiPointHandler is not only for touch handling anymore. DragHandler in particular needs to respect the acceptedButtons property. Fixes: QTBUG-76875 Fixes: QTBUG-76582 Change-Id: I414e785dd09b297c93e5e9f162be23e4a44eca54 Reviewed-by: Jan Arve Sæther --- src/quick/handlers/qquickhoverhandler.cpp | 3 + src/quick/handlers/qquickpointerdevicehandler.cpp | 4 ++ src/quick/handlers/qquicksinglepointhandler.cpp | 3 - .../qquickdraghandler/tst_qquickdraghandler.cpp | 82 +++++++++++++++------- 4 files changed, 62 insertions(+), 30 deletions(-) diff --git a/src/quick/handlers/qquickhoverhandler.cpp b/src/quick/handlers/qquickhoverhandler.cpp index 61955cad03..bdcd5dc4cc 100644 --- a/src/quick/handlers/qquickhoverhandler.cpp +++ b/src/quick/handlers/qquickhoverhandler.cpp @@ -38,6 +38,7 @@ ****************************************************************************/ #include "qquickhoverhandler_p.h" +#include QT_BEGIN_NAMESPACE @@ -59,6 +60,8 @@ Q_LOGGING_CATEGORY(lcHoverHandler, "qt.quick.handler.hover") QQuickHoverHandler::QQuickHoverHandler(QQuickItem *parent) : QQuickSinglePointHandler(parent) { + // Tell QQuickPointerDeviceHandler::wantsPointerEvent() to ignore button state + d_func()->acceptedButtons = Qt::NoButton; // Rule out the touchscreen for now (can be overridden in QML in case a hover-detecting touchscreen exists) setAcceptedDevices(static_cast( static_cast(QQuickPointerDevice::AllDevices) ^ static_cast(QQuickPointerDevice::TouchScreen))); diff --git a/src/quick/handlers/qquickpointerdevicehandler.cpp b/src/quick/handlers/qquickpointerdevicehandler.cpp index 096fad2071..246686e4f4 100644 --- a/src/quick/handlers/qquickpointerdevicehandler.cpp +++ b/src/quick/handlers/qquickpointerdevicehandler.cpp @@ -256,6 +256,10 @@ bool QQuickPointerDeviceHandler::wantsPointerEvent(QQuickPointerEvent *event) return false; if (d->acceptedModifiers != Qt::KeyboardModifierMask && event->modifiers() != d->acceptedModifiers) return false; + // HoverHandler sets acceptedButtons to Qt::NoButton to indicate that button state is irrelevant. + if (event->device()->pointerType() != QQuickPointerDevice::Finger && acceptedButtons() != Qt::NoButton && + (event->buttons() & acceptedButtons()) == 0 && (event->button() & acceptedButtons()) == 0) + return false; return true; } diff --git a/src/quick/handlers/qquicksinglepointhandler.cpp b/src/quick/handlers/qquicksinglepointhandler.cpp index ae162bed87..81859f6c80 100644 --- a/src/quick/handlers/qquicksinglepointhandler.cpp +++ b/src/quick/handlers/qquicksinglepointhandler.cpp @@ -67,9 +67,6 @@ bool QQuickSinglePointHandler::wantsPointerEvent(QQuickPointerEvent *event) { if (!QQuickPointerDeviceHandler::wantsPointerEvent(event)) return false; - if (event->device()->pointerType() != QQuickPointerDevice::Finger && - (event->buttons() & acceptedButtons()) == 0 && (event->button() & acceptedButtons()) == 0) - return false; if (m_pointInfo.m_id) { // We already know which one we want, so check whether it's there. diff --git a/tests/auto/quick/pointerhandlers/qquickdraghandler/tst_qquickdraghandler.cpp b/tests/auto/quick/pointerhandlers/qquickdraghandler/tst_qquickdraghandler.cpp index cc8c567e5c..fb0192893f 100644 --- a/tests/auto/quick/pointerhandlers/qquickdraghandler/tst_qquickdraghandler.cpp +++ b/tests/auto/quick/pointerhandlers/qquickdraghandler/tst_qquickdraghandler.cpp @@ -54,6 +54,7 @@ private slots: void defaultPropertyValues(); void touchDrag(); + void mouseDrag_data(); void mouseDrag(); void dragFromMargin(); void touchDragMulti(); @@ -193,8 +194,22 @@ void tst_DragHandler::touchDrag() QCOMPARE(centroidChangedSpy.count(), 5); } +void tst_DragHandler::mouseDrag_data() +{ + QTest::addColumn("acceptedButtons"); + QTest::addColumn("dragButton"); + QTest::newRow("left: drag") << Qt::MouseButtons(Qt::LeftButton) << Qt::MouseButtons(Qt::LeftButton); + QTest::newRow("right: don't drag") << Qt::MouseButtons(Qt::LeftButton) << Qt::MouseButtons(Qt::RightButton); + QTest::newRow("left: don't drag") << Qt::MouseButtons(Qt::RightButton | Qt::MiddleButton) << Qt::MouseButtons(Qt::LeftButton); + QTest::newRow("right or middle: drag") << Qt::MouseButtons(Qt::RightButton | Qt::MiddleButton) << Qt::MouseButtons(Qt::MiddleButton); +} + void tst_DragHandler::mouseDrag() { + QFETCH(Qt::MouseButtons, acceptedButtons); + QFETCH(Qt::MouseButtons, dragButton); + bool shouldDrag = bool(acceptedButtons & dragButton); + const int dragThreshold = QGuiApplication::styleHints()->startDragDistance(); QScopedPointer windowPtr; createView(windowPtr, "draggables.qml"); @@ -204,6 +219,7 @@ void tst_DragHandler::mouseDrag() QVERIFY(ball); QQuickDragHandler *dragHandler = ball->findChild(); QVERIFY(dragHandler); + dragHandler->setAcceptedButtons(acceptedButtons); // QTBUG-76875 QSignalSpy translationChangedSpy(dragHandler, SIGNAL(translationChanged())); QSignalSpy centroidChangedSpy(dragHandler, SIGNAL(centroidChanged())); @@ -211,45 +227,57 @@ void tst_DragHandler::mouseDrag() QPointF ballCenter = ball->clipRect().center(); QPointF scenePressPos = ball->mapToScene(ballCenter); QPoint p1 = scenePressPos.toPoint(); - QTest::mousePress(window, Qt::LeftButton, Qt::NoModifier, p1); + QTest::mousePress(window, static_cast(int(dragButton)), Qt::NoModifier, p1); QVERIFY(!dragHandler->active()); - QCOMPARE(dragHandler->centroid().position(), ballCenter); - QCOMPARE(dragHandler->centroid().pressPosition(), ballCenter); - QCOMPARE(dragHandler->centroid().scenePosition(), scenePressPos); - QCOMPARE(dragHandler->centroid().scenePressPosition(), scenePressPos); - QCOMPARE(dragHandler->centroid().velocity(), QVector2D()); - QCOMPARE(centroidChangedSpy.count(), 1); + if (shouldDrag) { + QCOMPARE(dragHandler->centroid().position(), ballCenter); + QCOMPARE(dragHandler->centroid().pressPosition(), ballCenter); + QCOMPARE(dragHandler->centroid().scenePosition(), scenePressPos); + QCOMPARE(dragHandler->centroid().scenePressPosition(), scenePressPos); + QCOMPARE(dragHandler->centroid().velocity(), QVector2D()); + QCOMPARE(centroidChangedSpy.count(), 1); + } p1 += QPoint(dragThreshold, 0); QTest::mouseMove(window, p1); - QTRY_VERIFY(dragHandler->centroid().velocity().x() > 0); - QCOMPARE(centroidChangedSpy.count(), 2); - QVERIFY(!dragHandler->active()); + if (shouldDrag) { + QTRY_VERIFY(dragHandler->centroid().velocity().x() > 0); + QCOMPARE(centroidChangedSpy.count(), 2); + QVERIFY(!dragHandler->active()); + } p1 += QPoint(1, 0); QTest::mouseMove(window, p1); - QTRY_VERIFY(dragHandler->active()); + if (shouldDrag) + QTRY_VERIFY(dragHandler->active()); + else + QVERIFY(!dragHandler->active()); QCOMPARE(translationChangedSpy.count(), 0); - QCOMPARE(centroidChangedSpy.count(), 3); + if (shouldDrag) + QCOMPARE(centroidChangedSpy.count(), 3); QCOMPARE(dragHandler->translation().x(), 0.0); QPointF sceneGrabPos = p1; - QCOMPARE(dragHandler->centroid().sceneGrabPosition(), sceneGrabPos); + if (shouldDrag) + QCOMPARE(dragHandler->centroid().sceneGrabPosition(), sceneGrabPos); p1 += QPoint(19, 0); QTest::mouseMove(window, p1); - QTRY_VERIFY(dragHandler->active()); - QCOMPARE(dragHandler->centroid().position(), ballCenter); - QCOMPARE(dragHandler->centroid().pressPosition(), ballCenter); - QCOMPARE(dragHandler->centroid().scenePosition(), ball->mapToScene(ballCenter)); - QCOMPARE(dragHandler->centroid().scenePressPosition(), scenePressPos); - QCOMPARE(dragHandler->centroid().sceneGrabPosition(), sceneGrabPos); - QCOMPARE(dragHandler->translation().x(), dragThreshold + 20.0); - QCOMPARE(dragHandler->translation().y(), 0.0); - QVERIFY(dragHandler->centroid().velocity().x() > 0); - QCOMPARE(centroidChangedSpy.count(), 4); - QTest::mouseRelease(window, Qt::LeftButton, Qt::NoModifier, p1); + QVERIFY(shouldDrag ? dragHandler->active() : !dragHandler->active()); + if (shouldDrag) { + QCOMPARE(dragHandler->centroid().position(), ballCenter); + QCOMPARE(dragHandler->centroid().pressPosition(), ballCenter); + QCOMPARE(dragHandler->centroid().scenePosition(), ball->mapToScene(ballCenter)); + QCOMPARE(dragHandler->centroid().scenePressPosition(), scenePressPos); + QCOMPARE(dragHandler->centroid().sceneGrabPosition(), sceneGrabPos); + QCOMPARE(dragHandler->translation().x(), dragThreshold + 20.0); + QCOMPARE(dragHandler->translation().y(), 0.0); + QVERIFY(dragHandler->centroid().velocity().x() > 0); + QCOMPARE(centroidChangedSpy.count(), 4); + } + QTest::mouseRelease(window, static_cast(int(dragButton)), Qt::NoModifier, p1); QTRY_VERIFY(!dragHandler->active()); QCOMPARE(dragHandler->centroid().pressedButtons(), Qt::NoButton); - QCOMPARE(ball->mapToScene(ballCenter).toPoint(), p1); - QCOMPARE(translationChangedSpy.count(), 1); - QCOMPARE(centroidChangedSpy.count(), 5); + if (shouldDrag) + QCOMPARE(ball->mapToScene(ballCenter).toPoint(), p1); + QCOMPARE(translationChangedSpy.count(), shouldDrag ? 1 : 0); + QCOMPARE(centroidChangedSpy.count(), shouldDrag ? 5 : 0); } void tst_DragHandler::dragFromMargin() // QTBUG-74966 -- cgit v1.2.3 From b15a745c384436f706cd9b87c04a7f252e109962 Mon Sep 17 00:00:00 2001 From: Marc Mutz Date: Sat, 6 Jul 2019 20:56:11 +0200 Subject: Fix compilation with C++20 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implicit capture of 'this' in [=] is deprecated in C++20. Fix by using explicit captures. Change-Id: I49b0fd2751c1d239c4f801224b71872c227fd697 Reviewed-by: Mårten Nordheim --- tests/auto/qml/ecmascripttests/qjstest/test262runner.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/auto/qml/ecmascripttests/qjstest/test262runner.cpp b/tests/auto/qml/ecmascripttests/qjstest/test262runner.cpp index cbe3be2e70..2f41e57324 100644 --- a/tests/auto/qml/ecmascripttests/qjstest/test262runner.cpp +++ b/tests/auto/qml/ecmascripttests/qjstest/test262runner.cpp @@ -597,7 +597,7 @@ void SingleTest::run() void SingleTest::runExternalTest() { - auto runTest = [=] (const char *header, TestCase::Result *result) { + auto runTest = [this] (const char *header, TestCase::Result *result) { QTemporaryFile tempFile; tempFile.open(); tempFile.write(header); -- cgit v1.2.3 From f3d40896c1d8601703fcbf30214e22f50eb72727 Mon Sep 17 00:00:00 2001 From: Fabian Kosmale Date: Tue, 9 Jul 2019 16:39:25 +0200 Subject: Fix promise chaining Fixes: QTBUG-71329 Change-Id: I261b25ff281bb44d03650ab05258743f104f3cc9 Reviewed-by: Ulf Hermann --- src/qml/jsruntime/qv4promiseobject.cpp | 201 ++++++++++++++++++----- src/qml/jsruntime/qv4promiseobject_p.h | 7 +- tests/auto/qml/qqmlpromise/data/promisechain.qml | 24 +++ tests/auto/qml/qqmlpromise/tst_qqmlpromise.cpp | 15 ++ 4 files changed, 201 insertions(+), 46 deletions(-) create mode 100644 tests/auto/qml/qqmlpromise/data/promisechain.qml diff --git a/src/qml/jsruntime/qv4promiseobject.cpp b/src/qml/jsruntime/qv4promiseobject.cpp index 8450655334..40a0dfaa57 100644 --- a/src/qml/jsruntime/qv4promiseobject.cpp +++ b/src/qml/jsruntime/qv4promiseobject.cpp @@ -86,6 +86,7 @@ namespace QV4 { namespace Promise { const int PROMISE_REACTION_EVENT = QEvent::registerEventType(); +const int PROMISE_RESOLVE_THENABLE_EVENT = QEvent::registerEventType(); struct ReactionEvent : public QEvent { @@ -99,6 +100,18 @@ struct ReactionEvent : public QEvent QV4::PersistentValue resolution; }; +struct ResolveThenableEvent : public QEvent +{ + ResolveThenableEvent(ExecutionEngine *e, const PromiseObject *promise_, const Object *thenable_, const FunctionObject *then_) + : QEvent(QEvent::Type(PROMISE_RESOLVE_THENABLE_EVENT)), promise(e, *promise_), thenable(e, *thenable_), then(e, *then_) + {} + + QV4::PersistentValue promise; + QV4::PersistentValue thenable; + QV4::PersistentValue then; +}; + + } // namespace Promise } // namespace QV4 QT_END_NAMESPACE @@ -115,6 +128,11 @@ void ReactionHandler::addReaction(ExecutionEngine *e, const Value *reaction, con QCoreApplication::postEvent(this, new ReactionEvent(e, reaction, value)); } +void ReactionHandler::addResolveThenable(ExecutionEngine *e, const PromiseObject *promise, const Object *thenable, const FunctionObject *then) +{ + QCoreApplication::postEvent(this, new ResolveThenableEvent(e, promise, thenable, then)); +} + void ReactionHandler::customEvent(QEvent *event) { if (event) @@ -122,6 +140,9 @@ void ReactionHandler::customEvent(QEvent *event) const int type = event->type(); if (type == PROMISE_REACTION_EVENT) executeReaction(static_cast(event)); + + if (type == PROMISE_RESOLVE_THENABLE_EVENT) + executeResolveThenable(static_cast(event)); } } @@ -159,6 +180,7 @@ void ReactionHandler::executeReaction(ReactionEvent *event) } } + namespace { class FunctionBuilder { @@ -200,6 +222,26 @@ public: } + +void ReactionHandler::executeResolveThenable(ResolveThenableEvent *event) +{ + Scope scope(event->then.engine()); + JSCallData jsCallData(scope, 2); + PromiseObject *promise = event->promise.as(); + ScopedFunctionObject resolve {scope, FunctionBuilder::makeResolveFunction(scope.engine, promise->d())}; + ScopedFunctionObject reject {scope, FunctionBuilder::makeRejectFunction(scope.engine, promise->d())}; + jsCallData->args[0] = resolve; + jsCallData.args[1] = reject; + jsCallData->thisObject = event->thenable.as(); + event->then.as()->call(jsCallData); + if (scope.engine->hasException) { + JSCallData rejectCallData(scope, 1); + rejectCallData->args[0] = scope.engine->catchException(); + Scoped reject {scope, scope.engine->memoryManager->allocate()}; + reject->call(rejectCallData); + } +} + void Heap::PromiseObject::setState(PromiseObject::State state) { this->state = state; @@ -363,23 +405,36 @@ void Heap::RejectWrapper::init() Heap::FunctionObject::init(); } +ReturnedValue PromiseCtor::virtualCall(const FunctionObject *f, const Value *, const Value *, int) +{ + // 25.4.3.1 Promise ( executor ) + // 1. If NewTarget is undefined, throw a TypeError exception. + Scope scope(f); + THROW_TYPE_ERROR(); +} ReturnedValue PromiseCtor::virtualCallAsConstructor(const FunctionObject *f, const Value *argv, int argc, const Value *newTarget) { + // 25.4.3.1 Promise ( executor ) + Scope scope(f); - if (argc != 1) + if (argc == 0) // If there are no arguments, argument 1 will be undefined ==> thus not callable ==> type error THROW_TYPE_ERROR(); ScopedFunctionObject executor(scope, argv[0].as()); - if (!executor) - THROW_TYPE_ERROR(); + if (!executor) //2. If IsCallable(executor) is false + THROW_TYPE_ERROR(); // throw a TypeError exception Scoped a(scope, scope.engine->newPromiseObject()); if (scope.engine->hasException) return Encode::undefined(); - a->d()->state = Heap::PromiseObject::Pending; + a->d()->state = Heap::PromiseObject::Pending; //4. Set promise.[[PromiseState]] to "pending" + // 5. Set promise.[[PromiseFulfillReactions]] to a new empty List. + // 6. Set promise.[[PromiseRejectReactions]] to a new empty List. + // 7. Set promise.[[PromiseIsHandled]] to false. + // happens in constructor of a ScopedFunctionObject resolve(scope, FunctionBuilder::makeResolveFunction(scope.engine, a->d())); ScopedFunctionObject reject(scope, FunctionBuilder::makeRejectFunction(scope.engine, a->d())); @@ -387,13 +442,15 @@ ReturnedValue PromiseCtor::virtualCallAsConstructor(const FunctionObject *f, con JSCallData jsCallData(scope, 2); jsCallData->args[0] = resolve; jsCallData->args[1] = reject; - jsCallData->thisObject = a; + //jsCallData->thisObject = a; VERIFY corretness, but this should be undefined (see below) - executor->call(jsCallData); + executor->call(jsCallData); // 9. Let completion be Call(executor, undefined, « resolvingFunctions.[[Resolve]], resolvingFunctions.[[Reject]] »). if (scope.engine->hasException) { - a->d()->state = Heap::PromiseObject::Rejected; - a->d()->resolution.set(scope.engine, Value::fromReturnedValue(scope.engine->catchException())); + ScopedValue exception {scope, scope.engine->catchException()}; + JSCallData callData {scope, 1}; + callData.args[0] = exception; + reject->call(callData); } if (newTarget) @@ -402,42 +459,42 @@ ReturnedValue PromiseCtor::virtualCallAsConstructor(const FunctionObject *f, con return a->asReturnedValue(); } -ReturnedValue PromiseCtor::virtualCall(const FunctionObject *f, const Value *, const Value *, int) -{ - Scope scope(f); - THROW_TYPE_ERROR(); -} ReturnedValue PromiseCtor::method_resolve(const FunctionObject *f, const Value *thisObject, const Value *argv, int argc) { + // 25.4.4.5Promise.resolve ( x ) Scope scope(f); ExecutionEngine* e = scope.engine; - if (!thisObject || !thisObject->isObject()) + if (!thisObject || !thisObject->isObject()) // 2. If Type(C) is not Object, throw a TypeError exception THROW_TYPE_ERROR(); - ScopedValue argument(scope); + ScopedValue x(scope); if (argc < 1) { - argument = Encode::undefined(); + x = Encode::undefined(); } else { - argument = argv[0]; + x = argv[0]; } - if (isPromise(argument) && argument->isObject()) { + // 3. If IsPromise(x) is true, then + if (isPromise(x) && x->isObject()) { ScopedObject so(scope, thisObject); - ScopedObject constructor(scope, argument->objectValue()->get(e->id_constructor())); - if (so->d() == constructor->d()) - return argument->asReturnedValue(); + // Let xConstructor be ? Get(x, "constructor"). + ScopedObject constructor(scope, x->objectValue()->get(e->id_constructor())); + if (so->d() == constructor->d()) // If SameValue(xConstructor, C) is true, return x. + return x->asReturnedValue(); } + // Let promiseCapability be ? NewPromiseCapability(C). Scoped capability(scope, e->memoryManager->allocate()); ScopedObject newPromise(scope, e->newPromiseObject(thisObject->as(), capability)); if (!newPromise || !isCallable(capability->d()->resolve) || !isCallable(capability->d()->reject)) THROW_TYPE_ERROR(); + // Perform ? Call(promiseCapability.[[Resolve]], undefined, « x »). ScopedValue undefined(scope, Value::undefinedValue()); ScopedFunctionObject resolve(scope, capability->d()->resolve); - resolve->call(undefined, argument, 1); + resolve->call(undefined, x, 1); return newPromise.asReturnedValue(); } @@ -447,16 +504,18 @@ ReturnedValue PromiseCtor::method_reject(const FunctionObject *f, const Value *t Scope scope(f); ExecutionEngine *e = scope.engine; + // 2. If Type(C) is not Object, throw a TypeError exception. if (!thisObject || !thisObject->isObject()) THROW_TYPE_ERROR(); - ScopedValue argument(scope); + ScopedValue r(scope); if (argc < 1) { - argument = Encode::undefined(); + r = Encode::undefined(); } else { - argument = argv[0]; + r = argv[0]; } + // 3. Let promiseCapability be ? NewPromiseCapability(C). Scoped capability(scope, e->memoryManager->allocate()); ScopedObject newPromise(scope, e->newPromiseObject(thisObject->as(), capability)); @@ -465,7 +524,8 @@ ReturnedValue PromiseCtor::method_reject(const FunctionObject *f, const Value *t ScopedValue undefined(scope, Value::undefinedValue()); ScopedFunctionObject reject(scope, capability->d()->reject.as()); - reject->call(undefined, argument, 1); + // Perform ? Call(promiseCapability.[[Reject]], undefined, « r »). + reject->call(undefined, r, 1); return newPromise.asReturnedValue(); } @@ -475,6 +535,7 @@ ReturnedValue PromiseCtor::method_all(const FunctionObject *f, const Value *this Scope scope(f); ExecutionEngine* e = scope.engine; + // 2. If Type(C) is not Object, throw a TypeError exception. if (!thisObject || !thisObject->isObject()) THROW_TYPE_ERROR(); @@ -777,6 +838,7 @@ void PromisePrototype::init(ExecutionEngine *engine, Object *ctor) ReturnedValue PromisePrototype::method_then(const FunctionObject *f, const Value *thisObject, const Value *argv, int argc) { + // 25.4.5.3 Promise.prototype.then Scope scope(f); ExecutionEngine* e = scope.engine; @@ -804,6 +866,7 @@ ReturnedValue PromisePrototype::method_then(const FunctionObject *f, const Value if (!constructor || scope.hasException()) THROW_TYPE_ERROR(); + // 4. Let resultCapability be ? NewPromiseCapability(C). ScopedObject nextPromise(scope, e->newPromiseObject(constructor, capability)); capability->d()->promise.set(scope.engine, nextPromise); @@ -811,21 +874,25 @@ ReturnedValue PromisePrototype::method_then(const FunctionObject *f, const Value Scoped rejectReaction(scope, Heap::PromiseReaction::createRejectReaction(scope.engine, capability, onRejected)); ScopedValue resolution(scope, promise->d()->resolution); - if (promise->d()->isPending()) { + if (promise->d()->isPending()) { // 7. If promise.[[PromiseState]] is "pending" { + // Append fulfillReaction as the last element of the List that is promise.[[PromiseFulfillReactions]]. ScopedArrayObject a(scope, promise->d()->fulfillReactions); ScopedValue newValue(scope, fulfillReaction->d()); a->push_back(newValue); } { + // Append rejectReaction as the last element of the List that is promise.[[PromiseRejectReactions]]. ScopedArrayObject a(scope, promise->d()->rejectReactions); ScopedValue newValue(scope, rejectReaction->d()); a->push_back(newValue); } - } else if (promise->d()->isFulfilled()) { - fulfillReaction->as()->d()->triggerWithValue(e, resolution); - } else if (promise->d()->isRejected()) { + } else if (promise->d()->isFulfilled()) { // 8. Else if promise.[[PromiseState]] is "fulfilled", then + // Perform EnqueueJob("PromiseJobs", PromiseReactionJob, « fulfillReaction, value »). + fulfillReaction->as()->d()->triggerWithValue(e, resolution); // Perform EnqueueJob("PromiseJobs", PromiseReactionJob, « fulfillReaction, value »). + } else if (promise->d()->isRejected()) { // 9. Else + // Perform EnqueueJob("PromiseJobs", PromiseReactionJob, « rejectReaction, reason »). rejectReaction->as()->d()->triggerWithValue(e, resolution); } else { Q_ASSERT(false); @@ -889,7 +956,6 @@ ReturnedValue CapabilitiesExecutorWrapper::virtualCall(const FunctionObject *f, if (argc >= 2 && !argv[1].isUndefined()) capabilities->reject.set(scope.engine, argv[1]); - // TODO: return? return Encode::undefined(); } @@ -929,27 +995,60 @@ ReturnedValue ResolveElementWrapper::virtualCall(const FunctionObject *f, const ReturnedValue ResolveWrapper::virtualCall(const FunctionObject *f, const Value *thisObject, const Value *argv, int argc) { + // 25.4.1.3.2 (ecmase-262/8.0) + Q_UNUSED(thisObject); Scope scope(f); const ResolveWrapper *self = static_cast(f); Scoped promise(scope, self->d()->promise); - if (self->d()->alreadyResolved || !promise->d()->isPending()) + // 4. If alreadyRseolved.[[Value]] is true, return undefined + if (self->d()->alreadyResolved || !promise->d()->isPending()) // Why check for pending? return Encode::undefined(); - ScopedValue value(scope); + // 5. Set alreadyResolved.[[Value]] to true + self->d()->alreadyResolved = true; + + ScopedValue resolution(scope); if (argc == 1) { - value = argv[0]; + resolution = argv[0]; } else { - value = Encode::undefined(); + resolution = Encode::undefined(); } - self->d()->alreadyResolved = true; - promise->d()->setState(Heap::PromiseObject::Fulfilled); - promise->d()->resolution.set(scope.engine, value); - - promise->d()->triggerFullfillReactions(scope.engine); + if (!resolution->isObject()) { // 7 If Type(resolution) is not Object + // then Return FullFillPromise(promise, resolution) + // (FullFillPromise will return undefined, so we share the return with the other path which also returns undefined + promise->d()->setState(Heap::PromiseObject::Fulfilled); + promise->d()->resolution.set(scope.engine, resolution); + promise->d()->triggerFullfillReactions(scope.engine); + } else { + //PromiseObject *promise = resolution->as(); + auto resolutionObject = resolution->as(); + ScopedString thenName(scope, scope.engine->newIdentifier(QStringLiteral("then"))); + + // 8. Let then be Get(resolution, then) + ScopedFunctionObject thenAction { scope, resolutionObject->get(thenName)}; + // 9. If then is an abrupt completion, then + if (scope.engine->hasException) { + // Return RecjectPromise(promise, then.[[Value]] + ScopedValue thenValue {scope, scope.engine->catchException()}; + promise->d()->setState(Heap::PromiseObject::Rejected); + promise->d()->resolution.set(scope.engine, thenValue); + promise->d()->triggerRejectReactions(scope.engine); + } else { + // 10. Let thenAction be then.[[Value]] + if (!thenAction) { // 11. If IsCallable(thenAction) is false + promise->d()->setState(Heap::PromiseObject::Fulfilled); + promise->d()->resolution.set(scope.engine, resolution); + promise->d()->triggerFullfillReactions(scope.engine); + } else { + // 12. Perform EnqueueJob("PromiseJobs", PromiseResolveThenableJob, « promise, resolution, thenAction »). + scope.engine->getPromiseReactionHandler()->addResolveThenable(scope.engine, promise.getPointer(), resolutionObject, thenAction); + } + } + } return Encode::undefined(); } @@ -972,11 +1071,25 @@ ReturnedValue RejectWrapper::virtualCall(const FunctionObject *f, const Value *t value = Encode::undefined(); } - self->d()->alreadyResolved = true; - promise->d()->setState(Heap::PromiseObject::Rejected); - promise->d()->resolution.set(scope.engine, value); + if (!isPromise(value)) { + self->d()->alreadyResolved = true; + promise->d()->setState(Heap::PromiseObject::Rejected); + promise->d()->resolution.set(scope.engine, value); + + promise->d()->triggerRejectReactions(scope.engine); - promise->d()->triggerRejectReactions(scope.engine); + } else { + PromiseObject *promise = value->as(); + ScopedString thenName(scope, scope.engine->newIdentifier(QStringLiteral("catch"))); + + ScopedFunctionObject then(scope, promise->get(thenName)); + JSCallData jsCallData(scope, 2); + jsCallData->args[0] = *f; + jsCallData->args[1] = Encode::undefined(); + jsCallData->thisObject = value; + + then->call(jsCallData); + } return Encode::undefined(); } diff --git a/src/qml/jsruntime/qv4promiseobject_p.h b/src/qml/jsruntime/qv4promiseobject_p.h index bce59b19a7..8a3724e07d 100644 --- a/src/qml/jsruntime/qv4promiseobject_p.h +++ b/src/qml/jsruntime/qv4promiseobject_p.h @@ -62,6 +62,7 @@ struct PromiseCapability; namespace Promise { struct ReactionEvent; +struct ResolveThenableEvent; class ReactionHandler : public QObject { @@ -69,13 +70,15 @@ class ReactionHandler : public QObject public: ReactionHandler(QObject *parent = nullptr); - virtual ~ReactionHandler(); + virtual ~ReactionHandler() override; void addReaction(ExecutionEngine *e, const Value *reaction, const Value *value); + void addResolveThenable(ExecutionEngine *e, const PromiseObject *promise, const Object *thenable, const FunctionObject *then); protected: - void customEvent(QEvent *event); + void customEvent(QEvent *event) override; void executeReaction(ReactionEvent *event); + void executeResolveThenable(ResolveThenableEvent *event); }; } // Promise diff --git a/tests/auto/qml/qqmlpromise/data/promisechain.qml b/tests/auto/qml/qqmlpromise/data/promisechain.qml new file mode 100644 index 0000000000..fa1809aef0 --- /dev/null +++ b/tests/auto/qml/qqmlpromise/data/promisechain.qml @@ -0,0 +1,24 @@ +import QtQml 2.0 + +QtObject { + property int x: 0 + id: root; + Component.onCompleted: { + new Promise((res) => { + res(1) + }) + .then((data) => { + console.debug(data) + return new Promise((res) => {res(2)}); + }) + .then((data) => { + console.debug(data) + return new Promise((res) => {res(3)}); + }) + .then((data) => { + console.debug(data); + root.x = 42; + }); + } + +} diff --git a/tests/auto/qml/qqmlpromise/tst_qqmlpromise.cpp b/tests/auto/qml/qqmlpromise/tst_qqmlpromise.cpp index 0f4bb5cdcc..41850d0263 100644 --- a/tests/auto/qml/qqmlpromise/tst_qqmlpromise.cpp +++ b/tests/auto/qml/qqmlpromise/tst_qqmlpromise.cpp @@ -82,6 +82,7 @@ private slots: void then_fulfilled_non_callable(); void then_reject_non_callable(); void then_resolve_multiple_then(); + void promiseChain(); private: void execute_test(QString testName); @@ -270,6 +271,20 @@ void tst_qqmlpromise::execute_test(QString testName) QTRY_COMPARE(object->property("wasTestSuccessful").toBool(), true); } +void tst_qqmlpromise::promiseChain() +{ + QQmlEngine engine; + QQmlComponent component(&engine, testFileUrl("promisechain.qml")); + QVERIFY(component.isReady()); + QTest::ignoreMessage(QtDebugMsg, "1"); + QTest::ignoreMessage(QtDebugMsg, "2"); + QTest::ignoreMessage(QtDebugMsg, "3"); + QScopedPointer root(component.create()); + QVERIFY(root); + QTRY_VERIFY(root->property("x") == 42); + +} + QTEST_MAIN(tst_qqmlpromise) -- cgit v1.2.3