diff options
author | Shawn Rutledge <[email protected]> | 2024-06-17 20:44:04 -0700 |
---|---|---|
committer | Shawn Rutledge <[email protected]> | 2024-06-28 17:12:14 -0700 |
commit | 0debcf3ed3f62416be1c172454f420119df0587e (patch) | |
tree | cfb3bb664151bd87d07f34d19f98160ff0f058ba | |
parent | 3b6355fbaab2559dab301c803780768295089637 (diff) |
Call QQuickItem::mouseUngrabEvent() on ungrab from QTabletEvent
As long as we rely on QGuiApplicationPrivate::processTabletEvent() to
synthesize mouse events from tablet events (we aren't doing it on the
fly as with touch events in 468626e99a90d6ac21cb311cde05c658ccb3b781),
and as long as most QQuickItems are not handling tablet events,
QQuickDeliveryAgentPrivate::onGrabChanged() almost always sees a
QMouseEvent losing its grab when e.g. ListView takes over from one of
its delegates while the user is trying to scroll with a stylus device.
This event's device is the stylus, though.
Whenever we see an EventPoint being canceled or ungrabbed, we must call
either mouseUngrabEvent() on the item, or touchUngrabEvent() if all the
points are released or cancelled, to avoid items getting "stuck" in
pressed state. It must not be skipped. So call mouseUngrabEvent()
whenever the event is a QSinglePointEvent, and touchUngrabEvent()
otherwise (since only touchscreens send multi-point events).
Make QQuickTest::pointerPress/Move/Release functions more correct for
stylus devices:
- we need the timestamp to monotonically increase, even though
QTest::defaultMouseDelay() is usually 0 (which isn't sensible)
- QTest::mouseEvent() calls qt_handleMouseEvent which converts
logical coordinates to native positions; but for tablet events,
do it here for now, since there are no QTest methods to generate them.
This helps QQuickFlickablePrivate::handleMoveEvent() to calculate
deltas correctly.
Fixes: QTBUG-118903
Pick-to: 6.8 6.7 6.6 6.5
Change-Id: I5ec54c5181f5b9137fe16248884010aea94f671a
Reviewed-by: Doris Verria <[email protected]>
-rw-r--r-- | src/quick/util/qquickdeliveryagent.cpp | 33 | ||||
-rw-r--r-- | src/quick/util/qquickdeliveryagent_p_p.h | 1 | ||||
-rw-r--r-- | src/quicktestutils/quick/viewtestutils.cpp | 35 | ||||
-rw-r--r-- | tests/auto/quick/qquicklistview/data/delegateWithMouseArea.qml | 3 | ||||
-rw-r--r-- | tests/auto/quick/qquicklistview/tst_qquicklistview.cpp | 65 |
5 files changed, 122 insertions, 15 deletions
diff --git a/src/quick/util/qquickdeliveryagent.cpp b/src/quick/util/qquickdeliveryagent.cpp index 1b2a888a22..9c63cf6b19 100644 --- a/src/quick/util/qquickdeliveryagent.cpp +++ b/src/quick/util/qquickdeliveryagent.cpp @@ -1538,6 +1538,28 @@ bool QQuickDeliveryAgentPrivate::isSynthMouse(const QPointerEvent *ev) return (!isEventFromMouseOrTouchpad(ev) && isMouseEvent(ev)); } +/*! + Returns \c true if \a dev is a type of device that only sends + QSinglePointEvents. +*/ +bool QQuickDeliveryAgentPrivate::isSinglePointDevice(const QInputDevice *dev) +{ + switch (dev->type()) { + case QInputDevice::DeviceType::Mouse: + case QInputDevice::DeviceType::TouchPad: + case QInputDevice::DeviceType::Puck: + case QInputDevice::DeviceType::Stylus: + case QInputDevice::DeviceType::Airbrush: + return true; + case QInputDevice::DeviceType::TouchScreen: + case QInputDevice::DeviceType::Keyboard: + case QInputDevice::DeviceType::Unknown: + case QInputDevice::DeviceType::AllDevices: + return false; + } + return false; +} + QQuickPointingDeviceExtra *QQuickDeliveryAgentPrivate::deviceExtra(const QInputDevice *device) { QInputDevicePrivate *devPriv = QInputDevicePrivate::get(const_cast<QInputDevice *>(device)); @@ -1843,17 +1865,18 @@ void QQuickDeliveryAgentPrivate::onGrabChanged(QObject *grabber, QPointingDevice switch (transition) { case QPointingDevice::CancelGrabExclusive: case QPointingDevice::UngrabExclusive: - if (isDeliveringTouchAsMouse() - || point.device()->type() == QInputDevice::DeviceType::Mouse - || point.device()->type() == QInputDevice::DeviceType::TouchPad) { + if (isDeliveringTouchAsMouse() || isSinglePointDevice(point.device())) { + // If an EventPoint from the mouse or the synth-mouse or from any + // mouse-like device is ungrabbed, call QQuickItem::mouseUngrabEvent(). QMutableSinglePointEvent e(QEvent::UngrabMouse, point.device(), point); hasFiltered.clear(); if (!sendFilteredMouseEvent(&e, grabberItem, grabberItem->parentItem())) { lastUngrabbed = grabberItem; grabberItem->mouseUngrabEvent(); } - } - if (point.device()->type() == QInputDevice::DeviceType::TouchScreen) { + } else { + // Multi-point event: call QQuickItem::touchUngrabEvent() only if + // all eventpoints are released or cancelled. bool allReleasedOrCancelled = true; if (transition == QPointingDevice::UngrabExclusive && event) { for (const auto &pt : event->points()) { diff --git a/src/quick/util/qquickdeliveryagent_p_p.h b/src/quick/util/qquickdeliveryagent_p_p.h index 878561bc12..3dd60db98f 100644 --- a/src/quick/util/qquickdeliveryagent_p_p.h +++ b/src/quick/util/qquickdeliveryagent_p_p.h @@ -151,6 +151,7 @@ public: static bool isTabletEvent(const QPointerEvent *ev); static bool isEventFromMouseOrTouchpad(const QPointerEvent *ev); static bool isSynthMouse(const QPointerEvent *ev); + static bool isSinglePointDevice(const QInputDevice *dev); static QQuickPointingDeviceExtra *deviceExtra(const QInputDevice *device); // delivery of pointer events: diff --git a/src/quicktestutils/quick/viewtestutils.cpp b/src/quicktestutils/quick/viewtestutils.cpp index 053e864660..40a67a305e 100644 --- a/src/quicktestutils/quick/viewtestutils.cpp +++ b/src/quicktestutils/quick/viewtestutils.cpp @@ -9,6 +9,7 @@ #include <QtQuick/QQuickView> #include <QtGui/QScreen> #include <QtGui/qpa/qwindowsysteminterface.h> +#include <QtGui/private/qhighdpiscaling_p.h> #include <QtTest/QTest> @@ -537,13 +538,17 @@ namespace QQuickTest { break; case QPointingDevice::DeviceType::Puck: case QPointingDevice::DeviceType::Stylus: - case QPointingDevice::DeviceType::Airbrush: - QTest::lastMouseTimestamp += QTest::defaultMouseDelay(); + case QPointingDevice::DeviceType::Airbrush:{ + const QPointF nativeLocal = QHighDpi::toNativeLocalPosition(p, window); + const QPointF nativeGlobal = QHighDpi::toNativeGlobalPosition(window->mapToGlobal(p), window); + const auto delay = QTest::defaultMouseDelay(); + QTest::lastMouseTimestamp += delay ? delay : 1; pressedTabletButton = button; pressedTabletModifiers = modifiers; - QWindowSystemInterface::handleTabletEvent(window, QTest::lastMouseTimestamp, dev, p, window->mapToGlobal(p), + QWindowSystemInterface::handleTabletEvent(window, QTest::lastMouseTimestamp, dev, nativeLocal, nativeGlobal, button, 0.8, 0, 0, 0, 0, 0, modifiers); break; + } default: qWarning() << "can't send a press event from" << dev; break; @@ -563,11 +568,17 @@ namespace QQuickTest { break; case QPointingDevice::DeviceType::Puck: case QPointingDevice::DeviceType::Stylus: - case QPointingDevice::DeviceType::Airbrush: - QTest::lastMouseTimestamp += QTest::defaultMouseDelay(); - QWindowSystemInterface::handleTabletEvent(window, QTest::lastMouseTimestamp, dev, p, window->mapToGlobal(p), - pressedTabletButton, 0, 0, 0, 0, 0, 0, pressedTabletModifiers); + case QPointingDevice::DeviceType::Airbrush: { + const QPointF nativeLocal = QHighDpi::toNativeLocalPosition(p, window); + const QPointF nativeGlobal = QHighDpi::toNativeGlobalPosition(window->mapToGlobal(p), window); + const auto delay = QTest::defaultMouseDelay(); + // often QTest::defaultMouseDelay() == 0; but avoid infinite velocity + QTest::lastMouseTimestamp += delay ? delay : 1; + QWindowSystemInterface::handleTabletEvent(window, QTest::lastMouseTimestamp, dev, nativeLocal, nativeGlobal, + pressedTabletButton, pressedTabletButton == Qt::NoButton ? 0 : 0.75, + 0, 0, 0, 0, 0, pressedTabletModifiers); break; + } default: qWarning() << "can't send a move event from" << dev; break; @@ -588,11 +599,15 @@ namespace QQuickTest { break; case QPointingDevice::DeviceType::Puck: case QPointingDevice::DeviceType::Stylus: - case QPointingDevice::DeviceType::Airbrush: - QTest::lastMouseTimestamp += QTest::defaultMouseDelay(); - QWindowSystemInterface::handleTabletEvent(window, QTest::lastMouseTimestamp, dev, p, window->mapToGlobal(p), + case QPointingDevice::DeviceType::Airbrush: { + const QPointF nativeLocal = QHighDpi::toNativeLocalPosition(p, window); + const QPointF nativeGlobal = QHighDpi::toNativeGlobalPosition(window->mapToGlobal(p), window); + const auto delay = QTest::defaultMouseDelay(); + QTest::lastMouseTimestamp += delay ? delay : 1; + QWindowSystemInterface::handleTabletEvent(window, QTest::lastMouseTimestamp, dev, nativeLocal, nativeGlobal, Qt::NoButton, 0, 0, 0, 0, 0, 0, modifiers); break; + } default: qWarning() << "can't send a press event from" << dev; break; diff --git a/tests/auto/quick/qquicklistview/data/delegateWithMouseArea.qml b/tests/auto/quick/qquicklistview/data/delegateWithMouseArea.qml index e0b8222bfb..4bdf7033a9 100644 --- a/tests/auto/quick/qquicklistview/data/delegateWithMouseArea.qml +++ b/tests/auto/quick/qquicklistview/data/delegateWithMouseArea.qml @@ -17,12 +17,15 @@ ListView { width: 500 height: 500 color: Qt.rgba(Math.random(), Math.random(), Math.random(), 1) + border.width: 4 + border.color: ma.pressed ? "red" : "white" Text { text: index font.pixelSize: 128 anchors.centerIn: parent } MouseArea { + id: ma anchors.fill: parent } } diff --git a/tests/auto/quick/qquicklistview/tst_qquicklistview.cpp b/tests/auto/quick/qquicklistview/tst_qquicklistview.cpp index a1e7d5b4ca..2629941135 100644 --- a/tests/auto/quick/qquicklistview/tst_qquicklistview.cpp +++ b/tests/auto/quick/qquicklistview/tst_qquicklistview.cpp @@ -39,6 +39,8 @@ Q_DECLARE_METATYPE(QQuickListView::Orientation) Q_DECLARE_METATYPE(QQuickFlickable::FlickableDirection) Q_DECLARE_METATYPE(Qt::Key) +Q_LOGGING_CATEGORY(lcTests, "qt.quick.tests") + using namespace QQuickViewTestUtils; using namespace QQuickVisualTestUtils; @@ -268,6 +270,8 @@ private slots: void addOnCompleted(); void setPositionOnLayout(); void touchCancel(); + void cancelDelegatePastDragThreshold_data(); + void cancelDelegatePastDragThreshold(); void resizeAfterComponentComplete(); void dragOverFloatingHeaderOrFooter(); @@ -340,6 +344,12 @@ private: QQuickView *m_view; QString testForView; QPointingDevice *touchDevice = QTest::createTouchDevice(); +#if QT_CONFIG(tabletevent) + QScopedPointer<const QPointingDevice> tabletStylusDevice = QScopedPointer<const QPointingDevice>( + QPointingDevicePrivate::tabletDevice(QInputDevice::DeviceType::Stylus, + QPointingDevice::PointerType::Pen, + QPointingDeviceUniqueId::fromNumericId(1234567890))); +#endif }; class TestObject : public QObject @@ -9744,6 +9754,61 @@ void tst_QQuickListView::touchCancel() // QTBUG-74679 QTRY_COMPARE(listview->contentY(), 500.0); } +void tst_QQuickListView::cancelDelegatePastDragThreshold_data() +{ + QTest::addColumn<const QPointingDevice *>("device"); + + QTest::newRow("primary") << QPointingDevice::primaryPointingDevice(); + QTest::newRow("touch") << static_cast<const QPointingDevice*>(touchDevice); // TODO QTBUG-107864 +#if QT_CONFIG(tabletevent) && !defined(Q_OS_QNX) + QTest::newRow("stylus") << tabletStylusDevice.get(); +#endif +} + +void tst_QQuickListView::cancelDelegatePastDragThreshold() // QTBUG-118903 +{ + const int dragThreshold = QGuiApplication::styleHints()->startDragDistance(); + QFETCH(const QPointingDevice *, device); + +#if QT_CONFIG(tabletevent) + QVERIFY(qApp->testAttribute(Qt::AA_SynthesizeMouseForUnhandledTabletEvents)); +#endif + + QScopedPointer<QQuickView> window(createView()); + window->setSource(testFileUrl("delegateWithMouseArea.qml")); + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window.data())); + + QQuickListView *listview = qobject_cast<QQuickListView *>(window->rootObject()); + QVERIFY(listview); + QQuickMouseArea *mouseArea = listview->currentItem()->findChild<QQuickMouseArea *>(); + QVERIFY(mouseArea); + QSignalSpy canceledSpy(mouseArea, &QQuickMouseArea::canceled); + + QPoint p = mouseArea->mapToScene(mouseArea->boundingRect().center()).toPoint(); + // MouseArea grabs on press + QQuickTest::pointerPress(device, window.get(), 1, p); + QTRY_VERIFY(mouseArea->isPressed()); + // drag past the drag threshold until ListView takes over the grab + p -= {0, dragThreshold + 1}; + QQuickTest::pointerMove(device, window.get(), 1, p); + int movesWhenGrabbed = 1; + for (int i = 2; i < 6; ++i) { + p -= {0, 1}; + QQuickTest::pointerMove(device, window.get(), 1, p); + if (device == tabletStylusDevice.get()) + QTest::qWait(1); + if (listview->isDragging()) + movesWhenGrabbed = i; + } + qCDebug(lcTests) << "ListView took over grab after" << movesWhenGrabbed << "moves"; + QVERIFY(listview->isDragging()); + // MouseArea's grab got canceled + QCOMPARE(canceledSpy.size(), 1); + QCOMPARE(mouseArea->isPressed(), false); + QQuickTest::pointerRelease(device, window.get(), 1, p); +} + void tst_QQuickListView::resizeAfterComponentComplete() // QTBUG-76487 { QScopedPointer<QQuickView> window(createView()); |