diff --git a/consumer/src/node.rs b/consumer/src/node.rs index 67405650..d603867e 100644 --- a/consumer/src/node.rs +++ b/consumer/src/node.rs @@ -609,8 +609,14 @@ impl<'a> Node<'a> { &self, filter: &impl Fn(&Node) -> FilterResult, ) -> Option { - self.selection_container(filter) - .and_then(|c| c.size_of_set()) + let mut parent = self.filtered_parent(filter); + while let Some(node) = parent { + if let Some(size_of_set) = node.size_of_set() { + return Some(size_of_set); + } + parent = node.filtered_parent(filter); + } + None } pub fn size_of_set(&self) -> Option { @@ -632,7 +638,12 @@ impl<'a> Node<'a> { } pub fn supports_expand_collapse(&self) -> bool { - self.data().is_expanded().is_some() + self.has_popup().is_some() + || self.data().is_expanded().is_some() + || matches!( + self.role(), + Role::ComboBox | Role::EditableComboBox | Role::DisclosureTriangle | Role::TreeItem + ) } pub fn is_invocable(&self, parent_filter: &impl Fn(&Node) -> FilterResult) -> bool { diff --git a/platforms/windows/src/node.rs b/platforms/windows/src/node.rs index b5dbba0b..fd3556f2 100644 --- a/platforms/windows/src/node.rs +++ b/platforms/windows/src/node.rs @@ -675,6 +675,13 @@ impl NodeWrapper<'_> { .and_then(|s| s.try_into().ok()) } + fn level(&self) -> Option { + self.0 + .level() + .and_then(|level| level.checked_add(1)) + .and_then(|level| level.try_into().ok()) + } + fn is_selection_pattern_supported(&self) -> bool { self.0.is_container_with_selectable_children() } @@ -687,6 +694,19 @@ impl NodeWrapper<'_> { self.0.supports_text_ranges() } + fn is_expand_collapse_pattern_supported(&self) -> bool { + self.0.supports_expand_collapse() + } + + fn expand_collapse_state(&self) -> ExpandCollapseState { + match self.0.data().is_expanded() { + Some(true) => ExpandCollapseState_Expanded, + Some(false) => ExpandCollapseState_Collapsed, + // TODO: Handle the menu button case. (#27) + None => ExpandCollapseState_LeafNode, + } + } + fn is_password(&self) -> bool { self.0.role() == Role::PasswordInput } @@ -763,6 +783,7 @@ impl NodeWrapper<'_> { ISelectionItemProvider, ISelectionProvider, ITextProvider, + IExpandCollapseProvider, IWindowProvider )] pub(crate) struct PlatformNode { @@ -921,6 +942,30 @@ impl PlatformNode { self.do_action(|| (Action::Click, None)) } + fn set_expanded(&self, expanded: bool) -> Result<()> { + self.do_complex_action(|node, target_node, target_tree| { + if node.is_disabled() { + return Err(element_not_enabled()); + } + let Some(current) = node.data().is_expanded() else { + return Err(invalid_operation()); + }; + if current == expanded { + return Err(invalid_operation()); + } + Ok(Some(ActionRequest { + action: if expanded { + Action::Expand + } else { + Action::Collapse + }, + target_tree, + target_node, + data: None, + })) + }) + } + fn set_selected(&self, selected: bool) -> Result<()> { self.do_complex_action(|node, target_node, target_tree| { if node.is_disabled() { @@ -1251,6 +1296,7 @@ properties! { (UIA_OrientationPropertyId, orientation), (UIA_IsRequiredForFormPropertyId, is_required), (UIA_IsPasswordPropertyId, is_password), + (UIA_LevelPropertyId, level), (UIA_PositionInSetPropertyId, position_in_set), (UIA_SizeOfSetPropertyId, size_of_set), (UIA_AriaPropertiesPropertyId, aria_properties), @@ -1407,6 +1453,17 @@ patterns! { }) } )), + (UIA_ExpandCollapsePatternId, IExpandCollapseProvider, IExpandCollapseProvider_Impl, is_expand_collapse_pattern_supported, ( + (UIA_ExpandCollapseExpandCollapseStatePropertyId, ExpandCollapseState, expand_collapse_state, ExpandCollapseState) + ), ( + fn Expand(&self) -> Result<()> { + self.set_expanded(true) + }, + + fn Collapse(&self) -> Result<()> { + self.set_expanded(false) + } + )), (UIA_WindowPatternId, IWindowProvider, IWindowProvider_Impl, is_window_pattern_supported, ( (UIA_WindowIsModalPropertyId, IsModal, is_modal, BOOL) ), ( diff --git a/platforms/windows/src/util.rs b/platforms/windows/src/util.rs index 44e21efa..57f352f7 100644 --- a/platforms/windows/src/util.rs +++ b/platforms/windows/src/util.rs @@ -122,6 +122,12 @@ impl From for Variant { } } +impl From for Variant { + fn from(value: ExpandCollapseState) -> Self { + Self(value.0.into()) + } +} + impl From for Variant { fn from(value: LiveSetting) -> Self { Self(value.0.into())