Skip to content

Commit d6dca15

Browse files
DataTrinymwcampbell
authored andcommitted
feat: Add list box support to the consumer and atspi-common crates
1 parent 4c10b80 commit d6dca15

File tree

5 files changed

+390
-12
lines changed

5 files changed

+390
-12
lines changed

consumer/src/node.rs

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,13 @@ impl<'a> Node<'a> {
405405
}
406406

407407
pub fn orientation(&self) -> Option<Orientation> {
408-
self.data().orientation()
408+
self.data().orientation().or_else(|| {
409+
if self.role() == Role::ListBox {
410+
Some(Orientation::Vertical)
411+
} else {
412+
None
413+
}
414+
})
409415
}
410416

411417
// When probing for supported actions as the next several functions do,
@@ -421,6 +427,33 @@ impl<'a> Node<'a> {
421427
self.supports_action(Action::Click)
422428
}
423429

430+
pub fn is_selectable(&self) -> bool {
431+
// It's selectable if it has the attribute, whether it's true or false.
432+
self.is_selected().is_some() && !self.is_disabled()
433+
}
434+
435+
pub fn is_multiselectable(&self) -> bool {
436+
self.data().is_multiselectable()
437+
}
438+
439+
pub fn size_of_set_from_container(
440+
&self,
441+
filter: &impl Fn(&Node) -> FilterResult,
442+
) -> Option<usize> {
443+
self.selection_container(filter)
444+
.and_then(|c| c.size_of_set())
445+
}
446+
447+
pub fn size_of_set(&self) -> Option<usize> {
448+
// TODO: compute this if it is not provided (#9).
449+
self.data().size_of_set()
450+
}
451+
452+
pub fn position_in_set(&self) -> Option<usize> {
453+
// TODO: compute this if it is not provided (#9).
454+
self.data().position_in_set()
455+
}
456+
424457
pub fn supports_toggle(&self) -> bool {
425458
self.toggled().is_some()
426459
}
@@ -621,6 +654,44 @@ impl<'a> Node<'a> {
621654
self.data().is_selected()
622655
}
623656

657+
pub fn is_item_like(&self) -> bool {
658+
matches!(
659+
self.role(),
660+
Role::Article
661+
| Role::Comment
662+
| Role::ListItem
663+
| Role::MenuItem
664+
| Role::MenuItemRadio
665+
| Role::Tab
666+
| Role::MenuItemCheckBox
667+
| Role::TreeItem
668+
| Role::ListBoxOption
669+
| Role::MenuListOption
670+
| Role::RadioButton
671+
| Role::DescriptionListTerm
672+
| Role::Term
673+
)
674+
}
675+
676+
pub fn is_container_with_selectable_children(&self) -> bool {
677+
matches!(
678+
self.role(),
679+
Role::ComboBox
680+
| Role::EditableComboBox
681+
| Role::Grid
682+
| Role::ListBox
683+
| Role::ListGrid
684+
| Role::Menu
685+
| Role::MenuBar
686+
| Role::MenuListPopup
687+
| Role::RadioGroup
688+
| Role::TabList
689+
| Role::Toolbar
690+
| Role::Tree
691+
| Role::TreeGrid
692+
)
693+
}
694+
624695
pub fn raw_text_selection(&self) -> Option<&TextSelection> {
625696
self.data().text_selection()
626697
}
@@ -688,6 +759,27 @@ impl<'a> Node<'a> {
688759
}
689760
None
690761
}
762+
763+
pub fn selection_container(&self, filter: &impl Fn(&Node) -> FilterResult) -> Option<Node<'a>> {
764+
self.filtered_parent(&|parent| match filter(parent) {
765+
FilterResult::Include if parent.is_container_with_selectable_children() => {
766+
FilterResult::Include
767+
}
768+
FilterResult::Include => FilterResult::ExcludeNode,
769+
filter_result => filter_result,
770+
})
771+
}
772+
773+
pub fn items(
774+
&self,
775+
filter: impl Fn(&Node) -> FilterResult + 'a,
776+
) -> impl DoubleEndedIterator<Item = Node<'a>> + FusedIterator<Item = Node<'a>> + 'a {
777+
self.filtered_children(move |child| match filter(child) {
778+
FilterResult::Include if child.is_item_like() => FilterResult::Include,
779+
FilterResult::Include => FilterResult::ExcludeNode,
780+
filter_result => filter_result,
781+
})
782+
}
691783
}
692784

693785
struct SpacePrefixingWriter<W: fmt::Write> {

platforms/atspi-common/src/adapter.rs

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ struct AdapterChangeHandler<'a> {
3232
added_nodes: HashSet<NodeId>,
3333
removed_nodes: HashSet<NodeId>,
3434
checked_text_change: HashSet<NodeId>,
35+
selection_changed: HashSet<NodeId>,
3536
}
3637

