From 685d247d7047225885e2a949fc3cbe58ed963710 Mon Sep 17 00:00:00 2001 From: Reza Bakhshi Laktasaraei Date: Wed, 26 Aug 2026 07:26:24 +0330 Subject: [PATCH 1/4] Let a button be exposed as a page tab A strip of real tab buttons (the forum topic tabs) is named tabs in the UI, so a screen reader should call its items tabs as well. setIsPageTab gives a button the PageTab role, and the selection interface of the container accepts those items alongside list items - the Windows bridge grants the Selection patterns by the interface, not the role, so a PageTabList container keeps working the same way a List does. --- ui/abstract_button.h | 11 ++++++++++- ui/accessible/ui_accessible_widget.cpp | 24 +++++++++++++++++------- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/ui/abstract_button.h b/ui/abstract_button.h index 2eea6dfcb..66816ac13 100644 --- a/ui/abstract_button.h +++ b/ui/abstract_button.h @@ -69,9 +69,17 @@ class AbstractButton : public RpWidget { [[nodiscard]] bool isListItem() const { return _listItem; } + void setIsPageTab(bool value) { + _pageTab = value; + } + [[nodiscard]] bool isPageTab() const { + return _pageTab; + } QAccessible::Role accessibilityRole() override { - return _listItem + return _pageTab + ? QAccessible::PageTab + : _listItem ? QAccessible::ListItem : _menuButton ? QAccessible::ButtonMenu @@ -132,6 +140,7 @@ class AbstractButton : public RpWidget { bool _triggerOnPress : 1 = false; bool _menuButton : 1 = false; bool _listItem : 1 = false; + bool _pageTab : 1 = false; Fn _clickedCallback; diff --git a/ui/accessible/ui_accessible_widget.cpp b/ui/accessible/ui_accessible_widget.cpp index aa538f74b..30d9e3394 100644 --- a/ui/accessible/ui_accessible_widget.cpp +++ b/ui/accessible/ui_accessible_widget.cpp @@ -289,10 +289,20 @@ void Widget::doAction(const QString &actionName) { }); } -// Selection. A selection item is a child with the ListItem role reporting -// selected = active; the selected one resolves independently of focus. Plain -// buttons among the children are excluded, and a locked folder reports -// selectable = false, so it is never claimed as a successful selection. +// Selection. A selection item is a child with the ListItem (or PageTab, for +// a strip of tabs) role reporting selected = active; the selected one +// resolves independently of focus. Plain buttons among the children are +// excluded, and a locked folder reports selectable = false, so it is never +// claimed as a successful selection. + +namespace { + +[[nodiscard]] bool IsSelectionItemRole(QAccessible::Role role) { + return (role == QAccessible::ListItem) + || (role == QAccessible::PageTab); +} + +} // namespace int Widget::selectedItemCount() const { return int(selectedItems().size()); @@ -304,7 +314,7 @@ QList Widget::selectedItems() const { for (auto i = 0; i != count; ++i) { const auto item = child(i); if (item - && item->role() == QAccessible::ListItem + && IsSelectionItemRole(item->role()) && item->state().selected) { result.append(item); } @@ -320,7 +330,7 @@ QAccessibleInterface *Widget::selectedItem(int selectionIndex) const { bool Widget::isSelected(QAccessibleInterface *childItem) const { return childItem && indexOfChild(childItem) >= 0 - && childItem->role() == QAccessible::ListItem + && IsSelectionItemRole(childItem->role()) && childItem->state().selected; } @@ -331,7 +341,7 @@ bool Widget::select(QAccessibleInterface *childItem) { // item only implements pressAction, so invoke that rather than toggleAction. if (!childItem || indexOfChild(childItem) < 0 - || childItem->role() != QAccessible::ListItem + || !IsSelectionItemRole(childItem->role()) || childItem->state().disabled || !childItem->state().selectable) { return false; From 7e8561bbac0294c94cf095880ab4d3592d342c02 Mon Sep 17 00:00:00 2001 From: Reza Bakhshi Laktasaraei Date: Wed, 26 Aug 2026 11:59:03 +0330 Subject: [PATCH 2/4] Let the browsed child win over the selection when forwarding focus A container with painted children holds real keyboard focus itself while the browse position moves between them, so forwarding container focus to the selected item would announce the current item when the screen reader asked for a merely browsed one. A child reporting focus now wins; the selection forwarding stays as the fallback. --- ui/accessible/ui_accessible_widget.cpp | 33 ++++++++++++++------------ 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/ui/accessible/ui_accessible_widget.cpp b/ui/accessible/ui_accessible_widget.cpp index 30d9e3394..85176866b 100644 --- a/ui/accessible/ui_accessible_widget.cpp +++ b/ui/accessible/ui_accessible_widget.cpp @@ -211,6 +211,24 @@ QAccessibleInterface* Widget::focusChild() const { ++ReentrancyDepth; struct Guard { ~Guard() { --ReentrancyDepth; } } guard; + // A container with painted children keeps its own browse position on + // them, so a child reporting focus wins over the selected item below: + // the container holds real keyboard focus the whole time, and forwarding + // to the selection would announce the current item when the screen + // reader asked for a different, merely browsed one. + const auto count = rp()->accessibilityChildCount(); + if (count >= 0 && widget()->hasFocus()) { + // Iterate through children to find focused one (Qt standard approach). + for (int i = 0; i < count; ++i) { + if (const auto iface = rp()->accessibilityChildInterface(i)) { + const auto s = iface->state(); + if (s.focused || s.active) { + return iface; + } + } + } + } + // A selection list forwards accessible focus to its current (selected) item, // so focusing the container lands the screen reader on a navigable item // rather than the inert container. Only while the container itself holds @@ -226,26 +244,11 @@ QAccessibleInterface* Widget::focusChild() const { // Only handle focus child for widgets with custom accessibility children. // For other widgets (containers, scroll areas), delegate to Qt immediately. - const auto count = rp()->accessibilityChildCount(); if (count < 0) { // No custom children - let Qt handle it normally. return QAccessibleWidget::focusChild(); } - if (!widget()->hasFocus()) { - return nullptr; - } - - // Iterate through children to find focused one (Qt standard approach). - for (int i = 0; i < count; ++i) { - if (const auto iface = rp()->accessibilityChildInterface(i)) { - const auto s = iface->state(); - if (s.focused || s.active) { - return iface; - } - } - } - // Has custom children but none focused - return null. return nullptr; } From e7bebe67f7e3adf1e0e5320dad7a4ef38e73409c Mon Sep 17 00:00:00 2001 From: Reza Bakhshi Laktasaraei Date: Wed, 26 Aug 2026 16:22:27 +0330 Subject: [PATCH 3/4] Move the sidebar button selection state to the tab role The selected state and the selection events of SideBarButton were gated on the list item role, so switching the folders sidebar to tabs silenced them. The sidebar is the only user of this widget and it is tabs now, so the gate simply follows the role along. --- ui/widgets/side_bar_button.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ui/widgets/side_bar_button.cpp b/ui/widgets/side_bar_button.cpp index 31a475401..763397201 100644 --- a/ui/widgets/side_bar_button.cpp +++ b/ui/widgets/side_bar_button.cpp @@ -53,7 +53,7 @@ void SideBarButton::setActive(bool active) { return; } _active = active; - if (isListItem()) { + if (isPageTab()) { // Announce selection via a dedicated selection event (the Windows // bridge maps these to UIA_SelectionItem_ElementSelected); a generic // state-change event is ignored for the selected state. @@ -66,12 +66,12 @@ void SideBarButton::setActive(bool active) { } AccessibilityState SideBarButton::accessibilityState() const { - // Merge the base state so plain buttons keep reporting `pressed`. A list - // item exposes the active one as selected - persistently, independent of + // Merge the base state so plain buttons keep reporting `pressed`. A tab + // exposes the active one as selected - persistently, independent of // keyboard focus. A locked (premium) folder stays a focusable, invokable - // list item but can never become current, so it isn't selectable. + // tab but can never become current, so it isn't selectable. auto state = RippleButton::accessibilityState(); - if (isListItem()) { + if (isPageTab()) { state.selectable = !_lock.locked; state.selected = _active; } From 8188a76f9ddbc04b4517c6ef0db3f7170062359f Mon Sep 17 00:00:00 2001 From: Reza Bakhshi Laktasaraei Date: Thu, 27 Aug 2026 05:04:52 +0330 Subject: [PATCH 4/4] Add a helper choosing where a context menu opens A menu asked for from the keyboard opens at the mouse cursor at every call site, which may sit nowhere near the control or outside the window altogether - and the position Qt puts into the keyboard event is synthesized from the input method rect, which plain controls leave empty. The helper anchors a keyboard-invoked menu on the control (or on a painted element inside it) and keeps the cursor position for a mouse-invoked one, so call sites can adopt the policy with one line. --- ui/widgets/popup_menu.cpp | 16 ++++++++++++++++ ui/widgets/popup_menu.h | 15 +++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/ui/widgets/popup_menu.cpp b/ui/widgets/popup_menu.cpp index 59bbdcae5..6d9728aaa 100644 --- a/ui/widgets/popup_menu.cpp +++ b/ui/widgets/popup_menu.cpp @@ -22,6 +22,7 @@ #include "ui/ui_utility.h" #include +#include #include #include #include @@ -1503,4 +1504,19 @@ PopupMenu::~PopupMenu() { } } +QPoint ContextMenuPosition( + not_null anchor, + not_null e) { + return ContextMenuPosition(anchor, e, anchor->rect()); +} + +QPoint ContextMenuPosition( + not_null anchor, + not_null e, + QRect rect) { + return (e->reason() == QContextMenuEvent::Keyboard) + ? anchor->mapToGlobal(rect.center()) + : QCursor::pos(); +} + } // namespace Ui diff --git a/ui/widgets/popup_menu.h b/ui/widgets/popup_menu.h index 7aeafbb1f..03cd4bee8 100644 --- a/ui/widgets/popup_menu.h +++ b/ui/widgets/popup_menu.h @@ -296,4 +296,19 @@ class PopupMenu : public RpWidget { }; +// Where to show a context menu for the given event: at the mouse cursor for +// a mouse-invoked one, on the anchor for a keyboard-invoked one - the mouse +// may sit nowhere near the control then (or outside the window altogether), +// and the position Qt puts into the keyboard event is no help either, it is +// synthesized from the input method rect, which plain controls leave empty. +// The rect is in the anchor's coordinates and defaults to its whole area - +// pass one to anchor on a painted element inside the anchor widget. +[[nodiscard]] QPoint ContextMenuPosition( + not_null anchor, + not_null e); +[[nodiscard]] QPoint ContextMenuPosition( + not_null anchor, + not_null e, + QRect rect); + } // namespace Ui