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
91 changes: 69 additions & 22 deletions ui/accessible/ui_accessible_widget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ void *Widget::interface_cast(QAccessible::InterfaceType type) {
&& rp()->accessibilityOrientation().has_value()) {
return static_cast<QAccessibleAttributesInterface*>(this);
}
if (type == QAccessible::ValueInterface
&& rp()->accessibilityValueRange().has_value()) {
return static_cast<QAccessibleValueInterface*>(this);
}
return QAccessibleWidget::interface_cast(type);
}

Expand Down Expand Up @@ -211,6 +215,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 +248,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 +296,22 @@ 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, or RadioButton, for a group of exclusive options) 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)
|| (role == QAccessible::RadioButton);
}

} // namespace

int Widget::selectedItemCount() const {
return int(selectedItems().size());
Expand All @@ -304,7 +323,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 +339,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 +350,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 Expand Up @@ -378,4 +397,32 @@ QVariant Widget::attributeValue(QAccessible::Attribute key) const {
return QVariant();
}

// Value. Reports the numeric range of a slider-like widget. The setter may be
// invoked by the platform on a background thread, so the widget must hop to
// the main thread itself before touching any state.

QVariant Widget::currentValue() const {
const auto range = rp()->accessibilityValueRange();
return range ? QVariant(range->current) : QVariant();
}

void Widget::setCurrentValue(const QVariant &value) {
rp()->accessibilitySetValue(value.toDouble());
}

QVariant Widget::maximumValue() const {
const auto range = rp()->accessibilityValueRange();
return range ? QVariant(range->maximum) : QVariant();
}

QVariant Widget::minimumValue() const {
const auto range = rp()->accessibilityValueRange();
return range ? QVariant(range->minimum) : QVariant();
}

QVariant Widget::minimumStepSize() const {
const auto range = rp()->accessibilityValueRange();
return range ? QVariant(range->step) : QVariant();
}

} // namespace Ui::Accessible
12 changes: 11 additions & 1 deletion ui/accessible/ui_accessible_widget.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ namespace Ui::Accessible {
class Widget
: public QAccessibleWidget
, public QAccessibleSelectionInterface
, public QAccessibleAttributesInterface {
, public QAccessibleAttributesInterface
, public QAccessibleValueInterface {
public:
explicit Widget(not_null<RpWidget*> widget);

Expand Down Expand Up @@ -66,6 +67,15 @@ class Widget
QList<QAccessible::Attribute> attributeKeys() const override;
QVariant attributeValue(QAccessible::Attribute key) const override;

// Value. Exposed (via interface_cast) only when the widget reports an
// accessibilityValueRange(), so UI Automation maps it to the RangeValue
// pattern and a screen reader reads and changes the numeric value.
QVariant currentValue() const override;
void setCurrentValue(const QVariant &value) override;
QVariant maximumValue() const override;
QVariant minimumValue() const override;
QVariant minimumStepSize() const override;

};

} // namespace Ui::Accessible
15 changes: 15 additions & 0 deletions ui/rp_widget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,12 @@ void RpWidget::accessibilityChildStateChanged(
QAccessible::updateAccessibility(&event);
}

void RpWidget::accessibilityChildSelectionChanged(int index) {
auto event = QAccessibleEvent(this, QAccessible::SelectionAdd);
event.setChild(index);
QAccessible::updateAccessibility(&event);
}

void RpWidget::accessibilityChildFocused(int index) {
QAccessibleEvent event(this, QAccessible::Focus);
event.setChild(index);
Expand Down Expand Up @@ -515,10 +521,19 @@ QString RpWidget::accessibilityValue() const {
}

void RpWidget::accessibilityValueChanged() {
// A string value routes to the textual Value pattern change, which is
// the one a screen reader announces and reads the value back from.
QAccessibleValueChangeEvent event(this, accessibilityValue());
QAccessible::updateAccessibility(&event);
}

std::optional<AccessibilityValueRange> RpWidget::accessibilityValueRange() const {
return std::nullopt;
}

void RpWidget::accessibilitySetValue(double value) {
}

QStringList RpWidget::accessibilityActionNames() {
return QStringList();
}
Expand Down
26 changes: 26 additions & 0 deletions ui/rp_widget.h
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,14 @@ struct AccessibilityState {
void writeTo(QAccessible::State &state);
};

// Numeric value of a control that picks a point on a range (e.g. a slider).
struct AccessibilityValueRange {
double current = 0.;
double minimum = 0.;
double maximum = 0.;
double step = 1.;
};

class RpWidget : public RpWidgetBase<QWidget> {
// The Q_OBJECT meta info is used for qobject_cast above!
Q_OBJECT
Expand Down Expand Up @@ -426,6 +434,14 @@ class RpWidget : public RpWidgetBase<QWidget> {
void accessibilityStateChanged(AccessibilityState changes);
[[nodiscard]] virtual QString accessibilityValue() const;
void accessibilityValueChanged();

// Numeric range value of a slider-like control. nullopt (the default)
// means the widget exposes no range; a widget returning one gets
// QAccessibleValueInterface, which the platform maps to a range value
// pattern so a screen reader can read and change the number.
[[nodiscard]] virtual std::optional<AccessibilityValueRange> accessibilityValueRange() const;
// The platform may deliver the new value on a background thread.
virtual void accessibilitySetValue(double value);
[[nodiscard]] virtual QStringList accessibilityActionNames();
virtual void accessibilityDoAction(const QString &name);
[[nodiscard]] virtual int accessibilityChildCount() const;
Expand Down Expand Up @@ -457,6 +473,16 @@ class RpWidget : public RpWidgetBase<QWidget> {
void accessibilityChildValueChanged(int index);
[[nodiscard]] virtual QAccessible::State accessibilityChildState(int index) const;
void accessibilityChildStateChanged(int index, AccessibilityState changes);

// Announces that a child was selected or deselected. A state change does
// not cover it: the Windows bridge looks at the checked flag there and
// ignores the selected one. Both directions raise the same event, the one
// that bridge forwards (SelectionRemove reaches no one) - a screen reader
// reads the new value off the child and announces the difference, so it
// only needs telling that the selection changed. SideBarButton does the
// same for the folders that are real widgets.
void accessibilityChildSelectionChanged(int index);

[[nodiscard]] virtual QAccessible::Role accessibilityChildRole() const;
[[nodiscard]] virtual QRect accessibilityChildRect(int index) const;
[[nodiscard]] virtual int accessibilityChildColumnCount(int row) const;
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