Skip to content

Add listbox support #498

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1631,7 +1631,12 @@ usize_property_methods! {
(RowSpan, row_span, set_row_span, clear_row_span),
(ColumnSpan, column_span, set_column_span, clear_column_span),
(Level, level, set_level, clear_level),
/// For containers like [`Role::ListBox`], specifies the total number of items.
(SizeOfSet, size_of_set, set_size_of_set, clear_size_of_set),
/// For items like [`Role::ListBoxOption`], specifies their index in the item list.
/// This may not exceed the value of [`size_of_set`] as set on the container.
///
/// [`size_of_set`]: Node::size_of_set
(PositionInSet, position_in_set, set_position_in_set, clear_position_in_set)
}

Expand Down
94 changes: 93 additions & 1 deletion consumer/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,13 @@ impl<'a> Node<'a> {
}

pub fn orientation(&self) -> Option<Orientation> {
self.data().orientation()
self.data().orientation().or_else(|| {
if self.role() == Role::ListBox {
Some(Orientation::Vertical)
} else {
None
}
})
}

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

pub fn is_selectable(&self) -> bool {
// It's selectable if it has the attribute, whether it's true or false.
self.is_selected().is_some() && !self.is_disabled()
}

pub fn is_multiselectable(&self) -> bool {
self.data().is_multiselectable()
}

pub fn size_of_set_from_container(
&self,
filter: &impl Fn(&Node) -> FilterResult,
) -> Option<usize> {
self.selection_container(filter)
.and_then(|c| c.size_of_set())
}

pub fn size_of_set(&self) -> Option<usize> {
// TODO: compute this if it is not provided (#9).
self.data().size_of_set()
}

pub fn position_in_set(&self) -> Option<usize> {
// TODO: compute this if it is not provided (#9).
self.data().position_in_set()
}

pub fn supports_toggle(&self) -> bool {
self.toggled().is_some()
}
Expand Down Expand Up @@ -621,6 +654,44 @@ impl<'a> Node<'a> {
self.data().is_selected()
}

pub fn is_item_like(&self) -> bool {
matches!(
self.role(),
Role::Article
| Role::Comment
| Role::ListItem
| Role::MenuItem
| Role::MenuItemRadio
| Role::Tab
| Role::MenuItemCheckBox
| Role::TreeItem
| Role::ListBoxOption
| Role::MenuListOption
| Role::RadioButton
| Role::DescriptionListTerm
| Role::Term
)
}

pub fn is_container_with_selectable_children(&self) -> bool {
matches!(
self.role(),
Role::ComboBox
| Role::EditableComboBox
| Role::Grid
| Role::ListBox
| Role::ListGrid
| Role::Menu
| Role::MenuBar
| Role::MenuListPopup
| Role::RadioGroup
| Role::TabList
| Role::Toolbar
| Role::Tree
| Role::TreeGrid
)
}

pub fn raw_text_selection(&self) -> Option<&TextSelection> {
self.data().text_selection()
}
Expand Down Expand Up @@ -688,6 +759,27 @@ impl<'a> Node<'a> {
}
None
}

pub fn selection_container(&self, filter: &impl Fn(&Node) -> FilterResult) -> Option<Node<'a>> {
self.filtered_parent(&|parent| match filter(parent) {
FilterResult::Include if parent.is_container_with_selectable_children() => {
FilterResult::Include
}
FilterResult::Include => FilterResult::ExcludeNode,
filter_result => filter_result,
})
}

pub fn items(
&self,
filter: impl Fn(&Node) -> FilterResult + 'a,
) -> impl DoubleEndedIterator<Item = Node<'a>> + FusedIterator<Item = Node<'a>> + 'a {
self.filtered_children(move |child| match filter(child) {
FilterResult::Include if child.is_item_like() => FilterResult::Include,
FilterResult::Include => FilterResult::ExcludeNode,
filter_result => filter_result,
})
}
}

struct SpacePrefixingWriter<W: fmt::Write> {
Expand Down
55 changes: 49 additions & 6 deletions platforms/atspi-common/src/adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ struct AdapterChangeHandler<'a> {
added_nodes: HashSet<NodeId>,
removed_nodes: HashSet<NodeId>,
checked_text_change: HashSet<NodeId>,
selection_changed: HashSet<NodeId>,
}

