Ensure that multiple HoverHandlers can react to multiple device types
1c44804600
had some known incompleteness in QQuickItemPrivate::effectiveCursorHandler because it couldn't be finished in Qt 5; but HoverHandlers with different acceptedDevices and acceptedPointerTypes were working together in Qt 6.0 and 6.1 to an extent. Perhaps for this case it helped that HoverHandlers got passive grabs, but we stopped that inbbcc2657fa
. So now, as with mouse events, we need to ensure that when a HoverHandler detects a particular stylus device in a QTabletEvent and chooses a different cursor, it is applied to the window. At this time, since QQuickDeliveryAgentPrivate::deliverHoverEvent() sends a synth-mouse event, it's not suitable for tablet hover; so we depend on correct implementation of allPointsGrabbed() to ensure that QQuickDeliveryAgentPrivate::deliverUpdatedPoints() will visit all the HoverHandlers, in this case only. Pick-to: 6.3 6.4 Fixes: QTBUG-101932 Change-Id: Ia8f31610e9252825afc7151be58765ac5217b0e8 Reviewed-by: Doris Verria <doris.verria@qt.io>
This commit is contained in:
parent
68dc224920
commit
79893e1773
|
@ -108,6 +108,9 @@ bool QQuickHoverHandler::wantsPointerEvent(QPointerEvent *event)
|
|||
// the hovered property to transition to false prematurely.
|
||||
// If a QQuickPointerTabletEvent caused the hovered property to become true,
|
||||
// then only another QQuickPointerTabletEvent can make it become false.
|
||||
// But after kCursorOverrideTimeout ms, QQuickItemPrivate::effectiveCursorHandler()
|
||||
// will ignore it, just in case there is no QQuickPointerTabletEvent to unset it.
|
||||
// For example, a tablet proximity leave event could occur, but we don't deliver it to the window.
|
||||
if (!(m_hoveredTablet && QQuickDeliveryAgentPrivate::isMouseEvent(event)))
|
||||
setHovered(false);
|
||||
|
||||
|
|
|
@ -647,6 +647,7 @@ void QQuickPointerHandler::handlePointerEvent(QPointerEvent *event)
|
|||
d->currentEvent = event;
|
||||
if (wants) {
|
||||
handlePointerEventImpl(event);
|
||||
d->lastEventTime = event->timestamp();
|
||||
} else {
|
||||
#if QT_CONFIG(gestures)
|
||||
if (event->type() != QEvent::NativeGesture)
|
||||
|
|
|
@ -43,6 +43,7 @@ public:
|
|||
QPointerEvent *currentEvent = nullptr;
|
||||
QQuickItem *target = nullptr;
|
||||
qreal m_margin = 0;
|
||||
quint64 lastEventTime = 0;
|
||||
qint16 dragThreshold = -1; // -1 means use the platform default
|
||||
uint8_t grabPermissions : 8;
|
||||
Qt::CursorShape cursorShape : 6;
|
||||
|
|
|
@ -62,6 +62,9 @@ Q_DECLARE_LOGGING_CATEGORY(lcTransient)
|
|||
Q_LOGGING_CATEGORY(lcHandlerParent, "qt.quick.handler.parent")
|
||||
Q_LOGGING_CATEGORY(lcVP, "qt.quick.viewport")
|
||||
|
||||
// after 100ms, a mouse/non-mouse cursor conflict is resolved in favor of the mouse handler
|
||||
static const quint64 kCursorOverrideTimeout = 100;
|
||||
|
||||
void debugFocusTree(QQuickItem *item, QQuickItem *scope = nullptr, int depth = 1)
|
||||
{
|
||||
if (lcFocus().isEnabled(QtDebugMsg)) {
|
||||
|
@ -7921,6 +7924,7 @@ void QQuickItem::setCursor(const QCursor &cursor)
|
|||
Q_D(QQuickItem);
|
||||
|
||||
Qt::CursorShape oldShape = d->extra.isAllocated() ? d->extra->cursor.shape() : Qt::ArrowCursor;
|
||||
qCDebug(lcHoverTrace) << oldShape << "->" << cursor.shape();
|
||||
|
||||
if (oldShape != cursor.shape() || oldShape >= Qt::LastCursor || cursor.shape() >= Qt::LastCursor) {
|
||||
d->extra.value().cursor = cursor;
|
||||
|
@ -7957,6 +7961,7 @@ void QQuickItem::setCursor(const QCursor &cursor)
|
|||
void QQuickItem::unsetCursor()
|
||||
{
|
||||
Q_D(QQuickItem);
|
||||
qCDebug(lcHoverTrace) << "clearing cursor";
|
||||
if (!d->hasCursor)
|
||||
return;
|
||||
d->hasCursor = false;
|
||||
|
@ -8008,27 +8013,82 @@ QCursor QQuickItemPrivate::effectiveCursor(const QQuickPointerHandler *handler)
|
|||
Returns the Pointer Handler that is currently attempting to set the cursor shape,
|
||||
or null if there is no such handler.
|
||||
|
||||
If there are multiple handlers attempting to set the cursor:
|
||||
\list
|
||||
\li an active handler has the highest priority (e.g. a DragHandler being dragged)
|
||||
\li any HoverHandler that is reacting to a non-mouse device has priority for
|
||||
kCursorOverrideTimeout ms (a tablet stylus is jittery so that's enough)
|
||||
\li otherwise a HoverHandler that is reacting to the mouse, if any
|
||||
\endlist
|
||||
|
||||
Within each category, if there are multiple handlers, the last-added one wins
|
||||
(the one that is declared at the bottom wins, because users may intuitively
|
||||
think it's "on top" even though there is no Z-order; or, one that is added
|
||||
in a specific use case overrides an imported component).
|
||||
|
||||
\sa QtQuick::PointerHandler::cursor
|
||||
*/
|
||||
QQuickPointerHandler *QQuickItemPrivate::effectiveCursorHandler() const
|
||||
{
|
||||
if (!hasPointerHandlers())
|
||||
return nullptr;
|
||||
QQuickPointerHandler *retHoverHandler = nullptr;
|
||||
QQuickPointerHandler* activeHandler = nullptr;
|
||||
QQuickPointerHandler* mouseHandler = nullptr;
|
||||
QQuickPointerHandler* nonMouseHandler = nullptr;
|
||||
for (QQuickPointerHandler *h : extra->pointerHandlers) {
|
||||
if (!h->isCursorShapeExplicitlySet())
|
||||
continue;
|
||||
QQuickHoverHandler *hoverHandler = qmlobject_cast<QQuickHoverHandler *>(h);
|
||||
// For now, we don't expect multiple hover handlers in one Item, so we choose the first one found;
|
||||
// but a future use case could be to have different cursors for different tablet stylus devices.
|
||||
// In that case, this function needs more information: which device did the event come from.
|
||||
// TODO Qt 6: add QPointerDevice* as argument to this function? (it doesn't exist yet in Qt 5)
|
||||
if (!retHoverHandler && hoverHandler)
|
||||
retHoverHandler = hoverHandler;
|
||||
// Prioritize any HoverHandler that is reacting to a non-mouse device.
|
||||
// Otherwise, choose the first hovered handler that is found.
|
||||
// TODO maybe: there was an idea to add QPointerDevice* as argument to this function
|
||||
// and check the device type, but why? HoverHandler already does that.
|
||||
if (!activeHandler && hoverHandler && hoverHandler->isHovered()) {
|
||||
qCDebug(lcHoverTrace) << hoverHandler << hoverHandler->acceptedDevices() << "wants to set cursor" << hoverHandler->cursorShape();
|
||||
if (hoverHandler->acceptedDevices().testFlag(QPointingDevice::DeviceType::Mouse)) {
|
||||
// If there's a conflict, the last-added HoverHandler wins. Maybe the user is overriding a default...
|
||||
if (mouseHandler && mouseHandler->cursorShape() != hoverHandler->cursorShape()) {
|
||||
qCDebug(lcHoverTrace) << "mouse cursor conflict:" << mouseHandler << "wants" << mouseHandler->cursorShape()
|
||||
<< "but" << hoverHandler << "wants" << hoverHandler->cursorShape();
|
||||
}
|
||||
mouseHandler = hoverHandler;
|
||||
} else {
|
||||
// If there's a conflict, the last-added HoverHandler wins.
|
||||
if (nonMouseHandler && nonMouseHandler->cursorShape() != hoverHandler->cursorShape()) {
|
||||
qCDebug(lcHoverTrace) << "non-mouse cursor conflict:" << nonMouseHandler << "wants" << nonMouseHandler->cursorShape()
|
||||
<< "but" << hoverHandler << "wants" << hoverHandler->cursorShape();
|
||||
}
|
||||
nonMouseHandler = hoverHandler;
|
||||
}
|
||||
}
|
||||
if (!hoverHandler && h->active())
|
||||
return h;
|
||||
activeHandler = h;
|
||||
}
|
||||
return retHoverHandler;
|
||||
if (activeHandler) {
|
||||
qCDebug(lcHoverTrace) << "active handler choosing cursor" << activeHandler << activeHandler->cursorShape();
|
||||
return activeHandler;
|
||||
}
|
||||
// Mouse events are often synthetic; so if a HoverHandler for a non-mouse device wanted to set the cursor,
|
||||
// let it win, unless more than kCursorOverrideTimeout ms have passed
|
||||
// since the last time the non-mouse handler actually reacted to an event.
|
||||
// We could miss the fact that a tablet stylus has left proximity, because we don't deliver proximity events to windows.
|
||||
if (nonMouseHandler) {
|
||||
if (mouseHandler) {
|
||||
const bool beforeTimeout =
|
||||
QQuickPointerHandlerPrivate::get(mouseHandler)->lastEventTime <
|
||||
QQuickPointerHandlerPrivate::get(nonMouseHandler)->lastEventTime + kCursorOverrideTimeout;
|
||||
QQuickPointerHandler *winner = (beforeTimeout ? nonMouseHandler : mouseHandler);
|
||||
qCDebug(lcHoverTrace) << "non-mouse handler reacted last time:" << QQuickPointerHandlerPrivate::get(nonMouseHandler)->lastEventTime
|
||||
<< "and mouse handler reacted at time:" << QQuickPointerHandlerPrivate::get(mouseHandler)->lastEventTime
|
||||
<< "choosing cursor according to" << winner << winner->cursorShape();
|
||||
return winner;
|
||||
}
|
||||
qCDebug(lcHoverTrace) << "non-mouse handler choosing cursor" << nonMouseHandler << nonMouseHandler->cursorShape();
|
||||
return nonMouseHandler;
|
||||
}
|
||||
if (mouseHandler)
|
||||
qCDebug(lcHoverTrace) << "mouse handler choosing cursor" << mouseHandler << mouseHandler->cursorShape();
|
||||
return mouseHandler;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
@ -53,6 +53,7 @@
|
|||
|
||||
QT_BEGIN_NAMESPACE
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(lcHoverTrace)
|
||||
Q_DECLARE_LOGGING_CATEGORY(lcMouse)
|
||||
Q_DECLARE_LOGGING_CATEGORY(lcTouch)
|
||||
Q_DECLARE_LOGGING_CATEGORY(lcPtr)
|
||||
|
@ -1655,10 +1656,14 @@ void QQuickWindowPrivate::updateCursor(const QPointF &scenePos, QQuickItem *root
|
|||
QWindow *window = renderWindow ? renderWindow : q;
|
||||
cursorItem = cursorItemAndHandler.first;
|
||||
cursorHandler = cursorItemAndHandler.second;
|
||||
if (cursorItem)
|
||||
window->setCursor(QQuickItemPrivate::get(cursorItem)->effectiveCursor(cursorHandler));
|
||||
else
|
||||
if (cursorItem) {
|
||||
const auto cursor = QQuickItemPrivate::get(cursorItem)->effectiveCursor(cursorHandler);
|
||||
qCDebug(lcHoverTrace) << "setting cursor" << cursor << "from" << cursorHandler << "or" << cursorItem;
|
||||
window->setCursor(cursor);
|
||||
} else {
|
||||
qCDebug(lcHoverTrace) << "unsetting cursor";
|
||||
window->unsetCursor();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -797,7 +797,13 @@ bool QQuickDeliveryAgent::event(QEvent *ev)
|
|||
case QEvent::TabletPress:
|
||||
case QEvent::TabletMove:
|
||||
case QEvent::TabletRelease:
|
||||
d->deliverPointerEvent(static_cast<QPointerEvent *>(ev));
|
||||
{
|
||||
auto *tabletEvent = static_cast<QTabletEvent *>(ev);
|
||||
d->deliverPointerEvent(tabletEvent); // visits HoverHandlers too (unlike the mouse event case)
|
||||
#if QT_CONFIG(cursor)
|
||||
QQuickWindowPrivate::get(d->rootItem->window())->updateCursor(tabletEvent->scenePosition(), d->rootItem);
|
||||
#endif
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
|
@ -1564,9 +1570,6 @@ void QQuickDeliveryAgentPrivate::handleMouseEvent(QMouseEvent *event)
|
|||
Q_QUICK_INPUT_PROFILE(QQuickProfiler::Mouse, QQuickProfiler::InputMouseMove,
|
||||
event->position().x(), event->position().y());
|
||||
|
||||
#if QT_CONFIG(cursor)
|
||||
QQuickWindowPrivate::get(rootItem->window())->updateCursor(event->scenePosition());
|
||||
#endif
|
||||
const QPointF last = lastMousePosition.isNull() ? event->scenePosition() : lastMousePosition;
|
||||
lastMousePosition = event->scenePosition();
|
||||
qCDebug(lcHoverTrace) << q << "mouse pos" << last << "->" << lastMousePosition;
|
||||
|
@ -1575,6 +1578,10 @@ void QQuickDeliveryAgentPrivate::handleMouseEvent(QMouseEvent *event)
|
|||
event->setAccepted(accepted);
|
||||
}
|
||||
deliverPointerEvent(event);
|
||||
#if QT_CONFIG(cursor)
|
||||
// The pointer event could result in a cursor change (reaction), so update it afterwards.
|
||||
QQuickWindowPrivate::get(rootItem->window())->updateCursor(event->scenePosition());
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
default:
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
import QtQuick
|
||||
|
||||
Item {
|
||||
width: 200; height: 200
|
||||
|
||||
HoverHandler {
|
||||
objectName: "stylus"
|
||||
acceptedDevices: PointerDevice.Stylus
|
||||
acceptedPointerTypes: PointerDevice.Pen
|
||||
cursorShape: Qt.CrossCursor
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
objectName: "stylus eraser"
|
||||
acceptedDevices: PointerDevice.Stylus
|
||||
acceptedPointerTypes: PointerDevice.Eraser
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
objectName: "airbrush"
|
||||
acceptedDevices: PointerDevice.Airbrush
|
||||
acceptedPointerTypes: PointerDevice.Pen
|
||||
cursorShape: Qt.BusyCursor
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
objectName: "airbrush eraser"
|
||||
acceptedDevices: PointerDevice.Airbrush
|
||||
acceptedPointerTypes: PointerDevice.Eraser
|
||||
cursorShape: Qt.OpenHandCursor
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
objectName: "mouse"
|
||||
acceptedDevices: PointerDevice.Mouse
|
||||
// acceptedPointerTypes can be omitted because Mouse is not ambiguous.
|
||||
// When a genuine mouse move is sent, there's a conflict, and this one should win.
|
||||
cursorShape: Qt.IBeamCursor
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
objectName: "conflictingMouse"
|
||||
acceptedDevices: PointerDevice.Mouse
|
||||
// acceptedPointerTypes can be omitted because Mouse is not ambiguous.
|
||||
// When a genuine mouse move is sent, there's a conflict, and this one should lose.
|
||||
cursorShape: Qt.ClosedHandCursor
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@
|
|||
#include <QtQuick/qquickview.h>
|
||||
#include <QtQuick/qquickitem.h>
|
||||
#include <QtQuick/private/qquickhoverhandler_p.h>
|
||||
#include <QtQuick/private/qquickpointerhandler_p_p.h>
|
||||
#include <QtQuick/private/qquickmousearea_p.h>
|
||||
#include <qpa/qwindowsysteminterface.h>
|
||||
|
||||
|
@ -41,6 +42,8 @@ private slots:
|
|||
void movingItemWithHoverHandler();
|
||||
void margin();
|
||||
void window();
|
||||
void deviceCursor_data();
|
||||
void deviceCursor();
|
||||
|
||||
private:
|
||||
void createView(QScopedPointer<QQuickView> &window, const char *fileName);
|
||||
|
@ -383,6 +386,101 @@ void tst_HoverHandler::window() // QTBUG-98717
|
|||
#endif
|
||||
}
|
||||
|
||||
void tst_HoverHandler::deviceCursor_data()
|
||||
{
|
||||
QTest::addColumn<bool>("synthMouseForTabletEvents");
|
||||
QTest::addColumn<bool>("earlierTabletBeforeMouse");
|
||||
|
||||
QTest::newRow("nosynth, tablet wins") << false << false;
|
||||
QTest::newRow("synth, tablet wins") << true << false;
|
||||
QTest::newRow("synth, mouse wins") << true << true;
|
||||
}
|
||||
|
||||
void tst_HoverHandler::deviceCursor()
|
||||
{
|
||||
QFETCH(bool, synthMouseForTabletEvents);
|
||||
QFETCH(bool, earlierTabletBeforeMouse);
|
||||
qApp->setAttribute(Qt::AA_SynthesizeMouseForUnhandledTabletEvents, synthMouseForTabletEvents);
|
||||
QQuickView window;
|
||||
QVERIFY(QQuickTest::showView(window, testFileUrl("hoverDeviceCursors.qml")));
|
||||
// Ensure that we don't get extra hover events delivered on the side
|
||||
QQuickWindowPrivate::get(&window)->deliveryAgentPrivate()->frameSynchronousHoverEnabled = false;
|
||||
// And flush out any mouse events that might be queued up in QPA, since QTest::mouseMove() calls processEvents.
|
||||
qGuiApp->processEvents();
|
||||
const QQuickItem *root = window.rootObject();
|
||||
QQuickHoverHandler *stylusHandler = root->findChild<QQuickHoverHandler *>("stylus");
|
||||
QVERIFY(stylusHandler);
|
||||
QQuickHoverHandler *eraserHandler = root->findChild<QQuickHoverHandler *>("stylus eraser");
|
||||
QVERIFY(eraserHandler);
|
||||
QQuickHoverHandler *aibrushHandler = root->findChild<QQuickHoverHandler *>("airbrush");
|
||||
QVERIFY(aibrushHandler);
|
||||
QQuickHoverHandler *airbrushEraserHandler = root->findChild<QQuickHoverHandler *>("airbrush eraser");
|
||||
QVERIFY(airbrushEraserHandler);
|
||||
QQuickHoverHandler *mouseHandler = root->findChild<QQuickHoverHandler *>("mouse");
|
||||
QVERIFY(mouseHandler);
|
||||
|
||||
QPoint point(100, 100);
|
||||
|
||||
#if QT_CONFIG(tabletevent)
|
||||
const qint64 stylusId = 1234567890;
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
auto testStylusDevice = [&](QInputDevice::DeviceType dt, QPointingDevice::PointerType pt,
|
||||
Qt::CursorShape expectedCursor, QQuickHoverHandler* expectedActiveHandler) {
|
||||
// We will follow up with a mouse event afterwards, and we want to simulate that the tablet events occur
|
||||
// either slightly before (earlierTabletBeforeMouse == true) or some time before.
|
||||
// It turns out that the first mouse move happens at timestamp 501 (simulated).
|
||||
const ulong timestamp = (earlierTabletBeforeMouse ? 0 : 400) + timer.elapsed();
|
||||
qCDebug(lcPointerTests) << "@" << timestamp << "sending" << dt << pt << "expecting" << expectedCursor << expectedActiveHandler->objectName();
|
||||
QWindowSystemInterface::handleTabletEvent(&window, timestamp, point, window.mapToGlobal(point),
|
||||
int(dt), int(pt), Qt::NoButton, 0, 0, 0, 0, 0, 0, stylusId, Qt::NoModifier);
|
||||
point += QPoint(1, 0);
|
||||
#if QT_CONFIG(cursor)
|
||||
// QQuickItem::setCursor() doesn't get called: we only have HoverHandlers in this test
|
||||
QCOMPARE(root->cursor().shape(), Qt::ArrowCursor);
|
||||
QTRY_COMPARE(window.cursor().shape(), expectedCursor);
|
||||
#endif
|
||||
QCOMPARE(stylusHandler->isHovered(), stylusHandler == expectedActiveHandler);
|
||||
QCOMPARE(eraserHandler->isHovered(), eraserHandler == expectedActiveHandler);
|
||||
QCOMPARE(aibrushHandler->isHovered(), aibrushHandler == expectedActiveHandler);
|
||||
QCOMPARE(airbrushEraserHandler->isHovered(), airbrushEraserHandler == expectedActiveHandler);
|
||||
};
|
||||
|
||||
// simulate move events from various tablet stylus types
|
||||
testStylusDevice(QInputDevice::DeviceType::Stylus, QPointingDevice::PointerType::Pen,
|
||||
Qt::CrossCursor, stylusHandler);
|
||||
testStylusDevice(QInputDevice::DeviceType::Stylus, QPointingDevice::PointerType::Eraser,
|
||||
Qt::PointingHandCursor, eraserHandler);
|
||||
testStylusDevice(QInputDevice::DeviceType::Airbrush, QPointingDevice::PointerType::Pen,
|
||||
Qt::BusyCursor, aibrushHandler);
|
||||
testStylusDevice(QInputDevice::DeviceType::Airbrush, QPointingDevice::PointerType::Eraser,
|
||||
Qt::OpenHandCursor, airbrushEraserHandler);
|
||||
|
||||
QTest::qWait(200);
|
||||
qCDebug(lcPointerTests) << "---- no more tablet events, now we send a mouse move";
|
||||
#endif
|
||||
|
||||
// move the mouse: the mouse-specific HoverHandler gets to set the cursor only if
|
||||
// more than kCursorOverrideTimeout ms have elapsed
|
||||
QTest::mouseMove(&window, point);
|
||||
QTRY_COMPARE(mouseHandler->isHovered(), true);
|
||||
const bool afterTimeout =
|
||||
QQuickPointerHandlerPrivate::get(airbrushEraserHandler)->lastEventTime + 100 <
|
||||
QQuickPointerHandlerPrivate::get(mouseHandler)->lastEventTime;
|
||||
qCDebug(lcPointerTests) << "airbrush handler reacted last time:" << QQuickPointerHandlerPrivate::get(airbrushEraserHandler)->lastEventTime
|
||||
<< "and the mouse handler reacted at time:" << QQuickPointerHandlerPrivate::get(mouseHandler)->lastEventTime
|
||||
<< "so > 100 ms have elapsed?" << afterTimeout;
|
||||
#if QT_CONFIG(cursor)
|
||||
QCOMPARE(window.cursor().shape(), afterTimeout ? Qt::IBeamCursor : Qt::OpenHandCursor);
|
||||
#endif
|
||||
QCOMPARE(stylusHandler->isHovered(), false);
|
||||
QCOMPARE(eraserHandler->isHovered(), false);
|
||||
QCOMPARE(aibrushHandler->isHovered(), false);
|
||||
#if QT_CONFIG(tabletevent)
|
||||
QCOMPARE(airbrushEraserHandler->isHovered(), true); // there was no fresh QTabletEvent to tell it not to be hovered
|
||||
#endif
|
||||
}
|
||||
|
||||
QTEST_MAIN(tst_HoverHandler)
|
||||
|
||||
#include "tst_qquickhoverhandler.moc"
|
||||
|
|
Loading…
Reference in New Issue