Skip to content

Commit 6cffcbf

Browse files
committed
feat(accessibility): add screen reader support for peer lists
1 parent ca3eb7d commit 6cffcbf

6 files changed

Lines changed: 252 additions & 1 deletion

File tree

Telegram/SourceFiles/boxes/peer_list_box.cpp

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ For license and copyright information please follow this link:
3838

3939
#include <xxhash.h> // XXH64.
4040
#include <QtWidgets/QApplication>
41+
#include "base/screen_reader_state.h"
4142

4243
[[nodiscard]] PeerListRowId UniqueRowIdFromString(const QString &d) {
4344
return XXH64(d.data(), d.size() * sizeof(ushort), 0);
@@ -452,6 +453,10 @@ std::unique_ptr<PeerListRow> PeerListController::createSearchRow(
452453
return nullptr;
453454
}
454455

456+
QString PeerListController::accessibilityName() const {
457+
return u"Peer list"_q;
458+
}
459+
455460
std::unique_ptr<PeerListState> PeerListController::saveState() const {
456461
return delegate()->peerListSaveState();
457462
}
@@ -600,6 +605,10 @@ PeerListRow::PeerListRow(PeerListRowId id)
600605

601606
PeerListRow::~PeerListRow() = default;
602607

608+
bool PeerListRow::checkable() const {
609+
return _checkbox != nullptr;
610+
}
611+
603612
bool PeerListRow::checked() const {
604613
return _checkbox && _checkbox->checked();
605614
}
@@ -1025,6 +1034,11 @@ PeerListContent::PeerListContent(
10251034
update();
10261035
}, lifetime());
10271036

1037+
base::ScreenReaderState::Instance()->activeValue(
1038+
) | rpl::on_next([this](bool active) {
1039+
setFocusPolicy(active ? Qt::TabFocus : Qt::NoFocus);
1040+
}, lifetime());
1041+
10281042
using UpdateFlag = Data::PeerUpdate::Flag;
10291043
_controller->session().changes().peerUpdates(
10301044
UpdateFlag::Name | UpdateFlag::Photo | UpdateFlag::EmojiStatus
@@ -1043,6 +1057,13 @@ PeerListContent::PeerListContent(
10431057
}, lifetime());
10441058

10451059
_repaintByStatus.setCallback([this] { update(); });
1060+
1061+
_selectedIndex.changes(
1062+
) | rpl::on_next([=](int index) {
1063+
if (index >= 0) {
1064+
accessibilityChildFocused(index);
1065+
}
1066+
}, lifetime());
10461067
}
10471068

