diff --git a/consumer/src/node.rs b/consumer/src/node.rs index 5154ac121..4053b61ba 100644 --- a/consumer/src/node.rs +++ b/consumer/src/node.rs @@ -415,6 +415,14 @@ impl<'a> Node<'a> { }) } + pub fn is_dialog(&self) -> bool { + matches!(self.role(), Role::AlertDialog | Role::Dialog) + } + + pub fn is_modal(&self) -> bool { + self.data().is_modal() + } + // When probing for supported actions as the next several functions do, // it's tempting to check the role. But it's better to not assume anything // beyond what the provider has explicitly told us. Rationale: diff --git a/consumer/src/tree.rs b/consumer/src/tree.rs index 371582410..dd386fbf3 100644 --- a/consumer/src/tree.rs +++ b/consumer/src/tree.rs @@ -221,6 +221,17 @@ impl State { self.focus_id().map(|id| self.node_by_id(id).unwrap()) } + pub fn active_dialog(&self) -> Option> { + let mut node = self.focus(); + while let Some(candidate) = node { + if candidate.is_dialog() { + return Some(candidate); + } + node = candidate.parent(); + } + None + } + pub fn toolkit_name(&self) -> Option<&str> { self.data.toolkit_name.as_deref() } diff --git a/platforms/atspi-common/src/node.rs b/platforms/atspi-common/src/node.rs index 2e47aa23e..e69d5a0af 100644 --- a/platforms/atspi-common/src/node.rs +++ b/platforms/atspi-common/src/node.rs @@ -284,7 +284,11 @@ impl NodeWrapper<'_> { let state = self.0; let atspi_role = self.role(); let mut atspi_state = StateSet::empty(); - if state.parent_id().is_none() && state.role() == Role::Window && is_window_focused { + if is_window_focused + && ((state.parent_id().is_none() && state.role() == Role::Window) + || (state.is_dialog() + && state.tree_state.active_dialog().map(|d| d.id()) == Some(state.id()))) + { atspi_state.insert(State::Active); } if state.is_text_input() && !state.is_read_only() { @@ -314,6 +318,9 @@ impl NodeWrapper<'_> { if atspi_role != AtspiRole::ToggleButton && state.toggled().is_some() { atspi_state.insert(State::Checkable); } + if state.is_modal() { + atspi_state.insert(State::Modal); + } if let Some(selected) = state.is_selected() { if !state.is_disabled() { atspi_state.insert(State::Selectable); diff --git a/platforms/windows/src/adapter.rs b/platforms/windows/src/adapter.rs index d24387a7f..0c889f34f 100644 --- a/platforms/windows/src/adapter.rs +++ b/platforms/windows/src/adapter.rs @@ -214,6 +214,14 @@ impl TreeChangeHandler for AdapterChangeHandler<'_> { return; } let wrapper = NodeWrapper(node); + if node.is_dialog() { + let platform_node = PlatformNode::new(self.context, node.id()); + let element: IRawElementProviderSimple = platform_node.into(); + self.queue.push(QueuedEvent::Simple { + element, + event_id: UIA_Window_WindowOpenedEventId, + }); + } if wrapper.name().is_some() && node.live() != Live::Off { let platform_node = PlatformNode::new(self.context, node.id()); let element: IRawElementProviderSimple = platform_node.into(); @@ -234,6 +242,14 @@ impl TreeChangeHandler for AdapterChangeHandler<'_> { let old_node_was_filtered_out = filter(old_node) != FilterResult::Include; if filter(new_node) != FilterResult::Include { if !old_node_was_filtered_out { + if old_node.is_dialog() { + let platform_node = PlatformNode::new(self.context, old_node.id()); + let element: IRawElementProviderSimple = platform_node.into(); + self.queue.push(QueuedEvent::Simple { + element, + event_id: UIA_Window_WindowClosedEventId, + }); + } let old_wrapper = NodeWrapper(old_node); if old_wrapper.is_selection_item_pattern_supported() && old_wrapper.is_selected() { self.handle_selection_state_change(old_node, false); @@ -263,6 +279,14 @@ impl TreeChangeHandler for AdapterChangeHandler<'_> { event_id: UIA_LiveRegionChangedEventId, }); } + if old_node_was_filtered_out && new_node.is_dialog() { + let platform_node = PlatformNode::new(self.context, new_node.id()); + let element: IRawElementProviderSimple = platform_node.into(); + self.queue.push(QueuedEvent::Simple { + element, + event_id: UIA_Window_WindowOpenedEventId, + }); + } if new_wrapper.is_selection_item_pattern_supported() && (new_wrapper.is_selected() != old_wrapper.is_selected() || (old_node_was_filtered_out && new_wrapper.is_selected())) @@ -282,6 +306,14 @@ impl TreeChangeHandler for AdapterChangeHandler<'_> { if filter(node) != FilterResult::Include { return; } + if node.is_dialog() { + let platform_node = PlatformNode::new(self.context, node.id()); + let element: IRawElementProviderSimple = platform_node.into(); + self.queue.push(QueuedEvent::Simple { + element, + event_id: UIA_Window_WindowClosedEventId, + }); + } let wrapper = NodeWrapper(node); if wrapper.is_selection_item_pattern_supported() { self.handle_selection_state_change(node, false); diff --git a/platforms/windows/src/node.rs b/platforms/windows/src/node.rs index e2ff22bd6..7c3584082 100644 --- a/platforms/windows/src/node.rs +++ b/platforms/windows/src/node.rs @@ -96,11 +96,10 @@ impl NodeWrapper<'_> { Role::Abbr => UIA_TextControlTypeId, Role::Alert => UIA_TextControlTypeId, Role::AlertDialog => { - // Chromium's implementation suggests the use of - // UIA_TextControlTypeId, not UIA_PaneControlTypeId, because some - // Windows screen readers are not compatible with - // Role::AlertDialog yet. - UIA_TextControlTypeId + // Documentation suggests the use of UIA_PaneControlTypeId, + // but Chromium's implementation uses UIA_WindowControlTypeId + // instead. + UIA_WindowControlTypeId } Role::Application => UIA_PaneControlTypeId, Role::Article => UIA_GroupControlTypeId, @@ -123,7 +122,12 @@ impl NodeWrapper<'_> { Role::DescriptionListDetail => UIA_TextControlTypeId, Role::DescriptionListTerm => UIA_ListItemControlTypeId, Role::Details => UIA_GroupControlTypeId, - Role::Dialog => UIA_PaneControlTypeId, + Role::Dialog => { + // Documentation suggests the use of UIA_PaneControlTypeId, + // but Chromium's implementation uses UIA_WindowControlTypeId + // instead. + UIA_WindowControlTypeId + } Role::Directory => UIA_ListControlTypeId, Role::DisclosureTriangle => UIA_ButtonControlTypeId, Role::Document | Role::Terminal => UIA_DocumentControlTypeId, @@ -261,6 +265,17 @@ impl NodeWrapper<'_> { self.0.role_description() } + fn aria_role(&self) -> Option<&str> { + match self.0.role() { + Role::AlertDialog => Some("alertdialog"), + Role::Dialog => Some("dialog"), + _ => { + // TODO: Expose more ARIA roles. + None + } + } + } + pub(crate) fn name(&self) -> Option { let mut result = WideString::default(); if self.0.label_comes_from_value() { @@ -443,6 +458,18 @@ impl NodeWrapper<'_> { self.0.role() == Role::PasswordInput } + fn is_dialog(&self) -> bool { + self.0.is_dialog() + } + + fn is_window_pattern_supported(&self) -> bool { + self.0.is_dialog() + } + + fn is_modal(&self) -> bool { + self.0.is_modal() + } + pub(crate) fn enqueue_property_changes( &self, queue: &mut Vec, @@ -501,7 +528,8 @@ impl NodeWrapper<'_> { IRangeValueProvider, ISelectionItemProvider, ISelectionProvider, - ITextProvider + ITextProvider, + IWindowProvider )] pub(crate) struct PlatformNode { pub(crate) context: Weak, @@ -948,6 +976,7 @@ macro_rules! patterns { properties! { (UIA_ControlTypePropertyId, control_type), (UIA_LocalizedControlTypePropertyId, localized_control_type), + (UIA_AriaRolePropertyId, aria_role), (UIA_NamePropertyId, name), (UIA_FullDescriptionPropertyId, description), (UIA_HelpTextPropertyId, placeholder), @@ -963,7 +992,8 @@ properties! { (UIA_IsRequiredForFormPropertyId, is_required), (UIA_IsPasswordPropertyId, is_password), (UIA_PositionInSetPropertyId, position_in_set), - (UIA_SizeOfSetPropertyId, size_of_set) + (UIA_SizeOfSetPropertyId, size_of_set), + (UIA_IsDialogPropertyId, is_dialog) } patterns! { @@ -1103,6 +1133,41 @@ patterns! { } }) } + )), + (UIA_WindowPatternId, IWindowProvider, IWindowProvider_Impl, is_window_pattern_supported, ( + (UIA_WindowIsModalPropertyId, IsModal, is_modal, BOOL) + ), ( + fn SetVisualState(&self, _: WindowVisualState) -> Result<()> { + Err(invalid_operation()) + }, + + fn Close(&self) -> Result<()> { + Err(not_supported()) + }, + + fn WaitForInputIdle(&self, _: i32) -> Result { + Err(not_supported()) + }, + + fn CanMaximize(&self) -> Result { + Err(not_supported()) + }, + + fn CanMinimize(&self) -> Result { + Err(not_supported()) + }, + + fn WindowVisualState(&self) -> Result { + Err(not_supported()) + }, + + fn WindowInteractionState(&self) -> Result { + Ok(WindowInteractionState_ReadyForUserInteraction) + }, + + fn IsTopmost(&self) -> Result { + Err(not_supported()) + } )) } diff --git a/platforms/windows/src/util.rs b/platforms/windows/src/util.rs index a1260cebb..321500780 100644 --- a/platforms/windows/src/util.rs +++ b/platforms/windows/src/util.rs @@ -237,6 +237,10 @@ pub(crate) fn invalid_operation() -> Error { HRESULT(UIA_E_INVALIDOPERATION as _).into() } +pub(crate) fn not_supported() -> Error { + HRESULT(UIA_E_NOTSUPPORTED as _).into() +} + pub(crate) fn client_top_left(hwnd: WindowHandle) -> Point { let mut result = POINT::default(); // If ClientToScreen fails, that means the window is gone.