Fix resolution of cursor when items are overlapping.
authorAndrew den Exter <andrew.den-exter@nokia.com>
Wed, 25 Jul 2012 04:04:47 +0000 (14:04 +1000)
committerQt by Nokia <qt-info@nokia.com>
Thu, 26 Jul 2012 03:54:37 +0000 (05:54 +0200)
If MouseArea with cursorShapes are overlapping then cursor shape
of the foremost item under the mouse cursor should be shown, but because
the hover events are delivered to the foremost items first the opposite
is occuring.  This makes QQuickWindow responsible for correctly setting
its own cursor instead of relying on items to work it out amongst
themselves.

Change-Id: Iedf144c583dfa3d1ff441e19db9601b5e171902a
Reviewed-by: Martin Jones <martin.jones@nokia.com>
src/quick/items/qquickitem.cpp
src/quick/items/qquickitem.h
src/quick/items/qquickitem_p.h
src/quick/items/qquickmousearea.cpp
src/quick/items/qquickmousearea_p.h
src/quick/items/qquickwindow.cpp
src/quick/items/qquickwindow_p.h
tests/auto/quick/qquickmousearea/tst_qquickmousearea.cpp
tests/auto/quick/qquickwindow/tst_qquickwindow.cpp

index fefddad..5930b5f 100644 (file)
 #include <private/qqmlaccessors_p.h>
 #include <QtQuick/private/qquickaccessibleattached_p.h>
 
+#ifndef QT_NO_CURSOR
+# include <QtGui/qcursor.h>
+#endif
+
 #include <float.h>
 
 // XXX todo Check that elements that create items handle memory correctly after visual ownership change
@@ -2266,6 +2270,10 @@ void QQuickItemPrivate::derefWindow()
     }
     if (c->mouseGrabberItem == q)
         c->mouseGrabberItem = 0;
+#ifndef QT_NO_CURSOR
+    if (c->cursorItem == q)
+        c->cursorItem = 0;
+#endif
     if ( hoverEnabled )
         c->hoverItems.removeAll(q);
     if (itemNodeInstance)
@@ -2429,6 +2437,7 @@ QQuickItemPrivate::QQuickItemPrivate()
     , inheritMirrorFromItem(false)
     , isAccessible(false)
     , culled(false)
+    , hasCursor(false)
     , dirtyAttributes(0)
     , nextDirtyItem(0)
     , prevDirtyItem(0)
@@ -5113,6 +5122,88 @@ void QQuickItem::setAcceptHoverEvents(bool enabled)
     d->hoverEnabled = enabled;
 }
 
+#ifndef QT_NO_CURSOR
+
+
+/*!
+    Returns the cursor shape for this item.
+
+    The mouse cursor will assume this shape when it is over this
+    item, unless an override cursor is set.
+    See the \l{Qt::CursorShape}{list of predefined cursor objects} for a
+    range of useful shapes.
+
+    If no cursor shape has been set this returns a cursor with the Qt::ArrowCursor shape, however
+    another cursor shape may be displayed if an overlapping item has a valid cursor.
+
+    \sa setCursor(), unsetCursor()
+*/
+
+QCursor QQuickItem::cursor() const
+{
+    Q_D(const QQuickItem);
+    return d->extra.isAllocated()
+            ? d->extra->cursor
+            : QCursor();
+}
+
+/*!
+    Sets the \a cursor shape for this item.
+
+    \sa cursor(), unsetCursor()
+*/
+
+void QQuickItem::setCursor(const QCursor &cursor)
+{
+    Q_D(QQuickItem);
+
+    Qt::CursorShape oldShape = d->extra.isAllocated() ? d->extra->cursor.shape() : Qt::ArrowCursor;
+
+    if (oldShape != cursor.shape() || oldShape >= Qt::LastCursor || cursor.shape() >= Qt::LastCursor) {
+        d->extra.value().cursor = cursor;
+        if (d->window) {
+            QQuickWindowPrivate *windowPrivate = QQuickWindowPrivate::get(d->window);
+            if (windowPrivate->cursorItem == this)
+                d->window->setCursor(cursor);
+        }
+    }
+
+    if (!d->hasCursor) {
+        d->hasCursor = true;
+        if (d->window) {
+            QPointF pos = d->window->mapFromGlobal(QGuiApplicationPrivate::lastCursorPosition.toPoint());
+            if (contains(mapFromScene(pos)))
+                QQuickWindowPrivate::get(d->window)->updateCursor(pos);
+        }
+    }
+}
+
+/*!
+    Clears the cursor shape for this item.
+
+    \sa cursor(), setCursor()
+*/
+
+void QQuickItem::unsetCursor()
+{
+    Q_D(QQuickItem);
+    if (!d->hasCursor)
+        return;
+    d->hasCursor = false;
+    if (d->extra.isAllocated())
+        d->extra->cursor = QCursor();
+
+    if (d->window) {
+        QQuickWindowPrivate *windowPrivate = QQuickWindowPrivate::get(d->window);
+        if (windowPrivate->cursorItem == this) {
+            QPointF pos = d->window->mapFromGlobal(QGuiApplicationPrivate::lastCursorPosition.toPoint());
+            windowPrivate->updateCursor(pos);
+        }
+    }
+}
+
+#endif
+
 void QQuickItem::grabMouse()
 {
     Q_D(QQuickItem);
index 61bb325..8854449 100644 (file)
@@ -80,6 +80,7 @@ private:
     Q_DECLARE_PRIVATE(QQuickTransform)
 };
 
