Skip to content

Add support for dialogs #549

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
8 changes: 8 additions & 0 deletions consumer/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
11 changes: 11 additions & 0 deletions consumer/src/tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,17 @@ impl State {
self.focus_id().map(|id| self.node_by_id(id).unwrap())
}

pub fn active_dialog(&self) -> Option<Node<'_>> {
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()
}
Expand Down
9 changes: 8 additions & 1 deletion platforms/atspi-common/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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);
Expand Down
32 changes: 32 additions & 0 deletions platforms/windows/src/adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
Expand Down Expand Up @@ -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()))
Expand All @@ -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);
Expand Down
81 changes: 73 additions & 8 deletions platforms/windows/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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<WideString> {
let mut result = WideString::default();
if self.0.label_comes_from_value() {
Expand Down Expand Up @@ -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<QueuedEvent>,
Expand Down Expand Up @@ -501,7 +528,8 @@ impl NodeWrapper<'_> {
IRangeValueProvider,
ISelectionItemProvider,
ISelectionProvider,
ITextProvider
ITextProvider,
IWindowProvider
)]
pub(crate) struct PlatformNode {
pub(crate) context: Weak<Context>,
Expand Down Expand Up @@ -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),
Expand All @@ -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! {
Expand Down Expand Up @@ -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<BOOL> {
Err(not_supported())
},

fn CanMaximize(&self) -> Result<BOOL> {
Err(not_supported())
},

fn CanMinimize(&self) -> Result<BOOL> {
Err(not_supported())
},

fn WindowVisualState(&self) -> Result<WindowVisualState> {
Err(not_supported())
},

fn WindowInteractionState(&self) -> Result<WindowInteractionState> {
Ok(WindowInteractionState_ReadyForUserInteraction)
},

fn IsTopmost(&self) -> Result<BOOL> {
Err(not_supported())
}
))
}

Expand Down
4 changes: 4 additions & 0 deletions platforms/windows/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading