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/common/accessibility_messages.h"
15 #include "content/renderer/accessibility/blink_ax_enum_conversion.h"
16 #include "content/renderer/render_frame_impl.h"
17 #include "content/renderer/render_view_impl.h"
18 #include "third_party/WebKit/public/web/WebAXObject.h"
19 #include "third_party/WebKit/public/web/WebDocument.h"
20 #include "third_party/WebKit/public/web/WebInputElement.h"
21 #include "third_party/WebKit/public/web/WebLocalFrame.h"
22 #include "third_party/WebKit/public/web/WebSettings.h"
23 #include "third_party/WebKit/public/web/WebView.h"
25 using blink::WebAXObject
;
26 using blink::WebDocument
;
27 using blink::WebLocalFrame
;
29 using blink::WebPoint
;
31 using blink::WebScopedAXContext
;
32 using blink::WebSettings
;
37 // Cap the number of nodes returned in an accessibility
38 // tree snapshot to avoid outrageous memory or bandwidth
40 const size_t kMaxSnapshotNodeCount
= 5000;
43 void RendererAccessibility::SnapshotAccessibilityTree(
44 RenderFrameImpl
* render_frame
,
45 ui::AXTreeUpdate
<content::AXContentNodeData
>* response
) {
48 if (!render_frame
->GetWebFrame())
51 WebDocument document
= render_frame
->GetWebFrame()->document();
52 WebScopedAXContext
context(document
);
53 BlinkAXTreeSource
tree_source(render_frame
);
54 tree_source
.SetRoot(context
.root());
55 BlinkAXTreeSerializer
serializer(&tree_source
);
56 serializer
.set_max_node_count(kMaxSnapshotNodeCount
);
57 serializer
.SerializeChanges(context
.root(), response
);
60 RendererAccessibility::RendererAccessibility(RenderFrameImpl
* render_frame
)
61 : RenderFrameObserver(render_frame
),
62 render_frame_(render_frame
),
63 tree_source_(render_frame
),
64 serializer_(&tree_source_
),
65 last_scroll_offset_(gfx::Size()),
69 WebView
* web_view
= render_frame_
->GetRenderView()->GetWebView();
70 WebSettings
* settings
= web_view
->settings();
71 settings
->setAccessibilityEnabled(true);
73 #if defined(OS_ANDROID)
74 // Password values are only passed through on Android.
75 settings
->setAccessibilityPasswordValuesEnabled(true);
78 #if !defined(OS_ANDROID)
79 // Inline text boxes are enabled for all nodes on all except Android.
80 settings
->setInlineTextBoxAccessibilityEnabled(true);
83 const WebDocument
& document
= GetMainDocument();
84 if (!document
.isNull()) {
85 // It's possible that the webview has already loaded a webpage without
86 // accessibility being enabled. Initialize the browser's cached
87 // accessibility tree by sending it a notification.
88 HandleAXEvent(document
.accessibilityObject(), ui::AX_EVENT_LAYOUT_COMPLETE
);
92 RendererAccessibility::~RendererAccessibility() {
95 bool RendererAccessibility::OnMessageReceived(const IPC::Message
& message
) {
97 IPC_BEGIN_MESSAGE_MAP(RendererAccessibility
, message
)
98 IPC_MESSAGE_HANDLER(AccessibilityMsg_SetFocus
, OnSetFocus
)
99 IPC_MESSAGE_HANDLER(AccessibilityMsg_DoDefaultAction
, OnDoDefaultAction
)
100 IPC_MESSAGE_HANDLER(AccessibilityMsg_Events_ACK
, OnEventsAck
)
101 IPC_MESSAGE_HANDLER(AccessibilityMsg_ScrollToMakeVisible
,
102 OnScrollToMakeVisible
)
103 IPC_MESSAGE_HANDLER(AccessibilityMsg_ScrollToPoint
, OnScrollToPoint
)
104 IPC_MESSAGE_HANDLER(AccessibilityMsg_SetScrollOffset
, OnSetScrollOffset
)
105 IPC_MESSAGE_HANDLER(AccessibilityMsg_SetTextSelection
, OnSetTextSelection
)
106 IPC_MESSAGE_HANDLER(AccessibilityMsg_SetValue
, OnSetValue
)
107 IPC_MESSAGE_HANDLER(AccessibilityMsg_ShowContextMenu
, OnShowContextMenu
)
108 IPC_MESSAGE_HANDLER(AccessibilityMsg_HitTest
, OnHitTest
)
109 IPC_MESSAGE_HANDLER(AccessibilityMsg_SetAccessibilityFocus
,
110 OnSetAccessibilityFocus
)
111 IPC_MESSAGE_HANDLER(AccessibilityMsg_Reset
, OnReset
)
112 IPC_MESSAGE_HANDLER(AccessibilityMsg_FatalError
, OnFatalError
)
113 IPC_MESSAGE_UNHANDLED(handled
= false)
114 IPC_END_MESSAGE_MAP()
118 void RendererAccessibility::HandleWebAccessibilityEvent(
119 const blink::WebAXObject
& obj
, blink::WebAXEvent event
) {
120 HandleAXEvent(obj
, AXEventFromBlink(event
));
123 void RendererAccessibility::HandleAccessibilityFindInPageResult(
126 const blink::WebAXObject
& start_object
,
128 const blink::WebAXObject
& end_object
,
130 AccessibilityHostMsg_FindInPageResultParams params
;
131 params
.request_id
= identifier
;
132 params
.match_index
= match_index
;
133 params
.start_id
= start_object
.axID();
134 params
.start_offset
= start_offset
;
135 params
.end_id
= end_object
.axID();
136 params
.end_offset
= end_offset
;
137 Send(new AccessibilityHostMsg_FindInPageResult(routing_id(), params
));
140 void RendererAccessibility::AccessibilityFocusedNodeChanged(
141 const WebNode
& node
) {
142 const WebDocument
& document
= GetMainDocument();
143 if (document
.isNull())
147 // When focus is cleared, implicitly focus the document.
148 // TODO(dmazzoni): Make Blink send this notification instead.
149 HandleAXEvent(document
.accessibilityObject(), ui::AX_EVENT_BLUR
);
153 void RendererAccessibility::DisableAccessibility() {
154 RenderView
* render_view
= render_frame_
->GetRenderView();
158 WebView
* web_view
= render_view
->GetWebView();
162 WebSettings
* settings
= web_view
->settings();
166 settings
->setAccessibilityEnabled(false);
169 void RendererAccessibility::HandleAXEvent(
170 const blink::WebAXObject
& obj
, ui::AXEvent event
) {
171 const WebDocument
& document
= GetMainDocument();
172 if (document
.isNull())
175 gfx::Size scroll_offset
= document
.frame()->scrollOffset();
176 if (scroll_offset
!= last_scroll_offset_
) {
177 // Make sure the browser is always aware of the scroll position of
178 // the root document element by posting a generic notification that
180 // TODO(dmazzoni): remove this as soon as
181 // https://bugs.webkit.org/show_bug.cgi?id=73460 is fixed.
182 last_scroll_offset_
= scroll_offset
;
183 if (!obj
.equals(document
.accessibilityObject())) {
184 HandleAXEvent(document
.accessibilityObject(),
185 ui::AX_EVENT_LAYOUT_COMPLETE
);
189 if (event
== ui::AX_EVENT_TEXT_SELECTION_CHANGED
&&
191 !obj
.equals(document
.accessibilityObject())) {
192 // Changing the text selection in a text field may invalidate
193 // the anchor/focus attributes on the tree root. Send a generic
194 // notification to have it updated.
195 HandleAXEvent(document
.accessibilityObject(), event
);
198 // Add the accessibility object to our cache and ensure it's valid.
199 AccessibilityHostMsg_EventParams acc_event
;
200 acc_event
.id
= obj
.axID();
201 acc_event
.event_type
= event
;
203 // Discard duplicate accessibility events.
204 for (uint32 i
= 0; i
< pending_events_
.size(); ++i
) {
205 if (pending_events_
[i
].id
== acc_event
.id
&&
206 pending_events_
[i
].event_type
== acc_event
.event_type
) {
210 pending_events_
.push_back(acc_event
);
212 if (!ack_pending_
&& !weak_factory_
.HasWeakPtrs()) {
213 // When no accessibility events are in-flight post a task to send
214 // the events to the browser. We use PostTask so that we can queue
215 // up additional events.
216 base::ThreadTaskRunnerHandle::Get()->PostTask(
218 base::Bind(&RendererAccessibility::SendPendingAccessibilityEvents
,
219 weak_factory_
.GetWeakPtr()));
223 WebDocument
RendererAccessibility::GetMainDocument() {
224 if (render_frame_
&& render_frame_
->GetWebFrame())
225 return render_frame_
->GetWebFrame()->document();
226 return WebDocument();
229 void RendererAccessibility::SendPendingAccessibilityEvents() {
230 const WebDocument
& document
= GetMainDocument();
231 if (document
.isNull())
234 if (pending_events_
.empty())
237 if (render_frame_
->is_swapped_out())
242 // Make a copy of the events, because it's possible that
243 // actions inside this loop will cause more events to be
245 std::vector
<AccessibilityHostMsg_EventParams
> src_events
= pending_events_
;
246 pending_events_
.clear();
248 // Generate an event message from each Blink event.
249 std::vector
<AccessibilityHostMsg_EventParams
> event_msgs
;
251 // If there's a layout complete message, we need to send location changes.
252 bool had_layout_complete_messages
= false;
254 // Loop over each event and generate an updated event message.
255 for (size_t i
= 0; i
< src_events
.size(); ++i
) {
256 AccessibilityHostMsg_EventParams
& event
= src_events
[i
];
257 if (event
.event_type
== ui::AX_EVENT_LAYOUT_COMPLETE
)
258 had_layout_complete_messages
= true;
260 WebAXObject obj
= document
.accessibilityObjectFromID(event
.id
);
262 // Make sure the object still exists.
263 if (!obj
.updateLayoutAndCheckValidity())
266 // If it's ignored, find the first ancestor that's not ignored.
267 while (!obj
.isDetached() && obj
.accessibilityIsIgnored())
268 obj
= obj
.parentObject();
270 // Make sure it's a descendant of our root node - exceptions include the
271 // scroll area that's the parent of the main document (we ignore it), and
272 // possibly nodes attached to a different document.
273 if (!tree_source_
.IsInTree(obj
))
276 AccessibilityHostMsg_EventParams event_msg
;
277 event_msg
.event_type
= event
.event_type
;
278 event_msg
.id
= event
.id
;
279 serializer_
.SerializeChanges(obj
, &event_msg
.update
);
280 event_msgs
.push_back(event_msg
);
282 // For each node in the update, set the location in our map from
284 for (size_t i
= 0; i
< event_msg
.update
.nodes
.size(); ++i
) {
285 locations_
[event_msg
.update
.nodes
[i
].id
] =
286 event_msg
.update
.nodes
[i
].location
;
289 DVLOG(0) << "Accessibility event: " << ui::ToString(event
.event_type
)
290 << " on node id " << event_msg
.id
291 << "\n" << event_msg
.update
.ToString();
294 Send(new AccessibilityHostMsg_Events(routing_id(), event_msgs
, reset_token_
));
297 if (had_layout_complete_messages
)
298 SendLocationChanges();
301 void RendererAccessibility::SendLocationChanges() {
302 std::vector
<AccessibilityHostMsg_LocationChangeParams
> messages
;
304 // Do a breadth-first explore of the whole blink AX tree.
305 base::hash_map
<int, gfx::Rect
> new_locations
;
306 std::queue
<WebAXObject
> objs_to_explore
;
307 objs_to_explore
.push(tree_source_
.GetRoot());
308 while (objs_to_explore
.size()) {
309 WebAXObject obj
= objs_to_explore
.front();
310 objs_to_explore
.pop();
312 // See if we had a previous location. If not, this whole subtree must
313 // be new, so don't continue to explore this branch.
315 base::hash_map
<int, gfx::Rect
>::iterator iter
= locations_
.find(id
);
316 if (iter
== locations_
.end())
319 // If the location has changed, append it to the IPC message.
320 gfx::Rect new_location
= obj
.boundingBoxRect();
321 if (iter
!= locations_
.end() && iter
->second
!= new_location
) {
322 AccessibilityHostMsg_LocationChangeParams message
;
324 message
.new_location
= new_location
;
325 messages
.push_back(message
);
328 // Save the new location.
329 new_locations
[id
] = new_location
;
331 // Explore children of this object.
332 std::vector
<blink::WebAXObject
> children
;
333 tree_source_
.GetChildren(obj
, &children
);
334 for (size_t i
= 0; i
< children
.size(); ++i
)
335 objs_to_explore
.push(children
[i
]);
337 locations_
.swap(new_locations
);
339 Send(new AccessibilityHostMsg_LocationChanges(routing_id(), messages
));
342 void RendererAccessibility::OnDoDefaultAction(int acc_obj_id
) {
343 const WebDocument
& document
= GetMainDocument();
344 if (document
.isNull())
347 WebAXObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
348 if (obj
.isDetached()) {
350 LOG(WARNING
) << "DoDefaultAction on invalid object id " << acc_obj_id
;
355 obj
.performDefaultAction();
358 void RendererAccessibility::OnEventsAck() {
359 DCHECK(ack_pending_
);
360 ack_pending_
= false;
361 SendPendingAccessibilityEvents();
364 void RendererAccessibility::OnFatalError() {
365 CHECK(false) << "Invalid accessibility tree.";
368 void RendererAccessibility::OnHitTest(gfx::Point point
) {
369 const WebDocument
& document
= GetMainDocument();
370 if (document
.isNull())
372 WebAXObject root_obj
= document
.accessibilityObject();
373 if (!root_obj
.updateLayoutAndCheckValidity())
376 WebAXObject obj
= root_obj
.hitTest(point
);
377 if (!obj
.isDetached())
378 HandleAXEvent(obj
, ui::AX_EVENT_HOVER
);
381 void RendererAccessibility::OnSetAccessibilityFocus(int acc_obj_id
) {
382 if (tree_source_
.accessibility_focus_id() == acc_obj_id
)
385 tree_source_
.set_accessiblity_focus_id(acc_obj_id
);
387 const WebDocument
& document
= GetMainDocument();
388 if (document
.isNull())
391 WebAXObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
393 // This object may not be a leaf node. Force the whole subtree to be
395 serializer_
.DeleteClientSubtree(obj
);
397 // Explicitly send a tree change update event now.
398 HandleAXEvent(obj
, ui::AX_EVENT_TREE_CHANGED
);
401 void RendererAccessibility::OnReset(int reset_token
) {
402 reset_token_
= reset_token
;
404 pending_events_
.clear();
406 const WebDocument
& document
= GetMainDocument();
407 if (!document
.isNull()) {
408 // Tree-only mode gets used by the automation extension API which requires a
409 // load complete event to invoke listener callbacks.
410 ui::AXEvent evt
= document
.accessibilityObject().isLoaded()
411 ? ui::AX_EVENT_LOAD_COMPLETE
: ui::AX_EVENT_LAYOUT_COMPLETE
;
412 HandleAXEvent(document
.accessibilityObject(), evt
);
416 void RendererAccessibility::OnScrollToMakeVisible(
417 int acc_obj_id
, gfx::Rect subfocus
) {
418 const WebDocument
& document
= GetMainDocument();
419 if (document
.isNull())
422 WebAXObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
423 if (obj
.isDetached()) {
425 LOG(WARNING
) << "ScrollToMakeVisible on invalid object id " << acc_obj_id
;
430 obj
.scrollToMakeVisibleWithSubFocus(
431 WebRect(subfocus
.x(), subfocus
.y(), subfocus
.width(), subfocus
.height()));
433 // Make sure the browser gets an event when the scroll
434 // position actually changes.
435 // TODO(dmazzoni): remove this once this bug is fixed:
436 // https://bugs.webkit.org/show_bug.cgi?id=73460
437 HandleAXEvent(document
.accessibilityObject(), ui::AX_EVENT_LAYOUT_COMPLETE
);
440 void RendererAccessibility::OnScrollToPoint(int acc_obj_id
, gfx::Point point
) {
441 const WebDocument
& document
= GetMainDocument();
442 if (document
.isNull())
445 WebAXObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
446 if (obj
.isDetached()) {
448 LOG(WARNING
) << "ScrollToPoint on invalid object id " << acc_obj_id
;
453 obj
.scrollToGlobalPoint(WebPoint(point
.x(), point
.y()));
455 // Make sure the browser gets an event when the scroll
456 // position actually changes.
457 // TODO(dmazzoni): remove this once this bug is fixed:
458 // https://bugs.webkit.org/show_bug.cgi?id=73460
459 HandleAXEvent(document
.accessibilityObject(), ui::AX_EVENT_LAYOUT_COMPLETE
);
462 void RendererAccessibility::OnSetScrollOffset(int acc_obj_id
,
464 const WebDocument
& document
= GetMainDocument();
465 if (document
.isNull())
468 WebAXObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
469 if (obj
.isDetached())
472 obj
.setScrollOffset(WebPoint(offset
.x(), offset
.y()));
475 void RendererAccessibility::OnSetFocus(int acc_obj_id
) {
476 const WebDocument
& document
= GetMainDocument();
477 if (document
.isNull())
480 WebAXObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
481 if (obj
.isDetached()) {
483 LOG(WARNING
) << "OnSetAccessibilityFocus on invalid object id "
489 WebAXObject root
= document
.accessibilityObject();
490 if (root
.isDetached()) {
492 LOG(WARNING
) << "OnSetAccessibilityFocus but root is invalid";
497 // By convention, calling SetFocus on the root of the tree should clear the
498 // current focus. Otherwise set the focus to the new node.
499 if (acc_obj_id
== root
.axID())
500 render_frame_
->GetRenderView()->GetWebView()->clearFocusedElement();
502 obj
.setFocused(true);
505 void RendererAccessibility::OnSetTextSelection(
506 int acc_obj_id
, int start_offset
, int end_offset
) {
507 const WebDocument
& document
= GetMainDocument();
508 if (document
.isNull())
511 WebAXObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
512 if (obj
.isDetached()) {
514 LOG(WARNING
) << "SetTextSelection on invalid object id " << acc_obj_id
;
519 obj
.setSelectedTextRange(start_offset
, end_offset
);
520 WebAXObject root
= document
.accessibilityObject();
521 if (root
.isDetached()) {
523 LOG(WARNING
) << "OnSetAccessibilityFocus but root is invalid";
527 HandleAXEvent(root
, ui::AX_EVENT_LAYOUT_COMPLETE
);
530 void RendererAccessibility::OnSetValue(
532 base::string16 value
) {
533 const WebDocument
& document
= GetMainDocument();
534 if (document
.isNull())
537 WebAXObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
538 if (obj
.isDetached()) {
540 LOG(WARNING
) << "SetTextSelection on invalid object id " << acc_obj_id
;
546 HandleAXEvent(obj
, ui::AX_EVENT_VALUE_CHANGED
);
549 void RendererAccessibility::OnShowContextMenu(int acc_obj_id
) {
550 const WebDocument
& document
= GetMainDocument();
551 if (document
.isNull())
554 WebAXObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
555 if (obj
.isDetached()) {
557 LOG(WARNING
) << "ShowContextMenu on invalid object id " << acc_obj_id
;
562 obj
.showContextMenu();
565 } // namespace content