+class QCursor;
 class QQuickItemLayer;
 class QQmlV8Function;
 class QQuickState;
@@ -282,6 +283,12 @@ public:
     bool acceptHoverEvents() const;
     void setAcceptHoverEvents(bool enabled);
 
+#ifndef QT_NO_CURSOR
+    QCursor cursor() const;
+    void setCursor(const QCursor &cursor);
+    void unsetCursor();
+#endif
+
     bool isUnderMouse() const;
     void grabMouse();
     void ungrabMouse();
index adfe384..9dc7344 100644 (file)
@@ -342,6 +342,9 @@ public:
         QQuickLayoutMirroringAttached* layoutDirectionAttached;
         QQuickItemKeyFilter *keyHandler;
         mutable QQuickItemLayer *layer;
+#ifndef QT_NO_CURSOR
+        QCursor cursor;
+#endif
         QPointF userTransformOriginPoint;
 
         int effectRefCount;
@@ -408,7 +411,8 @@ public:
     bool inheritMirrorFromItem:1;
     bool isAccessible:1;
     bool culled:1;
-    // bool dummy:2
+    bool hasCursor:1;
+    // bool dummy:1
     // Bit 32
 
     enum DirtyType {
index 5cfd62f..85a224f 100644 (file)
@@ -1136,15 +1136,6 @@ void QQuickMouseArea::setHovered(bool h)
         d->hovered = h;
         emit hoveredChanged();
         d->hovered ? emit entered() : emit exited();
-#ifndef QT_NO_CURSOR
-        if (d->cursor) {
-            if (d->hovered) {
-                window()->setCursor(QCursor(*d->cursor));
-            } else {
-                window()->unsetCursor();
-            }
-        }
-#endif
     }
 }
 
@@ -1274,19 +1265,19 @@ bool QQuickMouseArea::setPressed(Qt::MouseButton button, bool p)
 #ifndef QT_NO_CURSOR
 Qt::CursorShape QQuickMouseArea::cursorShape() const
 {
-    Q_D(const QQuickMouseArea);
-    if (d->cursor)
-        return d->cursor->shape();
-    return Qt::ArrowCursor;
+    return cursor().shape();
 }
 
 void QQuickMouseArea::setCursorShape(Qt::CursorShape shape)
 {
-    Q_D(QQuickMouseArea);
-    setHoverEnabled(true);
-    delete d->cursor;
-    d->cursor = new QCursor(shape);
+    if (cursor().shape() == shape)
+        return;
+
+    setCursor(shape);
+
+    emit cursorShapeChanged();
 }
