Skip to content

Commit 6c622cf

Browse files
DataTrinymwcampbell
authored andcommitted
fix: Add list box support to the platform adapters
1 parent d6dca15 commit 6c622cf

File tree

9 files changed

+482
-27
lines changed

9 files changed

+482
-27
lines changed

platforms/macos/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
# AccessKit macOS adapter
22

33
This is the macOS adapter for [AccessKit](https://accesskit.dev/). It exposes an AccessKit accessibility tree through the Cocoa `NSAccessibility` protocol.
4+
5+
## Known issues
6+
7+
- The selected state of ListBox items is not reported ([#520](https://github.com/AccessKit/accesskit/issues/520))

platforms/macos/src/event.rs

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ pub(crate) struct EventGenerator {
137137
context: Rc<Context>,
138138
events: Vec<QueuedEvent>,
139139
text_changed: HashSet<NodeId>,
140+
selected_rows_changed: HashSet<NodeId>,
140141
}
141142

142143
impl EventGenerator {
@@ -145,6 +146,7 @@ impl EventGenerator {
145146
context,
146147
events: Vec::new(),
147148
text_changed: HashSet::new(),
149+
selected_rows_changed: HashSet::new(),
148150
}
149151
}
150152

@@ -181,6 +183,30 @@ impl EventGenerator {
181183
self.insert_text_change_if_needed_parent(node);
182184
}
183185
}
186+
187+
fn enqueue_selected_rows_change_if_needed_parent(&mut self, node: Node) {
188+
if !node.is_container_with_selectable_children() {
189+
return;
190+
}
191+
let id = node.id();
192+
if self.selected_rows_changed.contains(&id) {
193+
return;
194+
}
195+
self.events.push(QueuedEvent::Generic {
196+
node_id: id,
197+
notification: unsafe { NSAccessibilitySelectedRowsChangedNotification },
198+
});
199+
self.selected_rows_changed.insert(id);
200+
}
201+
202+
fn enqueue_selected_rows_change_if_needed(&mut self, node: &Node) {
203+
if !node.is_item_like() {
204+
return;
205+
}
206+
if let Some(node) = node.selection_container(&filter) {
207+
self.enqueue_selected_rows_change_if_needed_parent(node);
208+
}
209+
}
184210
}
185211

186212
impl TreeChangeHandler for EventGenerator {
@@ -189,6 +215,9 @@ impl TreeChangeHandler for EventGenerator {
189215
if filter(node) != FilterResult::Include {
190216
return;
191217
}
218+
if let Some(true) = node.is_selected() {
219+
self.enqueue_selected_rows_change_if_needed(node);
220+
}
192221
if node.value().is_some() && node.live() != Live::Off {
193222
self.events
194223
.push(QueuedEvent::live_region_announcement(node));
@@ -199,7 +228,14 @@ impl TreeChangeHandler for EventGenerator {
199228
if old_node.raw_value() != new_node.raw_value() {
200229
self.insert_text_change_if_needed(new_node);
201230
}
231+
let old_node_was_filtered_out = filter(old_node) != FilterResult::Include;
202232
if filter(new_node) != FilterResult::Include {
233+
if !old_node_was_filtered_out
234+
&& old_node.is_item_like()
235+
&& old_node.is_selected() == Some(true)
236+
{
237+
self.enqueue_selected_rows_change_if_needed(old_node);
238+
}
203239
return;
204240
}
205241
let node_id = new_node.id();
@@ -230,11 +266,17 @@ impl TreeChangeHandler for EventGenerator {
230266
&& new_node.live() != Live::Off
231267
&& (new_node.value() != old_node.value()
232268
|| new_node.live() != old_node.live()
233-
|| filter(old_node) != FilterResult::Include)
269+
|| old_node_was_filtered_out)
234270
{
235271
self.events
236272
.push(QueuedEvent::live_region_announcement(new_node));
237273
}
274+
if new_node.is_item_like()
275+
&& (new_node.is_selected() != old_node.is_selected()
276+
|| (old_node_was_filtered_out && new_node.is_selected() == Some(true)))
277+
{
278+
self.enqueue_selected_rows_change_if_needed(new_node);
279+
}
238280
}
239281

240282
fn focus_moved(&mut self, _old_node: Option<&Node>, new_node: Option<&Node>) {
@@ -248,6 +290,9 @@ impl TreeChangeHandler for EventGenerator {
248290

249291
fn node_removed(&mut self, node: &Node) {
250292
self.insert_text_change_if_needed(node);
293+
if let Some(true) = node.is_selected() {
294+
self.enqueue_selected_rows_change_if_needed(node);
295+
}
251296
self.events.push(QueuedEvent::NodeDestroyed(node.id()));
252297
}
253298
}

platforms/macos/src/node.rs

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,22 @@ declare_class!(
411411
self.children_internal()
412412
}
413413

414+
#[method_id(accessibilitySelectedChildren)]
415+
fn selected_children(&self) -> Option<Id<NSArray<PlatformNode>>> {
416+
self.resolve_with_context(|node, context| {
417+
if !node.is_container_with_selectable_children() {
418+
return None;
419+
}
420+
let platform_nodes = node
421+
.items(filter)
422+
.filter(|item| item.is_selected() == Some(true))
423+
.map(|child| context.get_or_create_platform_node(child.id()))
424+
.collect::<Vec<Id<PlatformNode>>>();
425+
Some(NSArray::from_vec(platform_nodes))
426+
})
427+
.flatten()
428+
}
429+
414430
#[method(accessibilityFrame)]
415431
fn frame(&self) -> NSRect {
416432
self.resolve_with_context(|node, context| {
@@ -814,6 +830,75 @@ declare_class!(
814830
.unwrap_or(false)
815831
}
816832

833+
#[method(isAccessibilitySelected)]
834+
fn is_selected(&self) -> bool {
835+
self.resolve(|node| node.is_selected()).flatten().unwrap_or(false)
836+
}
837+
838+
#[method(setAccessibilitySelected:)]
839+
fn set_selected(&self, selected: bool) {
840+
self.resolve_with_context(|node, context| {
841+
if !node.is_clickable() || !node.is_selectable() {
842+
return;
843+
}
844+
if node.is_selected() == Some(selected) {
845+
return;
846+
}
847+
context.do_action(ActionRequest {
848+
action: Action::Click,
849+
target: node.id(),
850+
data: None,
851+
});
852+
});
853+
}
854+
855+
#[method_id(accessibilityRows)]
856+
fn rows(&self) -> Option<Id<NSArray<PlatformNode>>> {
857+
self.resolve_with_context(|node, context| {
858+
if !node.is_container_with_selectable_children() {
859+
return None;
860+
}
861+
let platform_nodes = node
862+
.items(filter)
863+
.map(|child| context.get_or_create_platform_node(child.id()))
864+
.collect::<Vec<Id<PlatformNode>>>();
865+
Some(NSArray::from_vec(platform_nodes))
866+
})
867+
.flatten()
868+
}
869+
870+
#[method_id(accessibilitySelectedRows)]
871+
fn selected_rows(&self) -> Option<Id<NSArray<PlatformNode>>> {
872+
self.resolve_with_context(|node, context| {
873+
if !node.is_container_with_selectable_children() {
874+
return None;
875+
}
876+
let platform_nodes = node
877+
.items(filter)
878+
.filter(|item| item.is_selected() == Some(true))
879+
.map(|child| context.get_or_create_platform_node(child.id()))
880+
.collect::<Vec<Id<PlatformNode>>>();
881+
Some(NSArray::from_vec(platform_nodes))
882+
})
883+
.flatten()
884+
}
885+
886+
#[method(accessibilityPerformPick)]
887+
fn pick(&self) -> bool {
888+
self.resolve_with_context(|node, context| {
889+
let selectable = node.is_clickable() && node.is_selectable();
890+
if selectable {
891+
context.do_action(ActionRequest {
892+
action: Action::Click,
893+
target: node.id(),
894+
data: None,
895+
});
896+
}
897+
selectable
898+
})
899+
.unwrap_or(false)
900+
}
901+
817902
#[method(isAccessibilitySelectorAllowed:)]
818903
fn is_selector_allowed(&self, selector: Sel) -> bool {
819904
self.resolve(|node| {
@@ -850,9 +935,23 @@ declare_class!(
850935
// the expected VoiceOver behavior.
851936
return node.supports_text_ranges() && !node.is_read_only();
852937
}
938+
if selector == sel!(isAccessibilitySelected) {
939+
return node.is_selectable();
940+
}
941+
if selector == sel!(accessibilityRows)
942+
|| selector == sel!(accessibilitySelectedRows)
943+
{
944+
return node.is_container_with_selectable_children()
945+
}
946+
if selector == sel!(setAccessibilitySelected:)
947+
|| selector == sel!(accessibilityPerformPick)
948+
{
949+
return node.is_clickable() && node.is_selectable();
950+
}
853951
selector == sel!(accessibilityParent)
854952
|| selector == sel!(accessibilityChildren)
855953
|| selector == sel!(accessibilityChildrenInNavigationOrder)
954+
|| selector == sel!(accessibilitySelectedChildren)
856955
|| selector == sel!(accessibilityFrame)
857956
|| selector == sel!(accessibilityRole)
858957
|| selector == sel!(accessibilitySubrole)

platforms/unix/src/atspi/bus.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,13 @@ impl Bus {
119119
)
120120
.await?;
121121
}
122+
if new_interfaces.contains(Interface::Selection) {
123+
self.register_interface(
124+
&path,
125+
SelectionInterface::new(bus_name.clone(), node.clone()),
126+
)
127+
.await?;
128+
}
122129
if new_interfaces.contains(Interface::Text) {
123130
self.register_interface(&path, TextInterface::new(node.clone()))
124131
.await?;
@@ -164,6 +171,10 @@ impl Bus {
164171
self.unregister_interface::<ComponentInterface>(&path)
165172
.await?;
166173
}
174+
if old_interfaces.contains(Interface::Selection) {
175+
self.unregister_interface::<SelectionInterface>(&path)
176+
.await?;
177+
}
167178
if old_interfaces.contains(Interface::Text) {
168179
self.unregister_interface::<TextInterface>(&path).await?;
169180
}
@@ -206,6 +217,7 @@ impl Bus {
206217
ObjectEvent::CaretMoved(_) => "TextCaretMoved",
207218
ObjectEvent::ChildAdded(_, _) | ObjectEvent::ChildRemoved(_) => "ChildrenChanged",
208219
ObjectEvent::PropertyChanged(_) => "PropertyChange",
220+
ObjectEvent::SelectionChanged => "SelectionChanged",
209221
ObjectEvent::StateChanged(_, _) => "StateChanged",
210222
ObjectEvent::TextInserted { .. } | ObjectEvent::TextRemoved { .. } => "TextChanged",
211223
ObjectEvent::TextSelectionChanged => "TextSelectionChanged",
@@ -285,6 +297,10 @@ impl Bus {
285297
};
286298
self.emit_event(target, interface, signal, body).await
287299
}
300+
ObjectEvent::SelectionChanged => {
301+
self.emit_event(target, interface, signal, EventBodyBorrowed::default())
302+
.await
303+
}
288304
ObjectEvent::StateChanged(state, value) => {
289305
let mut body = EventBodyBorrowed::default();
290306
body.kind = state.to_static_str();

platforms/unix/src/atspi/interfaces/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ mod accessible;
77
mod action;
88
mod application;
99
mod component;
10+
mod selection;
1011
mod text;
1112
mod value;
1213

@@ -31,5 +32,6 @@ pub(crate) use accessible::*;
3132
pub(crate) use action::*;
3233
pub(crate) use application::*;
3334
pub(crate) use component::*;
35+
pub(crate) use selection::*;
3436
pub(crate) use text::*;
3537
pub(crate) use value::*;
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright 2024 The AccessKit Authors. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0 (found in
3+
// the LICENSE-APACHE file) or the MIT license (found in
4+
// the LICENSE-MIT file), at your option.
5+
6+
use accesskit_atspi_common::PlatformNode;
7+
use zbus::{fdo, interface, names::OwnedUniqueName};
8+
9+
use crate::atspi::{ObjectId, OwnedObjectAddress};
10+
11+
pub(crate) struct SelectionInterface {
12+
bus_name: OwnedUniqueName,
13+
node: PlatformNode,
14+
}
15+
16+
impl SelectionInterface {
17+
pub fn new(bus_name: OwnedUniqueName, node: PlatformNode) -> Self {
18+
Self { bus_name, node }
19+
}
20+
21+
fn map_error(&self) -> impl '_ + FnOnce(accesskit_atspi_common::Error) -> fdo::Error {
22+
|error| crate::util::map_error_from_node(&self.node, error)
23+
}
24+
}
25+
26+
#[interface(name = "org.a11y.atspi.Selection")]
27+
impl SelectionInterface {
28+
#[zbus(property)]
29+
fn n_selected_children(&self) -> fdo::Result<i32> {
30+
self.node.n_selected_children().map_err(self.map_error())
31+
}
32+
33+
fn get_selected_child(&self, selected_child_index: i32) -> fdo::Result<(OwnedObjectAddress,)> {
34+
let child = self
35+
.node
36+
.selected_child(map_child_index(selected_child_index)?)
37+
.map_err(self.map_error())?
38+
.map(|child| ObjectId::Node {
39+
adapter: self.node.adapter_id(),
40+
node: child,
41+
});
42+
Ok(super::optional_object_address(&self.bus_name, child))
43+
}
44+
45+
fn select_child(&self, child_index: i32) -> fdo::Result<bool> {
46+
self.node
47+
.select_child(map_child_index(child_index)?)
48+
.map_err(self.map_error())
49+
}
50+
51+
fn deselect_selected_child(&self, selected_child_index: i32) -> fdo::Result<bool> {
52+
self.node
53+
.deselect_selected_child(map_child_index(selected_child_index)?)
54+
.map_err(self.map_error())
55+
}
56+
57+
fn is_child_selected(&self, child_index: i32) -> fdo::Result<bool> {
58+
self.node
59+
.is_child_selected(map_child_index(child_index)?)
60+
.map_err(self.map_error())
61+
}
62+
63+
fn select_all(&self) -> fdo::Result<bool> {
64+
self.node.select_all().map_err(self.map_error())
65+
}
66+
67+
fn clear_selection(&self) -> fdo::Result<bool> {
68+
self.node.clear_selection().map_err(self.map_error())
69+
}
70+
71+
fn deselect_child(&self, child_index: i32) -> fdo::Result<bool> {
72+
self.node
73+
.deselect_child(map_child_index(child_index)?)
74+
.map_err(self.map_error())
75+
}
76+
}
77+
78+
fn map_child_index(index: i32) -> fdo::Result<usize> {
79+
index
80+
.try_into()
81+
.map_err(|_| fdo::Error::InvalidArgs("Index can't be negative.".into()))
82+
}

0 commit comments

Comments
 (0)