Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion ui/abstract_button.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<void()> _clickedCallback;

Expand Down
57 changes: 35 additions & 22 deletions ui/accessible/ui_accessible_widget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}
Expand Down Expand Up @@ -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());
Expand All @@ -304,7 +317,7 @@ QList<QAccessibleInterface*> 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);
}
Expand All @@ -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;
}

Expand All @@ -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;
Expand Down
16 changes: 16 additions & 0 deletions ui/widgets/popup_menu.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include "ui/ui_utility.h"

#include <QtGui/QtEvents>
#include <QtGui/QCursor>
#include <QtGui/QPainter>
#include <QtGui/QScreen>
#include <QtGui/QWindow>
Expand Down Expand Up @@ -1503,4 +1504,19 @@ PopupMenu::~PopupMenu() {
}
}

QPoint ContextMenuPosition(
not_null<QWidget*> anchor,
not_null<QContextMenuEvent*> e) {
return ContextMenuPosition(anchor, e, anchor->rect());
}

QPoint ContextMenuPosition(
not_null<QWidget*> anchor,
not_null<QContextMenuEvent*> e,
QRect rect) {
return (e->reason() == QContextMenuEvent::Keyboard)
? anchor->mapToGlobal(rect.center())
: QCursor::pos();
}

} // namespace Ui
15 changes: 15 additions & 0 deletions ui/widgets/popup_menu.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<QWidget*> anchor,
not_null<QContextMenuEvent*> e);
[[nodiscard]] QPoint ContextMenuPosition(
not_null<QWidget*> anchor,
not_null<QContextMenuEvent*> e,
QRect rect);

} // namespace Ui
10 changes: 5 additions & 5 deletions ui/widgets/side_bar_button.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
}
Expand Down