3738
impl<'a> AdapterChangeHandler<'a> {
@@ -41,6 +42,7 @@ impl<'a> AdapterChangeHandler<'a> {
4142
added_nodes: HashSet::new(),
4243
removed_nodes: HashSet::new(),
4344
checked_text_change: HashSet::new(),
45+
selection_changed: HashSet::new(),
4446
}
4547
}
4648

@@ -53,8 +55,8 @@ impl<'a> AdapterChangeHandler<'a> {
5355

5456
let role = node.role();
5557
let is_root = node.is_root();
56-
let node = NodeWrapper(node);
57-
let interfaces = node.interfaces();
58+
let wrapper = NodeWrapper(node);
59+
let interfaces = wrapper.interfaces();
5860
self.adapter.register_interfaces(node.id(), interfaces);
5961
if is_root && role == Role::Window {
6062
let adapter_index = self
@@ -66,13 +68,16 @@ impl<'a> AdapterChangeHandler<'a> {
6668
self.adapter.window_created(adapter_index, node.id());
6769
}
6870

69-
let live = node.live();
71+
let live = wrapper.live();
7072
if live != Politeness::None {
71-
if let Some(name) = node.name() {
73+
if let Some(name) = wrapper.name() {
7274
self.adapter
7375
.emit_object_event(node.id(), ObjectEvent::Announcement(name, live));
7476
}
7577
}
78+
if let Some(true) = node.is_selected() {
79+
self.enqueue_selection_changed_if_needed(node);
80+
}
7681
}
7782

7883
fn add_subtree(&mut self, node: &Node) {
@@ -91,14 +96,17 @@ impl<'a> AdapterChangeHandler<'a> {
9196

9297
let role = node.role();
9398
let is_root = node.is_root();
94-
let node = NodeWrapper(node);
99+
let wrapper = NodeWrapper(node);
95100
if is_root && role == Role::Window {
96101
self.adapter.window_destroyed(node.id());
97102
}
98103
self.adapter
99104
.emit_object_event(node.id(), ObjectEvent::StateChanged(State::Defunct, true));
100105
self.adapter
101-
.unregister_interfaces(node.id(), node.interfaces());
106+
.unregister_interfaces(node.id(), wrapper.interfaces());
107+
if let Some(true) = node.is_selected() {
108+
self.enqueue_selection_changed_if_needed(node);
109+
}
102110
}
103111

104112
fn remove_subtree(&mut self, node: &Node) {
@@ -235,6 +243,36 @@ impl<'a> AdapterChangeHandler<'a> {
235243
}
236244
}
237245
}
246+
247+
fn enqueue_selection_changed_if_needed_parent(&mut self, node: Node) {
248+
if !node.is_container_with_selectable_children() {
249+
return;
250+
}
251+
let id = node.id();
252+
if self.selection_changed.contains(&id) {
253+
return;
254+
}
255+
self.selection_changed.insert(id);
256+
}
257+
258+
fn enqueue_selection_changed_if_needed(&mut self, node: &Node) {
259+
if !node.is_item_like() {
260+
return;
261+
}
262+
if let Some(node) = node.selection_container(&filter) {
263+
self.enqueue_selection_changed_if_needed_parent(node);
264+
}
265+
}
266+
267+
fn emit_selection_changed(&mut self) {
268+
for id in self.selection_changed.iter() {
269+
if self.removed_nodes.contains(id) {
270+
continue;
271+
}
272+
self.adapter
273+
.emit_object_event(*id, ObjectEvent::SelectionChanged);
274+
}
275+
}
238276
}
239277

240278
impl TreeChangeHandler for AdapterChangeHandler<'_> {
@@ -275,6 +313,9 @@ impl TreeChangeHandler for AdapterChangeHandler<'_> {
275313
let bounds = *self.adapter.context.read_root_window_bounds();
276314
new_wrapper.notify_changes(&bounds, self.adapter, &old_wrapper);
277315
self.emit_text_selection_change(Some(old_node), new_node);
316+
if new_node.is_selected() != old_node.is_selected() {
317+
self.enqueue_selection_changed_if_needed(new_node);
318+
}
278319
}
279320
}
280321

@@ -476,6 +517,8 @@ impl Adapter {
476517
let mut handler = AdapterChangeHandler::new(self);
477518
let mut tree = self.context.tree.write().unwrap();
478519
tree.update_and_process_changes(update, &mut handler);
520+
drop(tree);
521+
handler.emit_selection_changed();
479522
}
480523

481524
pub fn update_window_focus_state(&mut self, is_focused: bool) {

platforms/atspi-common/src/events.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ pub enum ObjectEvent {
4040
ChildAdded(usize, NodeId),
4141
ChildRemoved(NodeId),
4242
PropertyChanged(Property),
43+
SelectionChanged,
4344
StateChanged(State, bool),
4445
TextInserted {
4546
start_index: i32,

0 commit comments

Comments
 (0)