Commit 0ddc9744 authored by Ingo Klöcker's avatar Ingo Klöcker
Browse files

Make links in label text accessible

Add the AccessibleLink class (with role Link) that represents a link (or anchor)
of a rich text label. The anchors are enumerated and AccessibleRichTextLabel
returns them as accessible children. If a link gets the keyboard focus, then
HtmlLabel sends a corresponding focus event to the accessibility tools. This
makes links displayed by HtmlLabel accessible the same way as links displayed
by web browsers.

GnuPG-bug-id: 6034
parent 5a2eb383
......@@ -81,6 +81,8 @@ ki18n_wrap_ui(_kleopatra_uiserver_SRCS crypto/gui/signingcertificateselectionwid
set(_kleopatra_SRCS
${_kleopatra_extra_SRCS}
accessibility/accessiblelink.cpp
accessibility/accessiblelink_p.h
accessibility/accessiblerichtextlabel.cpp
accessibility/accessiblerichtextlabel_p.h
accessibility/accessiblewidgetfactory.cpp
......@@ -363,6 +365,7 @@ set(_kleopatra_SRCS
dialogs/weboftrustdialog.h
dialogs/weboftrustwidget.cpp
dialogs/weboftrustwidget.h
interfaces/anchorprovider.h
interfaces/focusfirstchild.h
newcertificatewizard/advancedsettingsdialog.cpp
newcertificatewizard/advancedsettingsdialog_p.h
......
/*
accessibility/accessiblelink.cpp
This file is part of Kleopatra, the KDE keymanager
SPDX-FileCopyrightText: 2022 g10 Code GmbH
SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include <config-kleopatra.h>
#include "accessiblelink_p.h"
#include <interfaces/anchorprovider.h>
#include <QWidget>
using namespace Kleo;
AccessibleLink::AccessibleLink(QWidget *label, int index)
: mLabel{label}
, mIndex{index}
{
}
AccessibleLink::~AccessibleLink() = default;
bool AccessibleLink::isValid() const
{
return mLabel;
}
QObject *AccessibleLink::object() const
{
return nullptr;
}
QWindow *AccessibleLink::window() const
{
if (auto p = parent()) {
return p->window();
}
return nullptr;
}
QAccessibleInterface *AccessibleLink::childAt(int, int) const
{
return nullptr;
}
QAccessibleInterface *AccessibleLink::parent() const
{
return QAccessible::queryAccessibleInterface(mLabel);
}
QAccessibleInterface *AccessibleLink::child(int) const
{
return nullptr;
}
int AccessibleLink::childCount() const
{
return 0;
}
int AccessibleLink::indexOfChild(const QAccessibleInterface *) const
{
return -1;
}
QString AccessibleLink::text(QAccessible::Text t) const
{
QString str;
switch (t) {
case QAccessible::Name:
if (auto ap = anchorProvider()) {
str = ap->anchorText(mIndex);
}
break;
default:
break;
}
return str;
}
void AccessibleLink::setText(QAccessible::Text /*t*/, const QString & /*text */)
{
}
QRect AccessibleLink::rect() const
{
if (auto p = parent()) {
return p->rect();
}
return {};
}
QAccessible::Role AccessibleLink::role() const
{
return QAccessible::Link;
}
QAccessible::State AccessibleLink::state() const
{
QAccessible::State s;
if (auto p = parent()) {
s = p->state();
}
if (auto ap = anchorProvider()) {
s.focused = ap->selectedAnchor() == mIndex;
}
return s;
}
int AccessibleLink::index() const
{
return mIndex;
}
AnchorProvider *AccessibleLink::anchorProvider() const
{
return dynamic_cast<AnchorProvider *>(mLabel.data());
}
/*
accessibility/accessiblelink_p.h
This file is part of Kleopatra, the KDE keymanager
SPDX-FileCopyrightText: 2022 g10 Code GmbH
SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include <QAccessible>
#include <QPointer>
class QWidget;
namespace Kleo
{
class AnchorProvider;
class AccessibleLink: public QAccessibleInterface
{
public:
AccessibleLink(QWidget *label, int index);
~AccessibleLink() override;
bool isValid() const override;
QObject *object() const override;
QWindow *window() const override;
QAccessibleInterface *childAt(int x, int y) const override;
QAccessibleInterface *parent() const override;
QAccessibleInterface *child(int index) const override;
int childCount() const override;
int indexOfChild(const QAccessibleInterface * child) const override;
QString text(QAccessible::Text t) const override;
void setText(QAccessible::Text t, const QString & text) override;
QRect rect() const override;
QAccessible::Role role() const override;
QAccessible::State state() const override;
int index() const;
private:
AnchorProvider *anchorProvider() const;
QPointer<QWidget> mLabel;
int mIndex;
};
}
......@@ -12,21 +12,35 @@
#include "accessiblerichtextlabel_p.h"
#include "accessiblelink_p.h"
#include <interfaces/anchorprovider.h>
#include <QLabel>
#include <QTextDocument>
using namespace Kleo;
struct AccessibleRichTextLabel::ChildData {
QAccessible::Id id = 0;
};
AccessibleRichTextLabel::AccessibleRichTextLabel(QWidget *w)
: QAccessibleWidget{w, QAccessible::StaticText}
{
Q_ASSERT(qobject_cast<QLabel *>(w));
}
AccessibleRichTextLabel::~AccessibleRichTextLabel()
{
clearChildCache();
}
void *AccessibleRichTextLabel::interface_cast(QAccessible::InterfaceType t)
{
if (t == QAccessible::TextInterface)
if (t == QAccessible::TextInterface) {
return static_cast<QAccessibleTextInterface *>(this);
}
return QAccessibleWidget::interface_cast(t);
}
......@@ -51,11 +65,55 @@ QString AccessibleRichTextLabel::text(QAccessible::Text t) const
default:
break;
}
if (str.isEmpty())
if (str.isEmpty()) {
str = QAccessibleWidget::text(t);
}
return str;
}
QAccessibleInterface *AccessibleRichTextLabel::focusChild() const
{
if (const auto *const ap = anchorProvider()) {
const int childIndex = ap->selectedAnchor();
if (childIndex >= 0) {
return child(childIndex);
}
}
return QAccessibleWidget::focusChild();
}
QAccessibleInterface *AccessibleRichTextLabel::child(int index) const
{
const auto *const ap = anchorProvider();
if (ap && index >= 0 && index < ap->numberOfAnchors()) {
auto &childData = childCache()[index];
if (childData.id != 0) {
return QAccessible::accessibleInterface(childData.id);
}
QAccessibleInterface *iface = new AccessibleLink{widget(), index};
childData.id = QAccessible::registerAccessibleInterface(iface);
return iface;
}
return nullptr;
}
int AccessibleRichTextLabel::childCount() const
{
if (const auto *const ap = anchorProvider()) {
return ap->numberOfAnchors();
}
return 0;
}
int AccessibleRichTextLabel::indexOfChild(const QAccessibleInterface *child) const
{
if ((child->role() == QAccessible::Link) && (child->parent() == this)) {
return static_cast<const AccessibleLink *>(child)->index();
}
return -1;
}
void AccessibleRichTextLabel::selection(int selectionIndex, int *startOffset, int *endOffset) const
{
*startOffset = *endOffset = 0;
......@@ -150,6 +208,11 @@ QLabel *AccessibleRichTextLabel::label() const
return qobject_cast<QLabel*>(object());
}
AnchorProvider *AccessibleRichTextLabel::anchorProvider() const
{
return dynamic_cast<AnchorProvider *>(object());
}
QString AccessibleRichTextLabel::displayText() const
{
// calculate an approximation of the displayed text without using private
......@@ -163,3 +226,27 @@ QString AccessibleRichTextLabel::displayText() const
}
return str;
}
std::vector<AccessibleRichTextLabel::ChildData> &AccessibleRichTextLabel::childCache() const
{
const auto *const ap = anchorProvider();
if (!ap || static_cast<int>(mChildCache.size()) == ap->numberOfAnchors()) {
return mChildCache;
}
clearChildCache();
// fill the cache with default-initialized child data
mChildCache.resize(ap->numberOfAnchors());
return mChildCache;
}
void AccessibleRichTextLabel::clearChildCache() const
{
std::for_each(std::cbegin(mChildCache), std::cend(mChildCache), [](const auto &child) {
if (child.id != 0) {
QAccessible::deleteAccessibleInterface(child.id);
}
});
mChildCache.clear();
}
......@@ -16,16 +16,26 @@ class QLabel;
namespace Kleo
{
class AnchorProvider;
class AccessibleRichTextLabel : public QAccessibleWidget, public QAccessibleTextInterface
{
public:
explicit AccessibleRichTextLabel(QWidget *o);
~AccessibleRichTextLabel() override;
void *interface_cast(QAccessible::InterfaceType t) override;
QAccessible::State state() const override;
QString text(QAccessible::Text t) const override;
// relations
QAccessibleInterface *focusChild() const override;
// navigation, hierarchy
QAccessibleInterface *child(int index) const override;
int childCount() const override;
int indexOfChild(const QAccessibleInterface *child) const override;
// QAccessibleTextInterface
// selection
void selection(int selectionIndex, int *startOffset, int *endOffset) const override;
......@@ -50,8 +60,16 @@ public:
QString attributes(int offset, int *startOffset, int *endOffset) const override;
private:
struct ChildData;
QLabel *label() const;
AnchorProvider *anchorProvider() const;
QString displayText() const;
std::vector<ChildData> &childCache() const;
void clearChildCache() const;
mutable std::vector<ChildData> mChildCache;
};
}
/* -*- mode: c++; c-basic-offset:4 -*-
interfaces/anchorprovider.h
This file is part of Kleopatra, the KDE keymanager
SPDX-FileCopyrightText: 2022 g10 Code GmbH
SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include <Qt>
namespace Kleo
{
class AnchorProvider
{
public:
virtual ~AnchorProvider() = default;
virtual int numberOfAnchors() const = 0;
virtual QString anchorText(int index) const = 0;
virtual QString anchorHref(int index) const = 0;
virtual int selectedAnchor() const = 0;
};
}
......@@ -14,9 +14,22 @@
#include "utils/accessibility.h"
#include <QAccessible>
#include <QTextBlock>
#include <QTextCursor>
#include <QTextDocument>
using namespace Kleo;
namespace
{
struct AnchorData {
int start;
int end;
QString text;
QString href;
};
}
class HtmlLabel::Private
{
HtmlLabel *q;
......@@ -25,6 +38,12 @@ public:
void updateText(const QString &newText = {});
std::vector<AnchorData> &anchors();
int anchorIndex(int start);
void invalidateAnchorCache();
bool mAnchorsValid = false;
std::vector<AnchorData> mAnchors;
QColor linkColor;
};
......@@ -42,6 +61,78 @@ void HtmlLabel::Private::updateText(const QString &newText)
} else {
q->setText(styleTag + newText);
}
invalidateAnchorCache();
}
std::vector<AnchorData> &HtmlLabel::Private::anchors()
{
if (mAnchorsValid) {
return mAnchors;
}
mAnchors.clear();
QTextDocument doc;
doc.setHtml(q->text());
// taken from QWidgetTextControl::setFocusToNextOrPreviousAnchor and QWidgetTextControl::findNextPrevAnchor
for (QTextBlock block = doc.begin(); block.isValid(); block = block.next()) {
QTextBlock::Iterator it = block.begin();
while (!it.atEnd()) {
const QTextFragment fragment = it.fragment();
const QTextCharFormat fmt = fragment.charFormat();
if (fmt.isAnchor() && fmt.hasProperty(QTextFormat::AnchorHref)) {
const int anchorStart = fragment.position();
const QString anchorHref = fmt.anchorHref();
int anchorEnd = -1;
// find next non-anchor fragment
for (; !it.atEnd(); ++it) {
const QTextFragment fragment = it.fragment();
const QTextCharFormat fmt = fragment.charFormat();
if (!fmt.isAnchor() || fmt.anchorHref() != anchorHref) {
anchorEnd = fragment.position();
break;
}
}
if (anchorEnd == -1) {
anchorEnd = block.position() + block.length() - 1;
}
QTextCursor cursor{&doc};
cursor.setPosition(anchorStart);
cursor.setPosition(anchorEnd, QTextCursor::KeepAnchor);
QString anchorText = cursor.selectedText();
mAnchors.push_back({anchorStart, anchorEnd, anchorText, anchorHref});
} else {
++it;
}
}
}
mAnchorsValid = true;
return mAnchors;
}
int HtmlLabel::Private::anchorIndex(int start)
{
anchors(); // ensure that the anchor cache is valid
auto it = std::find_if(std::cbegin(mAnchors), std::cend(mAnchors), [start](const auto &anchor) {
return anchor.start == start;
});
if (it != std::cend(mAnchors)) {
return std::distance(std::cbegin(mAnchors), it);
}
return -1;
}
void HtmlLabel::Private::invalidateAnchorCache()
{
mAnchorsValid = false;
}
HtmlLabel::HtmlLabel(QWidget *parent)
......@@ -64,6 +155,7 @@ void HtmlLabel::setHtml(const QString &html)
{
if (html.isEmpty()) {
clear();
d->invalidateAnchorCache();
return;
}
d->updateText(html);
......@@ -75,6 +167,32 @@ void HtmlLabel::setLinkColor(const QColor &color)
d->updateText();
}
int HtmlLabel::numberOfAnchors() const
{
return d->anchors().size();
}
QString HtmlLabel::anchorText(int index) const
{
if (index >= 0 && index < numberOfAnchors()) {
return d->anchors()[index].text;
}
return {};
}
QString HtmlLabel::anchorHref(int index) const
{
if (index >= 0 && index < numberOfAnchors()) {
return d->anchors()[index].href;
}
return {};
}
int HtmlLabel::selectedAnchor() const
{
return d->anchorIndex(selectionStart());
}
void HtmlLabel::focusInEvent(QFocusEvent *ev)
{
QLabel::focusInEvent(ev);
......@@ -93,9 +211,13 @@ void HtmlLabel::focusInEvent(QFocusEvent *ev)
bool HtmlLabel::focusNextPrevChild(bool next)
{
const bool result = QLabel::focusNextPrevChild(next);
if (hasFocus() && hasSelectedText()) {
QAccessibleTextSelectionEvent ev(this, selectionStart(), selectionStart() + selectedText().size());
QAccessible::updateAccessibility(&ev);
if (hasFocus() && QAccessible::isActive()) {
const int anchorIndex = selectedAnchor();
if (anchorIndex >= 0) {
QAccessibleEvent focusEvent(this, QAccessible::Focus);
focusEvent.setChild(anchorIndex);
QAccessible::updateAccessibility(&focusEvent);
}
}
return result;
}
......@@ -10,6 +10,8 @@
#pragma once
#include <interfaces/anchorprovider.h>
#include <QLabel>
#include <memory>
......@@ -17,7 +19,7 @@
namespace Kleo
{
class HtmlLabel : public QLabel
class HtmlLabel : public QLabel, public AnchorProvider
{
Q_OBJECT
public:
......@@ -29,6 +31,12 @@ public:
void setLinkColor(const QColor &color);
// AnchorProvider
int numberOfAnchors() const override;
QString anchorText(int index) const override;
QString anchorHref(int index) const override;
int selectedAnchor() const override;
protected:
void focusInEvent(QFocusEvent *ev) override;
bool focusNextPrevChild(bool next) override;
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment