Make `SqueezeLabel` [more] accessible (#6520)

Expose label text as accessible value instead of accessible name, and
get accessible name from buddy label as any proper input widget does.
Don't expose label tooltip as accessible description unless it's
different from its text (which isn't the case when displayed text is
truncated). Notify on label text and selection changes.

Switch to `SqueezeLabel` for values in statistics dialog which has
similar layout to information tab of torrent properties dialog.
This commit is contained in:
Mike Gelfand 2024-01-12 03:35:40 +00:00 committed by GitHub
parent 35847b3e75
commit 8e7fc76930
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 303 additions and 9 deletions

View File

@ -0,0 +1,144 @@
// This file Copyright © Mnemosyne LLC.
// It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only),
// or any future license endorsed by Mnemosyne LLC.
// License text can be found in the licenses/ folder.
#include <qtguiglobal.h>
#if QT_CONFIG(accessibility)
#include <QMetaProperty>
#include "AccessibleSqueezeLabel.h"
#include "SqueezeLabel.h"
AccessibleSqueezeLabel::AccessibleSqueezeLabel(QWidget* widget)
: QAccessibleWidget(widget, QAccessible::EditableText)
{
}
QString AccessibleSqueezeLabel::text(QAccessible::Text kind) const
{
switch (kind)
{
case QAccessible::Value:
return label()->text();
case QAccessible::Description:
return !label()->accessibleDescription().isEmpty() || label()->toolTip() != label()->text() ?
QAccessibleWidget::text(kind) :
QString{};
default:
return QAccessibleWidget::text(kind);
}
}
QAccessible::State AccessibleSqueezeLabel::state() const
{
auto result = QAccessibleWidget::state();
result.readOnly = true;
result.selectableText = true;
return result;
}
void* AccessibleSqueezeLabel::interface_cast(QAccessible::InterfaceType ifaceType)
{
if (ifaceType == QAccessible::TextInterface)
{
return static_cast<QAccessibleTextInterface*>(this);
}
return QAccessibleWidget::interface_cast(ifaceType);
}
void AccessibleSqueezeLabel::selection(int selectionIndex, int* startOffset, int* endOffset) const
{
if (selectionIndex != 0)
{
*startOffset = 0;
*endOffset = 0;
return;
}
*startOffset = label()->selectionStart();
*endOffset = *startOffset + label()->selectedText().size();
}
int AccessibleSqueezeLabel::selectionCount() const
{
return label()->hasSelectedText() ? 1 : 0;
}
void AccessibleSqueezeLabel::addSelection(int startOffset, int endOffset)
{
setSelection(0, startOffset, endOffset);
}
void AccessibleSqueezeLabel::removeSelection(int selectionIndex)
{
setSelection(selectionIndex, 0, 0);
}
void AccessibleSqueezeLabel::setSelection(int selectionIndex, int startOffset, int endOffset)
{
if (selectionIndex != 0 || startOffset > endOffset)
{
return;
}
label()->setSelection(startOffset, endOffset - startOffset);
}
int AccessibleSqueezeLabel::cursorPosition() const
{
// NOTE: Due to QLabel implementation specifics, this will return -1 unless some part of text is selected :(
return label()->selectionStart();
}
void AccessibleSqueezeLabel::setCursorPosition(int position)
{
setSelection(0, position, position);
}
QString AccessibleSqueezeLabel::text(int startOffset, int endOffset) const
{
return startOffset > endOffset ? QString{} : label()->text().mid(startOffset, endOffset - startOffset);
}
int AccessibleSqueezeLabel::characterCount() const
{
return label()->text().size();
}
QRect AccessibleSqueezeLabel::characterRect(int /*offset*/) const
{
// NOTE: Can't be easily implemented as needed info is internal to QLabel :(
return {};
}
int AccessibleSqueezeLabel::offsetAtPoint(QPoint const& /*point*/) const
{
// NOTE: Can't be easily implemented as needed info is internal to QLabel :(
return -1;
}
void AccessibleSqueezeLabel::scrollToSubstring(int startIndex, int endIndex)
{
setCursorPosition(endIndex);
setCursorPosition(startIndex);
}
QString AccessibleSqueezeLabel::attributes(int offset, int* startOffset, int* endOffset) const
{
*startOffset = offset;
*endOffset = offset;
return {};
}
SqueezeLabel* AccessibleSqueezeLabel::label() const
{
return qobject_cast<SqueezeLabel*>(object());
}
#endif // QT_CONFIG(accessibility)

View File

@ -0,0 +1,47 @@
// This file Copyright © Mnemosyne LLC.
// It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only),
// or any future license endorsed by Mnemosyne LLC.
// License text can be found in the licenses/ folder.
#pragma once
#include <qtguiglobal.h>
#if QT_CONFIG(accessibility)
#include <QAccessibleWidget>
class SqueezeLabel;
class AccessibleSqueezeLabel
: public QAccessibleWidget
, public QAccessibleTextInterface
{
public:
explicit AccessibleSqueezeLabel(QWidget* widget);
// QAccessibleWidget
[[nodiscard]] QString text(QAccessible::Text kind) const override;
[[nodiscard]] QAccessible::State state() const override;
void* interface_cast(QAccessible::InterfaceType ifaceType) override;
// QAccessibleTextInterface
void selection(int selectionIndex, int* startOffset, int* endOffset) const override;
[[nodiscard]] int selectionCount() const override;
void addSelection(int startOffset, int endOffset) override;
void removeSelection(int selectionIndex) override;
void setSelection(int selectionIndex, int startOffset, int endOffset) override;
[[nodiscard]] int cursorPosition() const override;
void setCursorPosition(int position) override;
[[nodiscard]] QString text(int startOffset, int endOffset) const override;
[[nodiscard]] int characterCount() const override;
[[nodiscard]] QRect characterRect(int offset) const override;
[[nodiscard]] int offsetAtPoint(QPoint const& point) const override;
void scrollToSubstring(int startIndex, int endIndex) override;
QString attributes(int offset, int* startOffset, int* endOffset) const override;
private:
[[nodiscard]] SqueezeLabel* label() const;
};
#endif // QT_CONFIG(accessibility)

View File

@ -25,6 +25,10 @@
#include <QDBusReply>
#endif
#if QT_CONFIG(accessibility)
#include <QAccessible>
#endif
#include <libtransmission/transmission.h>
#include <libtransmission/tr-getopt.h>
@ -32,6 +36,7 @@
#include <libtransmission/values.h>
#include <libtransmission/version.h>
#include "AccessibleSqueezeLabel.h"
#include "AddData.h"
#include "InteropHelper.h"
#include "MainWindow.h"
@ -90,6 +95,25 @@ bool loadTranslation(QTranslator& translator, QString const& name, QLocale const
return QIcon{ QStringLiteral(":/icons/transmission.svg") };
}
#if QT_CONFIG(accessibility)
QAccessibleInterface* accessibleFactory(QString const& className, QObject* object)
{
auto* widget = qobject_cast<QWidget*>(object);
if (widget != nullptr)
{
if (className == QStringLiteral("SqueezeLabel"))
{
return new AccessibleSqueezeLabel(widget);
}
}
return nullptr;
}
#endif // QT_CONFIG(accessibility)
} // namespace
Application::Application(int& argc, char** argv)
@ -273,6 +297,10 @@ Application::Application(int& argc, char** argv)
minimized = false;
}
#if QT_CONFIG(accessibility)
QAccessible::installFactory(&accessibleFactory);
#endif
session_ = std::make_unique<Session>(config_dir, *prefs_);
model_ = std::make_unique<TorrentModel>(*prefs_);
window_ = std::make_unique<MainWindow>(*session_, *prefs_, *model_, minimized);

View File

@ -9,6 +9,8 @@ target_sources(${TR_NAME}-qt
PRIVATE
AboutDialog.cc
AboutDialog.h
AccessibleSqueezeLabel.cc
AccessibleSqueezeLabel.h
AddData.cc
AddData.h
Application.cc

View File

@ -42,6 +42,11 @@
#include <QPainter>
#include <QStyle>
#include <QStyleOption>
#include <QTimer>
#if QT_CONFIG(accessibility)
#include <QAccessible>
#endif
#include "SqueezeLabel.h"
@ -57,6 +62,11 @@ SqueezeLabel::SqueezeLabel(QWidget* parent)
void SqueezeLabel::paintEvent(QPaintEvent* paint_event)
{
#if QT_CONFIG(accessibility)
// NOTE: QLabel doesn't notify on text/cursor changes so we're checking for it when repaint is requested
updateAccessibilityIfNeeded();
#endif
if (hasFocus() && (textInteractionFlags() & (Qt::TextSelectableByKeyboard | Qt::TextSelectableByMouse)) != 0)
{
return QLabel::paintEvent(paint_event);
@ -74,3 +84,48 @@ void SqueezeLabel::paintEvent(QPaintEvent* paint_event)
setToolTip(full_text != elided_text ? full_text : QString{});
#endif
}
#if QT_CONFIG(accessibility)
void SqueezeLabel::updateAccessibilityIfNeeded()
{
// NOTE: Dispatching events asynchronously to avoid blocking the painting
if (auto const new_text = text(); new_text != old_text_)
{
if (QAccessible::isActive())
{
QTimer::singleShot(
0,
this,
[this, old_text = old_text_, new_text]()
{
QAccessibleTextUpdateEvent event(this, 0, old_text, new_text);
event.setCursorPosition(selectionStart());
QAccessible::updateAccessibility(&event);
});
}
old_text_ = new_text;
}
// NOTE: Due to QLabel implementation specifics, this block will never be entered :(
if (auto const new_position = selectionStart(); new_position != old_position_ && !hasSelectedText())
{
if (QAccessible::isActive())
{
QTimer::singleShot(
0,
this,
[this, new_position]()
{
QAccessibleTextCursorEvent event(this, new_position);
QAccessible::updateAccessibility(&event);
});
}
old_position_ = new_position;
}
}
#endif // QT_CONFIG(accessibility)

View File

@ -57,4 +57,15 @@ public:
protected:
// QWidget
void paintEvent(QPaintEvent* paint_event) override;
private:
#if QT_CONFIG(accessibility)
void updateAccessibilityIfNeeded();
#endif
private:
#if QT_CONFIG(accessibility)
QString old_text_;
int old_position_ = -1;
#endif
};

View File

@ -34,7 +34,7 @@
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="currentUploadedValueLabel">
<widget class="SqueezeLabel" name="currentUploadedValueLabel">
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
</property>
@ -60,7 +60,7 @@
</widget>
</item>
<item row="1" column="1">
<widget class="QLabel" name="currentDownloadedValueLabel">
<widget class="SqueezeLabel" name="currentDownloadedValueLabel">
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
</property>
@ -86,7 +86,7 @@
</widget>
</item>
<item row="2" column="1">
<widget class="QLabel" name="currentRatioValueLabel">
<widget class="SqueezeLabel" name="currentRatioValueLabel">
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
</property>
@ -112,7 +112,7 @@
</widget>
</item>
<item row="3" column="1">
<widget class="QLabel" name="currentDurationValueLabel">
<widget class="SqueezeLabel" name="currentDurationValueLabel">
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
</property>
@ -137,7 +137,7 @@
</property>
<layout class="QGridLayout" name="totalSectionLayout" columnstretch="0,1">
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="startCountLabel">
<widget class="SqueezeLabel" name="startCountLabel">
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
</property>
@ -163,7 +163,7 @@
</widget>
</item>
<item row="1" column="1">
<widget class="QLabel" name="totalUploadedValueLabel">
<widget class="SqueezeLabel" name="totalUploadedValueLabel">
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
</property>
@ -189,7 +189,7 @@
</widget>
</item>
<item row="2" column="1">
<widget class="QLabel" name="totalDownloadedValueLabel">
<widget class="SqueezeLabel" name="totalDownloadedValueLabel">
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
</property>
@ -215,7 +215,7 @@
</widget>
</item>
<item row="3" column="1">
<widget class="QLabel" name="totalRatioValueLabel">
<widget class="SqueezeLabel" name="totalRatioValueLabel">
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
</property>
@ -241,7 +241,7 @@
</widget>
</item>
<item row="4" column="1">
<widget class="QLabel" name="totalDurationValueLabel">
<widget class="SqueezeLabel" name="totalDurationValueLabel">
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
</property>
@ -271,6 +271,13 @@
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>SqueezeLabel</class>
<extends>QLabel</extends>
<header>SqueezeLabel.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections>
<connection>