diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 8abe5965e2..9c7dc2a52d 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -5809,6 +5809,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_sr_message_invoice_paid" = "Paid"; "lng_sr_message_invoice_unpaid" = "Unpaid"; +"lng_sr_folder_locked" = "{text}, Premium required"; +"lng_sr_folder_locked_about" = "Press to view subscription options."; + "lng_media_save_progress" = "{ready} of {total} {mb}"; "lng_mediaview_save_as" = "Save As..."; "lng_mediaview_copy" = "Copy"; diff --git a/Telegram/SourceFiles/window/window_filters_menu.cpp b/Telegram/SourceFiles/window/window_filters_menu.cpp index a41c2d844f..68c5a434fc 100644 --- a/Telegram/SourceFiles/window/window_filters_menu.cpp +++ b/Telegram/SourceFiles/window/window_filters_menu.cpp @@ -65,6 +65,21 @@ public: // The folders strip is a vertically stacked tab control. return Qt::Vertical; } + std::vector> accessibilityChildWidgets() const override { + // Report the tab buttons in visual (row) order, which can differ from + // the QObject child order after a drag-reorder. This override lives + // here, on the one VerticalLayout that exposes an accessibility role, + // rather than in the base class: a role-less VerticalLayout gets no + // custom accessible interface, so it would never call this anyway, and + // the widely-used base type keeps Qt's default child enumeration. + auto result = std::vector>(); + const auto rows = count(); + result.reserve(rows); + for (auto i = 0; i != rows; ++i) { + result.push_back(widgetAt(i).get()); + } + return result; + } }; } // namespace @@ -283,16 +298,16 @@ void FiltersMenu::refresh() { if (nextIsLocked && (currentFilter == filter.id())) { _session->setActiveChatsFilter(FilterId(0)); } + // A locked (premium) folder can't become the current tab - pressing it + // opens the Premium box. prepareButton() exposes it as a plain button + // rather than a selectable page tab (configured before it is shown), so + // screen readers don't offer it as a tab. auto button = prepareButton( _list, filter.id(), filter.title(), - Ui::ComputeFilterIcon(filter)); - button->setLocked(nextIsLocked); - // A locked (premium) folder can't become the current tab - pressing it - // opens the Premium box. Expose it as a plain button rather than a - // selectable page tab, so screen readers don't offer it as a tab. - button->setIsPageTab(!nextIsLocked); + Ui::ComputeFilterIcon(filter), + nextIsLocked); now.emplace(filter.id(), std::move(button)); } _filters = std::move(now); @@ -308,7 +323,6 @@ void FiltersMenu::refresh() { void FiltersMenu::setupList() { _list = _container->add(object_ptr(_container)); _list->setAccessibleName(tr::lng_filters_title(tr::now)); - _list->setObjectName(u"Folders"_q); _setup = prepareButton( _container, -1, @@ -337,7 +351,13 @@ bool FiltersMenu::premium() const { } base::unique_qptr FiltersMenu::prepareAll() { - return prepareButton(_container, 0, {}, Ui::FilterIcon::All, true); + return prepareButton( + _container, + 0, + {}, + Ui::FilterIcon::All, + false, + true); } base::unique_qptr FiltersMenu::prepareButton( @@ -345,12 +365,19 @@ base::unique_qptr FiltersMenu::prepareButton( FilterId id, Data::ChatFilterTitle title, Ui::FilterIcon icon, + bool locked, bool toBeginning) { const auto isStatic = title.isStatic; const auto paused = [=] { return On(PowerSaving::kEmojiChat) || _session->isGifPausedAtLeastFor(Window::GifPauseReason::Any); }; + // A real folder (id >= 0) that isn't premium-locked behaves as a selectable + // page tab; locked folders and the "Edit" button (id < 0) stay plain + // buttons. Establish this before inserting the widget - insertion shows the + // child immediately, so configuring the role up front avoids a transient or + // separately-announced role change. + const auto pageTab = (id >= 0) && !locked; auto prepared = object_ptr( container, id ? title.text : TextWithEntities{ tr::lng_filters_all(tr::now) }, @@ -360,6 +387,8 @@ base::unique_qptr FiltersMenu::prepareButton( .customEmojiLoopLimit = isStatic ? -1 : 0, }), paused); + prepared->setLocked(locked); + prepared->setIsPageTab(pageTab); auto added = toBeginning ? container->insert(0, std::move(prepared)) : container->add(std::move(prepared)); @@ -373,6 +402,15 @@ base::unique_qptr FiltersMenu::prepareButton( : Ui::FilterIcon::All); raw->setIconOverride(icons.normal, icons.active); if (id >= 0) { + if (locked) { + // A locked folder isn't a tab - surface its premium-gated status + // and what pressing it does, which the visual lock glyph alone + // can't convey to a screen reader. + raw->setAccessibleName( + tr::lng_sr_folder_locked(tr::now, lt_text, nameText)); + raw->setAccessibleDescription( + tr::lng_sr_folder_locked_about(tr::now)); + } rpl::combine( Data::UnreadStateValue(&_session->session(), id), Data::IncludeMutedCounterFoldersValue() @@ -390,18 +428,19 @@ base::unique_qptr FiltersMenu::prepareButton( ? "99+" : QString::number(count); raw->setBadge(string, includeMuted && (count == muted)); - raw->setAccessibleName(count - ? tr::lng_filter_unread_chats( - tr::now, - lt_count, - count, - lt_text, - nameText) - : nameText); + if (!locked) { + raw->setAccessibleName(count + ? tr::lng_filter_unread_chats( + tr::now, + lt_count, + count, + lt_text, + nameText) + : nameText); + } }, raw->lifetime()); } - if (id >= 0) { - raw->setIsPageTab(true); + if (pageTab) { // Like a tab strip, only the active tab is reachable with the Tab // key. Drive the focus policy reactively so it stays correct as the // active filter and screen-reader mode change (the active tab also @@ -432,7 +471,8 @@ base::unique_qptr FiltersMenu::prepareButton( }, raw->lifetime()); // Up/Down move to the previous/next folder, Home/End to the first/last // one, activating it (the tabs are only focusable in screen-reader - // mode, so this is scoped to it). + // mode, so this is scoped to it). A locked folder isn't a page tab and + // keeps ordinary button keyboard behavior. base::install_event_filter(raw, [=](not_null event) { if (event->type() != QEvent::KeyPress) { return base::EventFilterResult::Continue; diff --git a/Telegram/SourceFiles/window/window_filters_menu.h b/Telegram/SourceFiles/window/window_filters_menu.h index 88eceb88ad..4eeb96ecf9 100644 --- a/Telegram/SourceFiles/window/window_filters_menu.h +++ b/Telegram/SourceFiles/window/window_filters_menu.h @@ -50,6 +50,7 @@ private: FilterId id, Data::ChatFilterTitle title, Ui::FilterIcon icon, + bool locked = false, bool toBeginning = false); void setupMainMenuIcon(); void showMenu(QPoint position, FilterId id); diff --git a/Telegram/build/prepare/prepare.py b/Telegram/build/prepare/prepare.py index 22cfdb0335..4997ba7cd0 100644 --- a/Telegram/build/prepare/prepare.py +++ b/Telegram/build/prepare/prepare.py @@ -455,7 +455,7 @@ if customRunCommand: stage('patches', """ git clone https://github.com/desktop-app/patches.git cd patches - git checkout 0da942abf8283add899ae783432093e5f8742a12 + git checkout 94441c000324599430fa126be92ff63b17a4e409 mac: git clone https://github.com/desktop-app/qt6_highsierra_patches.git qt6_highsierra cd qt6_highsierra diff --git a/Telegram/lib_ui b/Telegram/lib_ui index 0c0a44dae3..484d18ec6c 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit 0c0a44dae3f9de6d26e25657c27d10e182b3d3b1 +Subproject commit 484d18ec6c32c9cf4a99b39367ce772d705ca0ac