Skip to content

Commit e81c7bc

Browse files
feat(accessibility): add screen reader support for language list
1 parent b4f7c0f commit e81c7bc

2 files changed

Lines changed: 191 additions & 0 deletions

File tree

Telegram/Resources/langs/lang.strings

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7742,5 +7742,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
77427742
"lng_sr_bot_verified_badge" = "Verified Bot";
77437743
"lng_sr_profile_menu" = "Profile menu";
77447744
"lng_sr_close_panel" = "Close panel";
7745+
"lng_sr_languages_column_native" = "Native name";
7746+
"lng_sr_languages_column_name" = "Language name";
77457747

77467748
// Keys finished

Telegram/SourceFiles/boxes/language_box.cpp

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ For license and copyright information please follow this link:
99

1010
#include "data/data_peer_values.h"
1111
#include "lang/lang_keys.h"
12+
#include "base/screen_reader_state.h"
13+
#include "ui/accessible/ui_accessible_item.h"
1214
#include "ui/boxes/choose_language_box.h"
1315
#include "ui/widgets/checkbox.h"
1416
#include "ui/widgets/buttons.h"
@@ -51,6 +53,8 @@ For license and copyright information please follow this link:
5153
#include "styles/style_menu_icons.h"
5254
#include "styles/style_settings.h"
5355

56+
#include <unordered_map>
57+
5458
#include <QtGui/QGuiApplication>
5559
#include <QtGui/QClipboard>
5660

