Make QAccessibleQuickView::childAt() work properly with overlapping items
authorJan-Arve Saether <jan-arve.saether@nokia.com>
Tue, 3 Jan 2012 08:40:16 +0000 (09:40 +0100)
committerQt by Nokia <qt-info@nokia.com>
Thu, 12 Jan 2012 11:21:59 +0000 (12:21 +0100)
The previous code did not consider items that were overlapped due to
having different z coordinates.

The approach used is now the same as found in
QQuickCanvas::mousePressEvent().

Strictly speaking, this is a violation of childAt (since it will
disregard the implementation of childAt of all the descendants along
the path down to the item actually returned.)

However, I don't see any good reason for that the implementation for
childAt() would be different than how mousePressEvent behaves.
It should also perform better than any other solution I managed to
think of.

Change-Id: I2d3fa2282437c7b5533c6149c62fc456ccf2ccfa
Reviewed-by: Frederik Gladhorn <frederik.gladhorn@nokia.com>

src/plugins/accessible/quick/qaccessiblequickitem.cpp
src/plugins/accessible/quick/qaccessiblequickitem.h
src/plugins/accessible/quick/qaccessiblequickview.cpp
src/plugins/accessible/shared/qdeclarativeaccessible.cpp
tests/auto/declarative/qdeclarativeaccessibility/data/hittest.qml [new file with mode: 0644]
tests/auto/declarative/qdeclarativeaccessibility/data/widgets/TextRect.qml [new file with mode: 0644]
tests/auto/declarative/qdeclarativeaccessibility/tst_qdeclarativeaccessibility.cpp

index 5cedfe6..10442b2 100644 (file)
@@ -58,26 +58,7 @@ int QAccessibleQuickItem::childCount() const
 
 QRect QAccessibleQuickItem::rect() const
 {
-    // ### no canvas in some cases.
-    // ### Should we really check for 0 opacity?
-    if (!item()->canvas() ||!item()->isVisible() || qFuzzyIsNull(item()->opacity())) {
-        return QRect();
-    }
-
-    QSizeF size = QSizeF(item()->width(), item()->height());
-    // ### If the bounding rect fails, we first try the implicit size, then we go for the
-    // parent size. WE MIGHT HAVE TO REVISIT THESE FALLBACKS.
-    if (size.isEmpty()) {
-        size = QSizeF(item()->implicitWidth(), item()->implicitHeight());
-        if (size.isEmpty())
-            // ### Seems that the above fallback is not enough, fallback to use the parent size...
-            size = QSizeF(item()->parentItem()->width(), item()->parentItem()->height());
-    }
-
-    QRectF sceneRect = item()->mapRectToScene(QRectF(QPointF(0, 0), size));
-    QPoint screenPos = item()->canvas()->mapToGlobal(sceneRect.topLeft().toPoint());
-
-    QRect r = QRect(screenPos, sceneRect.size().toSize());
+    const QRect r = itemScreenRect(item());
 
     if (!r.isValid()) {
         qWarning() << item()->metaObject()->className() << item()->property("accessibleText") << r;
@@ -297,5 +278,33 @@ QVariant QAccessibleQuickItemValueInterface::minimumValue() const
     return item()->property("minimumValue");
 }
 
+/*!
+  \internal
+  Shared between QAccessibleQuickItem and QAccessibleQuickView
+*/
+QRect itemScreenRect(QQuickItem *item)
+{
+    // ### no canvas in some cases.
+    // ### Should we really check for 0 opacity?
+    if (!item->canvas() ||!item->isVisible() || qFuzzyIsNull(item->opacity())) {
+        return QRect();
+    }
+
+    QSize itemSize((int)item->width(), (int)item->height());
+    // ### If the bounding rect fails, we first try the implicit size, then we go for the
+    // parent size. WE MIGHT HAVE TO REVISIT THESE FALLBACKS.
+    if (itemSize.isEmpty()) {
+        itemSize = QSize((int)item->implicitWidth(), (int)item->implicitHeight());
+        if (itemSize.isEmpty())
+            // ### Seems that the above fallback is not enough, fallback to use the parent size...
+            itemSize = QSize((int)item->parentItem()->width(), (int)item->parentItem()->height());
+    }
+
+    QPointF scenePoint = item->mapToScene(QPointF(0, 0));
+    QPoint screenPos = item->canvas()->mapToGlobal(scenePoint.toPoint());
+    return QRect(screenPos, itemSize);
+}
+
+
 
 QT_END_NAMESPACE
index 5ef3a19..75e7081 100644 (file)
@@ -77,6 +77,9 @@ protected:
     QQuickItem *item() const { return static_cast<QQuickItem*>(object()); }
 };
 
