Update cursor if frame-synchronous hover update discovers hover change

Also mark cursor as dirty and force update after shape change.

Done-with: Matthias Rauter <matthias.rauter@qt.io>
Fixes: QTBUG-53987
Fixes: QTBUG-90457
Task-number: QTBUG-54019
Change-Id: I64d9f5d0a39dbf141a8e82bee824b47a8884139b
Reviewed-by: Richard Moe Gustavsen <richard.gustavsen@qt.io>
(cherry picked from commit cd7c5f94a0)
Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
(cherry picked from commit 4271d4586b)
This commit is contained in:
Shawn Rutledge 2023-11-10 17:07:15 -07:00 committed by Qt Cherry-pick Bot
parent 821aa3dcc2
commit 66610bcb57
8 changed files with 191 additions and 2 deletions

View File

@ -182,11 +182,13 @@ void QQuickPointerHandler::setCursorShape(Qt::CursorShape shape)
return; return;
d->cursorShape = shape; d->cursorShape = shape;
d->cursorSet = true; d->cursorSet = true;
d->cursorDirty = true;
if (auto *parent = parentItem()) { if (auto *parent = parentItem()) {
QQuickItemPrivate *itemPriv = QQuickItemPrivate::get(parent); QQuickItemPrivate *itemPriv = QQuickItemPrivate::get(parent);
itemPriv->hasCursorHandler = true; itemPriv->hasCursorHandler = true;
itemPriv->setHasCursorInChild(true); itemPriv->setHasCursorInChild(true);
} }
emit cursorShapeChanged(); emit cursorShapeChanged();
} }
@ -765,6 +767,7 @@ QQuickPointerHandlerPrivate::QQuickPointerHandlerPrivate()
, hadKeepMouseGrab(false) , hadKeepMouseGrab(false)
, hadKeepTouchGrab(false) , hadKeepTouchGrab(false)
, cursorSet(false) , cursorSet(false)
, cursorDirty(false)
{ {
} }

View File

@ -57,6 +57,7 @@ public:
bool hadKeepMouseGrab : 1; // some handlers override target()->setKeepMouseGrab(); this remembers previous state bool hadKeepMouseGrab : 1; // some handlers override target()->setKeepMouseGrab(); this remembers previous state
bool hadKeepTouchGrab : 1; // some handlers override target()->setKeepTouchGrab(); this remembers previous state bool hadKeepTouchGrab : 1; // some handlers override target()->setKeepTouchGrab(); this remembers previous state
bool cursorSet : 1; bool cursorSet : 1;
bool cursorDirty : 1;
}; };
QT_END_NAMESPACE QT_END_NAMESPACE

View File