@@ -71,22 +75,101 @@ class Rows : public Ui::RpWidget {
7175

7276
int count() const;
7377
int selected() const;
78+
int chosenIndex() const;
7479
void setSelected(int selected);
7580
rpl::producer<bool> hasSelection() const;
7681
rpl::producer<bool> isEmpty() const;
7782

7883
void activateSelected();
84+
void selectSkip(int dir);
7985
rpl::producer<Language> activations() const;
8086
void changeChosen(const QString &chosen);
8187

8288
Ui::ScrollToRequest rowScrollRequest(int index) const;
8389

8490
static int DefaultRowHeight();
8591

92+
QAccessible::Role accessibilityRole() override {
93+
return QAccessible::List;
94+
}
95+
96+
QAccessible::Role accessibilityChildRole() const override {
97+
return QAccessible::RadioButton;
98+
}
99+
100+
QAccessible::State accessibilityChildState(int index) const override {
101+
QAccessible::State state;
102+
if (base::ScreenReaderState::Instance()->active()) {
103+
state.focusable = true;
104+
}
105+
state.checkable = true;
106+
state.checked = (index == chosenIndex());
107+
if (index == selected()) {
108+
state.active = true;
109+
if (hasFocus()) {
110+
state.focused = true;
111+
}
112+
}
113+
return state;
114+
}
115+
116+
int accessibilityChildCount() const override {
117+
return count();
118+
}
119+
120+
QString accessibilityChildName(int index) const override {
121+
if (index < 0 || index >= count()) {
122+
return {};
123+
}
124+
const auto &row = rowByIndex(index);
125+
// Announce native name followed by English name.
126+
return row.data.nativeName + u", "_q + row.data.name;
127+
}
128+
129+
QRect accessibilityChildRect(int index) const override {
130+
if (index < 0 || index >= count()) {
131+
return QRect();
132+
}
133+
const auto &row = rowByIndex(index);
134+
return QRect(0, row.top, width(), row.height);
135+
}
136+
137+
int accessibilityChildColumnCount(int row) const override {
138+
return 2;
139+
}
140+
141+
QAccessible::Role accessibilityChildSubItemRole() const override {
142+
return QAccessible::Cell;
143+
}
144+
145+
QString accessibilityChildSubItemName(int row, int column) const override {
146+
if (column == 0) {
147+
return tr::lng_sr_languages_column_native(tr::now);
148+
} else if (column == 1) {
149+
return tr::lng_sr_languages_column_name(tr::now);
150+
}
151+
return {};
152+
}
153+
154+
QString accessibilityChildSubItemValue(int row, int column) const override {
155+
if (row < 0 || row >= count()) {
156+
return {};
157+
}
158+
const auto &data = rowByIndex(row).data;
159+
if (column == 0) {
160+
return data.nativeName;
161+
} else if (column == 1) {
162+
return data.name;
163+
}
164+
return {};
165+
}
166+
86167
protected:
87168
int resizeGetHeight(int newWidth) override;
88169

170+
void focusInEvent(QFocusEvent *e) override;
89171
void paintEvent(QPaintEvent *e) override;
172+
void keyPressEvent(QKeyEvent *e) override;
90173
void mouseMoveEvent(QMouseEvent *e) override;
91174
void mousePressEvent(QMouseEvent *e) override;
92175
void mouseReleaseEvent(QMouseEvent *e) override;
@@ -288,6 +371,63 @@ Rows::Rows(
288371
resizeToWidth(width());
289372
setAttribute(Qt::WA_MouseTracking);
290373
update();
374+
375+
setAccessibleName(tr::lng_languages(tr::now));
376+
377+
base::ScreenReaderState::Instance()->activeValue(
378+
) | rpl::on_next([=](bool active) {
379+
setFocusPolicy(active ? Qt::TabFocus : Qt::NoFocus);
380+
}, lifetime());
381+
}
382+
383+
void Rows::focusInEvent(QFocusEvent *e) {
384+
// Select first item or chosen item when focus enters.
385+
if (selected() < 0 && count() > 0) {
386+
const auto chosen = chosenIndex();
387+
setSelected(chosen >= 0 ? chosen : 0);
388+
}
389+
390+
RpWidget::focusInEvent(e);
391+
392+
if (base::ScreenReaderState::Instance()->active()) {
393+
const auto index = selected();
394+
if (index >= 0) {
395+
InvokeQueued(this, [=] {
396+
if (selected() != index || !hasFocus()) {
397+
return;
398+
}
399+
accessibilityChildFocused(index);
400+
});
401+
}
402+
}
403+
}
404+
405+
void Rows::keyPressEvent(QKeyEvent *e) {
406+
const auto key = e->key();
407+
if (key == Qt::Key_Down) {
408+
selectSkip(1);
409+
} else if (key == Qt::Key_Up) {
410+
selectSkip(-1);
411+
} else if (key == Qt::Key_PageDown || key == Qt::Key_PageUp) {
412+
const auto visibleHeight = visibleRegion().boundingRect().height();
413+
const auto rowsPerPage = std::max(visibleHeight / DefaultRowHeight(), 1);
414+
selectSkip(key == Qt::Key_PageDown ? rowsPerPage : -rowsPerPage);
415+
} else if (key == Qt::Key_Home) {
416+
if (count() > 0) {
417+
setSelected(0);
418+
}
419+
} else if (key == Qt::Key_End) {
420+
if (count() > 0) {
421+
setSelected(count() - 1);
422+
}
423+
} else if (!e->isAutoRepeat()
424+
&& (key == Qt::Key_Space
425+
|| key == Qt::Key_Return
426+
|| key == Qt::Key_Enter)) {
427+
activateSelected();
428+
} else {
429+
RpWidget::keyPressEvent(e);
430+
}
291431
}
292432

293433
void Rows::mouseMoveEvent(QMouseEvent *e) {
@@ -555,7 +695,12 @@ void Rows::setForceRippled(not_null<Row*> row, bool rippled) {
555695
}
556696

557697
void Rows::activateByIndex(int index) {
698+
_chosen = rowByIndex(index).data.id;
558699
_activations.fire_copy(rowByIndex(index).data);
700+
if (base::ScreenReaderState::Instance()->active()) {
701+
accessibilityChildStateChanged(index, { .checked = true });
702+
accessibilityChildNameChanged(index);
703+
}
559704
}
560705

561706
void Rows::leaveEventHook(QEvent *e) {
@@ -631,21 +776,60 @@ int Rows::selected() const {
631776
return indexFromSelection(_selected);
632777
}
633778

779+
int Rows::chosenIndex() const {
780+
for (auto i = 0, n = count(); i < n; ++i) {
781+
if (rowByIndex(i).data.id == _chosen) {
782+
return i;
783+
}
784+
}
785+
return -1;
786+
}
787+
634788
void Rows::activateSelected() {
635789
const auto index = selected();
636790
if (index >= 0) {
637791
activateByIndex(index);
638792
}
639793
}
640794

795+
void Rows::selectSkip(int dir) {
796+
const auto limit = count();
797+
auto now = selected();
798+
// If no keyboard selection, start from the checked item.
799+
if (now < 0) {
800+
now = chosenIndex();
801+
}
802+
if (now >= 0) {
803+
const auto changed = now + dir;
804+
if (changed < 0) {
805+
setSelected(0);
806+
} else if (changed >= limit) {
807+
setSelected(limit - 1);
808+
} else {
809+
setSelected(changed);
810+
}
811+
} else if (dir > 0) {
812+
setSelected(0);
813+
}
814+
}
815+
641816
rpl::producer<Language> Rows::activations() const {
642817
return _activations.events();
643818
}
644819

645820
void Rows::changeChosen(const QString &chosen) {
821+
const auto oldIndex = chosenIndex();
822+
_chosen = chosen;
646823
for (const auto &row : _rows) {
647824
row.check->setChecked(row.data.id == chosen, anim::type::normal);
648825
}
826+
if (base::ScreenReaderState::Instance()->active()) {
827+
const auto newIndex = chosenIndex();
828+
if (newIndex != oldIndex && newIndex >= 0) {
829+
accessibilityChildStateChanged(newIndex, { .checked = true });
830+
accessibilityChildNameChanged(newIndex);
831+
}
832+
}
649833
}
650834

651835
void Rows::setSelected(int selected) {
@@ -656,6 +840,11 @@ void Rows::setSelected(int selected) {
656840
} else {
657841
updateSelected({});
658842
}
843+
if (selected >= 0 && selected < limit
844+
&& base::ScreenReaderState::Instance()->active()) {
845+
accessibilityChildNameChanged(selected);
846+
accessibilityChildFocused(selected);
847+
}
659848
}
660849

661850
rpl::producer<bool> Rows::hasSelection() const {

0 commit comments

Comments
 (0)