diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index cf59430033..56e4a3014f 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -7919,5 +7919,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_mac_hold_to_quit" = "Hold {text} to Quit"; "lng_sr_country_column_name" = "Country name"; +"lng_sr_languages_column_native" = "Native name"; +"lng_sr_languages_column_name" = "Language name"; // Keys finished diff --git a/Telegram/SourceFiles/boxes/language_box.cpp b/Telegram/SourceFiles/boxes/language_box.cpp index c9b9b22a52..fb6ff36cca 100644 --- a/Telegram/SourceFiles/boxes/language_box.cpp +++ b/Telegram/SourceFiles/boxes/language_box.cpp @@ -9,6 +9,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_peer_values.h" #include "lang/lang_keys.h" +#include "base/screen_reader_state.h" +#include "ui/accessible/ui_accessible_item.h" #include "ui/boxes/choose_language_box.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/buttons.h" @@ -51,6 +53,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_menu_icons.h" #include "styles/style_settings.h" +#include + #include #include @@ -71,11 +75,13 @@ public: int count() const; int selected() const; + int chosenIndex() const; void setSelected(int selected); rpl::producer hasSelection() const; rpl::producer isEmpty() const; void activateSelected(); + void selectSkip(int dir); rpl::producer activations() const; void changeChosen(const QString &chosen); @@ -83,10 +89,87 @@ public: static int DefaultRowHeight(); + QAccessible::Role accessibilityRole() override { + return QAccessible::List; + } + + QAccessible::Role accessibilityChildRole() const override { + return QAccessible::RadioButton; + } + + QAccessible::State accessibilityChildState(int index) const override { + QAccessible::State state; + if (base::ScreenReaderState::Instance()->active()) { + state.focusable = true; + } + state.checkable = true; + state.checked = (index == chosenIndex()); + if (index == selected()) { + state.active = true; + if (hasFocus()) { + state.focused = true; + } + } + return state; + } + + int accessibilityChildCount() const override { + return count(); + } + + QString accessibilityChildName(int index) const override { + if (index < 0 || index >= count()) { + return {}; + } + const auto &row = rowByIndex(index); + // Announce native name followed by English name. + return row.data.nativeName + u", "_q + row.data.name; + } + + QRect accessibilityChildRect(int index) const override { + if (index < 0 || index >= count()) { + return QRect(); + } + const auto &row = rowByIndex(index); + return QRect(0, row.top, width(), row.height); + } + + int accessibilityChildColumnCount(int row) const override { + return 2; + } + + QAccessible::Role accessibilityChildSubItemRole() const override { + return QAccessible::Cell; + } + + QString accessibilityChildSubItemName(int row, int column) const override { + if (column == 0) { + return tr::lng_sr_languages_column_native(tr::now); + } else if (column == 1) { + return tr::lng_sr_languages_column_name(tr::now); + } + return {}; + } + + QString accessibilityChildSubItemValue(int row, int column) const override { + if (row < 0 || row >= count()) { + return {}; + } + const auto &data = rowByIndex(row).data; + if (column == 0) { + return data.nativeName; + } else if (column == 1) { + return data.name; + } + return {}; + } + protected: int resizeGetHeight(int newWidth) override; + void focusInEvent(QFocusEvent *e) override; void paintEvent(QPaintEvent *e) override; + void keyPressEvent(QKeyEvent *e) override; void mouseMoveEvent(QMouseEvent *e) override; void mousePressEvent(QMouseEvent *e) override; void mouseReleaseEvent(QMouseEvent *e) override; @@ -288,6 +371,63 @@ Rows::Rows( resizeToWidth(width()); setAttribute(Qt::WA_MouseTracking); update(); + + setAccessibleName(tr::lng_languages(tr::now)); + + base::ScreenReaderState::Instance()->activeValue( + ) | rpl::on_next([=](bool active) { + setFocusPolicy(active ? Qt::TabFocus : Qt::NoFocus); + }, lifetime()); +} + +void Rows::focusInEvent(QFocusEvent *e) { + // Select first item or chosen item when focus enters. + if (selected() < 0 && count() > 0) { + const auto chosen = chosenIndex(); + setSelected(chosen >= 0 ? chosen : 0); + } + + RpWidget::focusInEvent(e); + + if (base::ScreenReaderState::Instance()->active()) { + const auto index = selected(); + if (index >= 0) { + InvokeQueued(this, [=] { + if (selected() != index || !hasFocus()) { + return; + } + accessibilityChildFocused(index); + }); + } + } +} + +void Rows::keyPressEvent(QKeyEvent *e) { + const auto key = e->key(); + if (key == Qt::Key_Down) { + selectSkip(1); + } else if (key == Qt::Key_Up) { + selectSkip(-1); + } else if (key == Qt::Key_PageDown || key == Qt::Key_PageUp) { + const auto visibleHeight = visibleRegion().boundingRect().height(); + const auto rowsPerPage = std::max(visibleHeight / DefaultRowHeight(), 1); + selectSkip(key == Qt::Key_PageDown ? rowsPerPage : -rowsPerPage); + } else if (key == Qt::Key_Home) { + if (count() > 0) { + setSelected(0); + } + } else if (key == Qt::Key_End) { + if (count() > 0) { + setSelected(count() - 1); + } + } else if (!e->isAutoRepeat() + && (key == Qt::Key_Space + || key == Qt::Key_Return + || key == Qt::Key_Enter)) { + activateSelected(); + } else { + RpWidget::keyPressEvent(e); + } } void Rows::mouseMoveEvent(QMouseEvent *e) { @@ -555,7 +695,12 @@ void Rows::setForceRippled(not_null row, bool rippled) { } void Rows::activateByIndex(int index) { + _chosen = rowByIndex(index).data.id; _activations.fire_copy(rowByIndex(index).data); + if (base::ScreenReaderState::Instance()->active()) { + accessibilityChildStateChanged(index, { .checked = true }); + accessibilityChildNameChanged(index); + } } void Rows::leaveEventHook(QEvent *e) { @@ -631,6 +776,15 @@ int Rows::selected() const { return indexFromSelection(_selected); } +int Rows::chosenIndex() const { + for (auto i = 0, n = count(); i < n; ++i) { + if (rowByIndex(i).data.id == _chosen) { + return i; + } + } + return -1; +} + void Rows::activateSelected() { const auto index = selected(); if (index >= 0) { @@ -638,14 +792,44 @@ void Rows::activateSelected() { } } +void Rows::selectSkip(int dir) { + const auto limit = count(); + auto now = selected(); + // If no keyboard selection, start from the checked item. + if (now < 0) { + now = chosenIndex(); + } + if (now >= 0) { + const auto changed = now + dir; + if (changed < 0) { + setSelected(0); + } else if (changed >= limit) { + setSelected(limit - 1); + } else { + setSelected(changed); + } + } else if (dir > 0) { + setSelected(0); + } +} + rpl::producer Rows::activations() const { return _activations.events(); } void Rows::changeChosen(const QString &chosen) { + const auto oldIndex = chosenIndex(); + _chosen = chosen; for (const auto &row : _rows) { row.check->setChecked(row.data.id == chosen, anim::type::normal); } + if (base::ScreenReaderState::Instance()->active()) { + const auto newIndex = chosenIndex(); + if (newIndex != oldIndex && newIndex >= 0) { + accessibilityChildStateChanged(newIndex, { .checked = true }); + accessibilityChildNameChanged(newIndex); + } + } } void Rows::setSelected(int selected) { @@ -656,6 +840,11 @@ void Rows::setSelected(int selected) { } else { updateSelected({}); } + if (selected >= 0 && selected < limit + && base::ScreenReaderState::Instance()->active()) { + accessibilityChildNameChanged(selected); + accessibilityChildFocused(selected); + } } rpl::producer Rows::hasSelection() const {