+QRect itemScreenRect(QQuickItem *item);
+
+
 class QAccessibleQuickItemValueInterface: public QAccessibleQuickItem, public QAccessibleValueInterface
 {
 public:
index 6a896ef..d15e01d 100644 (file)
@@ -42,6 +42,7 @@
 #include "qaccessiblequickview.h"
 
 #include <QtQuick/qquickitem.h>
+#include <QtQuick/private/qquickitem_p.h>
 
 #include "qaccessiblequickitem.h"
 #include "qdeclarativeaccessible.h"
@@ -69,8 +70,8 @@ QAccessibleInterface *QAccessibleQuickView::parent() const
 QAccessibleInterface *QAccessibleQuickView::child(int index) const
 {
     if (index == 0) {
-        QQuickItem *declarativeRoot = view()->rootObject();
-        return new QAccessibleQuickItem(declarativeRoot);
+        if (QQuickItem *declarativeRoot = view()->rootObject())
+            return new QAccessibleQuickItem(declarativeRoot);
     }
     return 0;
 }
@@ -108,11 +109,48 @@ QString QAccessibleQuickView::text(QAccessible::Text text) const
     return view()->windowTitle();
 }
 
+
+/*!
+  \internal
+
+  Can also return \a item itself
+  */
+static QQuickItem *childAt_helper(QQuickItem *item, int x, int y)
+{
+    if (item->opacity() == 0.0 || !item->isVisible() || !item->isEnabled())
+        return 0;
+
+    if (item->flags() & QQuickItem::ItemClipsChildrenToShape) {
+        if (!itemScreenRect(item).contains(x, y))
+            return 0;
+    }
+
+    QQuickItemPrivate *itemPrivate = QQuickItemPrivate::get(item);
+
+    QList<QQuickItem *> children = itemPrivate->paintOrderChildItems();
+    for (int i = children.count() - 1; i >= 0; --i) {
+        QQuickItem *child = children.at(i);
+        if (QQuickItem *childChild = childAt_helper(child, x, y))
+            return childChild;
+    }
+
+    QRect screenRect = itemScreenRect(item);
+
+    if (screenRect.contains(x, y))
+        return item;
+
+    return 0;
+}
+
 QAccessibleInterface *QAccessibleQuickView::childAt(int x, int y) const
 {
-    Q_UNUSED(x);
-    Q_UNUSED(y);
-    return child(0); // return the top-level QML item
+    Q_ASSERT(view());
+    QQuickItem *root = view()->rootItem();
+    if (root) {
+        if (QQuickItem *item = childAt_helper(root, x, y))
+            return QAccessible::queryAccessibleInterface(item);
+    }
+    return 0;
 }
 
 int QAccessibleQuickView::indexOfChild(const QAccessibleInterface *iface) const
