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 // Add the accessibility object to our cache and ensure it's valid.
190 AccessibilityHostMsg_EventParams acc_event
;
191 acc_event
.id
= obj
.axID();
192 acc_event
.event_type
= event
;
194 // Discard duplicate accessibility events.
195 for (uint32 i
= 0; i
< pending_events_
.size(); ++i
) {
196 if (pending_events_
[i
].id
== acc_event
.id
&&
197 pending_events_
[i
].event_type
== acc_event
.event_type
) {
201 pending_events_
.push_back(acc_event
);
203 if (!ack_pending_
&& !weak_factory_
.HasWeakPtrs()) {
204 // When no accessibility events are in-flight post a task to send
205 // the events to the browser. We use PostTask so that we can queue
206 // up additional events.
207 base::ThreadTaskRunnerHandle::Get()->PostTask(
209 base::Bind(&RendererAccessibility::SendPendingAccessibilityEvents
,
210 weak_factory_
.GetWeakPtr()));
214 WebDocument
RendererAccessibility::GetMainDocument() {
215 if (render_frame_
&& render_frame_
->GetWebFrame())
216 return render_frame_
->GetWebFrame()->document();
217 return WebDocument();
220 void RendererAccessibility::SendPendingAccessibilityEvents() {
221 const WebDocument
& document
= GetMainDocument();
222 if (document
.isNull())
225 if (pending_events_
.empty())
228 if (render_frame_
->is_swapped_out())
233 // Make a copy of the events, because it's possible that
234 // actions inside this loop will cause more events to be
236 std::vector
<AccessibilityHostMsg_EventParams
> src_events
= pending_events_
;
237 pending_events_
.clear();
239 // Generate an event message from each Blink event.
240 std::vector
<AccessibilityHostMsg_EventParams
> event_msgs
;
242 // If there's a layout complete message, we need to send location changes.
243 bool had_layout_complete_messages
= false;
245 // Loop over each event and generate an updated event message.
246 for (size_t i
= 0; i
< src_events
.size(); ++i
) {
247 AccessibilityHostMsg_EventParams
& event
= src_events
[i
];
248 if (event
.event_type
== ui::AX_EVENT_LAYOUT_COMPLETE
)
249 had_layout_complete_messages
= true;
251 WebAXObject obj
= document
.accessibilityObjectFromID(event
.id
);
253 // Make sure the object still exists.
254 if (!obj
.updateLayoutAndCheckValidity())
257 // If it's ignored, find the first ancestor that's not ignored.
258 while (!obj
.isDetached() && obj
.accessibilityIsIgnored())
259 obj
= obj
.parentObject();
261 // Make sure it's a descendant of our root node - exceptions include the
262 // scroll area that's the parent of the main document (we ignore it), and
263 // possibly nodes attached to a different document.
264 if (!tree_source_
.IsInTree(obj
))
267 AccessibilityHostMsg_EventParams event_msg
;
268 event_msg
.event_type
= event
.event_type
;
269 event_msg
.id
= event
.id
;
270 serializer_
.SerializeChanges(obj
, &event_msg
.update
);
271 event_msgs
.push_back(event_msg
);
273 // For each node in the update, set the location in our map from
275 for (size_t i
= 0; i
< event_msg
.update
.nodes
.size(); ++i
) {
276 locations_
[event_msg
.update
.nodes
[i
].id
] =
277 event_msg
.update
.nodes
[i
].location
;
280 DVLOG(0) << "Accessibility event: " << ui::ToString(event
.event_type
)
281 << " on node id " << event_msg
.id
282 << "\n" << event_msg
.update
.ToString();
285 Send(new AccessibilityHostMsg_Events(routing_id(), event_msgs
, reset_token_
));
288 if (had_layout_complete_messages
)
289 SendLocationChanges();
292 void RendererAccessibility::SendLocationChanges() {
293 std::vector
<AccessibilityHostMsg_LocationChangeParams
> messages
;
295 // Do a breadth-first explore of the whole blink AX tree.
296 base::hash_map
<int, gfx::Rect
> new_locations
;
297 std::queue
<WebAXObject
> objs_to_explore
;
298 objs_to_explore
.push(tree_source_
.GetRoot());
299 while (objs_to_explore
.size()) {
300 WebAXObject obj
= objs_to_explore
.front();
301 objs_to_explore
.pop();
303 // See if we had a previous location. If not, this whole subtree must
304 // be new, so don't continue to explore this branch.
306 base::hash_map
<int, gfx::Rect
>::iterator iter
= locations_
.find(id
);
307 if (iter
== locations_
.end())
310 // If the location has changed, append it to the IPC message.
311 gfx::Rect new_location
= obj
.boundingBoxRect();
312 if (iter
!= locations_
.end() && iter
->second
!= new_location
) {
313 AccessibilityHostMsg_LocationChangeParams message
;
315 message
.new_location
= new_location
;
316 messages
.push_back(message
);
319 // Save the new location.
320 new_locations
[id
] = new_location
;
322 // Explore children of this object.
323 std::vector
<blink::WebAXObject
> children
;
324 tree_source_
.GetChildren(obj
, &children
);
325 for (size_t i
= 0; i
< children
.size(); ++i
)
326 objs_to_explore
.push(children
[i
]);
328 locations_
.swap(new_locations
);
330 Send(new AccessibilityHostMsg_LocationChanges(routing_id(), messages
));
333 void RendererAccessibility::OnDoDefaultAction(int acc_obj_id
) {
334 const WebDocument
& document
= GetMainDocument();
335 if (document
.isNull())
338 WebAXObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
339 if (obj
.isDetached()) {
341 LOG(WARNING
) << "DoDefaultAction on invalid object id " << acc_obj_id
;
346 obj
.performDefaultAction();
349 void RendererAccessibility::OnEventsAck() {
350 DCHECK(ack_pending_
);
351 ack_pending_
= false;
352 SendPendingAccessibilityEvents();
355 void RendererAccessibility::OnFatalError() {
356 CHECK(false) << "Invalid accessibility tree.";
359 void RendererAccessibility::OnHitTest(gfx::Point point
) {
360 const WebDocument
& document
= GetMainDocument();
361 if (document
.isNull())
363 WebAXObject root_obj
= document
.accessibilityObject();
364 if (!root_obj
.updateLayoutAndCheckValidity())
367 WebAXObject obj
= root_obj
.hitTest(point
);
368 if (!obj
.isDetached())
369 HandleAXEvent(obj
, ui::AX_EVENT_HOVER
);
372 void RendererAccessibility::OnSetAccessibilityFocus(int acc_obj_id
) {
373 if (tree_source_
.accessibility_focus_id() == acc_obj_id
)
376 tree_source_
.set_accessiblity_focus_id(acc_obj_id
);
378 const WebDocument
& document
= GetMainDocument();
379 if (document
.isNull())
382 WebAXObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
384 // This object may not be a leaf node. Force the whole subtree to be
386 serializer_
.DeleteClientSubtree(obj
);
388 // Explicitly send a tree change update event now.
389 HandleAXEvent(obj
, ui::AX_EVENT_TREE_CHANGED
);
392 void RendererAccessibility::OnReset(int reset_token
) {
393 reset_token_
= reset_token
;
395 pending_events_
.clear();
397 const WebDocument
& document
= GetMainDocument();
398 if (!document
.isNull()) {
399 // Tree-only mode gets used by the automation extension API which requires a
400 // load complete event to invoke listener callbacks.
401 ui::AXEvent evt
= document
.accessibilityObject().isLoaded()
402 ? ui::AX_EVENT_LOAD_COMPLETE
: ui::AX_EVENT_LAYOUT_COMPLETE
;
403 HandleAXEvent(document
.accessibilityObject(), evt
);
407 void RendererAccessibility::OnScrollToMakeVisible(
408 int acc_obj_id
, gfx::Rect subfocus
) {
409 const WebDocument
& document
= GetMainDocument();
410 if (document
.isNull())
413 WebAXObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
414 if (obj
.isDetached()) {
416 LOG(WARNING
) << "ScrollToMakeVisible on invalid object id " << acc_obj_id
;
421 obj
.scrollToMakeVisibleWithSubFocus(
422 WebRect(subfocus
.x(), subfocus
.y(), subfocus
.width(), subfocus
.height()));
424 // Make sure the browser gets an event when the scroll
425 // position actually changes.
426 // TODO(dmazzoni): remove this once this bug is fixed:
427 // https://bugs.webkit.org/show_bug.cgi?id=73460
428 HandleAXEvent(document
.accessibilityObject(), ui::AX_EVENT_LAYOUT_COMPLETE
);
431 void RendererAccessibility::OnScrollToPoint(int acc_obj_id
, gfx::Point point
) {
432 const WebDocument
& document
= GetMainDocument();
433 if (document
.isNull())
436 WebAXObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
437 if (obj
.isDetached()) {
439 LOG(WARNING
) << "ScrollToPoint on invalid object id " << acc_obj_id
;
444 obj
.scrollToGlobalPoint(WebPoint(point
.x(), point
.y()));
446 // Make sure the browser gets an event when the scroll
447 // position actually changes.
448 // TODO(dmazzoni): remove this once this bug is fixed:
449 // https://bugs.webkit.org/show_bug.cgi?id=73460
450 HandleAXEvent(document
.accessibilityObject(), ui::AX_EVENT_LAYOUT_COMPLETE
);
453 void RendererAccessibility::OnSetScrollOffset(int acc_obj_id
,
455 const WebDocument
& document
= GetMainDocument();
456 if (document
.isNull())
459 WebAXObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
460 if (obj
.isDetached())
463 obj
.setScrollOffset(WebPoint(offset
.x(), offset
.y()));
466 void RendererAccessibility::OnSetFocus(int acc_obj_id
) {
467 const WebDocument
& document
= GetMainDocument();
468 if (document
.isNull())
471 WebAXObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
472 if (obj
.isDetached()) {
474 LOG(WARNING
) << "OnSetAccessibilityFocus on invalid object id "
480 WebAXObject root
= document
.accessibilityObject();
481 if (root
.isDetached()) {
483 LOG(WARNING
) << "OnSetAccessibilityFocus but root is invalid";
488 // By convention, calling SetFocus on the root of the tree should clear the
489 // current focus. Otherwise set the focus to the new node.
490 if (acc_obj_id
== root
.axID())
491 render_frame_
->GetRenderView()->GetWebView()->clearFocusedElement();
493 obj
.setFocused(true);
496 void RendererAccessibility::OnSetTextSelection(
497 int acc_obj_id
, int start_offset
, int end_offset
) {
498 const WebDocument
& document
= GetMainDocument();
499 if (document
.isNull())
502 WebAXObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
503 if (obj
.isDetached()) {
505 LOG(WARNING
) << "SetTextSelection on invalid object id " << acc_obj_id
;
510 obj
.setSelectedTextRange(start_offset
, end_offset
);
511 WebAXObject root
= document
.accessibilityObject();
512 if (root
.isDetached()) {
514 LOG(WARNING
) << "OnSetAccessibilityFocus but root is invalid";
518 HandleAXEvent(root
, ui::AX_EVENT_LAYOUT_COMPLETE
);
521 void RendererAccessibility::OnSetValue(
523 base::string16 value
) {
524 const WebDocument
& document
= GetMainDocument();
525 if (document
.isNull())
528 WebAXObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
529 if (obj
.isDetached()) {
531 LOG(WARNING
) << "SetTextSelection on invalid object id " << acc_obj_id
;
537 HandleAXEvent(obj
, ui::AX_EVENT_VALUE_CHANGED
);
540 void RendererAccessibility::OnShowContextMenu(int acc_obj_id
) {
541 const WebDocument
& document
= GetMainDocument();
542 if (document
.isNull())
545 WebAXObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
546 if (obj
.isDetached()) {
548 LOG(WARNING
) << "ShowContextMenu on invalid object id " << acc_obj_id
;
553 obj
.showContextMenu();
556 } // namespace content