@ -12,6 +12,7 @@
#include <QtQuick/private/qsgrenderer_p.h> #include <QtQuick/private/qsgrenderer_p.h>
#include <QtQuick/private/qsgplaintexture_p.h> #include <QtQuick/private/qsgplaintexture_p.h>
#include <QtQuick/private/qquickpointerhandler_p.h> #include <QtQuick/private/qquickpointerhandler_p.h>
#include <QtQuick/private/qquickpointerhandler_p_p.h>
#include <private/qsgrenderloop_p.h> #include <private/qsgrenderloop_p.h>
#include <private/qsgrhisupport_p.h> #include <private/qsgrhisupport_p.h>
#include <private/qquickrendercontrol_p.h> #include <private/qquickrendercontrol_p.h>
@ -1684,11 +1685,14 @@ void QQuickWindowPrivate::updateCursor(const QPointF &scenePos, QQuickItem *root
if (!rootItem) if (!rootItem)
rootItem = contentItem; rootItem = contentItem;
auto cursorItemAndHandler = findCursorItemAndHandler(rootItem, scenePos); auto cursorItemAndHandler = findCursorItemAndHandler(rootItem, scenePos);
if (cursorItem != cursorItemAndHandler.first || cursorHandler != cursorItemAndHandler.second) { if (cursorItem != cursorItemAndHandler.first || cursorHandler != cursorItemAndHandler.second ||
(cursorItemAndHandler.second && QQuickPointerHandlerPrivate::get(cursorItemAndHandler.second)->cursorDirty)) {
QWindow *renderWindow = QQuickRenderControl::renderWindowFor(q); QWindow *renderWindow = QQuickRenderControl::renderWindowFor(q);
QWindow *window = renderWindow ? renderWindow : q; QWindow *window = renderWindow ? renderWindow : q;
cursorItem = cursorItemAndHandler.first; cursorItem = cursorItemAndHandler.first;
cursorHandler = cursorItemAndHandler.second; cursorHandler = cursorItemAndHandler.second;
if (cursorHandler)
QQuickPointerHandlerPrivate::get(cursorItemAndHandler.second)->cursorDirty = false;
if (cursorItem) { if (cursorItem) {
const auto cursor = QQuickItemPrivate::get(cursorItem)->effectiveCursor(cursorHandler); const auto cursor = QQuickItemPrivate::get(cursorItem)->effectiveCursor(cursorHandler);
qCDebug(lcHoverTrace) << "setting cursor" << cursor << "from" << cursorHandler << "or" << cursorItem; qCDebug(lcHoverTrace) << "setting cursor" << cursor << "from" << cursorHandler << "or" << cursorItem;

View File

@ -1686,7 +1686,13 @@ void QQuickDeliveryAgentPrivate::flushFrameSynchronousEvents(QQuickWindow *win)
if (frameSynchronousHoverEnabled && !win->mouseGrabberItem() && if (frameSynchronousHoverEnabled && !win->mouseGrabberItem() &&
!lastMousePosition.isNull() && QQuickWindowPrivate::get(win)->dirtyItemList) { !lastMousePosition.isNull() && QQuickWindowPrivate::get(win)->dirtyItemList) {
qCDebug(lcHoverTrace) << q << "delivering frame-sync hover to root @" << lastMousePosition; qCDebug(lcHoverTrace) << q << "delivering frame-sync hover to root @" << lastMousePosition;
deliverHoverEvent(lastMousePosition, lastMousePosition, QGuiApplication::keyboardModifiers(), 0); if (deliverHoverEvent(lastMousePosition, lastMousePosition, QGuiApplication::keyboardModifiers(), 0)) {
#if QT_CONFIG(cursor)
QQuickWindowPrivate::get(rootItem->window())->updateCursor(
sceneTransform ? sceneTransform->map(lastMousePosition) : lastMousePosition, rootItem);
#endif
}
qCDebug(lcHoverTrace) << q << "frame-sync hover delivery done"; qCDebug(lcHoverTrace) << q << "frame-sync hover delivery done";
} }
#else #else

View File

@ -0,0 +1,37 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
import QtQuick 2.15
Rectangle {
id: brownRect
objectName: "brownRect"
width: 400
height: 400
HoverHandler {
id: hh
cursorShape: parent.colorIndex == 0 ?
Qt.CrossCursor :
Qt.OpenHandCursor
}
property list<color> colors: ["beige", "brown"]
property int colorIndex: 0
color: colors[colorIndex]
Timer {
id: colorTimer
interval: 200
running: true
repeat: true
onTriggered: {
parent.colorIndex = (parent.colorIndex + 1) % parent.colors.length;
parent.color = parent.colors[parent.colorIndex];
}
}
}

View File

@ -48,6 +48,7 @@ private slots:
void deviceCursor(); void deviceCursor();
void addHandlerFromCpp(); void addHandlerFromCpp();
void ensureHoverHandlerWorksWhenItemHasHoverDisabled(); void ensureHoverHandlerWorksWhenItemHasHoverDisabled();
void changeCursor();
private: private:
void createView(QScopedPointer<QQuickView> &window, const char *fileName); void createView(QScopedPointer<QQuickView> &window, const char *fileName);
@ -671,6 +672,34 @@ void tst_HoverHandler::ensureHoverHandlerWorksWhenItemHasHoverDisabled()
QCOMPARE(spy.size(), 2); QCOMPARE(spy.size(), 2);
} }
void tst_HoverHandler::changeCursor()
{
QScopedPointer<QQuickView> windowPtr;
createView(windowPtr, "changingCursor.qml");
QQuickView * window = windowPtr.data();
window->show();
QVERIFY(QTest::qWaitForWindowExposed(window));
QQuickItem *item = window->findChild<QQuickItem *>("brownRect");
QVERIFY(item);
QQuickHoverHandler *hh = item->findChild<QQuickHoverHandler *>();
QVERIFY(hh);
QPoint itemCenter(item->mapToScene(QPointF(item->width() / 2, item->height() / 2)).toPoint());
QSignalSpy hoveredSpy(hh, SIGNAL(hoveredChanged()));
QTest::mouseMove(window, itemCenter);
QTRY_COMPARE(hoveredSpy.size(), 1);
#if QT_CONFIG(cursor)
QTRY_COMPARE(window->cursor().shape(), Qt::CrossCursor);
QTRY_COMPARE(window->cursor().shape(), Qt::OpenHandCursor);
QTRY_COMPARE(window->cursor().shape(), Qt::CrossCursor);
QTRY_COMPARE(window->cursor().shape(), Qt::OpenHandCursor);
#endif
}
QTEST_MAIN(tst_HoverHandler) QTEST_MAIN(tst_HoverHandler)
#include "tst_qquickhoverhandler.moc" #include "tst_qquickhoverhandler.moc"

View File

@ -0,0 +1,71 @@
import QtQuick
Item {
width: 600
height: 600
ListModel {
id: cursorsModel
ListElement { cursorShape: Qt.ArrowCursor; text: "Arrow" }
ListElement { cursorShape: Qt.UpArrowCursor; text: "UpArrow" }
ListElement { cursorShape: Qt.CrossCursor; text: "Cross" }
ListElement { cursorShape: Qt.WaitCursor; text: "Wait" }
ListElement { cursorShape: Qt.IBeamCursor; text: "IBeam" }
ListElement { cursorShape: Qt.SizeVerCursor; text: "SizeVer" }
ListElement { cursorShape: Qt.SizeHorCursor; text: "SizeHor" }
ListElement { cursorShape: Qt.SizeBDiagCursor; text: "SizeBDiag" }
ListElement { cursorShape: Qt.SizeFDiagCursor; text: "SizeFDiag" }
ListElement { cursorShape: Qt.SizeAllCursor; text: "SizeAll" }
ListElement { cursorShape: Qt.BlankCursor; text: "Blank" }
ListElement { cursorShape: Qt.SplitVCursor; text: "SplitV" }
ListElement { cursorShape: Qt.SplitHCursor; text: "SplitH" }
ListElement { cursorShape: Qt.PointingHandCursor; text: "PointingHand" }
ListElement { cursorShape: Qt.ForbiddenCursor; text: "Forbidden" }
ListElement { cursorShape: Qt.WhatsThisCursor; text: "WhatsThis" }
ListElement { cursorShape: Qt.BusyCursor; text: "Busy" }
ListElement { cursorShape: Qt.OpenHandCursor; text: "OpenHand" }
ListElement { cursorShape: Qt.ClosedHandCursor; text: "ClosedHand" }
ListElement { cursorShape: Qt.DragCopyCursor; text: "DragCopy" }
ListElement { cursorShape: Qt.DragMoveCursor; text: "DragMove" }
ListElement { cursorShape: Qt.DragLinkCursor; text: "DragLink" }
}
Flickable {
anchors.fill: parent
contentHeight: flow.height
Flow {
id: flow
width: parent.width
Repeater {
model: cursorsModel
Rectangle {
id: root
color: "white"
border.width: 5
border.color: "black"
width: 200
height: 200
Text {
id: textItem
anchors.fill: parent
anchors.margins: parent.width * 0.1
text: model.text
fontSizeMode: Text.Fit
minimumPixelSize: 10; font.pixelSize: height
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: model.cursorShape
}
}
}
}
}
}

View File

@ -23,6 +23,11 @@
Q_LOGGING_CATEGORY(lcTests, "qt.quick.tests") Q_LOGGING_CATEGORY(lcTests, "qt.quick.tests")
static bool isPlatformWayland()
{
return !QGuiApplication::platformName().compare(QLatin1String("wayland"), Qt::CaseInsensitive);
}
class CircleMask : public QObject class CircleMask : public QObject
{ {
Q_OBJECT Q_OBJECT
@ -121,6 +126,7 @@ private slots:
void changeAxis(); void changeAxis();
#if QT_CONFIG(cursor) #if QT_CONFIG(cursor)
void cursorShape(); void cursorShape();
void cursorUpdating();
#endif #endif
void moveAndReleaseWithoutPress(); void moveAndReleaseWithoutPress();
void nestedStopAtBounds(); void nestedStopAtBounds();
@ -1901,6 +1907,38 @@ void tst_QQuickMouseArea::cursorShape()
QCOMPARE(mouseArea->cursor().shape(), Qt::WaitCursor); QCOMPARE(mouseArea->cursor().shape(), Qt::WaitCursor);
QCOMPARE(spy.size(), 2); QCOMPARE(spy.size(), 2);
} }
void tst_QQuickMouseArea::cursorUpdating()
{
QQuickView window;
QVERIFY(QQuickTest::showView(window, testFileUrl("cursorUpdating.qml")));
QQuickItem *root = window.rootObject();
QVERIFY(root);
QQuickFlickable *flickable = root->findChild<QQuickFlickable*>();
QVERIFY(flickable);
QQuickItemPrivate *rootPrivate = QQuickItemPrivate::get(root);
QVERIFY(rootPrivate->subtreeCursorEnabled);
QTest::mouseMove(&window, QPoint(40, 40));
QCOMPARE(window.cursor().shape(), Qt::ArrowCursor);
QTest::mouseMove(&window, QPoint(240, 40));
QCOMPARE(window.cursor().shape(), Qt::UpArrowCursor);
if (isPlatformWayland())
QSKIP("Wayland: QCursor::setPos() doesn't work.");
// QTBUG-53987: with the cursor physically hovering, use wheel to
// position a different item that requests a different cursor
const QPoint p(240, 40);
const QPoint pg = window.mapToGlobal(p);
QCursor::setPos(pg);
QWheelEvent wheelEvent(p, pg, QPoint(60, -400), QPoint(0, -600),
Qt::NoButton, Qt::ControlModifier, Qt::NoScrollPhase, false);
QGuiApplication::sendEvent(&window, &wheelEvent);
QTRY_VERIFY(flickable->contentY() > 300);
QCOMPARE(window.cursor().shape(), Qt::IBeamCursor);
}
#endif #endif
void tst_QQuickMouseArea::moveAndReleaseWithoutPress() void tst_QQuickMouseArea::moveAndReleaseWithoutPress()