1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "content/browser/accessibility/browser_accessibility_manager_win.h"
6
7 #include <stddef.h>
8 #include <stdint.h>
9
10 #include <set>
11 #include <vector>
12
13 #include "base/command_line.h"
14 #include "base/stl_util.h"
15 #include "base/win/scoped_variant.h"
16 #include "base/win/windows_version.h"
17 #include "content/browser/accessibility/browser_accessibility_state_impl.h"
18 #include "content/browser/accessibility/browser_accessibility_win.h"
19 #include "content/browser/renderer_host/legacy_render_widget_host_win.h"
20 #include "content/public/common/content_switches.h"
21 #include "content/public/common/use_zoom_for_dsf_policy.h"
22 #include "ui/accessibility/accessibility_switches.h"
23 #include "ui/accessibility/ax_role_properties.h"
24 #include "ui/accessibility/platform/ax_fragment_root_win.h"
25 #include "ui/accessibility/platform/ax_platform_node_delegate_utils_win.h"
26 #include "ui/accessibility/platform/uia_registrar_win.h"
27 #include "ui/base/win/atl_module.h"
28
29 namespace content {
30
31 namespace {
32
33 #if DCHECK_IS_ON()
34 #define DCHECK_IN_ON_ACCESSIBILITY_EVENTS() \
35 DCHECK(in_on_accessibility_events_) \
36 << "This method should only be called during OnAccessibilityEvents " \
37 "because memoized information is cleared afterwards in " \
38 "FinalizeAccessibilityEvents"
39 #else
40 #define DCHECK_IN_ON_ACCESSIBILITY_EVENTS()
41 #endif // DCHECK_IS_ON()
42
GetUiaTextPatternProvider(BrowserAccessibility & node)43 BrowserAccessibility* GetUiaTextPatternProvider(BrowserAccessibility& node) {
44 for (BrowserAccessibility* current_node = &node; current_node;
45 current_node = current_node->PlatformGetParent()) {
46 if (ToBrowserAccessibilityWin(current_node)
47 ->GetCOM()
48 ->IsPatternProviderSupported(UIA_TextPatternId)) {
49 return current_node;
50 }
51 }
52
53 return nullptr;
54 }
55
56 } // namespace
57
58 // static
Create(const ui::AXTreeUpdate & initial_tree,BrowserAccessibilityDelegate * delegate)59 BrowserAccessibilityManager* BrowserAccessibilityManager::Create(
60 const ui::AXTreeUpdate& initial_tree,
61 BrowserAccessibilityDelegate* delegate) {
62 return new BrowserAccessibilityManagerWin(initial_tree, delegate);
63 }
64
65 BrowserAccessibilityManagerWin*
ToBrowserAccessibilityManagerWin()66 BrowserAccessibilityManager::ToBrowserAccessibilityManagerWin() {
67 return static_cast<BrowserAccessibilityManagerWin*>(this);
68 }
69
BrowserAccessibilityManagerWin(const ui::AXTreeUpdate & initial_tree,BrowserAccessibilityDelegate * delegate)70 BrowserAccessibilityManagerWin::BrowserAccessibilityManagerWin(
71 const ui::AXTreeUpdate& initial_tree,
72 BrowserAccessibilityDelegate* delegate)
73 : BrowserAccessibilityManager(delegate), load_complete_pending_(false) {
74 ui::win::CreateATLModuleIfNeeded();
75 Initialize(initial_tree);
76 }
77
78 BrowserAccessibilityManagerWin::~BrowserAccessibilityManagerWin() = default;
79
80 // static
GetEmptyDocument()81 ui::AXTreeUpdate BrowserAccessibilityManagerWin::GetEmptyDocument() {
82 ui::AXNodeData empty_document;
83 empty_document.id = 1;
84 empty_document.role = ax::mojom::Role::kRootWebArea;
85 empty_document.AddBoolAttribute(ax::mojom::BoolAttribute::kBusy, true);
86 ui::AXTreeUpdate update;
87 update.root_id = empty_document.id;
88 update.nodes.push_back(empty_document);
89 return update;
90 }
91
GetParentHWND()92 HWND BrowserAccessibilityManagerWin::GetParentHWND() {
93 BrowserAccessibilityDelegate* delegate = GetDelegateFromRootManager();
94 if (!delegate)
95 return NULL;
96 return delegate->AccessibilityGetAcceleratedWidget();
97 }
98
UserIsReloading()99 void BrowserAccessibilityManagerWin::UserIsReloading() {
100 if (GetRoot())
101 FireWinAccessibilityEvent(IA2_EVENT_DOCUMENT_RELOAD, GetRoot());
102 }
103
GetFocus() const104 BrowserAccessibility* BrowserAccessibilityManagerWin::GetFocus() const {
105 BrowserAccessibility* focus = BrowserAccessibilityManager::GetFocus();
106 return GetActiveDescendant(focus);
107 }
108
FireFocusEvent(BrowserAccessibility * node)109 void BrowserAccessibilityManagerWin::FireFocusEvent(
110 BrowserAccessibility* node) {
111 BrowserAccessibilityManager::FireFocusEvent(node);
112 DCHECK(node);
113 FireWinAccessibilityEvent(EVENT_OBJECT_FOCUS, node);
114 FireUiaAccessibilityEvent(UIA_AutomationFocusChangedEventId, node);
115 }
116
FireBlinkEvent(ax::mojom::Event event_type,BrowserAccessibility * node)117 void BrowserAccessibilityManagerWin::FireBlinkEvent(
118 ax::mojom::Event event_type,
119 BrowserAccessibility* node) {
120 BrowserAccessibilityManager::FireBlinkEvent(event_type, node);
121 switch (event_type) {
122 case ax::mojom::Event::kClicked:
123 if (node->GetData().IsInvocable())
124 FireUiaAccessibilityEvent(UIA_Invoke_InvokedEventId, node);
125 break;
126 case ax::mojom::Event::kEndOfTest:
127 // Event tests use kEndOfTest as a sentinel to mark the end of the test.
128 FireUiaAccessibilityEvent(
129 ui::UiaRegistrarWin::GetInstance().GetUiaTestCompleteEventId(), node);
130 break;
131 case ax::mojom::Event::kLocationChanged:
132 FireWinAccessibilityEvent(IA2_EVENT_VISIBLE_DATA_CHANGED, node);
133 break;
134 case ax::mojom::Event::kScrolledToAnchor:
135 FireWinAccessibilityEvent(EVENT_SYSTEM_SCROLLINGSTART, node);
136 break;
137 case ax::mojom::Event::kTextChanged:
138 // TODO(crbug.com/1049261) Remove when Views are exposed in the AXTree
139 // which will fire generated text-changed events.
140 if (!node->IsWebContent())
141 HandleTextChangedEvent(*node);
142 break;
143 case ax::mojom::Event::kTextSelectionChanged:
144 HandleTextSelectionChangedEvent(*node);
145 break;
146 default:
147 break;
148 }
149 }
150
FireGeneratedEvent(ui::AXEventGenerator::Event event_type,BrowserAccessibility * node)151 void BrowserAccessibilityManagerWin::FireGeneratedEvent(
152 ui::AXEventGenerator::Event event_type,
153 BrowserAccessibility* node) {
154 BrowserAccessibilityManager::FireGeneratedEvent(event_type, node);
155 bool can_fire_events = CanFireEvents();
156
157 if (event_type == ui::AXEventGenerator::Event::LOAD_COMPLETE &&
158 can_fire_events)
159 load_complete_pending_ = false;
160
161 if (load_complete_pending_ && can_fire_events && GetRoot()) {
162 load_complete_pending_ = false;
163 FireWinAccessibilityEvent(IA2_EVENT_DOCUMENT_LOAD_COMPLETE, GetRoot());
164 }
165
166 if (!can_fire_events && !load_complete_pending_ &&
167 event_type == ui::AXEventGenerator::Event::LOAD_COMPLETE && GetRoot() &&
168 !GetRoot()->IsOffscreen() && GetRoot()->PlatformChildCount() > 0) {
169 load_complete_pending_ = true;
170 }
171
172 switch (event_type) {
173 case ui::AXEventGenerator::Event::ACCESS_KEY_CHANGED:
174 FireUiaPropertyChangedEvent(UIA_AccessKeyPropertyId, node);
175 break;
176 case ui::AXEventGenerator::Event::ACTIVE_DESCENDANT_CHANGED:
177 FireWinAccessibilityEvent(IA2_EVENT_ACTIVE_DESCENDANT_CHANGED, node);
178 break;
179 case ui::AXEventGenerator::Event::ALERT:
180 FireWinAccessibilityEvent(EVENT_SYSTEM_ALERT, node);
181 FireUiaAccessibilityEvent(UIA_SystemAlertEventId, node);
182 break;
183 case ui::AXEventGenerator::Event::ATOMIC_CHANGED:
184 HandleAriaPropertiesChangedEvent(*node);
185 break;
186 case ui::AXEventGenerator::Event::BUSY_CHANGED:
187 HandleAriaPropertiesChangedEvent(*node);
188 break;
189 case ui::AXEventGenerator::Event::CHECKED_STATE_CHANGED:
190 // https://www.w3.org/TR/core-aam-1.1/#mapping_state-property_table
191 // SelectionItem.IsSelected is set according to the True or False value of
192 // aria-checked for 'radio' and 'menuitemradio' roles.
193 if (ui::IsRadio(node->GetRole())) {
194 HandleSelectedStateChanged(uia_selection_events_, node,
195 IsUIANodeSelected(node));
196 }
197 FireUiaPropertyChangedEvent(UIA_ToggleToggleStatePropertyId, node);
198 HandleAriaPropertiesChangedEvent(*node);
199 break;
200 case ui::AXEventGenerator::Event::CHILDREN_CHANGED: {
201 // If this node is ignored, fire the event on the platform parent since
202 // ignored nodes cannot raise events.
203 BrowserAccessibility* target_node =
204 node->IsIgnored() ? node->PlatformGetParent() : node;
205 if (target_node) {
206 FireWinAccessibilityEvent(EVENT_OBJECT_REORDER, target_node);
207 FireUiaStructureChangedEvent(StructureChangeType_ChildrenReordered,
208 target_node);
209 }
210 break;
211 }
212 case ui::AXEventGenerator::Event::CLASS_NAME_CHANGED:
213 FireUiaPropertyChangedEvent(UIA_ClassNamePropertyId, node);
214 break;
215 case ui::AXEventGenerator::Event::COLLAPSED:
216 case ui::AXEventGenerator::Event::EXPANDED:
217 FireUiaPropertyChangedEvent(
218 UIA_ExpandCollapseExpandCollapseStatePropertyId, node);
219 HandleAriaPropertiesChangedEvent(*node);
220 break;
221 case ui::AXEventGenerator::Event::CONTROLS_CHANGED:
222 FireUiaPropertyChangedEvent(UIA_ControllerForPropertyId, node);
223 break;
224 case ui::AXEventGenerator::Event::DESCRIBED_BY_CHANGED:
225 FireUiaPropertyChangedEvent(UIA_DescribedByPropertyId, node);
226 break;
227 case ui::AXEventGenerator::Event::DESCRIPTION_CHANGED:
228 FireUiaPropertyChangedEvent(UIA_FullDescriptionPropertyId, node);
229 break;
230 case ui::AXEventGenerator::Event::DOCUMENT_SELECTION_CHANGED: {
231 // Fire the event on the object where the focus of the selection is.
232 ui::AXNode::AXID focus_id =
233 ax_tree()->GetUnignoredSelection().focus_object_id;
234 BrowserAccessibility* focus_object = GetFromID(focus_id);
235 if (focus_object) {
236 HandleTextSelectionChangedEvent(*focus_object);
237 if (BrowserAccessibility* text_field =
238 focus_object->GetTextFieldAncestor()) {
239 HandleTextSelectionChangedEvent(*text_field);
240
241 // Plain text fields (including input and textarea elements) have
242 // descendant objects that are part of their internal implementation
243 // in Blink, which are not exposed to platform APIs in the
244 // accessibility tree. Firing an event on such descendants will not
245 // reach the assistive software.
246 if (text_field->IsPlainTextField()) {
247 FireWinAccessibilityEvent(IA2_EVENT_TEXT_CARET_MOVED, text_field);
248 } else {
249 FireWinAccessibilityEvent(IA2_EVENT_TEXT_CARET_MOVED, focus_object);
250 }
251 } else {
252 // Fire the event on the root object, which in the absence of a text
253 // field ancestor is the closest UIA text provider (other than the
254 // focused object) in which the selection has changed.
255 HandleTextSelectionChangedEvent(*node);
256
257 // "IA2_EVENT_TEXT_CARET_MOVED" should only be fired when a visible
258 // caret or a selection is present. In the case of a text field above,
259 // this is implicitly true.
260 if (node->HasVisibleCaretOrSelection())
261 FireWinAccessibilityEvent(IA2_EVENT_TEXT_CARET_MOVED, focus_object);
262 }
263 }
264 break;
265 }
266 // aria-dropeffect is deprecated in WAI-ARIA 1.1.
267 case ui::AXEventGenerator::Event::DROPEFFECT_CHANGED:
268 HandleAriaPropertiesChangedEvent(*node);
269 break;
270 case ui::AXEventGenerator::Event::EDITABLE_TEXT_CHANGED:
271 HandleTextChangedEvent(*node);
272 break;
273 case ui::AXEventGenerator::Event::ENABLED_CHANGED:
274 FireUiaPropertyChangedEvent(UIA_IsEnabledPropertyId, node);
275 HandleAriaPropertiesChangedEvent(*node);
276 break;
277 case ui::AXEventGenerator::Event::FLOW_FROM_CHANGED:
278 FireUiaPropertyChangedEvent(UIA_FlowsFromPropertyId, node);
279 break;
280 case ui::AXEventGenerator::Event::FLOW_TO_CHANGED:
281 FireUiaPropertyChangedEvent(UIA_FlowsToPropertyId, node);
282 break;
283 // aria-grabbed is deprecated in WAI-ARIA 1.1.
284 case ui::AXEventGenerator::Event::GRABBED_CHANGED:
285 HandleAriaPropertiesChangedEvent(*node);
286 break;
287 case ui::AXEventGenerator::Event::HASPOPUP_CHANGED:
288 HandleAriaPropertiesChangedEvent(*node);
289 break;
290 case ui::AXEventGenerator::Event::HIERARCHICAL_LEVEL_CHANGED:
291 FireUiaPropertyChangedEvent(UIA_LevelPropertyId, node);
292 HandleAriaPropertiesChangedEvent(*node);
293 break;
294 case ui::AXEventGenerator::Event::IGNORED_CHANGED:
295 if (node->IsIgnored()) {
296 FireWinAccessibilityEvent(EVENT_OBJECT_HIDE, node);
297 FireUiaStructureChangedEvent(StructureChangeType_ChildRemoved, node);
298 if (node->GetRole() == ax::mojom::Role::kMenu) {
299 FireWinAccessibilityEvent(EVENT_SYSTEM_MENUPOPUPEND, node);
300 FireUiaAccessibilityEvent(UIA_MenuClosedEventId, node);
301 }
302 }
303 HandleAriaPropertiesChangedEvent(*node);
304 break;
305 case ui::AXEventGenerator::Event::IMAGE_ANNOTATION_CHANGED:
306 FireWinAccessibilityEvent(EVENT_OBJECT_NAMECHANGE, node);
307 break;
308 case ui::AXEventGenerator::Event::INVALID_STATUS_CHANGED:
309 FireUiaPropertyChangedEvent(UIA_IsDataValidForFormPropertyId, node);
310 HandleAriaPropertiesChangedEvent(*node);
311 break;
312 case ui::AXEventGenerator::Event::KEY_SHORTCUTS_CHANGED:
313 FireUiaPropertyChangedEvent(UIA_AcceleratorKeyPropertyId, node);
314 break;
315 case ui::AXEventGenerator::Event::LABELED_BY_CHANGED:
316 FireUiaPropertyChangedEvent(UIA_LabeledByPropertyId, node);
317 break;
318 case ui::AXEventGenerator::Event::LANGUAGE_CHANGED:
319 FireUiaPropertyChangedEvent(UIA_CulturePropertyId, node);
320 break;
321 case ui::AXEventGenerator::Event::LIVE_REGION_CREATED:
322 FireUiaAccessibilityEvent(UIA_LiveRegionChangedEventId, node);
323 break;
324 case ui::AXEventGenerator::Event::LIVE_REGION_CHANGED:
325 // This event is redundant with the IA2_EVENT_TEXT_INSERTED events;
326 // however, JAWS 2018 and earlier do not process the text inserted
327 // events when "virtual cursor mode" is turned off (Insert+Z).
328 // Fortunately, firing the redudant event does not cause duplicate
329 // verbalizations in either screen reader.
330 // Future versions of JAWS may process the text inserted event when
331 // in focus mode, and so at some point the live region
332 // changed events may truly become redundant with the text inserted
333 // events. Note: Firefox does not fire this event, but JAWS processes
334 // Firefox live region events differently (utilizes MSAA's
335 // EVENT_OBJECT_SHOW).
336 FireWinAccessibilityEvent(EVENT_OBJECT_LIVEREGIONCHANGED, node);
337 FireUiaAccessibilityEvent(UIA_LiveRegionChangedEventId, node);
338 break;
339 case ui::AXEventGenerator::Event::LIVE_RELEVANT_CHANGED:
340 HandleAriaPropertiesChangedEvent(*node);
341 break;
342 case ui::AXEventGenerator::Event::LIVE_STATUS_CHANGED:
343 FireUiaPropertyChangedEvent(UIA_LiveSettingPropertyId, node);
344 HandleAriaPropertiesChangedEvent(*node);
345 break;
346 case ui::AXEventGenerator::Event::LOAD_COMPLETE:
347 FireWinAccessibilityEvent(IA2_EVENT_DOCUMENT_LOAD_COMPLETE, node);
348 break;
349 case ui::AXEventGenerator::Event::LAYOUT_INVALIDATED:
350 FireUiaAccessibilityEvent(UIA_LayoutInvalidatedEventId, node);
351 break;
352 case ui::AXEventGenerator::Event::MULTILINE_STATE_CHANGED:
353 HandleAriaPropertiesChangedEvent(*node);
354 break;
355 case ui::AXEventGenerator::Event::MULTISELECTABLE_STATE_CHANGED:
356 FireUiaPropertyChangedEvent(UIA_SelectionCanSelectMultiplePropertyId,
357 node);
358 HandleAriaPropertiesChangedEvent(*node);
359 break;
360 case ui::AXEventGenerator::Event::NAME_CHANGED:
361 if (ui::IsText(node->GetRole())) {
362 HandleTextChangedEvent(*node);
363 } else {
364 FireUiaPropertyChangedEvent(UIA_NamePropertyId, node);
365 }
366 // Only fire name changes when the name comes from an attribute, otherwise
367 // name changes are redundant with text removed/inserted events.
368 if (node->GetData().GetNameFrom() != ax::mojom::NameFrom::kContents)
369 FireWinAccessibilityEvent(EVENT_OBJECT_NAMECHANGE, node);
370 break;
371 case ui::AXEventGenerator::Event::OBJECT_ATTRIBUTE_CHANGED:
372 FireWinAccessibilityEvent(IA2_EVENT_OBJECT_ATTRIBUTE_CHANGED, node);
373 // TODO(crbug.com/1108871): Fire UIA event.
374 break;
375 case ui::AXEventGenerator::Event::PLACEHOLDER_CHANGED:
376 FireUiaPropertyChangedEvent(UIA_HelpTextPropertyId, node);
377 break;
378 case ui::AXEventGenerator::Event::POSITION_IN_SET_CHANGED:
379 FireUiaPropertyChangedEvent(UIA_PositionInSetPropertyId, node);
380 HandleAriaPropertiesChangedEvent(*node);
381 break;
382 case ui::AXEventGenerator::Event::RANGE_VALUE_CHANGED:
383 DCHECK(node->GetData().IsRangeValueSupported());
384 FireWinAccessibilityEvent(EVENT_OBJECT_VALUECHANGE, node);
385 FireUiaPropertyChangedEvent(UIA_RangeValueValuePropertyId, node);
386 HandleAriaPropertiesChangedEvent(*node);
387 break;
388 case ui::AXEventGenerator::Event::RANGE_VALUE_MAX_CHANGED:
389 DCHECK(node->GetData().IsRangeValueSupported());
390 FireUiaPropertyChangedEvent(UIA_RangeValueMaximumPropertyId, node);
391 HandleAriaPropertiesChangedEvent(*node);
392 break;
393 case ui::AXEventGenerator::Event::RANGE_VALUE_MIN_CHANGED:
394 DCHECK(node->GetData().IsRangeValueSupported());
395 FireUiaPropertyChangedEvent(UIA_RangeValueMinimumPropertyId, node);
396 HandleAriaPropertiesChangedEvent(*node);
397 break;
398 case ui::AXEventGenerator::Event::RANGE_VALUE_STEP_CHANGED:
399 DCHECK(node->GetData().IsRangeValueSupported());
400 FireUiaPropertyChangedEvent(UIA_RangeValueSmallChangePropertyId, node);
401 FireUiaPropertyChangedEvent(UIA_RangeValueLargeChangePropertyId, node);
402 HandleAriaPropertiesChangedEvent(*node);
403 break;
404 case ui::AXEventGenerator::Event::READONLY_CHANGED:
405 if (node->GetData().IsRangeValueSupported())
406 FireUiaPropertyChangedEvent(UIA_RangeValueIsReadOnlyPropertyId, node);
407 else if (ui::IsValuePatternSupported(node))
408 FireUiaPropertyChangedEvent(UIA_ValueIsReadOnlyPropertyId, node);
409 HandleAriaPropertiesChangedEvent(*node);
410 break;
411 case ui::AXEventGenerator::Event::REQUIRED_STATE_CHANGED:
412 FireUiaPropertyChangedEvent(UIA_IsRequiredForFormPropertyId, node);
413 HandleAriaPropertiesChangedEvent(*node);
414 break;
415 case ui::AXEventGenerator::Event::ROLE_CHANGED:
416 FireUiaPropertyChangedEvent(UIA_AriaRolePropertyId, node);
417 break;
418 case ui::AXEventGenerator::Event::SCROLL_HORIZONTAL_POSITION_CHANGED:
419 FireWinAccessibilityEvent(EVENT_SYSTEM_SCROLLINGEND, node);
420 FireUiaPropertyChangedEvent(UIA_ScrollHorizontalScrollPercentPropertyId,
421 node);
422 break;
423 case ui::AXEventGenerator::Event::SCROLL_VERTICAL_POSITION_CHANGED:
424 FireWinAccessibilityEvent(EVENT_SYSTEM_SCROLLINGEND, node);
425 FireUiaPropertyChangedEvent(UIA_ScrollVerticalScrollPercentPropertyId,
426 node);
427 break;
428 case ui::AXEventGenerator::Event::SELECTED_CHANGED:
429 HandleSelectedStateChanged(ia2_selection_events_, node,
430 IsIA2NodeSelected(node));
431 HandleSelectedStateChanged(uia_selection_events_, node,
432 IsUIANodeSelected(node));
433 HandleAriaPropertiesChangedEvent(*node);
434 break;
435 case ui::AXEventGenerator::Event::SELECTED_CHILDREN_CHANGED:
436 FireWinAccessibilityEvent(EVENT_OBJECT_SELECTIONWITHIN, node);
437 break;
438 case ui::AXEventGenerator::Event::SELECTED_VALUE_CHANGED:
439 DCHECK(ui::IsSelectElement(node->GetRole()));
440 FireWinAccessibilityEvent(EVENT_OBJECT_VALUECHANGE, node);
441 FireUiaPropertyChangedEvent(UIA_ValueValuePropertyId, node);
442 // By changing the value of a combo box, the document's text contents will
443 // also have changed.
444 HandleTextChangedEvent(*node);
445 break;
446 case ui::AXEventGenerator::Event::SET_SIZE_CHANGED:
447 FireUiaPropertyChangedEvent(UIA_SizeOfSetPropertyId, node);
448 HandleAriaPropertiesChangedEvent(*node);
449 break;
450 case ui::AXEventGenerator::Event::SORT_CHANGED:
451 FireWinAccessibilityEvent(IA2_EVENT_OBJECT_ATTRIBUTE_CHANGED, node);
452 HandleAriaPropertiesChangedEvent(*node);
453 break;
454 case ui::AXEventGenerator::Event::SUBTREE_CREATED:
455 FireWinAccessibilityEvent(EVENT_OBJECT_SHOW, node);
456 FireUiaStructureChangedEvent(StructureChangeType_ChildAdded, node);
457 if (node->GetRole() == ax::mojom::Role::kMenu) {
458 FireWinAccessibilityEvent(EVENT_SYSTEM_MENUPOPUPSTART, node);
459 FireUiaAccessibilityEvent(UIA_MenuOpenedEventId, node);
460 }
461 break;
462 case ui::AXEventGenerator::Event::TEXT_ATTRIBUTE_CHANGED:
463 FireWinAccessibilityEvent(IA2_EVENT_TEXT_ATTRIBUTE_CHANGED, node);
464 HandleTextChangedEvent(*node);
465 break;
466 case ui::AXEventGenerator::Event::VALUE_IN_TEXT_FIELD_CHANGED:
467 DCHECK(node->IsTextField());
468 FireWinAccessibilityEvent(EVENT_OBJECT_VALUECHANGE, node);
469 FireUiaPropertyChangedEvent(UIA_ValueValuePropertyId, node);
470 HandleTextChangedEvent(*node);
471 break;
472 case ui::AXEventGenerator::Event::WIN_IACCESSIBLE_STATE_CHANGED:
473 FireWinAccessibilityEvent(EVENT_OBJECT_STATECHANGE, node);
474 break;
475
476 // Currently unused events on this platform.
477 case ui::AXEventGenerator::Event::ATK_TEXT_OBJECT_ATTRIBUTE_CHANGED:
478 case ui::AXEventGenerator::Event::AUTO_COMPLETE_CHANGED:
479 case ui::AXEventGenerator::Event::DOCUMENT_TITLE_CHANGED:
480 case ui::AXEventGenerator::Event::FOCUS_CHANGED:
481 case ui::AXEventGenerator::Event::LIVE_REGION_NODE_CHANGED:
482 case ui::AXEventGenerator::Event::LOAD_START:
483 case ui::AXEventGenerator::Event::MENU_ITEM_SELECTED:
484 case ui::AXEventGenerator::Event::OTHER_ATTRIBUTE_CHANGED:
485 case ui::AXEventGenerator::Event::PARENT_CHANGED:
486 case ui::AXEventGenerator::Event::PORTAL_ACTIVATED:
487 case ui::AXEventGenerator::Event::RELATED_NODE_CHANGED:
488 case ui::AXEventGenerator::Event::ROW_COUNT_CHANGED:
489 case ui::AXEventGenerator::Event::SELECTION_IN_TEXT_FIELD_CHANGED:
490 case ui::AXEventGenerator::Event::STATE_CHANGED:
491 break;
492 }
493 }
494
FireWinAccessibilityEvent(LONG win_event_type,BrowserAccessibility * node)495 void BrowserAccessibilityManagerWin::FireWinAccessibilityEvent(
496 LONG win_event_type,
497 BrowserAccessibility* node) {
498 if (!ShouldFireEventForNode(node))
499 return;
500 // Suppress events when |IGNORED_CHANGED| except for related SHOW / HIDE.
501 // Also include MENUPOPUPSTART / MENUPOPUPEND since a change in the ignored
502 // state may show / hide a popup by exposing it to the tree or not.
503 // Also include focus events since a node may become visible at the same time
504 // it receives focus It's never good to suppress a po
505 if (base::Contains(ignored_changed_nodes_, node)) {
506 switch (win_event_type) {
507 case EVENT_OBJECT_HIDE:
508 case EVENT_OBJECT_SHOW:
509 case EVENT_OBJECT_FOCUS:
510 case EVENT_SYSTEM_MENUPOPUPEND:
511 case EVENT_SYSTEM_MENUPOPUPSTART:
512 break;
513 default:
514 return;
515 }
516 } else if (node->IsIgnored()) {
517 return;
518 }
519
520 HWND hwnd = GetParentHWND();
521 if (!hwnd)
522 return;
523
524 // Pass the negation of this node's unique id in the |child_id|
525 // argument to NotifyWinEvent; the AT client will then call get_accChild
526 // on the HWND's accessibility object and pass it that same id, which
527 // we can use to retrieve the IAccessible for this node.
528 LONG child_id = -(ToBrowserAccessibilityWin(node)->GetCOM()->GetUniqueId());
529 ::NotifyWinEvent(win_event_type, hwnd, OBJID_CLIENT, child_id);
530 }
531
FireUiaAccessibilityEvent(LONG uia_event,BrowserAccessibility * node)532 void BrowserAccessibilityManagerWin::FireUiaAccessibilityEvent(
533 LONG uia_event,
534 BrowserAccessibility* node) {
535 if (!::switches::IsExperimentalAccessibilityPlatformUIAEnabled())
536 return;
537 if (!ShouldFireEventForNode(node))
538 return;
539 // Suppress events when |IGNORED_CHANGED| except for MenuClosed / MenuOpen
540 // since a change in the ignored state may show / hide a popup by exposing
541 // it to the tree or not.
542 if (base::Contains(ignored_changed_nodes_, node)) {
543 switch (uia_event) {
544 case UIA_MenuClosedEventId:
545 case UIA_MenuOpenedEventId:
546 break;
547 default:
548 return;
549 }
550 } else if (node->IsIgnored()) {
551 return;
552 }
553
554 ::UiaRaiseAutomationEvent(ToBrowserAccessibilityWin(node)->GetCOM(),
555 uia_event);
556 }
557
FireUiaPropertyChangedEvent(LONG uia_property,BrowserAccessibility * node)558 void BrowserAccessibilityManagerWin::FireUiaPropertyChangedEvent(
559 LONG uia_property,
560 BrowserAccessibility* node) {
561 if (!::switches::IsExperimentalAccessibilityPlatformUIAEnabled())
562 return;
563 if (!ShouldFireEventForNode(node))
564 return;
565 // Suppress events when |IGNORED_CHANGED| with the exception for firing
566 // UIA_AriaPropertiesPropertyId-hidden event on non-text node marked as
567 // ignored.
568 if (node->IsIgnored() || base::Contains(ignored_changed_nodes_, node)) {
569 if (uia_property != UIA_AriaPropertiesPropertyId || node->IsText())
570 return;
571 }
572
573 // The old value is not used by the system
574 VARIANT old_value = {};
575 old_value.vt = VT_EMPTY;
576
577 auto* provider = ToBrowserAccessibilityWin(node)->GetCOM();
578 base::win::ScopedVariant new_value;
579 if (SUCCEEDED(
580 provider->GetPropertyValueImpl(uia_property, new_value.Receive()))) {
581 ::UiaRaiseAutomationPropertyChangedEvent(provider, uia_property, old_value,
582 new_value);
583 }
584 }
585
FireUiaStructureChangedEvent(StructureChangeType change_type,BrowserAccessibility * node)586 void BrowserAccessibilityManagerWin::FireUiaStructureChangedEvent(
587 StructureChangeType change_type,
588 BrowserAccessibility* node) {
589 if (!::switches::IsExperimentalAccessibilityPlatformUIAEnabled())
590 return;
591 if (!ShouldFireEventForNode(node))
592 return;
593 // Suppress events when |IGNORED_CHANGED| except for related structure changes
594 if (base::Contains(ignored_changed_nodes_, node)) {
595 switch (change_type) {
596 case StructureChangeType_ChildRemoved:
597 case StructureChangeType_ChildAdded:
598 break;
599 default:
600 return;
601 }
602 } else if (node->IsIgnored()) {
603 return;
604 }
605
606 auto* provider = ToBrowserAccessibilityWin(node);
607 auto* provider_com = provider ? provider->GetCOM() : nullptr;
608 if (!provider || !provider_com)
609 return;
610
611 switch (change_type) {
612 case StructureChangeType_ChildRemoved: {
613 // 'ChildRemoved' fires on the parent and provides the runtime ID of
614 // the removed child (which was passed as |node|).
615 auto* parent = ToBrowserAccessibilityWin(node->PlatformGetParent());
616 auto* parent_com = parent ? parent->GetCOM() : nullptr;
617 if (parent && parent_com) {
618 ui::AXPlatformNodeWin::RuntimeIdArray runtime_id;
619 provider_com->GetRuntimeIdArray(runtime_id);
620 UiaRaiseStructureChangedEvent(parent_com, change_type,
621 runtime_id.data(), runtime_id.size());
622 }
623 break;
624 }
625
626 default: {
627 // All other types are fired on |node|. For 'ChildAdded' |node| is the
628 // child that was added; for other types, it's the parent container.
629 UiaRaiseStructureChangedEvent(provider_com, change_type, nullptr, 0);
630 }
631 }
632 }
633
CanFireEvents() const634 bool BrowserAccessibilityManagerWin::CanFireEvents() const {
635 return BrowserAccessibilityManager::CanFireEvents() &&
636 GetDelegateFromRootManager() &&
637 GetDelegateFromRootManager()->AccessibilityGetAcceleratedWidget();
638 }
639
GetViewBoundsInScreenCoordinates() const640 gfx::Rect BrowserAccessibilityManagerWin::GetViewBoundsInScreenCoordinates()
641 const {
642 // We have to take the device scale factor into account on Windows.
643 BrowserAccessibilityDelegate* delegate = GetDelegateFromRootManager();
644 if (delegate) {
645 gfx::Rect bounds = delegate->AccessibilityGetViewBounds();
646
647 // http://www.chromium.org/developers/design-documents/blink-coordinate-spaces
648 // The bounds returned by the delegate are always in device-independent
649 // pixels (DIPs), meaning physical pixels divided by device scale factor
650 // (DSF). However, if UseZoomForDSF is enabled, then Blink does not apply
651 // DSF when going from physical to screen pixels. In that case, we need to
652 // multiply DSF back in to get to Blink's notion of "screen pixels."
653 if (IsUseZoomForDSFEnabled() && device_scale_factor() > 0.0 &&
654 device_scale_factor() != 1.0) {
655 bounds = ScaleToEnclosingRect(bounds, device_scale_factor());
656 }
657 return bounds;
658 }
659 return gfx::Rect();
660 }
661
OnSubtreeWillBeDeleted(ui::AXTree * tree,ui::AXNode * node)662 void BrowserAccessibilityManagerWin::OnSubtreeWillBeDeleted(ui::AXTree* tree,
663 ui::AXNode* node) {
664 BrowserAccessibility* obj = GetFromAXNode(node);
665 DCHECK(obj);
666 if (obj) {
667 FireWinAccessibilityEvent(EVENT_OBJECT_HIDE, obj);
668 FireUiaStructureChangedEvent(StructureChangeType_ChildRemoved, obj);
669 }
670 }
671
OnNodeWillBeDeleted(ui::AXTree * tree,ui::AXNode * node)672 void BrowserAccessibilityManagerWin::OnNodeWillBeDeleted(ui::AXTree* tree,
673 ui::AXNode* node) {
674 if (node->data().role == ax::mojom::Role::kMenu) {
675 BrowserAccessibility* obj = GetFromAXNode(node);
676 DCHECK(obj);
677 FireWinAccessibilityEvent(EVENT_SYSTEM_MENUPOPUPEND, obj);
678 FireUiaAccessibilityEvent(UIA_MenuClosedEventId, obj);
679 }
680 }
681
OnAtomicUpdateFinished(ui::AXTree * tree,bool root_changed,const std::vector<ui::AXTreeObserver::Change> & changes)682 void BrowserAccessibilityManagerWin::OnAtomicUpdateFinished(
683 ui::AXTree* tree,
684 bool root_changed,
685 const std::vector<ui::AXTreeObserver::Change>& changes) {
686 BrowserAccessibilityManager::OnAtomicUpdateFinished(tree, root_changed,
687 changes);
688
689 // Do a sequence of Windows-specific updates on each node. Each one is
690 // done in a single pass that must complete before the next step starts.
691 // The nodes that need to be updated are all of the nodes that were changed,
692 // plus some parents.
693 std::set<ui::AXPlatformNode*> objs_to_update;
694 CollectChangedNodesAndParentsForAtomicUpdate(tree, changes, &objs_to_update);
695
696 // The first step moves win_attributes_ to old_win_attributes_ and then
697 // recomputes all of win_attributes_ other than IAccessibleText.
698 for (auto* node : objs_to_update) {
699 static_cast<BrowserAccessibilityComWin*>(node)
700 ->UpdateStep1ComputeWinAttributes();
701 }
702
703 // The next step updates the hypertext of each node, which is a
704 // concatenation of all of its child text nodes, so it can't run until
705 // the text of all of the nodes was computed in the previous step.
706 for (auto* node : objs_to_update) {
707 static_cast<BrowserAccessibilityComWin*>(node)
708 ->UpdateStep2ComputeHypertext();
709 }
710
711 // The third step fires events on nodes based on what's changed - like
712 // if the name, value, or description changed, or if the hypertext had
713 // text inserted or removed. It's able to figure out exactly what changed
714 // because we still have old_win_attributes_ populated.
715 // This step has to run after the previous two steps complete because the
716 // client may walk the tree when it receives any of these events.
717 // At the end, it deletes old_win_attributes_ since they're not needed
718 // anymore.
719 for (auto* node : objs_to_update) {
720 static_cast<BrowserAccessibilityComWin*>(node)->UpdateStep3FireEvents();
721 }
722 }
723
724 // static
IsIA2NodeSelected(BrowserAccessibility * node)725 bool BrowserAccessibilityManagerWin::IsIA2NodeSelected(
726 BrowserAccessibility* node) {
727 return node->GetBoolAttribute(ax::mojom::BoolAttribute::kSelected);
728 }
729
730 // static
IsUIANodeSelected(BrowserAccessibility * node)731 bool BrowserAccessibilityManagerWin::IsUIANodeSelected(
732 BrowserAccessibility* node) {
733 // https://www.w3.org/TR/core-aam-1.1/#mapping_state-property_table
734 // SelectionItem.IsSelected is set according to the True or False value of
735 // aria-checked for 'radio' and 'menuitemradio' roles.
736 if (ui::IsRadio(node->GetRole()))
737 return node->GetData().GetCheckedState() == ax::mojom::CheckedState::kTrue;
738
739 return node->GetBoolAttribute(ax::mojom::BoolAttribute::kSelected);
740 }
741
FireIA2SelectionEvents(BrowserAccessibility * container,BrowserAccessibility * only_selected_child,const SelectionEvents & changes)742 void BrowserAccessibilityManagerWin::FireIA2SelectionEvents(
743 BrowserAccessibility* container,
744 BrowserAccessibility* only_selected_child,
745 const SelectionEvents& changes) {
746 if (only_selected_child) {
747 // Fire 'ElementSelected' on the only selected child.
748 FireWinAccessibilityEvent(EVENT_OBJECT_SELECTION, only_selected_child);
749 } else {
750 const bool container_is_multiselectable =
751 container && container->HasState(ax::mojom::State::kMultiselectable);
752 for (auto* item : changes.added) {
753 if (container_is_multiselectable)
754 FireWinAccessibilityEvent(EVENT_OBJECT_SELECTIONADD, item);
755 else
756 FireWinAccessibilityEvent(EVENT_OBJECT_SELECTION, item);
757 }
758 for (auto* item : changes.removed)
759 FireWinAccessibilityEvent(EVENT_OBJECT_SELECTIONREMOVE, item);
760 }
761 }
762
FireUIASelectionEvents(BrowserAccessibility * container,BrowserAccessibility * only_selected_child,const SelectionEvents & changes)763 void BrowserAccessibilityManagerWin::FireUIASelectionEvents(
764 BrowserAccessibility* container,
765 BrowserAccessibility* only_selected_child,
766 const SelectionEvents& changes) {
767 if (only_selected_child) {
768 // Fire 'ElementSelected' on the only selected child.
769 FireUiaAccessibilityEvent(UIA_SelectionItem_ElementSelectedEventId,
770 only_selected_child);
771 FireUiaPropertyChangedEvent(UIA_SelectionItemIsSelectedPropertyId,
772 only_selected_child);
773 for (auto* item : changes.removed)
774 FireUiaPropertyChangedEvent(UIA_SelectionItemIsSelectedPropertyId, item);
775 } else {
776 // Per UIA documentation, beyond the "invalidate limit" we're supposed to
777 // fire a 'SelectionInvalidated' event. The exact value isn't specified,
778 // but System.Windows.Automation.Provider uses a value of 20.
779 static const size_t kInvalidateLimit = 20;
780 if ((changes.added.size() + changes.removed.size()) > kInvalidateLimit) {
781 DCHECK_NE(container, nullptr);
782 FireUiaAccessibilityEvent(UIA_Selection_InvalidatedEventId, container);
783 } else {
784 const bool container_is_multiselectable =
785 container && container->HasState(ax::mojom::State::kMultiselectable);
786 for (auto* item : changes.added) {
787 if (container_is_multiselectable) {
788 FireUiaAccessibilityEvent(
789 UIA_SelectionItem_ElementAddedToSelectionEventId, item);
790 } else {
791 FireUiaAccessibilityEvent(UIA_SelectionItem_ElementSelectedEventId,
792 item);
793 }
794 FireUiaPropertyChangedEvent(UIA_SelectionItemIsSelectedPropertyId,
795 item);
796 }
797 for (auto* item : changes.removed) {
798 FireUiaAccessibilityEvent(
799 UIA_SelectionItem_ElementRemovedFromSelectionEventId, item);
800 FireUiaPropertyChangedEvent(UIA_SelectionItemIsSelectedPropertyId,
801 item);
802 }
803 }
804 }
805 }
806
807 // static
HandleSelectedStateChanged(SelectionEventsMap & selection_events_map,BrowserAccessibility * node,bool is_selected)808 void BrowserAccessibilityManagerWin::HandleSelectedStateChanged(
809 SelectionEventsMap& selection_events_map,
810 BrowserAccessibility* node,
811 bool is_selected) {
812 // If |node| belongs to a selection container, then map the events with the
813 // selection container as the key because |FinalizeSelectionEvents| needs to
814 // determine whether or not there is only one element selected in order to
815 // optimize what platform events are sent.
816 BrowserAccessibility* key = node;
817 if (auto* selection_container = node->PlatformGetSelectionContainer())
818 key = selection_container;
819
820 if (is_selected)
821 selection_events_map[key].added.push_back(node);
822 else
823 selection_events_map[key].removed.push_back(node);
824 }
825
826 // static
FinalizeSelectionEvents(SelectionEventsMap & selection_events_map,IsSelectedPredicate is_selected_predicate,FirePlatformSelectionEventsCallback fire_platform_events_callback)827 void BrowserAccessibilityManagerWin::FinalizeSelectionEvents(
828 SelectionEventsMap& selection_events_map,
829 IsSelectedPredicate is_selected_predicate,
830 FirePlatformSelectionEventsCallback fire_platform_events_callback) {
831 for (auto&& selected : selection_events_map) {
832 BrowserAccessibility* key_node = selected.first;
833 SelectionEvents& changes = selected.second;
834
835 // Determine if |node| is a selection container with one selected child in
836 // order to optimize what platform events are sent.
837 BrowserAccessibility* container = nullptr;
838 BrowserAccessibility* only_selected_child = nullptr;
839 if (ui::IsContainerWithSelectableChildren(key_node->GetRole())) {
840 container = key_node;
841 for (auto it = container->InternalChildrenBegin();
842 it != container->InternalChildrenEnd(); ++it) {
843 auto* child = it.get();
844 if (is_selected_predicate.Run(child)) {
845 if (!only_selected_child) {
846 only_selected_child = child;
847 continue;
848 }
849
850 only_selected_child = nullptr;
851 break;
852 }
853 }
854 }
855
856 fire_platform_events_callback.Run(container, only_selected_child, changes);
857 }
858
859 selection_events_map.clear();
860 }
861
HandleAriaPropertiesChangedEvent(BrowserAccessibility & node)862 void BrowserAccessibilityManagerWin::HandleAriaPropertiesChangedEvent(
863 BrowserAccessibility& node) {
864 DCHECK_IN_ON_ACCESSIBILITY_EVENTS();
865 aria_properties_events_.insert(&node);
866 }
867
HandleTextChangedEvent(BrowserAccessibility & node)868 void BrowserAccessibilityManagerWin::HandleTextChangedEvent(
869 BrowserAccessibility& node) {
870 DCHECK_IN_ON_ACCESSIBILITY_EVENTS();
871 if (BrowserAccessibility* text_provider = GetUiaTextPatternProvider(node))
872 text_changed_events_.insert(text_provider);
873 }
874
HandleTextSelectionChangedEvent(BrowserAccessibility & node)875 void BrowserAccessibilityManagerWin::HandleTextSelectionChangedEvent(
876 BrowserAccessibility& node) {
877 DCHECK_IN_ON_ACCESSIBILITY_EVENTS();
878 if (BrowserAccessibility* text_provider = GetUiaTextPatternProvider(node))
879 text_selection_changed_events_.insert(text_provider);
880 }
881
BeforeAccessibilityEvents()882 void BrowserAccessibilityManagerWin::BeforeAccessibilityEvents() {
883 BrowserAccessibilityManager::BeforeAccessibilityEvents();
884
885 DCHECK(aria_properties_events_.empty());
886 DCHECK(text_changed_events_.empty());
887 DCHECK(text_selection_changed_events_.empty());
888 DCHECK(ignored_changed_nodes_.empty());
889
890 for (const auto& targeted_event : event_generator()) {
891 if (targeted_event.event_params.event ==
892 ui::AXEventGenerator::Event::IGNORED_CHANGED) {
893 BrowserAccessibility* event_target = GetFromAXNode(targeted_event.node);
894 if (!event_target)
895 continue;
896
897 const auto insert_pair = ignored_changed_nodes_.insert(event_target);
898
899 // Expect that |IGNORED_CHANGED| only fires once for a given
900 // node in a given event frame.
901 DCHECK(insert_pair.second);
902 }
903 }
904 }
905
FinalizeAccessibilityEvents()906 void BrowserAccessibilityManagerWin::FinalizeAccessibilityEvents() {
907 BrowserAccessibilityManager::FinalizeAccessibilityEvents();
908
909 // Finalize aria properties events.
910 for (BrowserAccessibility* event_node : aria_properties_events_)
911 FireUiaPropertyChangedEvent(UIA_AriaPropertiesPropertyId, event_node);
912 aria_properties_events_.clear();
913
914 // Finalize selection changed events.
915 for (BrowserAccessibility* event_node : text_selection_changed_events_) {
916 DCHECK(event_node);
917 if (ToBrowserAccessibilityWin(event_node)
918 ->GetCOM()
919 ->IsPatternProviderSupported(UIA_TextPatternId)) {
920 FireUiaAccessibilityEvent(UIA_Text_TextSelectionChangedEventId,
921 event_node);
922 }
923 }
924 text_selection_changed_events_.clear();
925
926 // Finalize text changed events.
927 for (BrowserAccessibility* event_node : text_changed_events_)
928 FireUiaAccessibilityEvent(UIA_Text_TextChangedEventId, event_node);
929 text_changed_events_.clear();
930
931 // Finalize selection item events.
932 FinalizeSelectionEvents(
933 ia2_selection_events_, base::BindRepeating(&IsIA2NodeSelected),
934 base::BindRepeating(
935 &BrowserAccessibilityManagerWin::FireIA2SelectionEvents,
936 base::Unretained(this)));
937 FinalizeSelectionEvents(
938 uia_selection_events_, base::BindRepeating(&IsUIANodeSelected),
939 base::BindRepeating(
940 &BrowserAccessibilityManagerWin::FireUIASelectionEvents,
941 base::Unretained(this)));
942
943 ignored_changed_nodes_.clear();
944 }
945
946 BrowserAccessibilityManagerWin::SelectionEvents::SelectionEvents() = default;
947 BrowserAccessibilityManagerWin::SelectionEvents::~SelectionEvents() = default;
948
949 } // namespace content
950