mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2026-08-08 20:53:54 +00:00
Support screen reader focus and activate on message history rows
This commit is contained in:
committed by
John Preston
parent
27f41580d2
commit
aeb3e6be75
@@ -2408,6 +2408,10 @@ void HistoryInner::itemRemoved(not_null<const HistoryItem*> item) {
|
||||
if (_dragStateItem == item) {
|
||||
_dragStateItem = nullptr;
|
||||
}
|
||||
if (_accessibilityFocusedItem == item) {
|
||||
_accessibilityFocusedItem = nullptr;
|
||||
}
|
||||
_accessibilityIdentities.remove(item);
|
||||
|
||||
if ((_dragSelFrom && _dragSelFrom->data() == item)
|
||||
|| (_dragSelTo && _dragSelTo->data() == item)) {
|
||||
@@ -6440,3 +6444,142 @@ void HistoryInner::focusInEvent(QFocusEvent *e) {
|
||||
setAccessibilityFocusedItem(index, item);
|
||||
});
|
||||
}
|
||||
|
||||
bool HistoryInner::accessibilityChildSupportsActions(int index) const {
|
||||
// Every message row can be focused and activated and has a stable
|
||||
// identity below. Tying the opt-in to a valid identity keeps the
|
||||
// action interface off invalid indices and off the unread bar row,
|
||||
// which has no meaningful press action.
|
||||
return accessibilityChildIdentity(index) != 0;
|
||||
}
|
||||
|
||||
quintptr HistoryInner::accessibilityChildIdentity(int index) const {
|
||||
// Child indices shift whenever messages are inserted or removed and
|
||||
// the unread bar appears or goes away, so a queued action must not
|
||||
// be dispatched by index. A raw HistoryItem pointer is not a safe
|
||||
// token either: items are destroyed all the time and a new message
|
||||
// can be allocated at the same address, silently rebinding a stale
|
||||
// provider to an unrelated row (ABA). So the first request issues
|
||||
// the item a token from a monotonic counter; itemRemoved() erases
|
||||
// the pointer->token entry, and an item reusing the address gets a
|
||||
// fresh token, so stale identities resolve to nothing. The unread
|
||||
// bar row deliberately has no identity (and no action interface).
|
||||
const auto barIndex = accessibilityUnreadBarIndex();
|
||||
if (barIndex >= 0 && index == barIndex) {
|
||||
return 0;
|
||||
}
|
||||
const auto elements = accessibleElements();
|
||||
const auto elementIndex = (barIndex >= 0 && index > barIndex)
|
||||
? (index - 1)
|
||||
: index;
|
||||
if (elementIndex < 0 || elementIndex >= int(elements.size())) {
|
||||
return 0;
|
||||
}
|
||||
const auto item = elements[elementIndex]->data();
|
||||
const auto i = _accessibilityIdentities.find(item);
|
||||
if (i != _accessibilityIdentities.end()) {
|
||||
return i->second;
|
||||
}
|
||||
const auto token = ++_accessibilityIdentityCounter;
|
||||
_accessibilityIdentities.emplace(item, token);
|
||||
return token;
|
||||
}
|
||||
|
||||
int HistoryInner::accessibilityChildIndexByIdentity(
|
||||
quintptr identity) const {
|
||||
// One pass over the elements looking each item up in the issued
|
||||
// tokens map: only an item that was already handed out a token can
|
||||
// match, so rows never seen by the accessibility layer just do not
|
||||
// compare equal.
|
||||
if (!identity) {
|
||||
return -1;
|
||||
}
|
||||
const auto elements = accessibleElements();
|
||||
const auto barIndex = accessibilityUnreadBarIndex();
|
||||
for (auto i = 0, n = int(elements.size()); i != n; ++i) {
|
||||
const auto j = _accessibilityIdentities.find(
|
||||
elements[i]->data());
|
||||
if (j != _accessibilityIdentities.end()
|
||||
&& j->second == identity) {
|
||||
return (barIndex >= 0 && i >= barIndex) ? (i + 1) : i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void HistoryInner::applyAccessibilityFocus(
|
||||
int index,
|
||||
bool announceAlways) {
|
||||
const auto elements = accessibleElements();
|
||||
const auto barIndex = accessibilityUnreadBarIndex();
|
||||
const auto elementIndex = (barIndex >= 0 && index > barIndex)
|
||||
? (index - 1)
|
||||
: index;
|
||||
const auto item = (elementIndex >= 0
|
||||
&& elementIndex < int(elements.size()))
|
||||
? elements[elementIndex]->data().get()
|
||||
: nullptr;
|
||||
const auto changed = (_accessibilityFocusedIndex != index)
|
||||
|| (_accessibilityFocusedItem != item);
|
||||
_accessibilityFocusedIndex = index;
|
||||
_accessibilityFocusedItem = item;
|
||||
// Exactly one announcement: directly when the widget already has
|
||||
// focus, via focusInEvent when keyboard focus is being taken.
|
||||
if (hasFocus()) {
|
||||
if (changed || announceAlways) {
|
||||
announceAccessibilityFocus(index);
|
||||
}
|
||||
} else {
|
||||
setFocus();
|
||||
}
|
||||
const auto rect = accessibilityChildRect(index);
|
||||
if (!rect.isEmpty()) {
|
||||
if (rect.top() < _visibleAreaTop) {
|
||||
_scroll->scrollToY(rect.top());
|
||||
} else if (rect.bottom() > _visibleAreaBottom) {
|
||||
_scroll->scrollToY(rect.bottom()
|
||||
- (_visibleAreaBottom - _visibleAreaTop));
|
||||
}
|
||||
}
|
||||
if (_widget->markingMessagesRead()
|
||||
&& (barIndex < 0 || index != barIndex)
|
||||
&& elementIndex >= 0
|
||||
&& elementIndex < int(elements.size())) {
|
||||
session().data().histories().readInboxTill(
|
||||
elements[elementIndex]->data());
|
||||
}
|
||||
}
|
||||
|
||||
void HistoryInner::accessibilityChildSetFocus(quintptr identity) {
|
||||
// UIA invokes provider actions (SetFocus) on a background thread, so
|
||||
// hop to the main thread before touching any widget state. Resolve
|
||||
// the stable identity to its current index here (not on the
|
||||
// background thread) so a list mutation does not move focus to
|
||||
// another row.
|
||||
crl::on_main(this, [=] {
|
||||
// An explicit accessibility SetFocus is itself sufficient
|
||||
// authorization, so we do not gate it on the screen-reader-mode
|
||||
// detector: the UIA provider already reported success to the
|
||||
// caller, and the detector may still be false during startup or
|
||||
// for valid clients that are not on its allowlist.
|
||||
const auto index = accessibilityChildIndexByIdentity(identity);
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
applyAccessibilityFocus(index, true);
|
||||
});
|
||||
}
|
||||
|
||||
void HistoryInner::accessibilityChildActivate(quintptr identity) {
|
||||
// A mouse click on a message body performs no action, so Invoke
|
||||
// mirrors the click and only takes the accessibility focus onto the
|
||||
// row. Same background-thread hop and identity resolution as
|
||||
// SetFocus above.
|
||||
crl::on_main(this, [=] {
|
||||
const auto index = accessibilityChildIndexByIdentity(identity);
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
applyAccessibilityFocus(index, true);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -133,6 +133,12 @@ public:
|
||||
int row, int column) const override;
|
||||
QString accessibilityChildSubItemValue(
|
||||
int row, int column) const override;
|
||||
bool accessibilityChildSupportsActions(int index) const override;
|
||||
quintptr accessibilityChildIdentity(int index) const override;
|
||||
int accessibilityChildIndexByIdentity(
|
||||
quintptr identity) const override;
|
||||
void accessibilityChildSetFocus(quintptr identity) override;
|
||||
void accessibilityChildActivate(quintptr identity) override;
|
||||
|
||||
[[nodiscard]] Main::Session &session() const;
|
||||
[[nodiscard]] not_null<Ui::ChatTheme*> theme() const {
|
||||
@@ -307,6 +313,7 @@ private:
|
||||
void playPauseFocusedMedia();
|
||||
void setAccessibilityFocusedItem(int index, HistoryItem *item);
|
||||
void announceAccessibilityFocus(int index);
|
||||
void applyAccessibilityFocus(int index, bool announceAlways);
|
||||
[[nodiscard]] auto computeActiveColumns(int row) const
|
||||
-> const std::vector<HistoryView::MessageSubItem> &;
|
||||
|
||||
@@ -551,6 +558,10 @@ private:
|
||||
|
||||
int _accessibilityFocusedIndex = -1;
|
||||
HistoryItem *_accessibilityFocusedItem = nullptr;
|
||||
mutable base::flat_map<
|
||||
not_null<const HistoryItem*>,
|
||||
quintptr> _accessibilityIdentities;
|
||||
mutable quintptr _accessibilityIdentityCounter = 0;
|
||||
mutable const HistoryView::Element *_activeColumnsView = nullptr;
|
||||
mutable std::vector<HistoryView::MessageSubItem> _activeColumns;
|
||||
|
||||
|
||||
@@ -4899,6 +4899,7 @@ void ListWidget::itemRemoved(not_null<const HistoryItem*> item) {
|
||||
if (_accessibilityFocusedItem == item) {
|
||||
_accessibilityFocusedItem = nullptr;
|
||||
}
|
||||
_accessibilityIdentities.remove(item);
|
||||
const auto i = _views.find(item);
|
||||
if (i == end(_views)) {
|
||||
return;
|
||||
@@ -5391,6 +5392,144 @@ void ListWidget::focusInEvent(QFocusEvent *e) {
|
||||
});
|
||||
}
|
||||
|
||||
bool ListWidget::accessibilityChildSupportsActions(int index) const {
|
||||
// Every message row can be focused and activated and has a stable
|
||||
// identity below. Tying the opt-in to a valid identity keeps the
|
||||
// action interface off invalid indices and off the unread bar row,
|
||||
// which has no meaningful press action.
|
||||
return accessibilityChildIdentity(index) != 0;
|
||||
}
|
||||
|
||||
quintptr ListWidget::accessibilityChildIdentity(int index) const {
|
||||
// Child indices shift whenever messages are inserted or removed and
|
||||
// the unread bar appears or goes away, so a queued action must not
|
||||
// be dispatched by index. A raw HistoryItem pointer is not a safe
|
||||
// token either: items are destroyed all the time and a new message
|
||||
// can be allocated at the same address, silently rebinding a stale
|
||||
// provider to an unrelated row (ABA). So the first request issues
|
||||
// the item a token from a monotonic counter; itemRemoved() erases
|
||||
// the pointer->token entry, and an item reusing the address gets a
|
||||
// fresh token, so stale identities resolve to nothing. The unread
|
||||
// bar row deliberately has no identity (and no action interface).
|
||||
const auto barIndex = accessibilityUnreadBarIndex();
|
||||
if (barIndex >= 0 && index == barIndex) {
|
||||
return 0;
|
||||
}
|
||||
const auto elements = accessibleElements();
|
||||
const auto elementIndex = (barIndex >= 0 && index > barIndex)
|
||||
? (index - 1)
|
||||
: index;
|
||||
if (elementIndex < 0 || elementIndex >= int(elements.size())) {
|
||||
return 0;
|
||||
}
|
||||
const auto item = elements[elementIndex]->data();
|
||||
const auto i = _accessibilityIdentities.find(item);
|
||||
if (i != _accessibilityIdentities.end()) {
|
||||
return i->second;
|
||||
}
|
||||
const auto token = ++_accessibilityIdentityCounter;
|
||||
_accessibilityIdentities.emplace(item, token);
|
||||
return token;
|
||||
}
|
||||
|
||||
int ListWidget::accessibilityChildIndexByIdentity(
|
||||
quintptr identity) const {
|
||||
// One pass over the elements looking each item up in the issued
|
||||
// tokens map: only an item that was already handed out a token can
|
||||
// match, so rows never seen by the accessibility layer just do not
|
||||
// compare equal.
|
||||
if (!identity) {
|
||||
return -1;
|
||||
}
|
||||
const auto elements = accessibleElements();
|
||||
const auto barIndex = accessibilityUnreadBarIndex();
|
||||
for (auto i = 0, n = int(elements.size()); i != n; ++i) {
|
||||
const auto j = _accessibilityIdentities.find(
|
||||
elements[i]->data());
|
||||
if (j != _accessibilityIdentities.end()
|
||||
&& j->second == identity) {
|
||||
return (barIndex >= 0 && i >= barIndex) ? (i + 1) : i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void ListWidget::applyAccessibilityFocus(
|
||||
int index,
|
||||
bool announceAlways) {
|
||||
const auto elements = accessibleElements();
|
||||
const auto barIndex = accessibilityUnreadBarIndex();
|
||||
const auto elementIndex = (barIndex >= 0 && index > barIndex)
|
||||
? (index - 1)
|
||||
: index;
|
||||
const auto item = (elementIndex >= 0
|
||||
&& elementIndex < int(elements.size()))
|
||||
? elements[elementIndex]->data().get()
|
||||
: nullptr;
|
||||
const auto changed = (_accessibilityFocusedIndex != index)
|
||||
|| (_accessibilityFocusedItem != item);
|
||||
_accessibilityFocusedIndex = index;
|
||||
_accessibilityFocusedItem = item;
|
||||
// Exactly one announcement: directly when the widget already has
|
||||
// focus, via focusInEvent when keyboard focus is being taken.
|
||||
if (hasFocus()) {
|
||||
if (changed || announceAlways) {
|
||||
announceAccessibilityFocus(index);
|
||||
}
|
||||
} else {
|
||||
setFocus();
|
||||
}
|
||||
const auto rect = accessibilityChildRect(index);
|
||||
if (!rect.isEmpty()) {
|
||||
if (rect.top() < _visibleTop) {
|
||||
_delegate->listScrollTo(rect.top());
|
||||
} else if (rect.bottom() > _visibleBottom) {
|
||||
_delegate->listScrollTo(rect.bottom()
|
||||
- (_visibleBottom - _visibleTop));
|
||||
}
|
||||
}
|
||||
if (markingMessagesRead()
|
||||
&& (barIndex < 0 || index != barIndex)
|
||||
&& elementIndex >= 0
|
||||
&& elementIndex < int(elements.size())) {
|
||||
_delegate->listMarkReadTill(elements[elementIndex]->data());
|
||||
}
|
||||
}
|
||||
|
||||
void ListWidget::accessibilityChildSetFocus(quintptr identity) {
|
||||
// UIA invokes provider actions (SetFocus) on a background thread, so
|
||||
// hop to the main thread before touching any widget state. Resolve
|
||||
// the stable identity to its current index here (not on the
|
||||
// background thread) so a list mutation does not move focus to
|
||||
// another row.
|
||||
crl::on_main(this, [=] {
|
||||
// An explicit accessibility SetFocus is itself sufficient
|
||||
// authorization, so we do not gate it on the screen-reader-mode
|
||||
// detector: the UIA provider already reported success to the
|
||||
// caller, and the detector may still be false during startup or
|
||||
// for valid clients that are not on its allowlist.
|
||||
const auto index = accessibilityChildIndexByIdentity(identity);
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
applyAccessibilityFocus(index, true);
|
||||
});
|
||||
}
|
||||
|
||||
void ListWidget::accessibilityChildActivate(quintptr identity) {
|
||||
// A mouse click on a message body performs no action, so Invoke
|
||||
// mirrors the click and only takes the accessibility focus onto the
|
||||
// row. Same background-thread hop and identity resolution as
|
||||
// SetFocus above.
|
||||
crl::on_main(this, [=] {
|
||||
const auto index = accessibilityChildIndexByIdentity(identity);
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
applyAccessibilityFocus(index, true);
|
||||
});
|
||||
}
|
||||
|
||||
void ConfirmDeleteSelectedItems(not_null<ListWidget*> widget) {
|
||||
const auto items = widget->getSelectedItems();
|
||||
if (items.empty()) {
|
||||
|
||||
@@ -507,6 +507,12 @@ public:
|
||||
int row, int column) const override;
|
||||
QString accessibilityChildSubItemValue(
|
||||
int row, int column) const override;
|
||||
bool accessibilityChildSupportsActions(int index) const override;
|
||||
quintptr accessibilityChildIdentity(int index) const override;
|
||||
int accessibilityChildIndexByIdentity(
|
||||
quintptr identity) const override;
|
||||
void accessibilityChildSetFocus(quintptr identity) override;
|
||||
void accessibilityChildActivate(quintptr identity) override;
|
||||
|
||||
~ListWidget();
|
||||
|
||||
@@ -550,6 +556,7 @@ private:
|
||||
void playPauseFocusedMedia();
|
||||
void setAccessibilityFocusedItem(int index, HistoryItem *item);
|
||||
void announceAccessibilityFocus(int index);
|
||||
void applyAccessibilityFocus(int index, bool announceAlways);
|
||||
[[nodiscard]] auto computeActiveColumns(int row) const
|
||||
-> const std::vector<HistoryView::MessageSubItem> &;
|
||||
|
||||
@@ -931,6 +938,10 @@ private:
|
||||
|
||||
int _accessibilityFocusedIndex = -1;
|
||||
HistoryItem *_accessibilityFocusedItem = nullptr;
|
||||
mutable base::flat_map<
|
||||
not_null<const HistoryItem*>,
|
||||
quintptr> _accessibilityIdentities;
|
||||
mutable quintptr _accessibilityIdentityCounter = 0;
|
||||
mutable const HistoryView::Element *_activeColumnsView = nullptr;
|
||||
mutable std::vector<HistoryView::MessageSubItem> _activeColumns;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user