Skip to content

Commit 47d8d9f

Browse files
authored
feat: Live regions on macOS (#196)
1 parent 44d6c50 commit 47d8d9f

File tree

5 files changed

+121
-15
lines changed

5 files changed

+121
-15
lines changed

platforms/macos/src/appkit/accessibility_constants.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// the LICENSE-APACHE file) or the MIT license (found in
44
// the LICENSE-MIT file), at your option.
55

6-
use objc2::foundation::NSString;
6+
use objc2::foundation::{NSInteger, NSString};
77

88
#[link(name = "AppKit", kind = "framework")]
99
extern "C" {
@@ -13,6 +13,7 @@ extern "C" {
1313
pub(crate) static NSAccessibilityTitleChangedNotification: &'static NSString;
1414
pub(crate) static NSAccessibilityValueChangedNotification: &'static NSString;
1515
pub(crate) static NSAccessibilitySelectedTextChangedNotification: &'static NSString;
16+
pub(crate) static NSAccessibilityAnnouncementRequestedNotification: &'static NSString;
1617

1718
// Roles
1819
pub(crate) static NSAccessibilityButtonRole: &'static NSString;
@@ -46,4 +47,12 @@ extern "C" {
4647
pub(crate) static NSAccessibilityTextFieldRole: &'static NSString;
4748
pub(crate) static NSAccessibilityToolbarRole: &'static NSString;
4849
pub(crate) static NSAccessibilityUnknownRole: &'static NSString;
50+
51+
// Notification user info keys
52+
pub(crate) static NSAccessibilityAnnouncementKey: &'static NSString;
53+
pub(crate) static NSAccessibilityPriorityKey: &'static NSString;
4954
}
55+
56+
// Announcement priorities
57+
pub(crate) const NSAccessibilityPriorityMedium: NSInteger = 50;
58+
pub(crate) const NSAccessibilityPriorityHigh: NSInteger = 90;

platforms/macos/src/appkit/accessibility_functions.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,14 @@
33
// the LICENSE-APACHE file) or the MIT license (found in
44
// the LICENSE-MIT file), at your option.
55

6-
use objc2::foundation::{NSObject, NSString};
6+
use objc2::foundation::{NSDictionary, NSObject, NSString};
77

88
#[link(name = "AppKit", kind = "framework")]
99
extern "C" {
1010
pub(crate) fn NSAccessibilityPostNotification(element: &NSObject, notification: &NSString);
11+
pub(crate) fn NSAccessibilityPostNotificationWithUserInfo(
12+
element: &NSObject,
13+
notification: &NSString,
14+
user_info: &NSDictionary<NSString, NSObject>,
15+
);
1116
}

platforms/macos/src/appkit/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
// the LICENSE-APACHE file) or the MIT license (found in
44
// the LICENSE-MIT file), at your option.
55

6+
#![allow(non_upper_case_globals)]
7+
68
#[link(name = "AppKit", kind = "framework")]
79
extern "C" {}
810

platforms/macos/src/event.rs

Lines changed: 80 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,29 @@
33
// the LICENSE-APACHE file) or the MIT license (found in
44
// the LICENSE-MIT file), at your option.
55

6-
use accesskit::NodeId;
6+
use accesskit::{Live, NodeId};
77
use accesskit_consumer::{DetachedNode, FilterResult, Node, TreeChangeHandler, TreeState};
8-
use objc2::foundation::NSString;
8+
use objc2::{
9+
foundation::{NSInteger, NSMutableDictionary, NSNumber, NSObject, NSString},
10+
msg_send, Message,
11+
};
912
use std::rc::Rc;
1013

1114
use crate::{
1215
appkit::*,
1316
context::Context,
14-
node::{filter, NodeWrapper},
17+
node::{filter, filter_detached, NodeWrapper},
1518
};
1619

20+
// Workaround for https://github.com/madsmtm/objc2/issues/306
21+
fn set_object_for_key<K: Message, V: Message>(
22+
dictionary: &mut NSMutableDictionary<K, V>,
23+
value: &V,
24+
key: &K,
25+
) {
26+
let _: () = unsafe { msg_send![dictionary, setObject: value, forKey: key] };
27+
}
28+
1729
// This type is designed to be safe to create on a non-main thread
1830
// and send to the main thread. This ability isn't yet used though.
1931
pub(crate) enum QueuedEvent {
@@ -22,9 +34,24 @@ pub(crate) enum QueuedEvent {
2234
notification: &'static NSString,
2335
},
2436
NodeDestroyed(NodeId),
37+
Announcement {
38+
text: String,
39+
priority: NSInteger,
40+
},
2541
}
2642

2743
impl QueuedEvent {
44+
fn live_region_announcement(node: &Node) -> Self {
45+
Self::Announcement {
46+
text: node.name().unwrap(),
47+
priority: if node.live() == Live::Assertive {
48+
NSAccessibilityPriorityHigh
49+
} else {
50+
NSAccessibilityPriorityMedium
51+
},
52+
}
53+
}
54+
2855
fn raise(self, context: &Rc<Context>) {
2956
match self {
3057
Self::Generic {
@@ -44,6 +71,39 @@ impl QueuedEvent {
4471
};
4572
}
4673
}
74+
Self::Announcement { text, priority } => {
75+
let view = match context.view.load() {
76+
Some(view) => view,
77+
None => {
78+
return;
79+
}
80+
};
81+
82+
let window = match view.window() {
83+
Some(window) => window,
84+
None => {
85+
return;
86+
}
87+
};
88+
89+
let mut user_info = NSMutableDictionary::<_, NSObject>::new();
90+
let text = NSString::from_str(&text);
91+
set_object_for_key(&mut user_info, &*text, unsafe {
92+
NSAccessibilityAnnouncementKey
93+
});
94+
let priority = NSNumber::new_isize(priority);
95+
set_object_for_key(&mut user_info, &*priority, unsafe {
96+
NSAccessibilityPriorityKey
97+
});
98+
99+
unsafe {
100+
NSAccessibilityPostNotificationWithUserInfo(
101+
&window,
102+
NSAccessibilityAnnouncementRequestedNotification,
103+
&user_info,
104+
)
105+
};
106+
}
47107
}
48108
}
49109
}
@@ -91,8 +151,14 @@ impl EventGenerator {
91151
}
92152

93153
impl TreeChangeHandler for EventGenerator {
94-
fn node_added(&mut self, _node: &Node) {
95-
// TODO: text changes, live regions
154+
fn node_added(&mut self, node: &Node) {
155+
if filter(node) != FilterResult::Include {
156+
return;
157+
}
158+
if node.name().is_some() && node.live() != Live::Off {
159+
self.events
160+
.push(QueuedEvent::live_region_announcement(node));
161+
}
96162
}
97163

98164
fn node_updated(&mut self, old_node: &DetachedNode, new_node: &Node) {
@@ -124,6 +190,15 @@ impl TreeChangeHandler for EventGenerator {
124190
notification: unsafe { NSAccessibilitySelectedTextChangedNotification },
125191
});
126192
}
193+
if new_node.name().is_some()
194+
&& new_node.live() != Live::Off
195+
&& (new_node.name() != old_node.name()
196+
|| new_node.live() != old_node.live()
197+
|| filter_detached(old_node) != FilterResult::Include)
198+
{
199+
self.events
200+
.push(QueuedEvent::live_region_announcement(new_node));
201+
}
127202
}
128203

129204
fn focus_moved(&mut self, _old_node: Option<&DetachedNode>, new_node: Option<&Node>) {
@@ -139,7 +214,6 @@ impl TreeChangeHandler for EventGenerator {
139214
}
140215

141216
fn node_removed(&mut self, node: &DetachedNode, _current_state: &TreeState) {
142-
// TODO: text changes
143217
self.events.push(QueuedEvent::NodeDestroyed(node.id()));
144218
}
145219
}

platforms/macos/src/node.rs

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ use std::{
3030

3131
use crate::{appkit::*, context::Context, util::*};
3232

33-
fn ns_role(node: &Node) -> &'static NSString {
34-
let role = node.role();
33+
fn ns_role(node_state: &NodeState) -> &'static NSString {
34+
let role = node_state.role();
3535
// TODO: Handle special cases.
3636
unsafe {
3737
match role {
@@ -54,7 +54,7 @@ fn ns_role(node: &Node) -> &'static NSString {
5454
Role::CheckBox => NSAccessibilityCheckBoxRole,
5555
Role::RadioButton => NSAccessibilityRadioButtonRole,
5656
Role::TextField => {
57-
if node.is_multiline() {
57+
if node_state.is_multiline() {
5858
NSAccessibilityTextAreaRole
5959
} else {
6060
NSAccessibilityTextFieldRole
@@ -231,19 +231,35 @@ fn ns_role(node: &Node) -> &'static NSString {
231231
}
232232
}
233233

234-
pub(crate) fn filter(node: &Node) -> FilterResult {
235-
let ns_role = ns_role(node);
234+
fn filter_common(node_state: &NodeState) -> FilterResult {
235+
let ns_role = ns_role(node_state);
236236
if ns_role == unsafe { NSAccessibilityUnknownRole } {
237237
return FilterResult::ExcludeNode;
238238
}
239239

240-
if node.is_hidden() && !node.is_focused() {
240+
if node_state.is_hidden() {
241241
return FilterResult::ExcludeSubtree;
242242
}
243243

244244
FilterResult::Include
245245
}
246246

247+
pub(crate) fn filter(node: &Node) -> FilterResult {
248+
if node.is_focused() {
249+
return FilterResult::Include;
250+
}
251+
252+
filter_common(node.state())
253+
}
254+
255+
pub(crate) fn filter_detached(node: &DetachedNode) -> FilterResult {
256+
if node.is_focused() {
257+
return FilterResult::Include;
258+
}
259+
260+
filter_common(node.state())
261+
}
262+
247263
pub(crate) fn can_be_focused(node: &Node) -> bool {
248264
filter(node) == FilterResult::Include && node.role() != Role::Window
249265
}
@@ -396,7 +412,7 @@ declare_class!(
396412
#[sel(accessibilityRole)]
397413
fn role(&self) -> *mut NSString {
398414
let role = self
399-
.resolve(ns_role)
415+
.resolve(|node| ns_role(node.state()))
400416
.unwrap_or(unsafe { NSAccessibilityUnknownRole });
401417
Id::autorelease_return(role.copy())
402418
}

0 commit comments

Comments
 (0)