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..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; } @@ -289,10 +292,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 +317,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 +333,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 +344,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; 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 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; }