From 9ab84bd46861557afb190ba4cb2481b35a7401e5 Mon Sep 17 00:00:00 2001 From: Jan-Arve Saether Date: Tue, 3 Jan 2012 09:40:16 +0100 Subject: [PATCH] Make QAccessibleQuickView::childAt() work properly with overlapping items 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 --- .../accessible/quick/qaccessiblequickitem.cpp | 49 ++++--- .../accessible/quick/qaccessiblequickitem.h | 3 + .../accessible/quick/qaccessiblequickview.cpp | 48 +++++- .../accessible/shared/qdeclarativeaccessible.cpp | 5 +- .../qdeclarativeaccessibility/data/hittest.qml | 176 ++++++++++++++++++++ .../data/widgets/TextRect.qml | 26 +++ .../tst_qdeclarativeaccessibility.cpp | 47 ++++-- 7 files changed, 309 insertions(+), 45 deletions(-) create mode 100644 tests/auto/declarative/qdeclarativeaccessibility/data/hittest.qml create mode 100644 tests/auto/declarative/qdeclarativeaccessibility/data/widgets/TextRect.qml diff --git a/src/plugins/accessible/quick/qaccessiblequickitem.cpp b/src/plugins/accessible/quick/qaccessiblequickitem.cpp index 5cedfe6..10442b2 100644 --- a/src/plugins/accessible/quick/qaccessiblequickitem.cpp +++ b/src/plugins/accessible/quick/qaccessiblequickitem.cpp @@ -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 diff --git a/src/plugins/accessible/quick/qaccessiblequickitem.h b/src/plugins/accessible/quick/qaccessiblequickitem.h index 5ef3a19..75e7081 100644 --- a/src/plugins/accessible/quick/qaccessiblequickitem.h +++ b/src/plugins/accessible/quick/qaccessiblequickitem.h @@ -77,6 +77,9 @@ protected: QQuickItem *item() const { return static_cast(object()); } }; +QRect itemScreenRect(QQuickItem *item); + + class QAccessibleQuickItemValueInterface: public QAccessibleQuickItem, public QAccessibleValueInterface { public: diff --git a/src/plugins/accessible/quick/qaccessiblequickview.cpp b/src/plugins/accessible/quick/qaccessiblequickview.cpp index 6a896ef..d15e01d 100644 --- a/src/plugins/accessible/quick/qaccessiblequickview.cpp +++ b/src/plugins/accessible/quick/qaccessiblequickview.cpp @@ -42,6 +42,7 @@ #include "qaccessiblequickview.h" #include +#include #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 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 diff --git a/src/plugins/accessible/shared/qdeclarativeaccessible.cpp b/src/plugins/accessible/shared/qdeclarativeaccessible.cpp index 7db5f07..1b35594 100644 --- a/src/plugins/accessible/shared/qdeclarativeaccessible.cpp +++ b/src/plugins/accessible/shared/qdeclarativeaccessible.cpp @@ -70,9 +70,8 @@ QFlags 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 index 0000000..52b652e --- /dev/null +++ b/tests/auto/declarative/qdeclarativeaccessibility/data/hittest.qml @@ -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 index 0000000..9376869 --- /dev/null +++ b/tests/auto/declarative/qdeclarativeaccessibility/data/widgets/TextRect.qml @@ -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 + } + +} diff --git a/tests/auto/declarative/qdeclarativeaccessibility/tst_qdeclarativeaccessibility.cpp b/tests/auto/declarative/qdeclarativeaccessibility/tst_qdeclarativeaccessibility.cpp index 725d282..a20094d 100644 --- a/tests/auto/declarative/qdeclarativeaccessibility/tst_qdeclarativeaccessibility.cpp +++ b/tests/auto/declarative/qdeclarativeaccessibility/tst_qdeclarativeaccessibility.cpp @@ -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; } -- 1.7.2.5