Skip to content

Commit 1576a94

Browse files
committed
fix: Expose dialogs in the platform adapters
1 parent 028f611 commit 1576a94

File tree

5 files changed

+129
-12
lines changed

5 files changed

+129
-12
lines changed

platforms/atspi-common/src/node.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,11 @@ impl NodeWrapper<'_> {
279279
let state = self.0;
280280
let atspi_role = self.role();
281281
let mut atspi_state = StateSet::empty();
282-
if state.parent_id().is_none() && state.role() == Role::Window && is_window_focused {
282+
if is_window_focused
283+
&& ((state.parent_id().is_none() && state.role() == Role::Window)
284+
|| (state.is_dialog()
285+
&& state.tree_state.active_dialog().map(|d| d.id()) == Some(state.id())))
286+
{
283287
atspi_state.insert(State::Active);
284288
}
285289
if state.is_text_input() && !state.is_read_only() {
@@ -309,6 +313,9 @@ impl NodeWrapper<'_> {
309313
if atspi_role != AtspiRole::ToggleButton && state.toggled().is_some() {
310314
atspi_state.insert(State::Checkable);
311315
}
316+
if state.is_modal() {
317+
atspi_state.insert(State::Modal);
318+
}
312319
if let Some(selected) = state.is_selected() {
313320
if !state.is_disabled() {
314321
atspi_state.insert(State::Selectable);

platforms/macos/src/node.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ fn ns_role(node: &Node) -> &'static NSAccessibilityRole {
8787
Role::TimeInput => ns_string!("AXTimeField"),
8888
Role::Abbr => NSAccessibilityGroupRole,
8989
Role::Alert => NSAccessibilityGroupRole,
90-
Role::AlertDialog => NSAccessibilityGroupRole,
90+
Role::AlertDialog => NSAccessibilityWindowRole,
9191
Role::Application => NSAccessibilityGroupRole,
9292
Role::Article => NSAccessibilityGroupRole,
9393
Role::Audio => NSAccessibilityGroupRole,
@@ -108,7 +108,7 @@ fn ns_role(node: &Node) -> &'static NSAccessibilityRole {
108108
Role::Definition => NSAccessibilityGroupRole,
109109
Role::DescriptionList => NSAccessibilityListRole,
110110
Role::Details => NSAccessibilityGroupRole,
111-
Role::Dialog => NSAccessibilityGroupRole,
111+
Role::Dialog => NSAccessibilityWindowRole,
112112
Role::DisclosureTriangle => NSAccessibilityButtonRole,
113113
Role::Document => NSAccessibilityGroupRole,
114114
Role::EmbeddedObject => NSAccessibilityGroupRole,
@@ -235,7 +235,7 @@ fn ns_sub_role(node: &Node) -> &'static NSAccessibilitySubrole {
235235
unsafe {
236236
match role {
237237
Role::Alert => ns_string!("AXApplicationAlert"),
238-
Role::AlertDialog => ns_string!("AXApplicationAlertDialog"),
238+
Role::AlertDialog => NSAccessibilityDialogSubrole,
239239
Role::Article => ns_string!("AXDocumentArticle"),
240240
Role::Banner => ns_string!("AXLandmarkBanner"),
241241
Role::Button if node.toggled().is_some() => NSAccessibilityToggleSubrole,
@@ -996,6 +996,12 @@ declare_class!(
996996
.flatten()
997997
}
998998

999+
#[method(isAccessibilityModal)]
1000+
fn is_modal(&self) -> bool {
1001+
self.resolve(|node| node.is_modal())
1002+
.unwrap_or(false)
1003+
}
1004+
9991005
// We discovered through experimentation that when mixing the newer
10001006
// NSAccessibility protocols with the older informal protocol,
10011007
// the platform uses both protocols to discover which actions are
@@ -1080,6 +1086,9 @@ declare_class!(
10801086
if selector == sel!(accessibilityTabs) {
10811087
return node.role() == Role::TabList;
10821088
}
1089+
if selector == sel!(isAccessibilityModal) {
1090+
return node.is_dialog();
1091+
}
10831092
if selector == sel!(accessibilityAttributeValue:) {
10841093
return node.has_braille_label() || node.has_braille_role_description()
10851094
}

platforms/windows/src/adapter.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,14 @@ impl TreeChangeHandler for AdapterChangeHandler<'_> {
214214
return;
215215
}
216216
let wrapper = NodeWrapper(node);
217+
if node.is_dialog() {
218+
let platform_node = PlatformNode::new(self.context, node.id());
219+
let element: IRawElementProviderSimple = platform_node.into();
220+
self.queue.push(QueuedEvent::Simple {
221+
element,
222+
event_id: UIA_Window_WindowOpenedEventId,
223+
});
224+
}
217225
if wrapper.name().is_some() && node.live() != Live::Off {
218226
let platform_node = PlatformNode::new(self.context, node.id());
219227
let element: IRawElementProviderSimple = platform_node.into();
@@ -234,6 +242,14 @@ impl TreeChangeHandler for AdapterChangeHandler<'_> {
234242
let old_node_was_filtered_out = filter(old_node) != FilterResult::Include;
235243
if filter(new_node) != FilterResult::Include {
236244
if !old_node_was_filtered_out {
245+
if old_node.is_dialog() {
246+
let platform_node = PlatformNode::new(self.context, old_node.id());
247+
let element: IRawElementProviderSimple = platform_node.into();
248+
self.queue.push(QueuedEvent::Simple {
249+
element,
250+
event_id: UIA_Window_WindowClosedEventId,
251+
});
252+
}
237253
let old_wrapper = NodeWrapper(old_node);
238254
if old_wrapper.is_selection_item_pattern_supported() && old_wrapper.is_selected() {
239255
self.handle_selection_state_change(old_node, false);
@@ -263,6 +279,14 @@ impl TreeChangeHandler for AdapterChangeHandler<'_> {
263279
event_id: UIA_LiveRegionChangedEventId,
264280
});
265281
}
282+
if old_node_was_filtered_out && new_node.is_dialog() {
283+
let platform_node = PlatformNode::new(self.context, new_node.id());
284+
let element: IRawElementProviderSimple = platform_node.into();
285+
self.queue.push(QueuedEvent::Simple {
286+
element,
287+
event_id: UIA_Window_WindowOpenedEventId,
288+
});
289+
}
266290
if new_wrapper.is_selection_item_pattern_supported()
267291
&& (new_wrapper.is_selected() != old_wrapper.is_selected()
268292
|| (old_node_was_filtered_out && new_wrapper.is_selected()))
@@ -282,6 +306,14 @@ impl TreeChangeHandler for AdapterChangeHandler<'_> {
282306
if filter(node) != FilterResult::Include {
283307
return;
284308
}
309+
if node.is_dialog() {
310+
let platform_node = PlatformNode::new(self.context, node.id());
311+
let element: IRawElementProviderSimple = platform_node.into();
312+
self.queue.push(QueuedEvent::Simple {
313+
element,
314+
event_id: UIA_Window_WindowClosedEventId,
315+
});
316+
}
285317
let wrapper = NodeWrapper(node);
286318
if wrapper.is_selection_item_pattern_supported() {
287319
self.handle_selection_state_change(node, false);

platforms/windows/src/node.rs

Lines changed: 73 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -96,11 +96,10 @@ impl NodeWrapper<'_> {
9696
Role::Abbr => UIA_TextControlTypeId,
9797
Role::Alert => UIA_TextControlTypeId,
9898
Role::AlertDialog => {
99-
// Chromium's implementation suggests the use of
100-
// UIA_TextControlTypeId, not UIA_PaneControlTypeId, because some
101-
// Windows screen readers are not compatible with
102-
// Role::AlertDialog yet.
103-
UIA_TextControlTypeId
99+
// Documentation suggests the use of UIA_PaneControlTypeId,
100+
// but Chromium's implementation uses UIA_WindowControlTypeId
101+
// instead.
102+
UIA_WindowControlTypeId
104103
}
105104
Role::Application => UIA_PaneControlTypeId,
106105
Role::Article => UIA_GroupControlTypeId,
@@ -121,7 +120,12 @@ impl NodeWrapper<'_> {
121120
Role::Definition => UIA_GroupControlTypeId,
122121
Role::DescriptionList => UIA_ListControlTypeId,
123122
Role::Details => UIA_GroupControlTypeId,
124-
Role::Dialog => UIA_PaneControlTypeId,
123+
Role::Dialog => {
124+
// Documentation suggests the use of UIA_PaneControlTypeId,
125+
// but Chromium's implementation uses UIA_WindowControlTypeId
126+
// instead.
127+
UIA_WindowControlTypeId
128+
}
125129
Role::DisclosureTriangle => UIA_ButtonControlTypeId,
126130
Role::Document | Role::Terminal => UIA_DocumentControlTypeId,
127131
Role::EmbeddedObject => UIA_PaneControlTypeId,
@@ -256,6 +260,17 @@ impl NodeWrapper<'_> {
256260
self.0.role_description()
257261
}
258262

263+
fn aria_role(&self) -> Option<&str> {
264+
match self.0.role() {
265+
Role::AlertDialog => Some("alertdialog"),
266+
Role::Dialog => Some("dialog"),
267+
_ => {
268+
// TODO: Expose more ARIA roles.
269+
None
270+
}
271+
}
272+
}
273+
259274
pub(crate) fn name(&self) -> Option<WideString> {
260275
let mut result = WideString::default();
261276
if self.0.label_comes_from_value() {
@@ -467,6 +482,18 @@ impl NodeWrapper<'_> {
467482
self.0.role() == Role::PasswordInput
468483
}
469484

485+
fn is_dialog(&self) -> bool {
486+
self.0.is_dialog()
487+
}
488+
489+
fn is_window_pattern_supported(&self) -> bool {
490+
self.0.is_dialog()
491+
}
492+
493+
fn is_modal(&self) -> bool {
494+
self.0.is_modal()
495+
}
496+
470497
pub(crate) fn enqueue_property_changes(
471498
&self,
472499
queue: &mut Vec<QueuedEvent>,
@@ -526,7 +553,8 @@ impl NodeWrapper<'_> {
526553
IScrollItemProvider,
527554
ISelectionItemProvider,
528555
ISelectionProvider,
529-
ITextProvider
556+
ITextProvider,
557+
IWindowProvider
530558
)]
531559
pub(crate) struct PlatformNode {
532560
pub(crate) context: Weak<Context>,
@@ -980,6 +1008,7 @@ macro_rules! patterns {
9801008
properties! {
9811009
(UIA_ControlTypePropertyId, control_type),
9821010
(UIA_LocalizedControlTypePropertyId, localized_control_type),
1011+
(UIA_AriaRolePropertyId, aria_role),
9831012
(UIA_NamePropertyId, name),
9841013
(UIA_FullDescriptionPropertyId, description),
9851014
(UIA_CulturePropertyId, culture),
@@ -997,7 +1026,8 @@ properties! {
9971026
(UIA_IsPasswordPropertyId, is_password),
9981027
(UIA_PositionInSetPropertyId, position_in_set),
9991028
(UIA_SizeOfSetPropertyId, size_of_set),
1000-
(UIA_AriaPropertiesPropertyId, aria_properties)
1029+
(UIA_AriaPropertiesPropertyId, aria_properties),
1030+
(UIA_IsDialogPropertyId, is_dialog)
10011031
}
10021032

10031033
patterns! {
@@ -1148,6 +1178,41 @@ patterns! {
11481178
}
11491179
})
11501180
}
1181+
)),
1182+
(UIA_WindowPatternId, IWindowProvider, IWindowProvider_Impl, is_window_pattern_supported, (
1183+
(UIA_WindowIsModalPropertyId, IsModal, is_modal, BOOL)
1184+
), (
1185+
fn SetVisualState(&self, _: WindowVisualState) -> Result<()> {
1186+
Err(invalid_operation())
1187+
},
1188+
1189+
fn Close(&self) -> Result<()> {
1190+
Err(not_supported())
1191+
},
1192+
1193+
fn WaitForInputIdle(&self, _: i32) -> Result<BOOL> {
1194+
Err(not_supported())
1195+
},
1196+
1197+
fn CanMaximize(&self) -> Result<BOOL> {
1198+
Err(not_supported())
1199+
},
1200+
1201+
fn CanMinimize(&self) -> Result<BOOL> {
1202+
Err(not_supported())
1203+
},
1204+
1205+
fn WindowVisualState(&self) -> Result<WindowVisualState> {
1206+
Err(not_supported())
1207+
},
1208+
1209+
fn WindowInteractionState(&self) -> Result<WindowInteractionState> {
1210+
Ok(WindowInteractionState_ReadyForUserInteraction)
1211+
},
1212+
1213+
fn IsTopmost(&self) -> Result<BOOL> {
1214+
Err(not_supported())
1215+
}
11511216
))
11521217
}
11531218

platforms/windows/src/util.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,10 @@ pub(crate) fn invalid_operation() -> Error {
248248
HRESULT(UIA_E_INVALIDOPERATION as _).into()
249249
}
250250

251+
pub(crate) fn not_supported() -> Error {
252+
HRESULT(UIA_E_NOTSUPPORTED as _).into()
253+
}
254+
251255
pub(crate) fn client_top_left(hwnd: WindowHandle) -> Point {
252256
let mut result = POINT::default();
253257
// If ClientToScreen fails, that means the window is gone.

0 commit comments

Comments
 (0)