aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJan Arve Sæther <jan-arve.saether@qt.io>2021-04-27 16:23:26 +0200
committerJan Arve Sæther <jan-arve.saether@qt.io>2021-06-04 10:17:28 +0200
commit3f4088256c7712cbc757dd4d8835a3d4b272b4ee (patch)
tree612e4f6d152ea0e517e50e76dd68bedb57d36e43
parentcb20d7bcf8313fa3d39e07b37a7c9252fd28cde0 (diff)
Add support for hyperlinks in Text items
Task-number: QTBUG-67878 Change-Id: I1e990a5db8f0cf78e5cdcec7359e5aabffe42e3d Reviewed-by: Volker Hilsheimer <volker.hilsheimer@qt.io>
-rw-r--r--src/quick/accessible/qaccessiblequickitem.cpp280
-rw-r--r--src/quick/accessible/qaccessiblequickitem_p.h4
-rw-r--r--src/quick/items/qquickaccessibleattached.cpp11
-rw-r--r--src/quick/items/qquickaccessibleattached_p.h2
-rw-r--r--src/quick/items/qquicktext.cpp32
-rw-r--r--src/quick/items/qquicktext_p_p.h12
-rw-r--r--tests/auto/quick/qquickaccessible/data/text.qml10
-rw-r--r--tests/auto/quick/qquickaccessible/tst_qquickaccessible.cpp27
8 files changed, 369 insertions, 9 deletions
diff --git a/src/quick/accessible/qaccessiblequickitem.cpp b/src/quick/accessible/qaccessiblequickitem.cpp
index 4bb756a018..5e3a1108d6 100644
--- a/src/quick/accessible/qaccessiblequickitem.cpp
+++ b/src/quick/accessible/qaccessiblequickitem.cpp
@@ -43,6 +43,8 @@
#include "QtQuick/private/qquickitem_p.h"
#include "QtQuick/private/qquicktext_p.h"
+#include <private/qquicktext_p_p.h>
+
#include "QtQuick/private/qquicktextinput_p.h"
#include "QtQuick/private/qquickaccessibleattached_p.h"
#include "QtQuick/qquicktextdocument.h"
@@ -51,6 +53,194 @@ QT_BEGIN_NAMESPACE
#if QT_CONFIG(accessibility)
+class QAccessibleHyperlink : public QAccessibleInterface, public QAccessibleHyperlinkInterface {
+public:
+ QAccessibleHyperlink(QQuickItem *parentTextItem, int linkIndex);
+
+ // check for valid pointers
+ bool isValid() const override;
+ QObject *object() const override;
+ QWindow *window() const override;
+
+ // navigation, hierarchy
+ QAccessibleInterface *parent() const override;
+ QAccessibleInterface *child(int index) const override;
+ int childCount() const override;
+ int indexOfChild(const QAccessibleInterface *iface) const override;
+ QAccessibleInterface *childAt(int x, int y) const override;
+
+ // properties and state
+ QString text(QAccessible::Text) const override;
+ void setText(QAccessible::Text, const QString &text) override;
+ QRect rect() const override;
+ QAccessible::Role role() const override;
+ QAccessible::State state() const override;
+
+ void *interface_cast(QAccessible::InterfaceType t) override;
+
+ // QAccessibleHyperlinkInterface
+ QString anchor() const override
+ {
+ const QVector<QQuickTextPrivate::LinkDesc> links = QQuickTextPrivate::get(textItem())->getLinks();
+ if (linkIndex < links.count())
+ return links.at(linkIndex).m_anchor;
+ return QString();
+ }
+
+ QString anchorTarget() const override
+ {
+ const QVector<QQuickTextPrivate::LinkDesc> links = QQuickTextPrivate::get(textItem())->getLinks();
+ if (linkIndex < links.count())
+ return links.at(linkIndex).m_anchorTarget;
+ return QString();
+ }
+
+ int startIndex() const override
+ {
+ const QVector<QQuickTextPrivate::LinkDesc> links = QQuickTextPrivate::get(textItem())->getLinks();
+ if (linkIndex < links.count())
+ return links.at(linkIndex).m_startIndex;
+ return -1;
+ }
+
+ int endIndex() const override
+ {
+ const QVector<QQuickTextPrivate::LinkDesc> links = QQuickTextPrivate::get(textItem())->getLinks();
+ if (linkIndex < links.count())
+ return links.at(linkIndex).m_endIndex;
+ return -1;
+ }
+
+private:
+ QQuickText *textItem() const { return qobject_cast<QQuickText*>(parentTextItem); }
+ QQuickItem *parentTextItem;
+ const int linkIndex;
+
+ friend class QAccessibleQuickItem;
+};
+
+
+QAccessibleHyperlink::QAccessibleHyperlink(QQuickItem *parentTextItem, int linkIndex)
+ : parentTextItem(parentTextItem),
+ linkIndex(linkIndex)
+{
+}
+
+
+bool QAccessibleHyperlink::isValid() const
+{
+ return textItem();
+}
+
+
+QObject *QAccessibleHyperlink::object() const
+{
+ return nullptr;
+}
+
+
+QWindow *QAccessibleHyperlink::window() const
+{
+ return textItem()->window();
+}
+
+
+/*! \reimp */
+QRect QAccessibleHyperlink::rect() const
+{
+ const QVector<QQuickTextPrivate::LinkDesc> links = QQuickTextPrivate::get(textItem())->getLinks();
+ if (linkIndex < links.count()) {
+ const QPoint tl = itemScreenRect(textItem()).topLeft();
+ return links.at(linkIndex).rect.translated(tl);
+ }
+ return QRect();
+}
+
+/*! \reimp */
+QAccessibleInterface *QAccessibleHyperlink::childAt(int, int) const
+{
+ return nullptr;
+}
+
+/*! \reimp */
+QAccessibleInterface *QAccessibleHyperlink::parent() const
+{
+ return QAccessible::queryAccessibleInterface(textItem());
+}
+
+/*! \reimp */
+QAccessibleInterface *QAccessibleHyperlink::child(int) const
+{
+ return nullptr;
+}
+
+/*! \reimp */
+int QAccessibleHyperlink::childCount() const
+{
+ return 0;
+}
+
+/*! \reimp */
+int QAccessibleHyperlink::indexOfChild(const QAccessibleInterface *) const
+{
+ return -1;
+}
+
+/*! \reimp */
+QAccessible::State QAccessibleHyperlink::state() const
+{
+ QAccessible::State s;
+ s.selectable = true;
+ s.focusable = true;
+ s.selectableText = true;
+ s.selected = false;
+ return s;
+}
+
+/*! \reimp */
+QAccessible::Role QAccessibleHyperlink::role() const
+{
+ return QAccessible::Link;
+}
+
+/*! \reimp */
+QString QAccessibleHyperlink::text(QAccessible::Text t) const
+{
+ // AT servers have different behaviors:
+ // Wordpad on windows have this behavior:
+ // * Name returns the anchor target (URL)
+ // * Value returns the anchor target (URL)
+
+ // Other AT servers (e.g. MS Edge on Windows) does what seems to be more sensible:
+ // * Name returns the anchor name
+ // * Value returns the anchor target (URL)
+ if (t == QAccessible::Name)
+ return anchor();
+ if (t == QAccessible::Value)
+ return anchorTarget();
+ return QString();
+}
+
+/*! \reimp */
+void QAccessibleHyperlink::setText(QAccessible::Text, const QString &)
+{
+
+}
+
+/*! \reimp */
+void *QAccessibleHyperlink::interface_cast(QAccessible::InterfaceType t)
+{
+ if (t == QAccessible::HyperlinkInterface)
+ return static_cast<QAccessibleHyperlinkInterface*>(this);
+ return nullptr;
+}
+
+
+/*!
+ * \internal
+ * \brief QAccessibleQuickItem::QAccessibleQuickItem
+ * \param item
+ */
QAccessibleQuickItem::QAccessibleQuickItem(QQuickItem *item)
: QAccessibleObject(item), m_doc(textDocument())
{
@@ -75,7 +265,13 @@ QWindow *QAccessibleQuickItem::window() const
int QAccessibleQuickItem::childCount() const
{
- return childItems().count();
+ // see comment in QAccessibleQuickItem::child() as to why we do this
+ int cc = 0;
+ if (QQuickText *textItem = qobject_cast<QQuickText*>(item())) {
+ cc = QQuickTextPrivate::get(textItem)->getLinks().count();
+ }
+ cc += childItems().count();
+ return cc;
}
QRect QAccessibleQuickItem::rect() const
@@ -109,6 +305,18 @@ QAccessibleInterface *QAccessibleQuickItem::childAt(int x, int y) const
return nullptr;
}
+ // special case for text interfaces
+ if (QQuickText *textItem = qobject_cast<QQuickText*>(item())) {
+ const auto hyperLinkChildCount = QQuickTextPrivate::get(textItem)->getLinks().count();
+ for (auto i = 0; i < hyperLinkChildCount; i++) {
+ QAccessibleInterface *iface = child(i);
+ if (iface->rect().contains(x,y)) {
+ return iface;
+ }
+ }
+ }
+
+ // general item hit test
const QList<QQuickItem*> kids = accessibleUnignoredChildren(item(), true);
for (int i = kids.count() - 1; i >= 0; --i) {
QAccessibleInterface *childIface = QAccessible::queryAccessibleInterface(kids.at(i));
@@ -146,19 +354,73 @@ QAccessibleInterface *QAccessibleQuickItem::parent() const
QAccessibleInterface *QAccessibleQuickItem::child(int index) const
{
- QList<QQuickItem *> children = childItems();
+ /* Text with hyperlinks will have dedicated children interfaces representing each hyperlink.
+
+ For the pathological case when a Text node has hyperlinks in its text *and* accessible
+ quick items as children, we put the hyperlink a11y interfaces as the first children, then
+ the other interfaces follows the hyperlink children (as siblings).
- if (index < 0 || index >= children.count())
+ For example, suppose you have two links in the text and an image as a child of the text,
+ it will have the following a11y hierarchy:
+
+ [a11y:TextInterface]
+ |
+ +- [a11y:HyperlinkInterface]
+ +- [a11y:HyperlinkInterface]
+ +- [a11y:ImageInterface]
+
+ Having this order (as opposed to having hyperlink interfaces last) will at least
+ ensure that the child id of hyperlink children is not altered when child is added/removed
+ to the text item and marked accessible.
+ In addition, hyperlink interfaces as children should be the common case, so it is preferred
+ to explore those first when iterating.
+ */
+ if (index < 0)
return nullptr;
- QQuickItem *child = children.at(index);
- return QAccessible::queryAccessibleInterface(child);
+
+ if (QQuickText *textItem = qobject_cast<QQuickText*>(item())) {
+ const int hyperLinkChildCount = QQuickTextPrivate::get(textItem)->getLinks().count();
+ if (index < hyperLinkChildCount) {
+ auto it = m_childToId.constFind(index);
+ if (it != m_childToId.constEnd())
+ return QAccessible::accessibleInterface(it.value());
+
+ QAccessibleHyperlink *iface = new QAccessibleHyperlink(item(), index);
+ QAccessible::Id id = QAccessible::registerAccessibleInterface(iface);
+ m_childToId.insert(index, id);
+ return iface;
+ }
+ index -= hyperLinkChildCount;
+ }
+
+ QList<QQuickItem *> children = childItems();
+ if (index < children.count()) {
+ QQuickItem *child = children.at(index);
+ return QAccessible::queryAccessibleInterface(child);
+ }
+ return nullptr;
}
int QAccessibleQuickItem::indexOfChild(const QAccessibleInterface *iface) const
{
+ int hyperLinkChildCount = 0;
+ if (QQuickText *textItem = qobject_cast<QQuickText*>(item())) {
+ hyperLinkChildCount = QQuickTextPrivate::get(textItem)->getLinks().count();
+ if (QAccessibleHyperlinkInterface *hyperLinkIface = const_cast<QAccessibleInterface *>(iface)->hyperlinkInterface()) {
+ // ### assumes that there is only one subclass implementing QAccessibleHyperlinkInterface
+ // Alternatively, we could simply iterate with child() and do a linear search for it
+ QAccessibleHyperlink *hyperLink = static_cast<QAccessibleHyperlink*>(hyperLinkIface);
+ if (hyperLink->textItem() == static_cast<QQuickText*>(item())) {
+ return hyperLink->linkIndex;
+ }
+ }
+ }
QList<QQuickItem*> kids = childItems();
- return kids.indexOf(static_cast<QQuickItem*>(iface->object()));
+ int idx = kids.indexOf(static_cast<QQuickItem*>(iface->object()));
+ if (idx >= 0)
+ idx += hyperLinkChildCount;
+ return idx;
}
static void unignoredChildren(QQuickItem *item, QList<QQuickItem *> *items, bool paintOrder)
@@ -426,9 +688,11 @@ void *QAccessibleQuickItem::interface_cast(QAccessible::InterfaceType t)
r == QAccessible::ScrollBar))
return static_cast<QAccessibleValueInterface*>(this);
- if (t == QAccessible::TextInterface &&
- (r == QAccessible::EditableText))
+ if (t == QAccessible::TextInterface) {
+ if (r == QAccessible::EditableText ||
+ r == QAccessible::StaticText)
return static_cast<QAccessibleTextInterface*>(this);
+ }
return QAccessibleObject::interface_cast(t);
}
diff --git a/src/quick/accessible/qaccessiblequickitem_p.h b/src/quick/accessible/qaccessiblequickitem_p.h
index ccc9280ba0..8ed8c0d260 100644
--- a/src/quick/accessible/qaccessiblequickitem_p.h
+++ b/src/quick/accessible/qaccessiblequickitem_p.h
@@ -137,7 +137,11 @@ protected:
void *interface_cast(QAccessible::InterfaceType t) override;
private:
+ // for Text nodes:
QTextDocument *m_doc;
+ typedef QHash<int, QAccessible::Id> ChildCache;
+ mutable ChildCache m_childToId;
+
};
QRect itemScreenRect(QQuickItem *item);
diff --git a/src/quick/items/qquickaccessibleattached.cpp b/src/quick/items/qquickaccessibleattached.cpp
index 67d17c98e5..0b593c0089 100644
--- a/src/quick/items/qquickaccessibleattached.cpp
+++ b/src/quick/items/qquickaccessibleattached.cpp
@@ -499,6 +499,17 @@ void QQuickAccessibleAttached::availableActions(QStringList *actions) const
actions->append(QAccessibleActionInterface::nextPageAction());
}
+QString QQuickAccessibleAttached::stripHtml(const QString &html)
+{
+#ifndef QT_NO_TEXTHTMLPARSER
+ QTextDocument doc;
+ doc.setHtml(html);
+ return doc.toPlainText();
+#else
+ return html;
+#endif
+}
+
QT_END_NAMESPACE
#include "moc_qquickaccessibleattached_p.cpp"
diff --git a/src/quick/items/qquickaccessibleattached_p.h b/src/quick/items/qquickaccessibleattached_p.h
index 69c097344a..6d7d46bfd4 100644
--- a/src/quick/items/qquickaccessibleattached_p.h
+++ b/src/quick/items/qquickaccessibleattached_p.h
@@ -189,6 +189,8 @@ public:
bool doAction(const QString &actionName);
void availableActions(QStringList *actions) const;
+ Q_INVOKABLE static QString stripHtml(const QString &html);
+
public Q_SLOTS:
void valueChanged() {
QAccessibleValueChangeEvent ev(parent(), parent()->property("value"));
diff --git a/src/quick/items/qquicktext.cpp b/src/quick/items/qquicktext.cpp
index a18ab824e2..379fef7722 100644
--- a/src/quick/items/qquicktext.cpp
+++ b/src/quick/items/qquicktext.cpp
@@ -2873,6 +2873,38 @@ bool QQuickTextPrivate::isLinkHoveredConnected()
IS_SIGNAL_CONNECTED(q, QQuickText, linkHovered, (const QString &));
}
+static void getLinks_helper(const QTextLayout *layout, QVector<QQuickTextPrivate::LinkDesc> *links)
+{
+ for (const QTextLayout::FormatRange &formatRange : layout->formats()) {
+ if (formatRange.format.isAnchor()) {
+ const int start = formatRange.start;
+ const int len = formatRange.length;
+ QTextLine line = layout->lineForTextPosition(start);
+ QRectF r;
+ r.setTop(line.y());
+ r.setLeft(line.cursorToX(start, QTextLine::Leading));
+ r.setHeight(line.height());
+ r.setRight(line.cursorToX(start + len, QTextLine::Trailing));
+ // ### anchorNames() is empty?! Not sure why this doesn't work
+ // QString anchorName = formatRange.format.anchorNames().value(0); //### pick the first?
+ // Therefore, we resort to QString::mid()
+ QString anchorName = layout->text().mid(start, len);
+ const QString anchorHref = formatRange.format.anchorHref();
+ if (anchorName.isEmpty())
+ anchorName = anchorHref;
+ links->append( { anchorName, anchorHref, start, start + len, r.toRect()} );
+ }
+ }
+}
+
+QVector<QQuickTextPrivate::LinkDesc> QQuickTextPrivate::getLinks() const
+{
+ QVector<QQuickTextPrivate::LinkDesc> links;
+ getLinks_helper(&layout, &links);
+ return links;
+}
+
+
/*!
\qmlsignal QtQuick::Text::linkHovered(string link)
\since 5.2
diff --git a/src/quick/items/qquicktext_p_p.h b/src/quick/items/qquicktext_p_p.h
index da06c7e632..7d8744671a 100644
--- a/src/quick/items/qquicktext_p_p.h
+++ b/src/quick/items/qquicktext_p_p.h
@@ -199,6 +199,18 @@ public:
void setupCustomLineGeometry(QTextLine &line, qreal &height, int fullLayoutTextLength, int lineOffset = 0);
bool isLinkActivatedConnected();
bool isLinkHoveredConnected();
+ QStringList links() const;
+
+ struct LinkDesc {
+ QString m_anchor;
+ QString m_anchorTarget;
+ int m_startIndex;
+ int m_endIndex;
+ QRect rect;
+ };
+
+ QVector<LinkDesc> getLinks() const;
+
static QString anchorAt(const QTextLayout *layout, const QPointF &mousePos);
QString anchorAt(const QPointF &pos) const;
diff --git a/tests/auto/quick/qquickaccessible/data/text.qml b/tests/auto/quick/qquickaccessible/data/text.qml
index 6daeacfd81..4de461f2e1 100644
--- a/tests/auto/quick/qquickaccessible/data/text.qml
+++ b/tests/auto/quick/qquickaccessible/data/text.qml
@@ -57,4 +57,14 @@ Item {
Accessible.description: "description"
}
+ Text {
+ x: 100
+ y: 200
+ width: 100
+ height: 40
+ text : "<p>Rich text with links:</p><a href=\"https://qt.io\">Website</a> or <a href=\"https://qt.io/blog\">blog</a>"
+ Accessible.name: Accessible.stripHtml(text)
+ Accessible.description: "Rich text with two hyperlinks"
+ }
+
}
diff --git a/tests/auto/quick/qquickaccessible/tst_qquickaccessible.cpp b/tests/auto/quick/qquickaccessible/tst_qquickaccessible.cpp
index 4270ff958f..edaaed967a 100644
--- a/tests/auto/quick/qquickaccessible/tst_qquickaccessible.cpp
+++ b/tests/auto/quick/qquickaccessible/tst_qquickaccessible.cpp
@@ -363,7 +363,7 @@ void tst_QQuickAccessible::basicPropertiesTest()
QAccessibleInterface *item = iface->child(0);
QVERIFY(item);
- QCOMPARE(item->childCount(), 5);
+ QCOMPARE(item->childCount(), 6);
QCOMPARE(item->rect().size(), QSize(400, 400));
QCOMPARE(item->role(), QAccessible::Client);
QCOMPARE(iface->indexOfChild(item), 0);
@@ -448,6 +448,31 @@ void tst_QQuickAccessible::basicPropertiesTest()
QCOMPARE(text3->role(), QAccessible::StaticText);
QVERIFY(text3->state().readOnly);
+ // Text "Rich text"
+ QAccessibleInterface *richText = item->child(5);
+ QVERIFY(text3);
+ QCOMPARE(richText->childCount(), 2);
+ QCOMPARE(richText->text(QAccessible::Name), QLatin1String("Rich text with links:\nWebsite or blog"));
+ QCOMPARE(richText->role(), QAccessible::StaticText);
+ QCOMPARE(item->indexOfChild(richText), 5);
+ QVERIFY(!richText->state().editable);
+ QVERIFY(!richText->state().readOnly);
+
+ // Check for hyperlink child nodes
+ for (int i = 0; i < richText->childCount(); ++i) {
+ static const char *linkUrls[2][2] = {
+ {"Website", "https://qt.io"},
+ {"blog", "https://qt.io/blog"}
+ };
+ QAccessibleInterface *link1 = richText->child(i);
+ QVERIFY(link1);
+ QCOMPARE(link1->role(), QAccessible::Link);
+ QAccessibleHyperlinkInterface *link = link1->hyperlinkInterface();
+ QVERIFY(link);
+ QCOMPARE(link->anchor(), QLatin1String(linkUrls[i][0]));
+ QCOMPARE(link->anchorTarget(), QLatin1String(linkUrls[i][1]));
+ }
+
// see if implicit changes back
attached->setRole(QAccessible::EditableText);
QEXPECT_FAIL("", "EditableText does not implicitly set readOnly to false", Continue);