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.
5 #include "content/renderer/accessibility/renderer_accessibility.h"
10 #include "base/location.h"
11 #include "base/single_thread_task_runner.h"
12 #include "base/strings/utf_string_conversions.h"
13 #include "base/thread_task_runner_handle.h"
14 #include "content/renderer/accessibility/blink_ax_enum_conversion.h"
15 #include "content/renderer/render_frame_impl.h"
16 #include "content/renderer/render_view_impl.h"
17 #include "third_party/WebKit/public/web/WebAXObject.h"
18 #include "third_party/WebKit/public/web/WebDocument.h"
19 #include "third_party/WebKit/public/web/WebInputElement.h"
20 #include "third_party/WebKit/public/web/WebLocalFrame.h"
21 #include "third_party/WebKit/public/web/WebSettings.h"
22 #include "third_party/WebKit/public/web/WebView.h"
24 using blink::WebAXObject
;
25 using blink::WebDocument
;
26 using blink::WebLocalFrame
;
28 using blink::WebPoint
;
30 using blink::WebScopedAXContext
;
31 using blink::WebSettings
;
36 // Cap the number of nodes returned in an accessibility
37 // tree snapshot to avoid outrageous memory or bandwidth
39 const size_t kMaxSnapshotNodeCount
= 5000;
42 void RendererAccessibility::SnapshotAccessibilityTree(
43 RenderFrameImpl
* render_frame
,
44 ui::AXTreeUpdate
* response
) {
47 if (!render_frame
->GetWebFrame())
50 WebDocument document
= render_frame
->GetWebFrame()->document();
51 WebScopedAXContext
context(document
);
52 BlinkAXTreeSource
tree_source(render_frame
);
53 tree_source
.SetRoot(context
.root());
54 ui::AXTreeSerializer
<blink::WebAXObject
> serializer(&tree_source
);
55 serializer
.set_max_node_count(kMaxSnapshotNodeCount
);
56 serializer
.SerializeChanges(context
.root(), response
);
59 RendererAccessibility::RendererAccessibility(RenderFrameImpl
* render_frame
)
60 : RenderFrameObserver(render_frame
),
61 render_frame_(render_frame
),
62 tree_source_(render_frame
),
63 serializer_(&tree_source_
),
64 last_scroll_offset_(gfx::Size()),
68 WebView
* web_view
= render_frame_
->GetRenderView()->GetWebView();
69 WebSettings
* settings
= web_view
->settings();
70 settings
->setAccessibilityEnabled(true);
72 #if defined(OS_ANDROID)
73 // Password values are only passed through on Android.
74 settings
->setAccessibilityPasswordValuesEnabled(true);
77 #if !defined(OS_ANDROID)
78 // Inline text boxes are enabled for all nodes on all except Android.
79 settings
->setInlineTextBoxAccessibilityEnabled(true);
82 const WebDocument
& document
= GetMainDocument();
83 if (!document
.isNull()) {
84 // It's possible that the webview has already loaded a webpage without
85 // accessibility being enabled. Initialize the browser's cached
86 // accessibility tree by sending it a notification.
87 HandleAXEvent(document
.accessibilityObject(), ui::AX_EVENT_LAYOUT_COMPLETE
);
91 RendererAccessibility::~RendererAccessibility() {
94 bool RendererAccessibility::OnMessageReceived(const IPC::Message
& message
) {
96 IPC_BEGIN_MESSAGE_MAP(RendererAccessibility
, message
)
97 IPC_MESSAGE_HANDLER(AccessibilityMsg_SetFocus
, OnSetFocus
)
98 IPC_MESSAGE_HANDLER(AccessibilityMsg_DoDefaultAction
, OnDoDefaultAction
)
99 IPC_MESSAGE_HANDLER(AccessibilityMsg_Events_ACK
, OnEventsAck
)
100 IPC_MESSAGE_HANDLER(AccessibilityMsg_ScrollToMakeVisible
,
101 OnScrollToMakeVisible
)
102 IPC_MESSAGE_HANDLER(AccessibilityMsg_ScrollToPoint
, OnScrollToPoint
)
103 IPC_MESSAGE_HANDLER(AccessibilityMsg_SetScrollOffset
, OnSetScrollOffset
)
104 IPC_MESSAGE_HANDLER(AccessibilityMsg_SetTextSelection
, OnSetTextSelection
)
105 IPC_MESSAGE_HANDLER(AccessibilityMsg_SetValue
, OnSetValue
)
106 IPC_MESSAGE_HANDLER(AccessibilityMsg_ShowContextMenu
, OnShowContextMenu
)
107 IPC_MESSAGE_HANDLER(AccessibilityMsg_HitTest
, OnHitTest
)
108 IPC_MESSAGE_HANDLER(AccessibilityMsg_SetAccessibilityFocus
,
109 OnSetAccessibilityFocus
)
110 IPC_MESSAGE_HANDLER(AccessibilityMsg_Reset
, OnReset
)
111 IPC_MESSAGE_HANDLER(AccessibilityMsg_FatalError
, OnFatalError
)
112 IPC_MESSAGE_UNHANDLED(handled
= false)
113 IPC_END_MESSAGE_MAP()
117 void RendererAccessibility::HandleWebAccessibilityEvent(
118 const blink::WebAXObject
& obj
, blink::WebAXEvent event
) {
119 HandleAXEvent(obj
, AXEventFromBlink(event
));
122 void RendererAccessibility::HandleAccessibilityFindInPageResult(
125 const blink::WebAXObject
& start_object
,
127 const blink::WebAXObject
& end_object
,
129 AccessibilityHostMsg_FindInPageResultParams params
;
130 params
.request_id
= identifier
;
131 params
.match_index
= match_index
;
132 params
.start_id
= start_object
.axID();
133 params
.start_offset
= start_offset
;
134 params
.end_id
= end_object
.axID();
135 params
.end_offset
= end_offset
;
136 Send(new AccessibilityHostMsg_FindInPageResult(routing_id(), params
));
139 void RendererAccessibility::AccessibilityFocusedNodeChanged(
140 const WebNode
& node
) {
141 const WebDocument
& document
= GetMainDocument();
142 if (document
.isNull())
146 // When focus is cleared, implicitly focus the document.
147 // TODO(dmazzoni): Make Blink send this notification instead.
148 HandleAXEvent(document
.accessibilityObject(), ui::AX_EVENT_BLUR
);
152 void RendererAccessibility::DisableAccessibility() {
153 RenderView
* render_view
= render_frame_
->GetRenderView();
157 WebView
* web_view
= render_view
->GetWebView();
161 WebSettings
* settings
= web_view
->settings();
165 settings
->setAccessibilityEnabled(false);
168 void RendererAccessibility::HandleAXEvent(
169 const blink::WebAXObject
& obj
, ui::AXEvent event
) {
170 const WebDocument
& document
= GetMainDocument();
171 if (document
.isNull())
174 gfx::Size scroll_offset
= document
.frame()->scrollOffset();
175 if (scroll_offset
!= last_scroll_offset_
) {
176 // Make sure the browser is always aware of the scroll position of
177 // the root document element by posting a generic notification that
179 // TODO(dmazzoni): remove this as soon as
180 // https://bugs.webkit.org/show_bug.cgi?id=73460 is fixed.
181 last_scroll_offset_
= scroll_offset
;
182 if (!obj
.equals(document
.accessibilityObject())) {
183 HandleAXEvent(document
.accessibilityObject(),
184 ui::AX_EVENT_LAYOUT_COMPLETE
);
188 // Add the accessibility object to our cache and ensure it's valid.
189 AccessibilityHostMsg_EventParams acc_event
;
190 acc_event
.id
= obj
.axID();
191 acc_event
.event_type
= event
;
193 // Discard duplicate accessibility events.
194 for (uint32 i
= 0; i
< pending_events_
.size(); ++i
) {
195 if (pending_events_
[i
].id
== acc_event
.id
&&
196 pending_events_
[i
].event_type
== acc_event
.event_type
) {
200 pending_events_
.push_back(acc_event
);
202 if (!ack_pending_
&& !weak_factory_
.HasWeakPtrs()) {
203 // When no accessibility events are in-flight post a task to send
204 // the events to the browser. We use PostTask so that we can queue
205 // up additional events.
206 base::ThreadTaskRunnerHandle::Get()->PostTask(
208 base::Bind(&RendererAccessibility::SendPendingAccessibilityEvents
,
209 weak_factory_
.GetWeakPtr()));
213 WebDocument
RendererAccessibility::GetMainDocument() {
214 if (render_frame_
&& render_frame_
->GetWebFrame())
215 return render_frame_
->GetWebFrame()->document();
216 return WebDocument();
219 void RendererAccessibility::SendPendingAccessibilityEvents() {
220 const WebDocument
& document
= GetMainDocument();
221 if (document
.isNull())
224 if (pending_events_
.empty())
227 if (render_frame_
->is_swapped_out())
232 // Make a copy of the events, because it's possible that
233 // actions inside this loop will cause more events to be
235 std::vector
<AccessibilityHostMsg_EventParams
> src_events
= pending_events_
;
236 pending_events_
.clear();
238 // Generate an event message from each Blink event.
239 std::vector
<AccessibilityHostMsg_EventParams
> event_msgs
;
241 // If there's a layout complete message, we need to send location changes.
242 bool had_layout_complete_messages
= false;
244 // Loop over each event and generate an updated event message.
245 for (size_t i
= 0; i
< src_events
.size(); ++i
) {
246 AccessibilityHostMsg_EventParams
& event
= src_events
[i
];
247 if (event
.event_type
== ui::AX_EVENT_LAYOUT_COMPLETE
)
248 had_layout_complete_messages
= true;
250 WebAXObject obj
= document
.accessibilityObjectFromID(event
.id
);
252 // Make sure the object still exists.
253 if (!obj
.updateLayoutAndCheckValidity())
256 // If it's ignored, find the first ancestor that's not ignored.
257 while (!obj
.isDetached() && obj
.accessibilityIsIgnored())
258 obj
= obj
.parentObject();
260 // Make sure it's a descendant of our root node - exceptions include the
261 // scroll area that's the parent of the main document (we ignore it), and
262 // possibly nodes attached to a different document.
263 if (!tree_source_
.IsInTree(obj
))
266 AccessibilityHostMsg_EventParams event_msg
;
267 tree_source_
.CollectChildFrameIdMapping(
268 &event_msg
.node_to_frame_routing_id_map
,
269 &event_msg
.node_to_browser_plugin_instance_id_map
);
270 event_msg
.event_type
= event
.event_type
;
271 event_msg
.id
= event
.id
;
272 serializer_
.SerializeChanges(obj
, &event_msg
.update
);
273 event_msgs
.push_back(event_msg
);
275 // For each node in the update, set the location in our map from
277 for (size_t i
= 0; i
< event_msg
.update
.nodes
.size(); ++i
) {
278 locations_
[event_msg
.update
.nodes
[i
].id
] =
279 event_msg
.update
.nodes
[i
].location
;
282 DVLOG(0) << "Accessibility event: " << ui::ToString(event
.event_type
)
283 << " on node id " << event_msg
.id
284 << "\n" << event_msg
.update
.ToString();
287 Send(new AccessibilityHostMsg_Events(routing_id(), event_msgs
, reset_token_
));
290 if (had_layout_complete_messages
)
291 SendLocationChanges();
294 void RendererAccessibility::SendLocationChanges() {
295 std::vector
<AccessibilityHostMsg_LocationChangeParams
> messages
;
297 // Do a breadth-first explore of the whole blink AX tree.
298 base::hash_map
<int, gfx::Rect
> new_locations
;
299 std::queue
<WebAXObject
> objs_to_explore
;
300 objs_to_explore
.push(tree_source_
.GetRoot());
301 while (objs_to_explore
.size()) {
302 WebAXObject obj
= objs_to_explore
.front();
303 objs_to_explore
.pop();
305 // See if we had a previous location. If not, this whole subtree must
306 // be new, so don't continue to explore this branch.
308 base::hash_map
<int, gfx::Rect
>::iterator iter
= locations_
.find(id
);
309 if (iter
== locations_
.end())
312 // If the location has changed, append it to the IPC message.
313 gfx::Rect new_location
= obj
.boundingBoxRect();
314 if (iter
!= locations_
.end() && iter
->second
!= new_location
) {
315 AccessibilityHostMsg_LocationChangeParams message
;
317 message
.new_location
= new_location
;
318 messages
.push_back(message
);
321 // Save the new location.
322 new_locations
[id
] = new_location
;
324 // Explore children of this object.
325 std::vector
<blink::WebAXObject
> children
;
326 tree_source_
.GetChildren(obj
, &children
);
327 for (size_t i
= 0; i
< children
.size(); ++i
)
328 objs_to_explore
.push(children
[i
]);
330 locations_
.swap(new_locations
);
332 Send(new AccessibilityHostMsg_LocationChanges(routing_id(), messages
));
335 void RendererAccessibility::OnDoDefaultAction(int acc_obj_id
) {
336 const WebDocument
& document
= GetMainDocument();
337 if (document
.isNull())
340 WebAXObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
341 if (obj
.isDetached()) {
343 LOG(WARNING
) << "DoDefaultAction on invalid object id " << acc_obj_id
;
348 obj
.performDefaultAction();
351 void RendererAccessibility::OnEventsAck() {
352 DCHECK(ack_pending_
);
353 ack_pending_
= false;
354 SendPendingAccessibilityEvents();
357 void RendererAccessibility::OnFatalError() {
358 CHECK(false) << "Invalid accessibility tree.";
361 void RendererAccessibility::OnHitTest(gfx::Point point
) {
362 const WebDocument
& document
= GetMainDocument();
363 if (document
.isNull())
365 WebAXObject root_obj
= document
.accessibilityObject();
366 if (!root_obj
.updateLayoutAndCheckValidity())
369 WebAXObject obj
= root_obj
.hitTest(point
);
370 if (!obj
.isDetached())
371 HandleAXEvent(obj
, ui::AX_EVENT_HOVER
);
374 void RendererAccessibility::OnSetAccessibilityFocus(int acc_obj_id
) {
375 if (tree_source_
.accessibility_focus_id() == acc_obj_id
)
378 tree_source_
.set_accessiblity_focus_id(acc_obj_id
);
380 const WebDocument
& document
= GetMainDocument();
381 if (document
.isNull())
384 WebAXObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
386 // This object may not be a leaf node. Force the whole subtree to be
388 serializer_
.DeleteClientSubtree(obj
);
390 // Explicitly send a tree change update event now.
391 HandleAXEvent(obj
, ui::AX_EVENT_TREE_CHANGED
);
394 void RendererAccessibility::OnReset(int reset_token
) {
395 reset_token_
= reset_token
;
397 pending_events_
.clear();
399 const WebDocument
& document
= GetMainDocument();
400 if (!document
.isNull()) {
401 // Tree-only mode gets used by the automation extension API which requires a
402 // load complete event to invoke listener callbacks.
403 ui::AXEvent evt
= document
.accessibilityObject().isLoaded()
404 ? ui::AX_EVENT_LOAD_COMPLETE
: ui::AX_EVENT_LAYOUT_COMPLETE
;
405 HandleAXEvent(document
.accessibilityObject(), evt
);
409 void RendererAccessibility::OnScrollToMakeVisible(
410 int acc_obj_id
, gfx::Rect subfocus
) {
411 const WebDocument
& document
= GetMainDocument();
412 if (document
.isNull())
415 WebAXObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
416 if (obj
.isDetached()) {
418 LOG(WARNING
) << "ScrollToMakeVisible on invalid object id " << acc_obj_id
;
423 obj
.scrollToMakeVisibleWithSubFocus(
424 WebRect(subfocus
.x(), subfocus
.y(), subfocus
.width(), subfocus
.height()));
426 // Make sure the browser gets an event when the scroll
427 // position actually changes.
428 // TODO(dmazzoni): remove this once this bug is fixed:
429 // https://bugs.webkit.org/show_bug.cgi?id=73460
430 HandleAXEvent(document
.accessibilityObject(), ui::AX_EVENT_LAYOUT_COMPLETE
);
433 void RendererAccessibility::OnScrollToPoint(int acc_obj_id
, gfx::Point point
) {
434 const WebDocument
& document
= GetMainDocument();
435 if (document
.isNull())
438 WebAXObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
439 if (obj
.isDetached()) {
441 LOG(WARNING
) << "ScrollToPoint on invalid object id " << acc_obj_id
;
446 obj
.scrollToGlobalPoint(WebPoint(point
.x(), point
.y()));
448 // Make sure the browser gets an event when the scroll
449 // position actually changes.
450 // TODO(dmazzoni): remove this once this bug is fixed:
451 // https://bugs.webkit.org/show_bug.cgi?id=73460
452 HandleAXEvent(document
.accessibilityObject(), ui::AX_EVENT_LAYOUT_COMPLETE
);
455 void RendererAccessibility::OnSetScrollOffset(int acc_obj_id
,
457 const WebDocument
& document
= GetMainDocument();
458 if (document
.isNull())
461 WebAXObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
462 if (obj
.isDetached())
465 obj
.setScrollOffset(WebPoint(offset
.x(), offset
.y()));
468 void RendererAccessibility::OnSetFocus(int acc_obj_id
) {
469 const WebDocument
& document
= GetMainDocument();
470 if (document
.isNull())
473 WebAXObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
474 if (obj
.isDetached()) {
476 LOG(WARNING
) << "OnSetAccessibilityFocus on invalid object id "
482 WebAXObject root
= document
.accessibilityObject();
483 if (root
.isDetached()) {
485 LOG(WARNING
) << "OnSetAccessibilityFocus but root is invalid";
490 // By convention, calling SetFocus on the root of the tree should clear the
491 // current focus. Otherwise set the focus to the new node.
492 if (acc_obj_id
== root
.axID())
493 render_frame_
->GetRenderView()->GetWebView()->clearFocusedElement();
495 obj
.setFocused(true);
498 void RendererAccessibility::OnSetTextSelection(
499 int acc_obj_id
, int start_offset
, int end_offset
) {
500 const WebDocument
& document
= GetMainDocument();
501 if (document
.isNull())
504 WebAXObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
505 if (obj
.isDetached()) {
507 LOG(WARNING
) << "SetTextSelection on invalid object id " << acc_obj_id
;
512 obj
.setSelectedTextRange(start_offset
, end_offset
);
515 void RendererAccessibility::OnSetValue(
517 base::string16 value
) {
518 const WebDocument
& document
= GetMainDocument();
519 if (document
.isNull())
522 WebAXObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
523 if (obj
.isDetached()) {
525 LOG(WARNING
) << "SetTextSelection on invalid object id " << acc_obj_id
;
531 HandleAXEvent(obj
, ui::AX_EVENT_VALUE_CHANGED
);
534 void RendererAccessibility::OnShowContextMenu(int acc_obj_id
) {
535 const WebDocument
& document
= GetMainDocument();
536 if (document
.isNull())
539 WebAXObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
540 if (obj
.isDetached()) {
542 LOG(WARNING
) << "ShowContextMenu on invalid object id " << acc_obj_id
;
547 obj
.showContextMenu();
550 } // namespace content