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_frame_impl.h"
13 #include "content/renderer/render_view_impl.h"
14 #include "third_party/WebKit/public/web/WebAXObject.h"
15 #include "third_party/WebKit/public/web/WebDocument.h"
16 #include "third_party/WebKit/public/web/WebInputElement.h"
17 #include "third_party/WebKit/public/web/WebLocalFrame.h"
18 #include "third_party/WebKit/public/web/WebNode.h"
19 #include "third_party/WebKit/public/web/WebView.h"
20 #include "ui/accessibility/ax_tree.h"
22 using blink::WebAXObject
;
23 using blink::WebDocument
;
25 using blink::WebPoint
;
31 RendererAccessibilityComplete::RendererAccessibilityComplete(
32 RenderFrameImpl
* render_frame
)
33 : RendererAccessibility(render_frame
),
35 tree_source_(render_frame
),
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_HitTest
, OnHitTest
)
77 IPC_MESSAGE_HANDLER(AccessibilityMsg_FatalError
, OnFatalError
)
78 IPC_MESSAGE_UNHANDLED(handled
= false)
83 void RendererAccessibilityComplete::FocusedNodeChanged(const WebNode
& node
) {
84 const WebDocument
& document
= GetMainDocument();
85 if (document
.isNull())
89 // When focus is cleared, implicitly focus the document.
90 // TODO(dmazzoni): Make Blink send this notification instead.
91 HandleAXEvent(document
.accessibilityObject(), ui::AX_EVENT_BLUR
);
95 void RendererAccessibilityComplete::HandleWebAccessibilityEvent(
96 const blink::WebAXObject
& obj
, blink::WebAXEvent event
) {
97 HandleAXEvent(obj
, AXEventFromBlink(event
));
100 void RendererAccessibilityComplete::HandleAXEvent(
101 const blink::WebAXObject
& obj
, ui::AXEvent event
) {
102 const WebDocument
& document
= GetMainDocument();
103 if (document
.isNull())
106 gfx::Size scroll_offset
= document
.frame()->scrollOffset();
107 if (scroll_offset
!= last_scroll_offset_
) {
108 // Make sure the browser is always aware of the scroll position of
109 // the root document element by posting a generic notification that
111 // TODO(dmazzoni): remove this as soon as
112 // https://bugs.webkit.org/show_bug.cgi?id=73460 is fixed.
113 last_scroll_offset_
= scroll_offset
;
114 if (!obj
.equals(document
.accessibilityObject())) {
115 HandleAXEvent(document
.accessibilityObject(),
116 ui::AX_EVENT_LAYOUT_COMPLETE
);
120 // Add the accessibility object to our cache and ensure it's valid.
121 AccessibilityHostMsg_EventParams acc_event
;
122 acc_event
.id
= obj
.axID();
123 acc_event
.event_type
= event
;
125 // Discard duplicate accessibility events.
126 for (uint32 i
= 0; i
< pending_events_
.size(); ++i
) {
127 if (pending_events_
[i
].id
== acc_event
.id
&&
128 pending_events_
[i
].event_type
==
129 acc_event
.event_type
) {
133 pending_events_
.push_back(acc_event
);
135 if (!ack_pending_
&& !weak_factory_
.HasWeakPtrs()) {
136 // When no accessibility events are in-flight post a task to send
137 // the events to the browser. We use PostTask so that we can queue
138 // up additional events.
139 base::MessageLoop::current()->PostTask(
141 base::Bind(&RendererAccessibilityComplete::
142 SendPendingAccessibilityEvents
,
143 weak_factory_
.GetWeakPtr()));
147 RendererAccessibilityType
RendererAccessibilityComplete::GetType() {
148 return RendererAccessibilityTypeComplete
;
151 void RendererAccessibilityComplete::SendPendingAccessibilityEvents() {
152 const WebDocument
& document
= GetMainDocument();
153 if (document
.isNull())
156 if (pending_events_
.empty())
159 if (render_frame_
->is_swapped_out())
164 // Make a copy of the events, because it's possible that
165 // actions inside this loop will cause more events to be
167 std::vector
<AccessibilityHostMsg_EventParams
> src_events
=
169 pending_events_
.clear();
171 // Generate an event message from each Blink event.
172 std::vector
<AccessibilityHostMsg_EventParams
> event_msgs
;
174 // If there's a layout complete message, we need to send location changes.
175 bool had_layout_complete_messages
= false;
177 // Loop over each event and generate an updated event message.
178 for (size_t i
= 0; i
< src_events
.size(); ++i
) {
179 AccessibilityHostMsg_EventParams
& event
= src_events
[i
];
180 if (event
.event_type
== ui::AX_EVENT_LAYOUT_COMPLETE
)
181 had_layout_complete_messages
= true;
183 WebAXObject obj
= document
.accessibilityObjectFromID(event
.id
);
185 // Make sure the object still exists.
186 if (!obj
.updateBackingStoreAndCheckValidity())
189 // Make sure it's a descendant of our root node - exceptions include the
190 // scroll area that's the parent of the main document (we ignore it), and
191 // possibly nodes attached to a different document.
192 if (!tree_source_
.IsInTree(obj
))
195 // When we get a "selected children changed" event, Blink
196 // doesn't also send us events for each child that changed
197 // selection state, so make sure we re-send that whole subtree.
198 if (event
.event_type
==
199 ui::AX_EVENT_SELECTED_CHILDREN_CHANGED
) {
200 serializer_
.DeleteClientSubtree(obj
);
203 AccessibilityHostMsg_EventParams event_msg
;
204 event_msg
.event_type
= event
.event_type
;
205 event_msg
.id
= event
.id
;
206 serializer_
.SerializeChanges(obj
, &event_msg
.update
);
207 event_msgs
.push_back(event_msg
);
209 // For each node in the update, set the location in our map from
211 for (size_t i
= 0; i
< event_msg
.update
.nodes
.size(); ++i
) {
212 locations_
[event_msg
.update
.nodes
[i
].id
] =
213 event_msg
.update
.nodes
[i
].location
;
216 VLOG(0) << "Accessibility event: " << ui::ToString(event
.event_type
)
217 << " on node id " << event_msg
.id
218 << "\n" << event_msg
.update
.ToString();
221 Send(new AccessibilityHostMsg_Events(routing_id(), event_msgs
));
223 if (had_layout_complete_messages
)
224 SendLocationChanges();
227 void RendererAccessibilityComplete::SendLocationChanges() {
228 std::vector
<AccessibilityHostMsg_LocationChangeParams
> messages
;
230 // Do a breadth-first explore of the whole blink AX tree.
231 base::hash_map
<int, gfx::Rect
> new_locations
;
232 std::queue
<WebAXObject
> objs_to_explore
;
233 objs_to_explore
.push(tree_source_
.GetRoot());
234 while (objs_to_explore
.size()) {
235 WebAXObject obj
= objs_to_explore
.front();
236 objs_to_explore
.pop();
238 // See if we had a previous location. If not, this whole subtree must
239 // be new, so don't continue to explore this branch.
241 base::hash_map
<int, gfx::Rect
>::iterator iter
= locations_
.find(id
);
242 if (iter
== locations_
.end())
245 // If the location has changed, append it to the IPC message.
246 gfx::Rect new_location
= obj
.boundingBoxRect();
247 if (iter
!= locations_
.end() && iter
->second
!= new_location
) {
248 AccessibilityHostMsg_LocationChangeParams message
;
250 message
.new_location
= new_location
;
251 messages
.push_back(message
);
254 // Save the new location.
255 new_locations
[id
] = new_location
;
257 // Explore children of this object.
258 std::vector
<blink::WebAXObject
> children
;
259 tree_source_
.GetChildren(obj
, &children
);
260 for (size_t i
= 0; i
< children
.size(); ++i
)
261 objs_to_explore
.push(children
[i
]);
263 locations_
.swap(new_locations
);
265 Send(new AccessibilityHostMsg_LocationChanges(routing_id(), messages
));
268 void RendererAccessibilityComplete::OnDoDefaultAction(int acc_obj_id
) {
269 const WebDocument
& document
= GetMainDocument();
270 if (document
.isNull())
273 WebAXObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
274 if (obj
.isDetached()) {
276 LOG(WARNING
) << "DoDefaultAction on invalid object id " << acc_obj_id
;
281 obj
.performDefaultAction();
284 void RendererAccessibilityComplete::OnScrollToMakeVisible(
285 int acc_obj_id
, gfx::Rect subfocus
) {
286 const WebDocument
& document
= GetMainDocument();
287 if (document
.isNull())
290 WebAXObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
291 if (obj
.isDetached()) {
293 LOG(WARNING
) << "ScrollToMakeVisible on invalid object id " << acc_obj_id
;
298 obj
.scrollToMakeVisibleWithSubFocus(
299 WebRect(subfocus
.x(), subfocus
.y(),
300 subfocus
.width(), subfocus
.height()));
302 // Make sure the browser gets an event when the scroll
303 // position actually changes.
304 // TODO(dmazzoni): remove this once this bug is fixed:
305 // https://bugs.webkit.org/show_bug.cgi?id=73460
306 HandleAXEvent(document
.accessibilityObject(),
307 ui::AX_EVENT_LAYOUT_COMPLETE
);
310 void RendererAccessibilityComplete::OnScrollToPoint(
311 int acc_obj_id
, gfx::Point point
) {
312 const WebDocument
& document
= GetMainDocument();
313 if (document
.isNull())
316 WebAXObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
317 if (obj
.isDetached()) {
319 LOG(WARNING
) << "ScrollToPoint on invalid object id " << acc_obj_id
;
324 obj
.scrollToGlobalPoint(WebPoint(point
.x(), point
.y()));
326 // Make sure the browser gets an event when the scroll
327 // position actually changes.
328 // TODO(dmazzoni): remove this once this bug is fixed:
329 // https://bugs.webkit.org/show_bug.cgi?id=73460
330 HandleAXEvent(document
.accessibilityObject(),
331 ui::AX_EVENT_LAYOUT_COMPLETE
);
334 void RendererAccessibilityComplete::OnSetTextSelection(
335 int acc_obj_id
, int start_offset
, int end_offset
) {
336 const WebDocument
& document
= GetMainDocument();
337 if (document
.isNull())
340 WebAXObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
341 if (obj
.isDetached()) {
343 LOG(WARNING
) << "SetTextSelection on invalid object id " << acc_obj_id
;
348 // TODO(dmazzoni): support elements other than <input>.
349 blink::WebNode node
= obj
.node();
350 if (!node
.isNull() && node
.isElementNode()) {
351 blink::WebElement element
= node
.to
<blink::WebElement
>();
352 blink::WebInputElement
* input_element
=
353 blink::toWebInputElement(&element
);
354 if (input_element
&& input_element
->isTextField())
355 input_element
->setSelectionRange(start_offset
, end_offset
);
359 void RendererAccessibilityComplete::OnHitTest(gfx::Point point
) {
360 const WebDocument
& document
= GetMainDocument();
361 if (document
.isNull())
363 WebAXObject root_obj
= document
.accessibilityObject();
364 if (!root_obj
.updateBackingStoreAndCheckValidity())
367 WebAXObject obj
= root_obj
.hitTest(point
);
368 if (!obj
.isDetached())
369 HandleAXEvent(obj
, ui::AX_EVENT_HOVER
);
372 void RendererAccessibilityComplete::OnEventsAck() {
373 DCHECK(ack_pending_
);
374 ack_pending_
= false;
375 SendPendingAccessibilityEvents();
378 void RendererAccessibilityComplete::OnSetFocus(int acc_obj_id
) {
379 const WebDocument
& document
= GetMainDocument();
380 if (document
.isNull())
383 WebAXObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
384 if (obj
.isDetached()) {
386 LOG(WARNING
) << "OnSetAccessibilityFocus on invalid object id "
392 WebAXObject root
= document
.accessibilityObject();
393 if (root
.isDetached()) {
395 LOG(WARNING
) << "OnSetAccessibilityFocus but root is invalid";
400 // By convention, calling SetFocus on the root of the tree should clear the
401 // current focus. Otherwise set the focus to the new node.
402 if (acc_obj_id
== root
.axID())
403 render_frame_
->GetRenderView()->GetWebView()->clearFocusedElement();
405 obj
.setFocused(true);
408 void RendererAccessibilityComplete::OnFatalError() {
409 CHECK(false) << "Invalid accessibility tree.";
412 } // namespace content