10481069
void PeerListContent::setMode(Mode mode) {
@@ -1114,6 +1135,12 @@ void PeerListContent::changeCheckState(
11141135
row->setChecked(checked, _st.item.checkbox, animated, [=] {
11151136
updateRow(row);
11161137
});
1138+
if (const auto index = findRowIndex(row, RowIndex()); index.value >= 0) {
1139+
accessibilityChildStateChanged(index.value, {
1140+
.checked = true,
1141+
.selected = true,
1142+
});
1143+
}
11171144
}
11181145

11191146
void PeerListContent::setRowHidden(not_null<PeerListRow*> row, bool hidden) {
@@ -1603,6 +1630,50 @@ int PeerListContent::resizeGetHeight(int newWidth) {
16031630
return belowTop + _belowHeight;
16041631
}
16051632

1633+
void PeerListContent::keyPressEvent(QKeyEvent *e) {
1634+
if (e->key() == Qt::Key_Up) {
1635+
selectSkip(-1);
1636+
e->accept();
1637+
} else if (e->key() == Qt::Key_Down) {
1638+
selectSkip(1);
1639+
e->accept();
1640+
} else if (e->key() == Qt::Key_PageDown) {
1641+
selectSkipPage(height(), 1);
1642+
e->accept();
1643+
} else if (e->key() == Qt::Key_PageUp) {
1644+
selectSkipPage(height(), -1);
1645+
e->accept();
1646+
} else if (e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter || e->key() == Qt::Key_Space) {
1647+
submitted();
1648+
e->accept();
1649+
} else {
1650+
e->ignore();
1651+
}
1652+
}
1653+
1654+
void PeerListContent::focusInEvent(QFocusEvent *e) {
1655+
RpWidget::focusInEvent(e);
1656+
if (!base::ScreenReaderState::Instance()->active()) {
1657+
return;
1658+
}
1659+
InvokeQueued(this, [=] {
1660+
if (!hasFocus()) {
1661+
return;
1662+
}
1663+
const auto count = shownRowsCount();
1664+
if (count <= 0) {
1665+
return;
1666+
}
1667+
const auto current = _selectedIndex.current();
1668+
if (current >= 0 && current < count) {
1669+
accessibilityChildFocused(current);
1670+
return;
1671+
}
1672+
setSelected({ RowIndex(0), 0 });
1673+
accessibilityChildFocused(0);
1674+
});
1675+
}
1676+
16061677
void PeerListContent::enterEventHook(QEnterEvent *e) {
16071678
setMouseTracking(true);
16081679
}
@@ -2443,6 +2514,19 @@ PeerListRow *PeerListContent::getRow(RowIndex index) {
24432514
return nullptr;
24442515
}
24452516

2517+
const PeerListRow *PeerListContent::getRow(RowIndex index) const {
2518+
if (index.value >= 0) {
2519+
if (showingSearch()) {
2520+
if (index.value < _filterResults.size()) {
2521+
return _filterResults[index.value];
2522+
}
2523+
} else if (index.value < _rows.size()) {
2524+
return _rows[index.value].get();
2525+
}
2526+
}
2527+
return nullptr;
2528+
}
2529+
24462530
PeerListContent::RowIndex PeerListContent::findRowIndex(
24472531
not_null<PeerListRow*> row,
24482532
RowIndex hint) {
@@ -2491,3 +2575,97 @@ void PeerListContentDelegate::peerListShowRowMenu(
24912575
Fn<void(not_null<Ui::PopupMenu *>)> destroyed) {
24922576
_content->showRowMenu(row, highlightRow, std::move(destroyed));
24932577
}
2578+
2579+
QAccessible::Role PeerListContent::accessibilityRole() {
2580+
return QAccessible::List;
2581+
}
2582+
2583+
QString PeerListContent::accessibilityName() {
2584+
const auto fromUi = accessibleName();
2585+
return fromUi.isEmpty() ? _controller->accessibilityName() : fromUi;
2586+
}
2587+
2588+
Ui::AccessibilityState PeerListContent::accessibilityState() const {
2589+
return {};
2590+
}
2591+
2592+
int PeerListContent::accessibilityChildCount() const {
2593+
return shownRowsCount();
2594+
}
2595+
2596+
QAccessible::Role PeerListContent::accessibilityChildRole() const {
2597+
return QAccessible::ListItem;
2598+
}
2599+
2600+
QString PeerListContent::accessibilityChildName(int index) const {
2601+
if (index >= 0 && index < shownRowsCount()) {
2602+
const auto row = getRow(RowIndex(index));
2603+
if (!row) {
2604+
return QString();
2605+
}
2606+
if (!row->name().isEmpty()) {
2607+
return row->name().toString();
2608+
}
2609+
return const_cast<PeerListRow*>(row)->generateName();
2610+
}
2611+
return QString();
2612+
}
2613+
2614+
QAccessible::State PeerListContent::accessibilityChildState(int index) const {
2615+
auto state = QAccessible::State();
2616+
if (index >= 0 && index < shownRowsCount()) {
2617+
if (base::ScreenReaderState::Instance()->active()) {
2618+
state.focusable = true;
2619+
}
2620+
state.selectable = true;
2621+
2622+
const auto row = getRow(RowIndex(index));
2623+
if (row->checkable() && !row->special()) {
2624+
state.checkable = true;
2625+
state.checked = row->checked();
2626+
if (row->checked()) {
2627+
state.selected = true;
2628+
}
2629+
}
2630+
const auto top = getRowTop(RowIndex(index));
2631+
const auto bottom = top + _rowHeight;
2632+
if (bottom <= _visibleTop || top >= _visibleBottom) {
2633+
state.invisible = true;
2634+
}
2635+
if (index == _selectedIndex.current()) {
2636+
state.focused = true;
2637+
state.invisible = false;
2638+
}
2639+
}
2640+
return state;
2641+
}
2642+
2643+
QRect PeerListContent::accessibilityChildRect(int index) const {
2644+
if (index >= 0 && index < shownRowsCount()) {
2645+
const auto top = getRowTop(RowIndex(index));
2646+
return QRect(0, top, width(), _st.item.height);
2647+
}
2648+
return QRect();
2649+
}
2650+
2651+
int PeerListContent::accessibilityChildColumnCount(int row) const {
2652+
return 1;
2653+
}
2654+
2655+
QAccessible::Role PeerListContent::accessibilityChildSubItemRole() const {
2656+
return QAccessible::Cell;
2657+
}
2658+
2659+
QString PeerListContent::accessibilityChildSubItemName(int row, int column) const {
2660+
if (column == 0) {
2661+
return _controller->accessibilityName();
2662+
}
2663+
return QString();
2664+
}
2665+
2666+
QString PeerListContent::accessibilityChildSubItemValue(int row, int column) const {
2667+
if (column == 0) {
2668+
return accessibilityChildName(row);
2669+
}
2670+
return QString();
2671+
}

Telegram/SourceFiles/boxes/peer_list_box.h

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ class PeerListRow {
7575
// not by the row itself, so there is no setChecked() method.
7676
// We can query the checked state from row, but before it is
7777
// added to the box it is always false.
78+
[[nodiscard]] bool checkable() const;
7879
[[nodiscard]] bool checked() const;
7980

8081
[[nodiscard]] bool special() const {
@@ -596,6 +597,8 @@ class PeerListController : public PeerListSearchDelegate {
596597
return _lifetime;
597598
}
598599

600+
[[nodiscard]] virtual QString accessibilityName() const;
601+
599602
virtual ~PeerListController() = default;
600603

601604
protected:
@@ -742,13 +745,28 @@ class PeerListContent : public Ui::RpWidget {
742745

743746
~PeerListContent();
744747

748+
QAccessible::Role accessibilityRole() override;
749+
QString accessibilityName() override;
750+
Ui::AccessibilityState accessibilityState() const override;
751+
int accessibilityChildCount() const override;
752+
QAccessible::Role accessibilityChildRole() const override;
753+
QString accessibilityChildName(int index) const override;
754+
QAccessible::State accessibilityChildState(int index) const override;
755+
QRect accessibilityChildRect(int index) const override;
756+
int accessibilityChildColumnCount(int row) const override;
757+
QAccessible::Role accessibilityChildSubItemRole() const override;
758+
QString accessibilityChildSubItemName(int row, int column) const override;
759+
QString accessibilityChildSubItemValue(int row, int column) const override;
760+
745761
protected:
746762
int resizeGetHeight(int newWidth) override;
747763
void visibleTopBottomUpdated(
748764
int visibleTop,
749765
int visibleBottom) override;
750766

751767
void paintEvent(QPaintEvent *e) override;
768+
void keyPressEvent(QKeyEvent *e) override;
769+
void focusInEvent(QFocusEvent *e) override;
752770
void enterEventHook(QEnterEvent *e) override;
753771
void leaveEventHook(QEvent *e) override;
754772
void mouseMoveEvent(QMouseEvent *e) override;
@@ -821,6 +839,7 @@ class PeerListContent : public Ui::RpWidget {
821839
void updateRow(RowIndex row);
822840
int getRowTop(RowIndex row) const;
823841
PeerListRow *getRow(RowIndex element);
842+
const PeerListRow *getRow(RowIndex element) const;
824843
RowIndex findRowIndex(
825844
not_null<PeerListRow*> row,
826845
RowIndex hint = RowIndex());
@@ -1127,7 +1146,13 @@ class PeerListBox
11271146
[[nodiscard]] rpl::producer<> noSearchSubmits() const;
11281147

11291148
void peerListSetTitle(rpl::producer<QString> title) override {
1130-
setTitle(std::move(title));
1149+
auto shared = rpl::duplicate(title);
1150+
setTitle(std::move(shared));
1151+
if (const auto c = content()) {
1152+
std::move(title) | rpl::on_next([=](const QString &t) {
1153+
c->setAccessibleName(t);
1154+
}, c->lifetime());
1155+
}
11311156
}
11321157
void peerListSetAdditionalTitle(rpl::producer<QString> title) override {
11331158
setAdditionalTitle(std::move(title));

Telegram/SourceFiles/boxes/peer_list_controllers.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,10 @@ Main::Session &ContactsBoxController::session() const {
659659
return *_session;
660660
}
661661

662+
QString ContactsBoxController::accessibilityName() const {
663+
return tr::lng_contacts_header(tr::now);
664+
}
665+
662666
void ContactsBoxController::prepare() {
663667
setSearchNoResultsText(tr::lng_blocked_list_not_found(tr::now));
664668
delegate()->peerListSetSearchMode(PeerListSearchMode::Enabled);
@@ -958,6 +962,10 @@ QString ChooseRecipientBoxController::savedMessagesChatStatus() const {
958962
return tr::lng_saved_forward_here(tr::now);
959963
}
960964

965+
QString ChooseRecipientBoxController::accessibilityName() const {
966+
return tr::lng_forward_choose(tr::now);
967+
}
968+
961969
auto ChooseRecipientBoxController::createRow(
962970
not_null<History*> history) -> std::unique_ptr<Row> {
963971
const auto peer = history->peer;
@@ -1253,6 +1261,10 @@ std::unique_ptr<PeerListRow> ChooseTopicBoxController::createSearchRow(
12531261
return nullptr;
12541262
}
12551263

1264+
QString ChooseTopicBoxController::accessibilityName() const {
1265+
return tr::lng_forward_choose(tr::now);
1266+
}
1267+
12561268
std::unique_ptr<PeerListRow> ChooseTopicBoxController::MakeRow(
12571269
not_null<Data::ForumTopic*> topic) {
12581270
return std::make_unique<Row>(topic);
@@ -1358,6 +1370,10 @@ std::unique_ptr<PeerListRow> ChooseSublistBoxController::createSearchRow(
13581370
return nullptr;
13591371
}
13601372

1373+
QString ChooseSublistBoxController::accessibilityName() const {
1374+
return tr::lng_forward_choose(tr::now);
1375+
}
1376+
13611377
auto ChooseSublistBoxController::createRow(
13621378
not_null<Data::SavedSublist*> sublist)
13631379
-> std::unique_ptr<PeerListRow> {

Telegram/SourceFiles/boxes/peer_list_controllers.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,8 @@ class ContactsBoxController : public PeerListController {
255255
return !_stories;
256256
}
257257

258+
[[nodiscard]] QString accessibilityName() const override;
259+
258260
enum class SortMode {
259261
Alphabet,
260262
Online,
@@ -309,6 +311,8 @@ class ChooseRecipientBoxController
309311

310312
QString savedMessagesChatStatus() const override;
311313

314+
[[nodiscard]] QString accessibilityName() const override;
315+
312316
protected:
313317
void prepareViewHook() override;
314318
std::unique_ptr<Row> createRow(not_null<History*> history) override;
@@ -364,6 +368,8 @@ class ChooseTopicBoxController final
364368
void loadMoreRows() override;
365369
std::unique_ptr<PeerListRow> createSearchRow(PeerListRowId id) override;
366370

371+
[[nodiscard]] QString accessibilityName() const override;
372+
367373
[[nodiscard]] static std::unique_ptr<PeerListRow> MakeRow(
368374
not_null<Data::ForumTopic*> topic);
369375

@@ -440,6 +446,8 @@ class ChooseSublistBoxController final
440446
void loadMoreRows() override;
441447
std::unique_ptr<PeerListRow> createSearchRow(PeerListRowId id) override;
442448

449+
[[nodiscard]] QString accessibilityName() const override;
450+
443451
private:
444452
void refreshRows(bool initial = false);
445453
[[nodiscard]] std::unique_ptr<PeerListRow> createRow(

0 commit comments

Comments
 (0)