index 7db5f07..1b35594 100644 (file)
@@ -70,9 +70,8 @@ QFlags<QAccessible::RelationFlag> QDeclarativeAccessible::relationTo(const QAcce
 
 QAccessibleInterface *QDeclarativeAccessible::childAt(int x, int y) const
 {
-    // Look for children first.
-    // Start with the last child first, because children are ordered in paint order
-    // (which is opposite of hit test order)
+    // Note that this function will disregard stacking order.
+    // (QAccessibleQuickView::childAt() does this correctly and more efficient)
 
     // If the item clips its children, we can return early if the coordinate is outside its rect
     if (clipsChildren()) {
diff --git a/tests/auto/declarative/qdeclarativeaccessibility/data/hittest.qml b/tests/auto/declarative/qdeclarativeaccessibility/data/hittest.qml
new file mode 100644 (file)
index 0000000..52b652e
--- /dev/null
@@ -0,0 +1,176 @@
+/****************************************************************************
+**
+** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies).
+** All rights reserved.
+** Contact: Nokia Corporation (qt-info@nokia.com)
+**
+** This file is part of the QtDeclarative module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** GNU Lesser General Public License Usage
+** This file may be used under the terms of the GNU Lesser General Public
+** License version 2.1 as published by the Free Software Foundation and
+** appearing in the file LICENSE.LGPL included in the packaging of this
+** file. Please review the following information to ensure the GNU Lesser
+** General Public License version 2.1 requirements will be met:
+** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+**
+** In addition, as a special exception, Nokia gives you certain additional
+** rights. These rights are described in the Nokia Qt LGPL Exception
+** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU General
+** Public License version 3.0 as published by the Free Software Foundation
+** and appearing in the file LICENSE.GPL included in the packaging of this
+** file. Please review the following information to ensure the GNU General
+** Public License version 3.0 requirements will be met:
+** http://www.gnu.org/copyleft/gpl.html.
+**
+** Other Usage
+** Alternatively, this file may be used in accordance with the terms and
+** conditions contained in a signed written agreement between you and Nokia.
+**
+**
+**
+**
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+
+import QtQuick 2.0
+import "widgets"
+
+Rectangle {
+    id: page
+    width: 640
+    height: 480
+    color: "white"
+    Rectangle {
+        id: header
+        color: "#c0c0c0"
+        height: usage.height + chkClip.height
+        anchors.left: parent.left
+        anchors.right: parent.right
+        Text {
+            id: usage
+            text: "Use an a11y inspect tool to see if all visible rectangles can be found with hit testing."
+        }
+        Rectangle {
+            id: chkClip
+            property bool checked: true
+
+            color: (checked ? "#f0f0f0" : "#c0c0c0")
+            height: label.height
+            width: label.width
+            anchors.left: parent.left
+            anchors.bottom: parent.bottom
+
+            MouseArea {
+                anchors.fill: parent
+                onClicked: chkClip.checked = !chkClip.checked
+            }
+            Text {
+                id: label
+                text: "Click here to toggle clipping"
+            }
+        }
+    }
+    TextRect {
+        clip: chkClip.checked
+        z: 2
+        id: rect1
+        text: "rect1"
+        width: 100
+        height: 100
+        color: "#ffc0c0"
+        anchors.top: header.bottom
+        TextRect {
+            id: rect10
+            text: "rect10"
+            width: 100
+            height: 100
+            x: 50
+            y: 50
+            color: "#ffa0a0"
+            TextRect {
+                id: rect100
+                text: "rect100"
+                width: 100
+                height: 100
+                x: 80
+                y: 80
+                color: "#ff8080"
+            }
+            TextRect {
+                id: rect101
+                text: "rect101"
+                x: 100
+                y: 70
+                z: 3
+                width: 100
+                height: 100
+                color: "#e06060"
+            }
+            TextRect {
+                id: rect102
+                text: "rect102"
+                width: 100
+                height: 100
+                x: 150
+                y: 60
+                color: "#c04040"
+            }
+        }
+    }
+
+    TextRect {
+        x: 0
+        y: 50
+        id: rect2
+        text: "rect2"
+        width: 100
+        height: 100
+        color: "#c0c0ff"
+        TextRect {
+            id: rect20
+            text: "rect20"
+            width: 100
+            height: 100
+            x: 50
+            y: 50
+            color: "#a0a0ff"
+            TextRect {
+                id: rect200
+                text: "rect200"
+                width: 100
+                height: 100
+                x: 80
+                y: 80
+                color: "#8080ff"
+            }
+            TextRect {
+                id: rect201
+                text: "rect201"
+                x: 100
+                y: 70
+                z: 100
+                width: 100
+                height: 100
+                color: "#6060e0"
+            }
+            TextRect {
+                id: rect202
+                text: "rect202"
+                width: 100
+                height: 100
+                x: 150
+                y: 60
+                color: "#4040c0"
+            }
+        }
+    }
+
+}
diff --git a/tests/auto/declarative/qdeclarativeaccessibility/data/widgets/TextRect.qml b/tests/auto/declarative/qdeclarativeaccessibility/data/widgets/TextRect.qml
new file mode 100644 (file)
index 0000000..9376869
--- /dev/null
@@ -0,0 +1,26 @@
+import QtQuick 2.0
+
+Rectangle {
+    id: button
+
+    property alias text : buttonText.text
+    Accessible.name: text
+    Accessible.description: "This button does " + text
+    Accessible.role: Accessible.Client
+
+    signal clicked
+
+    width: 40
+    height: 40
+    border.width: 2
+    border.color: "black";
+
+    Text {
+        id: buttonText
+        text: "TextRect"
+        anchors.centerIn: parent
+        font.pixelSize: parent.height * .1
+        style: Text.Sunken; color: "white"; styleColor: "black"; smooth: true
+    }
+
+}
index 725d282..a20094d 100644 (file)
@@ -421,33 +421,46 @@ QAI topLevelChildAt(QAccessibleInterface *iface, int x, int y)
 
 void tst_QDeclarativeAccessibility::hitTest()
 {
-    QQuickView *canvas = new QQuickView();
-    canvas->setSource(testFileUrl("statictext.qml"));
+    QQuickView *canvas = new QQuickView;
+    canvas->setSource(testFileUrl("hittest.qml"));
     canvas->show();
 
     QAI iface = QAI(QAccessible::queryAccessibleInterface(canvas));
     QVERIFY(iface.data());
-    QAI item = QAI(iface->child(0));
-    QRect itemRect = item->rect();
+    QAI rootItem = QAI(iface->child(0));
+    QRect rootRect = rootItem->rect();
 
     // hit the root item
-    QAI itemHit = QAI(iface->childAt(itemRect.x() + 5, itemRect.y() + 5));
+    QAI itemHit(iface->childAt(rootRect.x() + 200, rootRect.y() + 50));
     QVERIFY(itemHit);
-    QCOMPARE(itemRect, itemHit->rect());
+    QCOMPARE(rootRect, itemHit->rect());
 
-    // hit a text element
-    QAI textChild = QAI(item->child(0));
-    QAI textChildHit = topLevelChildAt(iface.data(), itemRect.x() + 105, itemRect.y() + 25);
-    QVERIFY(textChildHit);
-    QCOMPARE(textChild->rect(), textChildHit->rect());
-    QCOMPARE(textChildHit->text(QAccessible::Name), QLatin1String("Hello Accessibility"));
+    // hit rect1
+    QAI rect1(rootItem->child(1));
+    QRect rect1Rect = rect1->rect();
+    itemHit = QAI(rootItem->childAt(rect1Rect.x() + 10, rect1Rect.y() + 10));
+    QVERIFY(itemHit);
+    QCOMPARE(rect1Rect, itemHit->rect());
+    QCOMPARE(itemHit->text(QAccessible::Name), QLatin1String("rect1"));
 
     // should also work from top level (app)
-    QAI app = QAI(QAccessible::queryAccessibleInterface(qApp));
-    QAI textChildHit2 = topLevelChildAt(app.data(), itemRect.x() + 105, itemRect.y() + 25);
-    QVERIFY(textChildHit2);
-    QCOMPARE(textChild->rect(), textChildHit2->rect());
-    QCOMPARE(textChildHit2->text(QAccessible::Name), QLatin1String("Hello Accessibility"));
+    QAI app(QAccessible::queryAccessibleInterface(qApp));
+    QAI itemHit2(topLevelChildAt(app.data(), rect1Rect.x() + 10, rect1Rect.y() + 10));
+    QVERIFY(itemHit2);
+    QCOMPARE(itemHit2->rect(), rect1Rect);
+    QCOMPARE(itemHit2->text(QAccessible::Name), QLatin1String("rect1"));
+
+    // hit rect201
+    QAI rect2(rootItem->child(2));
+    QAI rect20(rect2->child(1));
+    QAI rect201(rect20->child(2));
+    QVERIFY(rect201);
+
+    QRect rect201Rect = rect201->rect();
+    itemHit = QAI(iface->childAt(rect201Rect.x() + 20, rect201Rect.y() + 20));
+    QVERIFY(itemHit);
+    QCOMPARE(itemHit->rect(), rect201Rect);
+    QCOMPARE(itemHit->text(QAccessible::Name), QLatin1String("rect201"));
 
     delete canvas;
 }