Skip to content

Commit 341a11b

Browse files
committed
fix: Expose tabs in the platform adapters
1 parent b1fb5b3 commit 341a11b

File tree

6 files changed

+195
-29
lines changed

6 files changed

+195
-29
lines changed

platforms/macos/src/event.rs

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ use objc2_app_kit::*;
1111
use objc2_foundation::{NSMutableDictionary, NSNumber, NSString};
1212
use std::rc::Rc;
1313

14-
use crate::{context::Context, filters::filter, node::NodeWrapper};
14+
use crate::{
15+
context::Context,
16+
filters::filter,
17+
node::{NodeWrapper, Value},
18+
};
1519

1620
// This type is designed to be safe to create on a non-main thread
1721
// and send to the main thread. This ability isn't yet used though.
@@ -185,9 +189,6 @@ impl EventGenerator {
185189
}
186190

187191
fn enqueue_selected_rows_change_if_needed_parent(&mut self, node: Node) {
188-
if !node.is_container_with_selectable_children() {
189-
return;
190-
}
191192
let id = node.id();
192193
if self.selected_rows_changed.contains(&id) {
193194
return;
@@ -200,7 +201,8 @@ impl EventGenerator {
200201
}
201202

202203
fn enqueue_selected_rows_change_if_needed(&mut self, node: &Node) {
203-
if !node.is_item_like() {
204+
let wrapper = NodeWrapper(node);
205+
if !wrapper.is_item_like() {
204206
return;
205207
}
206208
if let Some(node) = node.selection_container(&filter) {
@@ -230,10 +232,7 @@ impl TreeChangeHandler for EventGenerator {
230232
}
231233
let old_node_was_filtered_out = filter(old_node) != FilterResult::Include;
232234
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-
{
235+
if !old_node_was_filtered_out && old_node.is_selected() == Some(true) {
237236
self.enqueue_selected_rows_change_if_needed(old_node);
238237
}
239238
return;
@@ -247,11 +246,26 @@ impl TreeChangeHandler for EventGenerator {
247246
notification: unsafe { NSAccessibilityTitleChangedNotification },
248247
});
249248
}
250-
if old_wrapper.value() != new_wrapper.value() {
251-
self.events.push(QueuedEvent::Generic {
252-
node_id,
253-
notification: unsafe { NSAccessibilityValueChangedNotification },
254-
});
249+
let new_value = new_wrapper.value();
250+
if old_wrapper.value() != new_value {
251+
if !new_node.is_focused() && new_value.is_some_and(|v| matches!(v, Value::Bool(_))) {
252+
// Bool value changed event for the focused node must come last
253+
// in order for VoiceOver to announce it. Otherwise, if we raise
254+
// bool value changed events for other nodes after this one, VoiceOver
255+
// will announce them instead.
256+
self.events.insert(
257+
0,
258+
QueuedEvent::Generic {
259+
node_id,
260+
notification: unsafe { NSAccessibilityValueChangedNotification },
261+
},
262+
);
263+
} else {
264+
self.events.push(QueuedEvent::Generic {
265+
node_id,
266+
notification: unsafe { NSAccessibilityValueChangedNotification },
267+
});
268+
}
255269
}
256270
if old_wrapper.supports_text_ranges()
257271
&& new_wrapper.supports_text_ranges()
@@ -271,9 +285,8 @@ impl TreeChangeHandler for EventGenerator {
271285
self.events
272286
.push(QueuedEvent::live_region_announcement(new_node));
273287
}
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)))
288+
if new_node.is_selected() != old_node.is_selected()
289+
|| (old_node_was_filtered_out && new_node.is_selected() == Some(true))
277290
{
278291
self.enqueue_selected_rows_change_if_needed(new_node);
279292
}

platforms/macos/src/node.rs

Lines changed: 77 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,8 @@ impl NodeWrapper<'_> {
325325
return Some(Value::Bool(toggled != Toggled::False));
326326
}
327327
if self.0.role() == Role::Tab {
328+
// On Mac, tabs are exposed as radio buttons, and are treated as checkable.
329+
// Also, `Node::is_selected` is mapped to checked via `accessibilityValue`.
328330
return Some(Value::Bool(self.0.is_selected().unwrap_or(false)));
329331
}
330332
if let Some(value) = self.0.value() {
@@ -343,6 +345,14 @@ impl NodeWrapper<'_> {
343345
pub(crate) fn raw_text_selection(&self) -> Option<&TextSelection> {
344346
self.0.raw_text_selection()
345347
}
348+
349+
fn is_container_with_selectable_children(&self) -> bool {
350+
self.0.is_container_with_selectable_children() && self.0.role() != Role::TabList
351+
}
352+
353+
pub(crate) fn is_item_like(&self) -> bool {
354+
self.0.is_item_like() && self.0.role() != Role::Tab
355+
}
346356
}
347357

348358
pub(crate) struct PlatformNodeIvars {
@@ -417,7 +427,8 @@ declare_class!(
417427
#[method_id(accessibilitySelectedChildren)]
418428
fn selected_children(&self) -> Option<Id<NSArray<PlatformNode>>> {
419429
self.resolve_with_context(|node, context| {
420-
if !node.is_container_with_selectable_children() {
430+
let wrapper = NodeWrapper(node);
431+
if !wrapper.is_container_with_selectable_children() {
421432
return None;
422433
}
423434
let platform_nodes = node
@@ -835,13 +846,23 @@ declare_class!(
835846

836847
#[method(isAccessibilitySelected)]
837848
fn is_selected(&self) -> bool {
838-
self.resolve(|node| node.is_selected()).flatten().unwrap_or(false)
849+
self.resolve(|node| {
850+
let wrapper = NodeWrapper(node);
851+
wrapper.is_item_like()
852+
&& node.is_selectable()
853+
&& node.is_selected().unwrap_or(false)
854+
})
855+
.unwrap_or(false)
839856
}
840857

841858
#[method(setAccessibilitySelected:)]
842859
fn set_selected(&self, selected: bool) {
843860
self.resolve_with_context(|node, context| {
844-
if !node.is_clickable() || !node.is_selectable() {
861+
let wrapper = NodeWrapper(node);
862+
if !node.is_clickable()
863+
|| !wrapper.is_item_like()
864+
|| !node.is_selectable()
865+
{
845866
return;
846867
}
847868
if node.is_selected() == Some(selected) {
@@ -858,7 +879,8 @@ declare_class!(
858879
#[method_id(accessibilityRows)]
859880
fn rows(&self) -> Option<Id<NSArray<PlatformNode>>> {
860881
self.resolve_with_context(|node, context| {
861-
if !node.is_container_with_selectable_children() {
882+
let wrapper = NodeWrapper(node);
883+
if !wrapper.is_container_with_selectable_children() {
862884
return None;
863885
}
864886
let platform_nodes = node
@@ -873,7 +895,8 @@ declare_class!(
873895
#[method_id(accessibilitySelectedRows)]
874896
fn selected_rows(&self) -> Option<Id<NSArray<PlatformNode>>> {
875897
self.resolve_with_context(|node, context| {
876-
if !node.is_container_with_selectable_children() {
898+
let wrapper = NodeWrapper(node);
899+
if !wrapper.is_container_with_selectable_children() {
877900
return None;
878901
}
879902
let platform_nodes = node
@@ -889,7 +912,10 @@ declare_class!(
889912
#[method(accessibilityPerformPick)]
890913
fn pick(&self) -> bool {
891914
self.resolve_with_context(|node, context| {
892-
let selectable = node.is_clickable() && node.is_selectable();
915+
let wrapper = NodeWrapper(node);
916+
let selectable = node.is_clickable()
917+
&& wrapper.is_item_like()
918+
&& node.is_selectable();
893919
if selectable {
894920
context.do_action(ActionRequest {
895921
action: Action::Click,
@@ -902,6 +928,39 @@ declare_class!(
902928
.unwrap_or(false)
903929
}
904930

931+
#[method_id(accessibilityLinkedUIElements)]
932+
fn linked_ui_elements(&self) -> Option<Id<NSArray<PlatformNode>>> {
933+
self.resolve_with_context(|node, context| {
934+
let platform_nodes: Vec<Id<PlatformNode>> = node
935+
.controls()
936+
.filter(|controlled| filter(controlled) == FilterResult::Include)
937+
.map(|controlled| context.get_or_create_platform_node(controlled.id()))
938+
.collect();
939+
if platform_nodes.is_empty() {
940+
None
941+
} else {
942+
Some(NSArray::from_vec(platform_nodes))
943+
}
944+
})
945+
.flatten()
946+
}
947+
948+
#[method_id(accessibilityTabs)]
949+
fn tabs(&self) -> Option<Id<NSArray<PlatformNode>>> {
950+
self.resolve_with_context(|node, context| {
951+
if node.role() != Role::TabList {
952+
return None;
953+
}
954+
let platform_nodes = node
955+
.filtered_children(filter)
956+
.filter(|child| child.role() == Role::Tab)
957+
.map(|tab| context.get_or_create_platform_node(tab.id()))
958+
.collect::<Vec<Id<PlatformNode>>>();
959+
Some(NSArray::from_vec(platform_nodes))
960+
})
961+
.flatten()
962+
}
963+
905964
#[method(isAccessibilitySelectorAllowed:)]
906965
fn is_selector_allowed(&self, selector: Sel) -> bool {
907966
self.resolve(|node| {
@@ -939,17 +998,25 @@ declare_class!(
939998
return node.supports_text_ranges() && !node.is_read_only();
940999
}
9411000
if selector == sel!(isAccessibilitySelected) {
942-
return node.is_selectable();
1001+
let wrapper = NodeWrapper(node);
1002+
return wrapper.is_item_like();
9431003
}
9441004
if selector == sel!(accessibilityRows)
9451005
|| selector == sel!(accessibilitySelectedRows)
9461006
{
947-
return node.is_container_with_selectable_children()
1007+
let wrapper = NodeWrapper(node);
1008+
return wrapper.is_container_with_selectable_children()
9481009
}
9491010
if selector == sel!(setAccessibilitySelected:)
9501011
|| selector == sel!(accessibilityPerformPick)
9511012
{
952-
return node.is_clickable() && node.is_selectable();
1013+
let wrapper = NodeWrapper(node);
1014+
return node.is_clickable()
1015+
&& wrapper.is_item_like()
1016+
&& node.is_selectable();
1017+
}
1018+
if selector == sel!(accessibilityTabs) {
1019+
return node.role() == Role::TabList;
9531020
}
9541021
selector == sel!(accessibilityParent)
9551022
|| selector == sel!(accessibilityChildren)
@@ -961,6 +1028,7 @@ declare_class!(
9611028
|| selector == sel!(isAccessibilityEnabled)
9621029
|| selector == sel!(accessibilityWindow)
9631030
|| selector == sel!(accessibilityTopLevelUIElement)
1031+
|| selector == sel!(accessibilityLinkedUIElements)
9641032
|| selector == sel!(accessibilityRoleDescription)
9651033
|| selector == sel!(accessibilityIdentifier)
9661034
|| selector == sel!(accessibilityTitle)

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

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
use std::collections::HashMap;
77

88
use accesskit_atspi_common::{NodeIdOrRoot, PlatformNode, PlatformRoot};
9-
use atspi::{Interface, InterfaceSet, Role, StateSet};
9+
use atspi::{Interface, InterfaceSet, RelationType, Role, StateSet};
1010
use zbus::{fdo, interface, names::OwnedUniqueName};
1111

1212
use super::map_root_error;
@@ -99,6 +99,22 @@ impl NodeAccessibleInterface {
9999
self.node.index_in_parent().map_err(self.map_error())
100100
}
101101

102+
fn get_relation_set(&self) -> fdo::Result<Vec<(RelationType, Vec<OwnedObjectAddress>)>> {
103+
self.node
104+
.relation_set(|relation| {
105+
ObjectId::Node {
106+
adapter: self.node.adapter_id(),
107+
node: relation,
108+
}
109+
.to_address(self.bus_name.inner())
110+
})
111+
.map(|set| {
112+
set.into_iter()
113+
.collect::<Vec<(RelationType, Vec<OwnedObjectAddress>)>>()
114+
})
115+
.map_err(self.map_error())
116+
}
117+
102118
fn get_role(&self) -> fdo::Result<Role> {
103119
self.node.role().map_err(self.map_error())
104120
}
@@ -191,6 +207,10 @@ impl RootAccessibleInterface {
191207
-1
192208
}
193209

210+
fn get_relation_set(&self) -> Vec<(RelationType, Vec<OwnedObjectAddress>)> {
211+
Vec::new()
212+
}
213+
194214
fn get_role(&self) -> Role {
195215
Role::Application
196216
}

platforms/windows/src/adapter.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,12 @@ impl TreeChangeHandler for AdapterChangeHandler<'_> {
245245
let element: IRawElementProviderSimple = platform_node.into();
246246
let old_wrapper = NodeWrapper(old_node);
247247
let new_wrapper = NodeWrapper(new_node);
248-
new_wrapper.enqueue_property_changes(&mut self.queue, &element, &old_wrapper);
248+
new_wrapper.enqueue_property_changes(
249+
&mut self.queue,
250+
&PlatformNode::new(self.context, new_node.id()),
251+
&element,
252+
&old_wrapper,
253+
);
249254
let new_name = new_wrapper.name();
250255
if new_name.is_some()
251256
&& new_node.live() != Live::Off

platforms/windows/src/node.rs

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -446,10 +446,11 @@ impl NodeWrapper<'_> {
446446
pub(crate) fn enqueue_property_changes(
447447
&self,
448448
queue: &mut Vec<QueuedEvent>,
449+
platform_node: &PlatformNode,
449450
element: &IRawElementProviderSimple,
450451
old: &NodeWrapper,
451452
) {
452-
self.enqueue_simple_property_changes(queue, element, old);
453+
self.enqueue_simple_property_changes(queue, platform_node, element, old);
453454
self.enqueue_pattern_property_changes(queue, element, old);
454455
self.enqueue_property_implied_events(queue, element, old);
455456
}
@@ -692,6 +693,16 @@ impl IRawElementProviderSimple_Impl for PlatformNode_Impl {
692693
match property_id {
693694
UIA_FrameworkIdPropertyId => result = state.toolkit_name().into(),
694695
UIA_ProviderDescriptionPropertyId => result = toolkit_description(state).into(),
696+
UIA_ControllerForPropertyId => {
697+
let controlled: Vec<IUnknown> = node
698+
.controls()
699+
.filter(|controlled| filter(controlled) == FilterResult::Include)
700+
.map(|controlled| self.relative(controlled.id()))
701+
.map(IRawElementProviderSimple::from)
702+
.filter_map(|controlled| controlled.cast::<IUnknown>().ok())
703+
.collect();
704+
result = controlled.into();
705+
}
695706
_ => (),
696707
}
697708
}
@@ -826,6 +837,7 @@ macro_rules! properties {
826837
fn enqueue_simple_property_changes(
827838
&self,
828839
queue: &mut Vec<QueuedEvent>,
840+
platform_node: &PlatformNode,
829841
element: &IRawElementProviderSimple,
830842
old: &NodeWrapper,
831843
) {
@@ -842,6 +854,32 @@ macro_rules! properties {
842854
);
843855
}
844856
})*
857+
858+
let mut old_controls = old.0.controls().filter(|controlled| filter(controlled) == FilterResult::Include);
859+
let mut new_controls = self.0.controls().filter(|controlled| filter(controlled) == FilterResult::Include);
860+
let mut are_equal = true;
861+
let mut controls: Vec<IUnknown> = Vec::new();
862+
loop {
863+
let old_controlled = old_controls.next();
864+
let new_controlled = new_controls.next();
865+
match (old_controlled, new_controlled) {
866+
(Some(a), Some(b)) => {
867+
are_equal = are_equal && a.id() == b.id();
868+
controls.push(platform_node.relative(b.id()).into());
869+
}
870+
(None, None) => break,
871+
_ => are_equal = false,
872+
}
873+
}
874+
if !are_equal {
875+
self.enqueue_property_change(
876+
queue,
877+
&element,
878+
UIA_ControllerForPropertyId,
879+
Variant::empty(),
880+
controls.into(),
881+
);
882+
}
845883
}
846884
}
847885
};

0 commit comments

Comments
 (0)