+
 #endif
 
 /*!
index 8a8de3b..3385485 100644 (file)
@@ -144,7 +144,7 @@ class Q_QUICK_PRIVATE_EXPORT QQuickMouseArea : public QQuickItem
     Q_PROPERTY(bool preventStealing READ preventStealing WRITE setPreventStealing NOTIFY preventStealingChanged)
     Q_PROPERTY(bool propagateComposedEvents READ propagateComposedEvents WRITE setPropagateComposedEvents NOTIFY propagateComposedEventsChanged)
 #ifndef QT_NO_CURSOR
-    Q_PROPERTY(Qt::CursorShape cursorShape READ cursorShape WRITE setCursorShape NOTIFY cursorShapeChanged)
+    Q_PROPERTY(Qt::CursorShape cursorShape READ cursorShape WRITE setCursorShape RESET unsetCursor NOTIFY cursorShapeChanged)
 #endif
 
 public:
index 1d3adea..4cfe1a1 100644 (file)
@@ -326,6 +326,9 @@ QQuickWindowPrivate::QQuickWindowPrivate()
     : rootItem(0)
     , activeFocusItem(0)
     , mouseGrabberItem(0)
+#ifndef QT_NO_CURSOR
+    , cursorItem(0)
+#endif
     , touchMouseId(-1)
     , touchMousePressTimestamp(0)
     , renderWithoutShowing(false)
@@ -1301,6 +1304,10 @@ void QQuickWindow::mouseMoveEvent(QMouseEvent *event)
     qWarning() << "QQuickWindow::mouseMoveEvent()" << event->localPos() << event->button() << event->buttons();
 #endif
 
+#ifndef QT_NO_CURSOR
+    d->updateCursor(event->windowPos());
+#endif
+
     if (!d->mouseGrabberItem) {
         if (d->lastMousePosition.isNull())
             d->lastMousePosition = event->windowPos();
@@ -1832,6 +1839,49 @@ bool QQuickWindowPrivate::deliverDragEvent(QQuickDragGrabber *grabber, QQuickIte
 }
 #endif // QT_NO_DRAGANDDROP
 
+#ifndef QT_NO_CURSOR
+void QQuickWindowPrivate::updateCursor(const QPointF &scenePos)
+{
+    Q_Q(QQuickWindow);
+
+    QQuickItem *oldCursorItem = cursorItem;
+    cursorItem = findCursorItem(rootItem, scenePos);
+
+    if (cursorItem != oldCursorItem) {
+        if (cursorItem)
+            q->setCursor(cursorItem->cursor());
+        else
+            q->unsetCursor();
+    }
+}
+
+QQuickItem *QQuickWindowPrivate::findCursorItem(QQuickItem *item, const QPointF &scenePos)
+{
+    QQuickItemPrivate *itemPrivate = QQuickItemPrivate::get(item);
+    if (itemPrivate->flags & QQuickItem::ItemClipsChildrenToShape) {
+        QPointF p = item->mapFromScene(scenePos);
+        if (!item->contains(p))
+            return 0;
+    }
+
+    QList<QQuickItem *> children = itemPrivate->paintOrderChildItems();
+    for (int ii = children.count() - 1; ii >= 0; --ii) {
+        QQuickItem *child = children.at(ii);
+        if (!child->isVisible() || !child->isEnabled())
+            continue;
+        if (QQuickItem *cursorItem = findCursorItem(child, scenePos))
+            return cursorItem;
+    }
+
+    if (itemPrivate->hasCursor) {
+        QPointF p = item->mapFromScene(scenePos);
+        if (item->contains(p))
+            return item;
+    }
+    return 0;
+}
+#endif
+
 bool QQuickWindowPrivate::sendFilteredTouchEvent(QQuickItem *target, QQuickItem *item, QTouchEvent *event)
 {
     if (!target)
index 52e46ca..267c8d3 100644 (file)
@@ -113,6 +113,9 @@ public:
 
     // Keeps track of the item currently receiving mouse events
     QQuickItem *mouseGrabberItem;
+#ifndef QT_NO_CURSOR
+    QQuickItem *cursorItem;
+#endif
 #ifndef QT_NO_DRAGANDDROP
     QQuickDragGrabber dragGrabber;
 #endif
@@ -145,6 +148,10 @@ public:
     void deliverDragEvent(QQuickDragGrabber *, QEvent *);
     bool deliverDragEvent(QQuickDragGrabber *, QQuickItem *, QDragMoveEvent *);
 #endif
+#ifndef QT_NO_CURSOR
+    void updateCursor(const QPointF &scenePos);
+    QQuickItem *findCursorItem(QQuickItem *item, const QPointF &scenePos);
+#endif
 
     QList<QQuickItem*> hoverItems;
     enum FocusOption {
index 67a36af..f7ccd67 100644 (file)
@@ -85,6 +85,9 @@ private slots:
     void pressedMultipleButtons_data();
     void pressedMultipleButtons();
     void changeAxis();
+#ifndef QT_NO_CURSOR
+    void cursorShape();
+#endif
 
 private:
     void acceptedButton_data();
@@ -1366,6 +1369,36 @@ void tst_QQuickMouseArea::changeAxis()
     delete view;
 }
 
+#ifndef QT_NO_CURSOR
+void tst_QQuickMouseArea::cursorShape()
+{
+    QQmlEngine engine;
+    QQmlComponent component(&engine);
+    component.setData("import QtQuick 2.0\n MouseArea {}", QUrl());
+    QScopedPointer<QObject> object(component.create());
+    QQuickMouseArea *mouseArea = qobject_cast<QQuickMouseArea *>(object.data());
+    QVERIFY(mouseArea);
+
+    QSignalSpy spy(mouseArea, SIGNAL(cursorShapeChanged()));
+
+    QCOMPARE(mouseArea->cursorShape(), Qt::ArrowCursor);
+    QCOMPARE(mouseArea->cursor().shape(), Qt::ArrowCursor);
+
+    mouseArea->setCursorShape(Qt::IBeamCursor);
+    QCOMPARE(mouseArea->cursorShape(), Qt::IBeamCursor);
+    QCOMPARE(mouseArea->cursor().shape(), Qt::IBeamCursor);
+    QCOMPARE(spy.count(), 1);
+
+    mouseArea->setCursorShape(Qt::IBeamCursor);
+    QCOMPARE(spy.count(), 1);
+
+    mouseArea->setCursorShape(Qt::WaitCursor);
+    QCOMPARE(mouseArea->cursorShape(), Qt::WaitCursor);
+    QCOMPARE(mouseArea->cursor().shape(), Qt::WaitCursor);
+    QCOMPARE(spy.count(), 2);
+}
+#endif
+
 QTEST_MAIN(tst_QQuickMouseArea)
 
 #include "tst_qquickmousearea.moc"
index 9800c72..01d6036 100644 (file)
@@ -310,6 +310,11 @@ private slots:
     void ignoreUnhandledMouseEvents();
 
     void ownershipRootItem();
+
+#ifndef QT_NO_CURSOR
+    void cursor();
+#endif
+
 private:
     QTouchDevice *touchDevice;
     QTouchDevice *touchDeviceWithVelocity;
@@ -1061,6 +1066,144 @@ void tst_qquickwindow::ownershipRootItem()
     QCoreApplication::processEvents();
     QVERIFY(!accessor->isRootItemDestroyed());
 }
+
+#ifndef QT_NO_CURSOR
+void tst_qquickwindow::cursor()
+{
+    QQuickWindow window;
+    window.resize(320, 240);
+
+    QQuickItem parentItem;
+    parentItem.setPos(QPointF(0, 0));
+    parentItem.setSize(QSizeF(180, 180));
+    parentItem.setParentItem(window.rootItem());
+
+    QQuickItem childItem;
+    childItem.setPos(QPointF(60, 90));
+    childItem.setSize(QSizeF(120, 120));
+    childItem.setParentItem(&parentItem);
+
+    QQuickItem clippingItem;
+    clippingItem.setPos(QPointF(120, 120));
+    clippingItem.setSize(QSizeF(180, 180));
+    clippingItem.setClip(true);
+    clippingItem.setParentItem(window.rootItem());
+
+    QQuickItem clippedItem;
+    clippedItem.setPos(QPointF(-30, -30));
+    clippedItem.setSize(QSizeF(120, 120));
+    clippedItem.setParentItem(&clippingItem);
+
+    window.show();
+    QVERIFY(QTest::qWaitForWindowExposed(&window));
+
+    // Position the cursor over the parent and child item and the clipped section of clippedItem.
+    QTest::mouseMove(&window, QPoint(100, 100));
+
+    // No items cursors, window cursor is the default arrow.
+    QCOMPARE(window.cursor().shape(), Qt::ArrowCursor);
+
+    // The section of clippedItem under the cursor is clipped, and so doesn't affect the window cursor.
+    clippedItem.setCursor(Qt::ForbiddenCursor);
+    QCOMPARE(clippedItem.cursor().shape(), Qt::ForbiddenCursor);
+    QCOMPARE(window.cursor().shape(), Qt::ArrowCursor);
+
+    // parentItem is under the cursor, so the window cursor is changed.
+    parentItem.setCursor(Qt::IBeamCursor);
+    QCOMPARE(parentItem.cursor().shape(), Qt::IBeamCursor);
+    QCOMPARE(window.cursor().shape(), Qt::IBeamCursor);
+
+    // childItem is under the cursor and is in front of its parent, so the window cursor is changed.
+    childItem.setCursor(Qt::WaitCursor);
+    QCOMPARE(childItem.cursor().shape(), Qt::WaitCursor);
+    QCOMPARE(window.cursor().shape(), Qt::WaitCursor);
+
+    childItem.setCursor(Qt::PointingHandCursor);
+    QCOMPARE(childItem.cursor().shape(), Qt::PointingHandCursor);
+    QCOMPARE(window.cursor().shape(), Qt::PointingHandCursor);
+
+    // childItem is the current cursor item, so this has no effect on the window cursor.
+    parentItem.unsetCursor();
+    QCOMPARE(parentItem.cursor().shape(), Qt::ArrowCursor);
+    QCOMPARE(window.cursor().shape(), Qt::PointingHandCursor);
+
+    parentItem.setCursor(Qt::IBeamCursor);
+    QCOMPARE(parentItem.cursor().shape(), Qt::IBeamCursor);
+    QCOMPARE(window.cursor().shape(), Qt::PointingHandCursor);
+
+    // With the childItem cursor cleared, parentItem is now foremost.
+    childItem.unsetCursor();
+    QCOMPARE(childItem.cursor().shape(), Qt::ArrowCursor);
+    QCOMPARE(window.cursor().shape(), Qt::IBeamCursor);
+
+    // Setting the childItem cursor to the default still takes precedence over parentItem.
+    childItem.setCursor(Qt::ArrowCursor);
+    QCOMPARE(childItem.cursor().shape(), Qt::ArrowCursor);
+    QCOMPARE(window.cursor().shape(), Qt::ArrowCursor);
+
+    childItem.setCursor(Qt::WaitCursor);
+    QCOMPARE(childItem.cursor().shape(), Qt::WaitCursor);
+    QCOMPARE(window.cursor().shape(), Qt::WaitCursor);
+
+    // Move the cursor so it is over just parentItem.
+    QTest::mouseMove(&window, QPoint(20, 20));
+    QCOMPARE(window.cursor().shape(), Qt::IBeamCursor);
+
+    // Move the cursor so that is over all items, clippedItem wins because its a child of
+    // clippingItem which is in from of parentItem in painting order.
+    QTest::mouseMove(&window, QPoint(125, 125));
+    QCOMPARE(window.cursor().shape(), Qt::ForbiddenCursor);
+
+    // Over clippingItem only, so no cursor.
+    QTest::mouseMove(&window, QPoint(200, 280));
+    QCOMPARE(window.cursor().shape(), Qt::ArrowCursor);
+
+    // Over no item, so no cursor.
+    QTest::mouseMove(&window, QPoint(10, 280));
+    QCOMPARE(window.cursor().shape(), Qt::ArrowCursor);
+
+    // back to the start.
+    QTest::mouseMove(&window, QPoint(100, 100));
+    QCOMPARE(window.cursor().shape(), Qt::WaitCursor);
+
+    // Try with the mouse pressed.
+    QTest::mousePress(&window, Qt::LeftButton, 0, QPoint(100, 100));
+    QTest::mouseMove(&window, QPoint(20, 20));
+    QCOMPARE(window.cursor().shape(), Qt::IBeamCursor);
+    QTest::mouseMove(&window, QPoint(125, 125));
+    QCOMPARE(window.cursor().shape(), Qt::ForbiddenCursor);
+    QTest::mouseMove(&window, QPoint(200, 280));
+    QCOMPARE(window.cursor().shape(), Qt::ArrowCursor);
+    QTest::mouseMove(&window, QPoint(10, 280));
+    QCOMPARE(window.cursor().shape(), Qt::ArrowCursor);
+    QTest::mouseMove(&window, QPoint(100, 100));
+    QCOMPARE(window.cursor().shape(), Qt::WaitCursor);
+    QTest::mouseRelease(&window, Qt::LeftButton, 0, QPoint(100, 100));
+
+    // Remove the cursor item from the scene. Theoretically this should make parentItem the
+    // cursorItem, but given the situation will correct itself after the next mouse move it's
+    // probably better left as is to avoid unnecessary work during tear down.
+    childItem.setParentItem(0);
+    QCOMPARE(window.cursor().shape(), Qt::WaitCursor);
+
+    parentItem.setCursor(Qt::SizeAllCursor);
+    QCOMPARE(parentItem.cursor().shape(), Qt::SizeAllCursor);
+    QCOMPARE(window.cursor().shape(), Qt::WaitCursor);
+
+    // Changing the cursor of an un-parented item doesn't affect the window's cursor.
+    childItem.setCursor(Qt::ClosedHandCursor);
+    QCOMPARE(childItem.cursor().shape(), Qt::ClosedHandCursor);
+    QCOMPARE(window.cursor().shape(), Qt::WaitCursor);
+
+    childItem.unsetCursor();
+    QCOMPARE(childItem.cursor().shape(), Qt::ArrowCursor);
+    QCOMPARE(window.cursor().shape(), Qt::WaitCursor);
+
+    QTest::mouseRelease(&window, Qt::LeftButton, 0, QPoint(100, 101));
+    QCOMPARE(window.cursor().shape(), Qt::SizeAllCursor);
+}
+#endif
+
 QTEST_MAIN(tst_qquickwindow)
 
 #include "tst_qquickwindow.moc"