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_complete.h"
10 #include "base/message_loop/message_loop.h"
11 #include "content/renderer/accessibility/blink_ax_enum_conversion.h"
12 #include "content/renderer/render_view_impl.h"
13 #include "third_party/WebKit/public/web/WebAXObject.h"
14 #include "third_party/WebKit/public/web/WebDocument.h"
15 #include "third_party/WebKit/public/web/WebInputElement.h"
16 #include "third_party/WebKit/public/web/WebLocalFrame.h"
17 #include "third_party/WebKit/public/web/WebNode.h"
18 #include "third_party/WebKit/public/web/WebView.h"
19 #include "ui/accessibility/ax_tree.h"
21 using blink::WebAXObject
;
22 using blink::WebDocument
;
24 using blink::WebPoint
;
31 RendererAccessibilityComplete::RendererAccessibilityComplete(
32 RenderViewImpl
* render_view
)
33 : RendererAccessibility(render_view
),
35 tree_source_(render_view
),
36 serializer_(&tree_source_
),
37 last_scroll_offset_(gfx::Size()),
39 WebAXObject::enableAccessibility();
41 #if !defined(OS_ANDROID)
42 // Skip inline text boxes on Android - since there are no native Android
43 // APIs that compute the bounds of a range of text, it's a waste to
44 // include these in the AX tree.
45 WebAXObject::enableInlineTextBoxAccessibility();
48 const WebDocument
& document
= GetMainDocument();
49 if (!document
.isNull()) {
50 // It's possible that the webview has already loaded a webpage without
51 // accessibility being enabled. Initialize the browser's cached
52 // accessibility tree by sending it a notification.
53 HandleAXEvent(document
.accessibilityObject(),
54 ui::AX_EVENT_LAYOUT_COMPLETE
);
58 RendererAccessibilityComplete::~RendererAccessibilityComplete() {
61 bool RendererAccessibilityComplete::OnMessageReceived(
62 const IPC::Message
& message
) {
64 IPC_BEGIN_MESSAGE_MAP(RendererAccessibilityComplete
, message
)
65 IPC_MESSAGE_HANDLER(AccessibilityMsg_SetFocus
, OnSetFocus
)
66 IPC_MESSAGE_HANDLER(AccessibilityMsg_DoDefaultAction
,
68 IPC_MESSAGE_HANDLER(AccessibilityMsg_Events_ACK
,
70 IPC_MESSAGE_HANDLER(AccessibilityMsg_ScrollToMakeVisible
,
71 OnScrollToMakeVisible
)
72 IPC_MESSAGE_HANDLER(AccessibilityMsg_ScrollToPoint
,
74 IPC_MESSAGE_HANDLER(AccessibilityMsg_SetTextSelection
,
76 IPC_MESSAGE_HANDLER(AccessibilityMsg_FatalError
, OnFatalError
)
77 IPC_MESSAGE_UNHANDLED(handled
= false)
82 void RendererAccessibilityComplete::FocusedNodeChanged(const WebNode
& node
) {
83 const WebDocument
& document
= GetMainDocument();
84 if (document
.isNull())
88 // When focus is cleared, implicitly focus the document.
89 // TODO(dmazzoni): Make Blink send this notification instead.
90 HandleAXEvent(document
.accessibilityObject(), ui::AX_EVENT_BLUR
);
94 void RendererAccessibilityComplete::DidFinishLoad(blink::WebLocalFrame
* frame
) {
95 const WebDocument
& document
= GetMainDocument();
96 if (document
.isNull())
101 void RendererAccessibilityComplete::HandleWebAccessibilityEvent(
102 const blink::WebAXObject
& obj
, blink::WebAXEvent event
) {
103 HandleAXEvent(obj
, AXEventFromBlink(event
));
106 void RendererAccessibilityComplete::HandleAXEvent(
107 const blink::WebAXObject
& obj
, ui::AXEvent event
) {
108 const WebDocument
& document
= GetMainDocument();
109 if (document
.isNull())
112 gfx::Size scroll_offset
= document
.frame()->scrollOffset();
113 if (scroll_offset
!= last_scroll_offset_
) {
114 // Make sure the browser is always aware of the scroll position of
115 // the root document element by posting a generic notification that
117 // TODO(dmazzoni): remove this as soon as
118 // https://bugs.webkit.org/show_bug.cgi?id=73460 is fixed.
119 last_scroll_offset_
= scroll_offset
;
120 if (!obj
.equals(document
.accessibilityObject())) {
121 HandleAXEvent(document
.accessibilityObject(),
122 ui::AX_EVENT_LAYOUT_COMPLETE
);
126 // Add the accessibility object to our cache and ensure it's valid.
127 AccessibilityHostMsg_EventParams acc_event
;
128 acc_event
.id
= obj
.axID();
129 acc_event
.event_type
= event
;
131 // Discard duplicate accessibility events.
132 for (uint32 i
= 0; i
< pending_events_
.size(); ++i
) {
133 if (pending_events_
[i
].id
== acc_event
.id
&&
134 pending_events_
[i
].event_type
==
135 acc_event
.event_type
) {
139 pending_events_
.push_back(acc_event
);
141 if (!ack_pending_
&& !weak_factory_
.HasWeakPtrs()) {
142 // When no accessibility events are in-flight post a task to send
143 // the events to the browser. We use PostTask so that we can queue
144 // up additional events.
145 base::MessageLoop::current()->PostTask(
147 base::Bind(&RendererAccessibilityComplete::
148 SendPendingAccessibilityEvents
,
149 weak_factory_
.GetWeakPtr()));
153 RendererAccessibilityType
RendererAccessibilityComplete::GetType() {
154 return RendererAccessibilityTypeComplete
;
157 void RendererAccessibilityComplete::SendPendingAccessibilityEvents() {
158 const WebDocument
& document
= GetMainDocument();
159 if (document
.isNull())
162 if (pending_events_
.empty())
165 if (render_view_
->is_swapped_out())
170 // Make a copy of the events, because it's possible that
171 // actions inside this loop will cause more events to be
173 std::vector
<AccessibilityHostMsg_EventParams
> src_events
=
175 pending_events_
.clear();
177 // Generate an event message from each Blink event.
178 std::vector
<AccessibilityHostMsg_EventParams
> event_msgs
;
180 // Loop over each event and generate an updated event message.
181 for (size_t i
= 0; i
< src_events
.size(); ++i
) {
182 AccessibilityHostMsg_EventParams
& event
=
185 WebAXObject obj
= document
.accessibilityObjectFromID(
187 if (!obj
.updateBackingStoreAndCheckValidity())
190 // When we get a "selected children changed" event, Blink
191 // doesn't also send us events for each child that changed
192 // selection state, so make sure we re-send that whole subtree.
193 if (event
.event_type
==
194 ui::AX_EVENT_SELECTED_CHILDREN_CHANGED
) {
195 serializer_
.DeleteClientSubtree(obj
);
198 AccessibilityHostMsg_EventParams event_msg
;
199 event_msg
.event_type
= event
.event_type
;
200 event_msg
.id
= event
.id
;
201 serializer_
.SerializeChanges(obj
, &event_msg
.update
);
202 event_msgs
.push_back(event_msg
);
205 VLOG(0) << "Accessibility update: \n"
206 << "routing id=" << routing_id()
208 << AccessibilityEventToString(event
.event_type
)
209 << "\n" << event_msg
.update
.ToString();
213 Send(new AccessibilityHostMsg_Events(routing_id(), event_msgs
));
215 SendLocationChanges();
218 void RendererAccessibilityComplete::SendLocationChanges() {
219 std::vector
<AccessibilityHostMsg_LocationChangeParams
> messages
;
221 // Do a breadth-first explore of the whole blink AX tree.
222 base::hash_map
<int, gfx::Rect
> new_locations
;
223 std::queue
<WebAXObject
> objs_to_explore
;
224 objs_to_explore
.push(tree_source_
.GetRoot());
225 while (objs_to_explore
.size()) {
226 WebAXObject obj
= objs_to_explore
.front();
227 objs_to_explore
.pop();
229 // See if we had a previous location. If not, this whole subtree must
230 // be new, so don't continue to explore this branch.
232 base::hash_map
<int, gfx::Rect
>::iterator iter
= locations_
.find(id
);
233 if (iter
== locations_
.end())
236 // If the location has changed, append it to the IPC message.
237 gfx::Rect new_location
= obj
.boundingBoxRect();
238 if (iter
!= locations_
.end() && iter
->second
!= new_location
) {
239 AccessibilityHostMsg_LocationChangeParams message
;
241 message
.new_location
= new_location
;
242 messages
.push_back(message
);
245 // Save the new location.
246 new_locations
[id
] = new_location
;
248 locations_
.swap(new_locations
);
250 Send(new AccessibilityHostMsg_LocationChanges(routing_id(), messages
));
253 void RendererAccessibilityComplete::OnDoDefaultAction(int acc_obj_id
) {
254 const WebDocument
& document
= GetMainDocument();
255 if (document
.isNull())
258 WebAXObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
259 if (obj
.isDetached()) {
261 LOG(WARNING
) << "DoDefaultAction on invalid object id " << acc_obj_id
;
266 obj
.performDefaultAction();
269 void RendererAccessibilityComplete::OnScrollToMakeVisible(
270 int acc_obj_id
, gfx::Rect subfocus
) {
271 const WebDocument
& document
= GetMainDocument();
272 if (document
.isNull())
275 WebAXObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
276 if (obj
.isDetached()) {
278 LOG(WARNING
) << "ScrollToMakeVisible on invalid object id " << acc_obj_id
;
283 obj
.scrollToMakeVisibleWithSubFocus(
284 WebRect(subfocus
.x(), subfocus
.y(),
285 subfocus
.width(), subfocus
.height()));
287 // Make sure the browser gets an event when the scroll
288 // position actually changes.
289 // TODO(dmazzoni): remove this once this bug is fixed:
290 // https://bugs.webkit.org/show_bug.cgi?id=73460
291 HandleAXEvent(document
.accessibilityObject(),
292 ui::AX_EVENT_LAYOUT_COMPLETE
);
295 void RendererAccessibilityComplete::OnScrollToPoint(
296 int acc_obj_id
, gfx::Point point
) {
297 const WebDocument
& document
= GetMainDocument();
298 if (document
.isNull())
301 WebAXObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
302 if (obj
.isDetached()) {
304 LOG(WARNING
) << "ScrollToPoint on invalid object id " << acc_obj_id
;
309 obj
.scrollToGlobalPoint(WebPoint(point
.x(), point
.y()));
311 // Make sure the browser gets an event when the scroll
312 // position actually changes.
313 // TODO(dmazzoni): remove this once this bug is fixed:
314 // https://bugs.webkit.org/show_bug.cgi?id=73460
315 HandleAXEvent(document
.accessibilityObject(),
316 ui::AX_EVENT_LAYOUT_COMPLETE
);
319 void RendererAccessibilityComplete::OnSetTextSelection(
320 int acc_obj_id
, int start_offset
, int end_offset
) {
321 const WebDocument
& document
= GetMainDocument();
322 if (document
.isNull())
325 WebAXObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
326 if (obj
.isDetached()) {
328 LOG(WARNING
) << "SetTextSelection on invalid object id " << acc_obj_id
;
333 // TODO(dmazzoni): support elements other than <input>.
334 blink::WebNode node
= obj
.node();
335 if (!node
.isNull() && node
.isElementNode()) {
336 blink::WebElement element
= node
.to
<blink::WebElement
>();
337 blink::WebInputElement
* input_element
=
338 blink::toWebInputElement(&element
);
339 if (input_element
&& input_element
->isTextField())
340 input_element
->setSelectionRange(start_offset
, end_offset
);
344 void RendererAccessibilityComplete::OnEventsAck() {
345 DCHECK(ack_pending_
);
346 ack_pending_
= false;
347 SendPendingAccessibilityEvents();
350 void RendererAccessibilityComplete::OnSetFocus(int acc_obj_id
) {
351 const WebDocument
& document
= GetMainDocument();
352 if (document
.isNull())
355 WebAXObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
356 if (obj
.isDetached()) {
358 LOG(WARNING
) << "OnSetAccessibilityFocus on invalid object id "
364 WebAXObject root
= document
.accessibilityObject();
365 if (root
.isDetached()) {
367 LOG(WARNING
) << "OnSetAccessibilityFocus but root is invalid";
372 // By convention, calling SetFocus on the root of the tree should clear the
373 // current focus. Otherwise set the focus to the new node.
374 if (acc_obj_id
== root
.axID())
375 render_view()->GetWebView()->clearFocusedElement();
377 obj
.setFocused(true);
380 void RendererAccessibilityComplete::OnFatalError() {
381 CHECK(false) << "Invalid accessibility tree.";
384 } // namespace content