impl<'a> AdapterChangeHandler<'a> {
Expand All @@ -41,6 +42,7 @@ impl<'a> AdapterChangeHandler<'a> {
added_nodes: HashSet::new(),
removed_nodes: HashSet::new(),
checked_text_change: HashSet::new(),
selection_changed: HashSet::new(),
}
}

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

let role = node.role();
let is_root = node.is_root();
let node = NodeWrapper(node);
let interfaces = node.interfaces();
let wrapper = NodeWrapper(node);
let interfaces = wrapper.interfaces();
self.adapter.register_interfaces(node.id(), interfaces);
if is_root && role == Role::Window {
let adapter_index = self
Expand All @@ -66,13 +68,16 @@ impl<'a> AdapterChangeHandler<'a> {
self.adapter.window_created(adapter_index, node.id());
}

let live = node.live();
let live = wrapper.live();
if live != Politeness::None {
if let Some(name) = node.name() {
if let Some(name) = wrapper.name() {
self.adapter
.emit_object_event(node.id(), ObjectEvent::Announcement(name, live));
}
}
if let Some(true) = node.is_selected() {
self.enqueue_selection_changed_if_needed(node);
}
}

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

let role = node.role();
let is_root = node.is_root();
let node = NodeWrapper(node);
let wrapper = NodeWrapper(node);
if is_root && role == Role::Window {
self.adapter.window_destroyed(node.id());
}
self.adapter
.emit_object_event(node.id(), ObjectEvent::StateChanged(State::Defunct, true));
self.adapter
.unregister_interfaces(node.id(), node.interfaces());
.unregister_interfaces(node.id(), wrapper.interfaces());
if let Some(true) = node.is_selected() {
self.enqueue_selection_changed_if_needed(node);
}
}

fn remove_subtree(&mut self, node: &Node) {
Expand Down Expand Up @@ -235,6 +243,36 @@ impl<'a> AdapterChangeHandler<'a> {
}
}
}

fn enqueue_selection_changed_if_needed_parent(&mut self, node: Node) {
if !node.is_container_with_selectable_children() {
return;
}
let id = node.id();
if self.selection_changed.contains(&id) {
return;
}
self.selection_changed.insert(id);
}

fn enqueue_selection_changed_if_needed(&mut self, node: &Node) {
if !node.is_item_like() {
return;
}
if let Some(node) = node.selection_container(&filter) {
self.enqueue_selection_changed_if_needed_parent(node);
}
}

fn emit_selection_changed(&mut self) {
for id in self.selection_changed.iter() {
if self.removed_nodes.contains(id) {
continue;
}
self.adapter
.emit_object_event(*id, ObjectEvent::SelectionChanged);
}
}
}

impl TreeChangeHandler for AdapterChangeHandler<'_> {
Expand Down Expand Up @@ -275,6 +313,9 @@ impl TreeChangeHandler for AdapterChangeHandler<'_> {
let bounds = *self.adapter.context.read_root_window_bounds();
new_wrapper.notify_changes(&bounds, self.adapter, &old_wrapper);
self.emit_text_selection_change(Some(old_node), new_node);
if new_node.is_selected() != old_node.is_selected() {
self.enqueue_selection_changed_if_needed(new_node);
}
}
}

Expand Down Expand Up @@ -476,6 +517,8 @@ impl Adapter {
let mut handler = AdapterChangeHandler::new(self);
let mut tree = self.context.tree.write().unwrap();
tree.update_and_process_changes(update, &mut handler);
drop(tree);
handler.emit_selection_changed();
}

pub fn update_window_focus_state(&mut self, is_focused: bool) {
Expand Down
1 change: 1 addition & 0 deletions platforms/atspi-common/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ pub enum ObjectEvent {
ChildAdded(usize, NodeId),
ChildRemoved(NodeId),
PropertyChanged(Property),
SelectionChanged,
StateChanged(State, bool),
TextInserted {
start_index: i32,
Expand Down
Loading