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/message_loop/message_loop.h"
11 #include "base/strings/utf_string_conversions.h"
12 #include "content/renderer/accessibility/blink_ax_enum_conversion.h"
13 #include "content/renderer/render_frame_impl.h"
14 #include "content/renderer/render_view_impl.h"
15 #include "third_party/WebKit/public/web/WebAXObject.h"
16 #include "third_party/WebKit/public/web/WebDocument.h"
17 #include "third_party/WebKit/public/web/WebInputElement.h"
18 #include "third_party/WebKit/public/web/WebLocalFrame.h"
19 #include "third_party/WebKit/public/web/WebSettings.h"
20 #include "third_party/WebKit/public/web/WebView.h"
22 using blink::WebAXObject
;
23 using blink::WebDocument
;
24 using blink::WebLocalFrame
;
26 using blink::WebPoint
;
28 using blink::WebScopedAXContext
;
29 using blink::WebSettings
;
35 void RendererAccessibility::SnapshotAccessibilityTree(
36 RenderFrameImpl
* render_frame
,
37 ui::AXTreeUpdate
* response
) {
40 if (!render_frame
->GetWebFrame())
43 WebDocument document
= render_frame
->GetWebFrame()->document();
44 WebScopedAXContext
context(document
);
45 BlinkAXTreeSource
tree_source(render_frame
);
46 tree_source
.SetRoot(context
.root());
47 ui::AXTreeSerializer
<blink::WebAXObject
> serializer(&tree_source
);
48 serializer
.SerializeChanges(context
.root(), response
);
51 RendererAccessibility::RendererAccessibility(RenderFrameImpl
* render_frame
)
52 : RenderFrameObserver(render_frame
),
53 render_frame_(render_frame
),
54 tree_source_(render_frame
),
55 serializer_(&tree_source_
),
56 last_scroll_offset_(gfx::Size()),
60 WebView
* web_view
= render_frame_
->GetRenderView()->GetWebView();
61 WebSettings
* settings
= web_view
->settings();
62 settings
->setAccessibilityEnabled(true);
64 #if defined(OS_ANDROID)
65 // Password values are only passed through on Android.
66 settings
->setAccessibilityPasswordValuesEnabled(true);
69 #if !defined(OS_ANDROID)
70 // Inline text boxes are enabled for all nodes on all except Android.
71 settings
->setInlineTextBoxAccessibilityEnabled(true);
74 const WebDocument
& document
= GetMainDocument();
75 if (!document
.isNull()) {
76 // It's possible that the webview has already loaded a webpage without
77 // accessibility being enabled. Initialize the browser's cached
78 // accessibility tree by sending it a notification.
79 HandleAXEvent(document
.accessibilityObject(), ui::AX_EVENT_LAYOUT_COMPLETE
);
83 RendererAccessibility::~RendererAccessibility() {
86 bool RendererAccessibility::OnMessageReceived(const IPC::Message
& message
) {
88 IPC_BEGIN_MESSAGE_MAP(RendererAccessibility
, message
)
89 IPC_MESSAGE_HANDLER(AccessibilityMsg_SetFocus
, OnSetFocus
)
90 IPC_MESSAGE_HANDLER(AccessibilityMsg_DoDefaultAction
, OnDoDefaultAction
)
91 IPC_MESSAGE_HANDLER(AccessibilityMsg_Events_ACK
, OnEventsAck
)
92 IPC_MESSAGE_HANDLER(AccessibilityMsg_ScrollToMakeVisible
,
93 OnScrollToMakeVisible
)
94 IPC_MESSAGE_HANDLER(AccessibilityMsg_ScrollToPoint
, OnScrollToPoint
)
95 IPC_MESSAGE_HANDLER(AccessibilityMsg_SetTextSelection
, OnSetTextSelection
)
96 IPC_MESSAGE_HANDLER(AccessibilityMsg_SetValue
, OnSetValue
)
97 IPC_MESSAGE_HANDLER(AccessibilityMsg_HitTest
, OnHitTest
)
98 IPC_MESSAGE_HANDLER(AccessibilityMsg_SetAccessibilityFocus
,
99 OnSetAccessibilityFocus
)
100 IPC_MESSAGE_HANDLER(AccessibilityMsg_Reset
, OnReset
)
101 IPC_MESSAGE_HANDLER(AccessibilityMsg_FatalError
, OnFatalError
)
102 IPC_MESSAGE_UNHANDLED(handled
= false)
103 IPC_END_MESSAGE_MAP()
107 void RendererAccessibility::HandleWebAccessibilityEvent(
108 const blink::WebAXObject
& obj
, blink::WebAXEvent event
) {
109 HandleAXEvent(obj
, AXEventFromBlink(event
));
112 void RendererAccessibility::HandleAccessibilityFindInPageResult(
115 const blink::WebAXObject
& start_object
,
117 const blink::WebAXObject
& end_object
,
119 AccessibilityHostMsg_FindInPageResultParams params
;
120 params
.request_id
= identifier
;
121 params
.match_index
= match_index
;
122 params
.start_id
= start_object
.axID();
123 params
.start_offset
= start_offset
;
124 params
.end_id
= end_object
.axID();
125 params
.end_offset
= end_offset
;
126 Send(new AccessibilityHostMsg_FindInPageResult(routing_id(), params
));
129 void RendererAccessibility::AccessibilityFocusedNodeChanged(
130 const WebNode
& node
) {
131 const WebDocument
& document
= GetMainDocument();
132 if (document
.isNull())
136 // When focus is cleared, implicitly focus the document.
137 // TODO(dmazzoni): Make Blink send this notification instead.
138 HandleAXEvent(document
.accessibilityObject(), ui::AX_EVENT_BLUR
);
142 void RendererAccessibility::DisableAccessibility() {
143 RenderView
* render_view
= render_frame_
->GetRenderView();
147 WebView
* web_view
= render_view
->GetWebView();
151 WebSettings
* settings
= web_view
->settings();
155 settings
->setAccessibilityEnabled(false);
158 void RendererAccessibility::HandleAXEvent(
159 const blink::WebAXObject
& obj
, ui::AXEvent event
) {
160 const WebDocument
& document
= GetMainDocument();
161 if (document
.isNull())
164 gfx::Size scroll_offset
= document
.frame()->scrollOffset();
165 if (scroll_offset
!= last_scroll_offset_
) {
166 // Make sure the browser is always aware of the scroll position of
167 // the root document element by posting a generic notification that
169 // TODO(dmazzoni): remove this as soon as
170 // https://bugs.webkit.org/show_bug.cgi?id=73460 is fixed.
171 last_scroll_offset_
= scroll_offset
;
172 if (!obj
.equals(document
.accessibilityObject())) {
173 HandleAXEvent(document
.accessibilityObject(),
174 ui::AX_EVENT_LAYOUT_COMPLETE
);
178 // Add the accessibility object to our cache and ensure it's valid.
179 AccessibilityHostMsg_EventParams acc_event
;
180 acc_event
.id
= obj
.axID();
181 acc_event
.event_type
= event
;
183 // Discard duplicate accessibility events.
184 for (uint32 i
= 0; i
< pending_events_
.size(); ++i
) {
185 if (pending_events_
[i
].id
== acc_event
.id
&&
186 pending_events_
[i
].event_type
== acc_event
.event_type
) {
190 pending_events_
.push_back(acc_event
);
192 if (!ack_pending_
&& !weak_factory_
.HasWeakPtrs()) {
193 // When no accessibility events are in-flight post a task to send
194 // the events to the browser. We use PostTask so that we can queue
195 // up additional events.
196 base::MessageLoop::current()->PostTask(
198 base::Bind(&RendererAccessibility::SendPendingAccessibilityEvents
,
199 weak_factory_
.GetWeakPtr()));
203 WebDocument
RendererAccessibility::GetMainDocument() {
204 if (render_frame_
&& render_frame_
->GetWebFrame())
205 return render_frame_
->GetWebFrame()->document();
206 return WebDocument();
209 void RendererAccessibility::SendPendingAccessibilityEvents() {
210 const WebDocument
& document
= GetMainDocument();
211 if (document
.isNull())
214 if (pending_events_
.empty())
217 if (render_frame_
->is_swapped_out())
222 // Make a copy of the events, because it's possible that
223 // actions inside this loop will cause more events to be
225 std::vector
<AccessibilityHostMsg_EventParams
> src_events
= pending_events_
;
226 pending_events_
.clear();
228 // Generate an event message from each Blink event.
229 std::vector
<AccessibilityHostMsg_EventParams
> event_msgs
;
231 // If there's a layout complete message, we need to send location changes.
232 bool had_layout_complete_messages
= false;
234 // Loop over each event and generate an updated event message.
235 for (size_t i
= 0; i
< src_events
.size(); ++i
) {
236 AccessibilityHostMsg_EventParams
& event
= src_events
[i
];
237 if (event
.event_type
== ui::AX_EVENT_LAYOUT_COMPLETE
)
238 had_layout_complete_messages
= true;
240 WebAXObject obj
= document
.accessibilityObjectFromID(event
.id
);
242 // Make sure the object still exists.
243 if (!obj
.updateLayoutAndCheckValidity())
246 // If it's ignored, find the first ancestor that's not ignored.
247 while (!obj
.isDetached() && obj
.accessibilityIsIgnored())
248 obj
= obj
.parentObject();
250 // Make sure it's a descendant of our root node - exceptions include the
251 // scroll area that's the parent of the main document (we ignore it), and
252 // possibly nodes attached to a different document.
253 if (!tree_source_
.IsInTree(obj
))
256 // When we get a "selected children changed" event, Blink
257 // doesn't also send us events for each child that changed
258 // selection state, so make sure we re-send that whole subtree.
259 if (event
.event_type
== ui::AX_EVENT_SELECTED_CHILDREN_CHANGED
) {
260 serializer_
.DeleteClientSubtree(obj
);
263 AccessibilityHostMsg_EventParams event_msg
;
264 tree_source_
.CollectChildFrameIdMapping(
265 &event_msg
.node_to_frame_routing_id_map
,
266 &event_msg
.node_to_browser_plugin_instance_id_map
);
267 event_msg
.event_type
= event
.event_type
;
268 event_msg
.id
= event
.id
;
269 serializer_
.SerializeChanges(obj
, &event_msg
.update
);
270 event_msgs
.push_back(event_msg
);
272 // For each node in the update, set the location in our map from
274 for (size_t i
= 0; i
< event_msg
.update
.nodes
.size(); ++i
) {
275 locations_
[event_msg
.update
.nodes
[i
].id
] =
276 event_msg
.update
.nodes
[i
].location
;
279 DVLOG(0) << "Accessibility event: " << ui::ToString(event
.event_type
)
280 << " on node id " << event_msg
.id
281 << "\n" << event_msg
.update
.ToString();
284 Send(new AccessibilityHostMsg_Events(routing_id(), event_msgs
, reset_token_
));
287 if (had_layout_complete_messages
)
288 SendLocationChanges();
291 void RendererAccessibility::SendLocationChanges() {
292 std::vector
<AccessibilityHostMsg_LocationChangeParams
> messages
;
294 // Do a breadth-first explore of the whole blink AX tree.
295 base::hash_map
<int, gfx::Rect
> new_locations
;
296 std::queue
<WebAXObject
> objs_to_explore
;
297 objs_to_explore
.push(tree_source_
.GetRoot());
298 while (objs_to_explore
.size()) {
299 WebAXObject obj
= objs_to_explore
.front();
300 objs_to_explore
.pop();
302 // See if we had a previous location. If not, this whole subtree must
303 // be new, so don't continue to explore this branch.
305 base::hash_map
<int, gfx::Rect
>::iterator iter
= locations_
.find(id
);
306 if (iter
== locations_
.end())
309 // If the location has changed, append it to the IPC message.
310 gfx::Rect new_location
= obj
.boundingBoxRect();
311 if (iter
!= locations_
.end() && iter
->second
!= new_location
) {
312 AccessibilityHostMsg_LocationChangeParams message
;
314 message
.new_location
= new_location
;
315 messages
.push_back(message
);
318 // Save the new location.
319 new_locations
[id
] = new_location
;
321 // Explore children of this object.
322 std::vector
<blink::WebAXObject
> children
;
323 tree_source_
.GetChildren(obj
, &children
);
324 for (size_t i
= 0; i
< children
.size(); ++i
)
325 objs_to_explore
.push(children
[i
]);
327 locations_
.swap(new_locations
);
329 Send(new AccessibilityHostMsg_LocationChanges(routing_id(), messages
));
332 void RendererAccessibility::OnDoDefaultAction(int acc_obj_id
) {
333 const WebDocument
& document
= GetMainDocument();
334 if (document
.isNull())
337 WebAXObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
338 if (obj
.isDetached()) {
340 LOG(WARNING
) << "DoDefaultAction on invalid object id " << acc_obj_id
;
345 obj
.performDefaultAction();
348 void RendererAccessibility::OnEventsAck() {
349 DCHECK(ack_pending_
);
350 ack_pending_
= false;
351 SendPendingAccessibilityEvents();
354 void RendererAccessibility::OnFatalError() {
355 CHECK(false) << "Invalid accessibility tree.";
358 void RendererAccessibility::OnHitTest(gfx::Point point
) {
359 const WebDocument
& document
= GetMainDocument();
360 if (document
.isNull())
362 WebAXObject root_obj
= document
.accessibilityObject();
363 if (!root_obj
.updateLayoutAndCheckValidity())
366 WebAXObject obj
= root_obj
.hitTest(point
);
367 if (!obj
.isDetached())
368 HandleAXEvent(obj
, ui::AX_EVENT_HOVER
);
371 void RendererAccessibility::OnSetAccessibilityFocus(int acc_obj_id
) {
372 if (tree_source_
.accessibility_focus_id() == acc_obj_id
)
375 tree_source_
.set_accessiblity_focus_id(acc_obj_id
);
377 const WebDocument
& document
= GetMainDocument();
378 if (document
.isNull())
381 WebAXObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
383 // This object may not be a leaf node. Force the whole subtree to be
385 serializer_
.DeleteClientSubtree(obj
);
387 // Explicitly send a tree change update event now.
388 HandleAXEvent(obj
, ui::AX_EVENT_TREE_CHANGED
);
391 void RendererAccessibility::OnReset(int reset_token
) {
392 reset_token_
= reset_token
;
394 pending_events_
.clear();
396 const WebDocument
& document
= GetMainDocument();
397 if (!document
.isNull())
398 HandleAXEvent(document
.accessibilityObject(), ui::AX_EVENT_LAYOUT_COMPLETE
);
401 void RendererAccessibility::OnScrollToMakeVisible(
402 int acc_obj_id
, gfx::Rect subfocus
) {
403 const WebDocument
& document
= GetMainDocument();
404 if (document
.isNull())
407 WebAXObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
408 if (obj
.isDetached()) {
410 LOG(WARNING
) << "ScrollToMakeVisible on invalid object id " << acc_obj_id
;
415 obj
.scrollToMakeVisibleWithSubFocus(
416 WebRect(subfocus
.x(), subfocus
.y(), subfocus
.width(), subfocus
.height()));
418 // Make sure the browser gets an event when the scroll
419 // position actually changes.
420 // TODO(dmazzoni): remove this once this bug is fixed:
421 // https://bugs.webkit.org/show_bug.cgi?id=73460
422 HandleAXEvent(document
.accessibilityObject(), ui::AX_EVENT_LAYOUT_COMPLETE
);
425 void RendererAccessibility::OnScrollToPoint(int acc_obj_id
, gfx::Point point
) {
426 const WebDocument
& document
= GetMainDocument();
427 if (document
.isNull())
430 WebAXObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
431 if (obj
.isDetached()) {
433 LOG(WARNING
) << "ScrollToPoint on invalid object id " << acc_obj_id
;
438 obj
.scrollToGlobalPoint(WebPoint(point
.x(), point
.y()));
440 // Make sure the browser gets an event when the scroll
441 // position actually changes.
442 // TODO(dmazzoni): remove this once this bug is fixed:
443 // https://bugs.webkit.org/show_bug.cgi?id=73460
444 HandleAXEvent(document
.accessibilityObject(), ui::AX_EVENT_LAYOUT_COMPLETE
);
447 void RendererAccessibility::OnSetFocus(int acc_obj_id
) {
448 const WebDocument
& document
= GetMainDocument();
449 if (document
.isNull())
452 WebAXObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
453 if (obj
.isDetached()) {
455 LOG(WARNING
) << "OnSetAccessibilityFocus on invalid object id "
461 WebAXObject root
= document
.accessibilityObject();
462 if (root
.isDetached()) {
464 LOG(WARNING
) << "OnSetAccessibilityFocus but root is invalid";
469 // By convention, calling SetFocus on the root of the tree should clear the
470 // current focus. Otherwise set the focus to the new node.
471 if (acc_obj_id
== root
.axID())
472 render_frame_
->GetRenderView()->GetWebView()->clearFocusedElement();
474 obj
.setFocused(true);
477 void RendererAccessibility::OnSetTextSelection(
478 int acc_obj_id
, int start_offset
, int end_offset
) {
479 const WebDocument
& document
= GetMainDocument();
480 if (document
.isNull())
483 WebAXObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
484 if (obj
.isDetached()) {
486 LOG(WARNING
) << "SetTextSelection on invalid object id " << acc_obj_id
;
491 obj
.setSelectedTextRange(start_offset
, end_offset
);
494 void RendererAccessibility::OnSetValue(
496 base::string16 value
) {
497 const WebDocument
& document
= GetMainDocument();
498 if (document
.isNull())
501 WebAXObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
502 if (obj
.isDetached()) {
504 LOG(WARNING
) << "SetTextSelection on invalid object id " << acc_obj_id
;
510 HandleAXEvent(obj
, ui::AX_EVENT_VALUE_CHANGED
);
513 } // namespace content