OLD | NEW |
| (Empty) |
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. | |
4 | |
5 #include "content/renderer/renderer_accessibility_complete.h" | |
6 | |
7 #include "base/bind.h" | |
8 #include "base/message_loop.h" | |
9 #include "content/renderer/accessibility_node_serializer.h" | |
10 #include "content/renderer/render_view_impl.h" | |
11 #include "third_party/WebKit/Source/WebKit/chromium/public/WebAccessibilityObjec
t.h" | |
12 #include "third_party/WebKit/Source/WebKit/chromium/public/WebDocument.h" | |
13 #include "third_party/WebKit/Source/WebKit/chromium/public/WebFrame.h" | |
14 #include "third_party/WebKit/Source/WebKit/chromium/public/WebInputElement.h" | |
15 #include "third_party/WebKit/Source/WebKit/chromium/public/WebNode.h" | |
16 #include "third_party/WebKit/Source/WebKit/chromium/public/WebView.h" | |
17 | |
18 using WebKit::WebAccessibilityNotification; | |
19 using WebKit::WebAccessibilityObject; | |
20 using WebKit::WebDocument; | |
21 using WebKit::WebFrame; | |
22 using WebKit::WebNode; | |
23 using WebKit::WebPoint; | |
24 using WebKit::WebRect; | |
25 using WebKit::WebSize; | |
26 using WebKit::WebView; | |
27 | |
28 namespace content { | |
29 | |
30 bool WebAccessibilityNotificationToAccessibilityNotification( | |
31 WebAccessibilityNotification notification, | |
32 AccessibilityNotification* type) { | |
33 switch (notification) { | |
34 case WebKit::WebAccessibilityNotificationActiveDescendantChanged: | |
35 *type = AccessibilityNotificationActiveDescendantChanged; | |
36 break; | |
37 case WebKit::WebAccessibilityNotificationCheckedStateChanged: | |
38 *type = AccessibilityNotificationCheckStateChanged; | |
39 break; | |
40 case WebKit::WebAccessibilityNotificationChildrenChanged: | |
41 *type = AccessibilityNotificationChildrenChanged; | |
42 break; | |
43 case WebKit::WebAccessibilityNotificationFocusedUIElementChanged: | |
44 *type = AccessibilityNotificationFocusChanged; | |
45 break; | |
46 case WebKit::WebAccessibilityNotificationLayoutComplete: | |
47 *type = AccessibilityNotificationLayoutComplete; | |
48 break; | |
49 case WebKit::WebAccessibilityNotificationLiveRegionChanged: | |
50 *type = AccessibilityNotificationLiveRegionChanged; | |
51 break; | |
52 case WebKit::WebAccessibilityNotificationLoadComplete: | |
53 *type = AccessibilityNotificationLoadComplete; | |
54 break; | |
55 case WebKit::WebAccessibilityNotificationMenuListItemSelected: | |
56 *type = AccessibilityNotificationMenuListItemSelected; | |
57 break; | |
58 case WebKit::WebAccessibilityNotificationMenuListValueChanged: | |
59 *type = AccessibilityNotificationMenuListValueChanged; | |
60 break; | |
61 case WebKit::WebAccessibilityNotificationRowCollapsed: | |
62 *type = AccessibilityNotificationRowCollapsed; | |
63 break; | |
64 case WebKit::WebAccessibilityNotificationRowCountChanged: | |
65 *type = AccessibilityNotificationRowCountChanged; | |
66 break; | |
67 case WebKit::WebAccessibilityNotificationRowExpanded: | |
68 *type = AccessibilityNotificationRowExpanded; | |
69 break; | |
70 case WebKit::WebAccessibilityNotificationScrolledToAnchor: | |
71 *type = AccessibilityNotificationScrolledToAnchor; | |
72 break; | |
73 case WebKit::WebAccessibilityNotificationSelectedChildrenChanged: | |
74 *type = AccessibilityNotificationSelectedChildrenChanged; | |
75 break; | |
76 case WebKit::WebAccessibilityNotificationSelectedTextChanged: | |
77 *type = AccessibilityNotificationSelectedTextChanged; | |
78 break; | |
79 case WebKit::WebAccessibilityNotificationValueChanged: | |
80 *type = AccessibilityNotificationValueChanged; | |
81 break; | |
82 default: | |
83 DLOG(WARNING) | |
84 << "WebKit accessibility notification not handled in switch!"; | |
85 return false; | |
86 } | |
87 return true; | |
88 } | |
89 | |
90 RendererAccessibilityComplete::RendererAccessibilityComplete( | |
91 RenderViewImpl* render_view) | |
92 : RendererAccessibility(render_view), | |
93 ALLOW_THIS_IN_INITIALIZER_LIST(weak_factory_(this)), | |
94 browser_root_(NULL), | |
95 last_scroll_offset_(gfx::Size()), | |
96 ack_pending_(false) { | |
97 WebAccessibilityObject::enableAccessibility(); | |
98 | |
99 const WebDocument& document = GetMainDocument(); | |
100 if (!document.isNull()) { | |
101 // It's possible that the webview has already loaded a webpage without | |
102 // accessibility being enabled. Initialize the browser's cached | |
103 // accessibility tree by sending it a 'load complete' notification. | |
104 HandleAccessibilityNotification( | |
105 document.accessibilityObject(), | |
106 AccessibilityNotificationLayoutComplete); | |
107 } | |
108 } | |
109 | |
110 RendererAccessibilityComplete::~RendererAccessibilityComplete() { | |
111 } | |
112 | |
113 bool RendererAccessibilityComplete::OnMessageReceived( | |
114 const IPC::Message& message) { | |
115 bool handled = true; | |
116 IPC_BEGIN_MESSAGE_MAP(RendererAccessibilityComplete, message) | |
117 IPC_MESSAGE_HANDLER(AccessibilityMsg_SetFocus, OnSetFocus) | |
118 IPC_MESSAGE_HANDLER(AccessibilityMsg_DoDefaultAction, | |
119 OnDoDefaultAction) | |
120 IPC_MESSAGE_HANDLER(AccessibilityMsg_Notifications_ACK, | |
121 OnNotificationsAck) | |
122 IPC_MESSAGE_HANDLER(AccessibilityMsg_ScrollToMakeVisible, | |
123 OnScrollToMakeVisible) | |
124 IPC_MESSAGE_HANDLER(AccessibilityMsg_ScrollToPoint, | |
125 OnScrollToPoint) | |
126 IPC_MESSAGE_HANDLER(AccessibilityMsg_SetTextSelection, | |
127 OnSetTextSelection) | |
128 IPC_MESSAGE_UNHANDLED(handled = false) | |
129 IPC_END_MESSAGE_MAP() | |
130 return handled; | |
131 } | |
132 | |
133 void RendererAccessibilityComplete::FocusedNodeChanged(const WebNode& node) { | |
134 const WebDocument& document = GetMainDocument(); | |
135 if (document.isNull()) | |
136 return; | |
137 | |
138 if (node.isNull()) { | |
139 // When focus is cleared, implicitly focus the document. | |
140 // TODO(dmazzoni): Make WebKit send this notification instead. | |
141 HandleAccessibilityNotification( | |
142 document.accessibilityObject(), | |
143 AccessibilityNotificationBlur); | |
144 } | |
145 } | |
146 | |
147 void RendererAccessibilityComplete::DidFinishLoad(WebKit::WebFrame* frame) { | |
148 const WebDocument& document = GetMainDocument(); | |
149 if (document.isNull()) | |
150 return; | |
151 | |
152 // Check to see if the root accessibility object has changed, to work | |
153 // around WebKit bugs that cause AXObjectCache to be cleared | |
154 // unnecessarily. | |
155 // TODO(dmazzoni): remove this once rdar://5794454 is fixed. | |
156 WebAccessibilityObject new_root = document.accessibilityObject(); | |
157 if (!browser_root_ || new_root.axID() != browser_root_->id) { | |
158 HandleAccessibilityNotification( | |
159 new_root, | |
160 AccessibilityNotificationLayoutComplete); | |
161 } | |
162 } | |
163 | |
164 void RendererAccessibilityComplete::HandleWebAccessibilityNotification( | |
165 const WebAccessibilityObject& obj, | |
166 WebAccessibilityNotification notification) { | |
167 AccessibilityNotification temp; | |
168 if (!WebAccessibilityNotificationToAccessibilityNotification( | |
169 notification, &temp)) { | |
170 return; | |
171 } | |
172 | |
173 HandleAccessibilityNotification(obj, temp); | |
174 } | |
175 | |
176 void RendererAccessibilityComplete::HandleAccessibilityNotification( | |
177 const WebKit::WebAccessibilityObject& obj, | |
178 AccessibilityNotification notification) { | |
179 const WebDocument& document = GetMainDocument(); | |
180 if (document.isNull()) | |
181 return; | |
182 | |
183 gfx::Size scroll_offset = document.frame()->scrollOffset(); | |
184 if (scroll_offset != last_scroll_offset_) { | |
185 // Make sure the browser is always aware of the scroll position of | |
186 // the root document element by posting a generic notification that | |
187 // will update it. | |
188 // TODO(dmazzoni): remove this as soon as | |
189 // https://bugs.webkit.org/show_bug.cgi?id=73460 is fixed. | |
190 last_scroll_offset_ = scroll_offset; | |
191 if (!obj.equals(document.accessibilityObject())) { | |
192 HandleAccessibilityNotification( | |
193 document.accessibilityObject(), | |
194 AccessibilityNotificationLayoutComplete); | |
195 } | |
196 } | |
197 | |
198 // Add the accessibility object to our cache and ensure it's valid. | |
199 AccessibilityHostMsg_NotificationParams acc_notification; | |
200 acc_notification.id = obj.axID(); | |
201 acc_notification.notification_type = notification; | |
202 | |
203 // Discard duplicate accessibility notifications. | |
204 for (uint32 i = 0; i < pending_notifications_.size(); ++i) { | |
205 if (pending_notifications_[i].id == acc_notification.id && | |
206 pending_notifications_[i].notification_type == | |
207 acc_notification.notification_type) { | |
208 return; | |
209 } | |
210 } | |
211 pending_notifications_.push_back(acc_notification); | |
212 | |
213 if (!ack_pending_ && !weak_factory_.HasWeakPtrs()) { | |
214 // When no accessibility notifications are in-flight post a task to send | |
215 // the notifications to the browser. We use PostTask so that we can queue | |
216 // up additional notifications. | |
217 MessageLoop::current()->PostTask( | |
218 FROM_HERE, | |
219 base::Bind( | |
220 &RendererAccessibilityComplete:: | |
221 SendPendingAccessibilityNotifications, | |
222 weak_factory_.GetWeakPtr())); | |
223 } | |
224 } | |
225 | |
226 RendererAccessibilityComplete::BrowserTreeNode::BrowserTreeNode() : id(0) {} | |
227 | |
228 RendererAccessibilityComplete::BrowserTreeNode::~BrowserTreeNode() {} | |
229 | |
230 void RendererAccessibilityComplete::SendPendingAccessibilityNotifications() { | |
231 const WebDocument& document = GetMainDocument(); | |
232 if (document.isNull()) | |
233 return; | |
234 | |
235 if (pending_notifications_.empty()) | |
236 return; | |
237 | |
238 ack_pending_ = true; | |
239 | |
240 // Make a copy of the notifications, because it's possible that | |
241 // actions inside this loop will cause more notifications to be | |
242 // queued up. | |
243 std::vector<AccessibilityHostMsg_NotificationParams> src_notifications = | |
244 pending_notifications_; | |
245 pending_notifications_.clear(); | |
246 | |
247 // Allow WebKit to cache intermediate results since we're doing a bunch | |
248 // of read-only queries at once. | |
249 WebAccessibilityObject rootObject = document.accessibilityObject(); | |
250 rootObject.startCachingComputedObjectAttributesUntilTreeMutates(); | |
251 | |
252 // Generate a notification message from each WebKit notification. | |
253 std::vector<AccessibilityHostMsg_NotificationParams> notification_msgs; | |
254 | |
255 // Loop over each notification and generate an updated notification message. | |
256 for (size_t i = 0; i < src_notifications.size(); ++i) { | |
257 AccessibilityHostMsg_NotificationParams& notification = | |
258 src_notifications[i]; | |
259 | |
260 // TODO(dtseng): Come up with a cleaner way of deciding to include children. | |
261 int root_id = rootObject.axID(); | |
262 bool includes_children = ShouldIncludeChildren(notification) || | |
263 root_id == notification.id; | |
264 WebAccessibilityObject obj = document.accessibilityObjectFromID( | |
265 notification.id); | |
266 if (!obj.updateBackingStoreAndCheckValidity()) | |
267 continue; | |
268 | |
269 // The browser may not have this object yet, for example if we get a | |
270 // notification on an object that was recently added, or if we get a | |
271 // notification on a node before the page has loaded. Work our way | |
272 // up the parent chain until we find a node the browser has, or until | |
273 // we reach the root. | |
274 while (browser_id_map_.find(obj.axID()) == browser_id_map_.end() && | |
275 !obj.isDetached() && | |
276 obj.axID() != root_id) { | |
277 obj = obj.parentObject(); | |
278 includes_children = true; | |
279 if (notification.notification_type == | |
280 AccessibilityNotificationChildrenChanged) { | |
281 notification.id = obj.axID(); | |
282 } | |
283 } | |
284 | |
285 if (obj.isDetached()) { | |
286 #ifndef NDEBUG | |
287 if (logging_) | |
288 LOG(WARNING) << "Got notification on object that is invalid or has" | |
289 << " invalid ancestor. Id: " << obj.axID(); | |
290 #endif | |
291 continue; | |
292 } | |
293 | |
294 // Another potential problem is that this notification may be on an | |
295 // object that is detached from the tree. Determine if this node is not a | |
296 // child of its parent, and if so move the notification to the parent. | |
297 // TODO(dmazzoni): see if this can be removed after | |
298 // https://bugs.webkit.org/show_bug.cgi?id=68466 is fixed. | |
299 if (obj.axID() != root_id) { | |
300 WebAccessibilityObject parent = obj.parentObject(); | |
301 while (!parent.isDetached() && | |
302 parent.accessibilityIsIgnored()) { | |
303 parent = parent.parentObject(); | |
304 } | |
305 | |
306 if (parent.isDetached()) { | |
307 NOTREACHED(); | |
308 continue; | |
309 } | |
310 bool is_child_of_parent = false; | |
311 for (unsigned int i = 0; i < parent.childCount(); ++i) { | |
312 if (parent.childAt(i).equals(obj)) { | |
313 is_child_of_parent = true; | |
314 break; | |
315 } | |
316 } | |
317 | |
318 if (!is_child_of_parent) { | |
319 obj = parent; | |
320 notification.id = obj.axID(); | |
321 includes_children = true; | |
322 } | |
323 } | |
324 | |
325 AccessibilityHostMsg_NotificationParams notification_msg; | |
326 notification_msg.notification_type = notification.notification_type; | |
327 notification_msg.id = notification.id; | |
328 notification_msg.includes_children = includes_children; | |
329 SerializeAccessibilityNode(obj, | |
330 ¬ification_msg.acc_tree, | |
331 includes_children); | |
332 if (obj.axID() == root_id) { | |
333 DCHECK_EQ(notification_msg.acc_tree.role, | |
334 AccessibilityNodeData::ROLE_WEB_AREA); | |
335 notification_msg.acc_tree.role = | |
336 AccessibilityNodeData::ROLE_ROOT_WEB_AREA; | |
337 } | |
338 notification_msgs.push_back(notification_msg); | |
339 | |
340 if (includes_children) | |
341 UpdateBrowserTree(notification_msg.acc_tree); | |
342 | |
343 #ifndef NDEBUG | |
344 if (logging_) { | |
345 LOG(INFO) << "Accessibility update: \n" | |
346 << "routing id=" << routing_id() | |
347 << " notification=" | |
348 << AccessibilityNotificationToString(notification.notification_type) | |
349 << "\n" << notification_msg.acc_tree.DebugString(true); | |
350 } | |
351 #endif | |
352 } | |
353 | |
354 Send(new AccessibilityHostMsg_Notifications(routing_id(), notification_msgs)); | |
355 } | |
356 | |
357 void RendererAccessibilityComplete::UpdateBrowserTree( | |
358 const AccessibilityNodeData& renderer_node) { | |
359 BrowserTreeNode* browser_node = NULL; | |
360 base::hash_map<int32, BrowserTreeNode*>::iterator iter = | |
361 browser_id_map_.find(renderer_node.id); | |
362 if (iter != browser_id_map_.end()) { | |
363 browser_node = iter->second; | |
364 ClearBrowserTreeNode(browser_node); | |
365 } else { | |
366 DCHECK_EQ(renderer_node.role, AccessibilityNodeData::ROLE_ROOT_WEB_AREA); | |
367 if (browser_root_) { | |
368 ClearBrowserTreeNode(browser_root_); | |
369 browser_id_map_.erase(browser_root_->id); | |
370 delete browser_root_; | |
371 } | |
372 browser_root_ = new BrowserTreeNode; | |
373 browser_node = browser_root_; | |
374 browser_node->id = renderer_node.id; | |
375 browser_id_map_[browser_node->id] = browser_node; | |
376 } | |
377 browser_node->children.reserve(renderer_node.children.size()); | |
378 for (size_t i = 0; i < renderer_node.children.size(); ++i) { | |
379 BrowserTreeNode* browser_child_node = new BrowserTreeNode; | |
380 browser_child_node->id = renderer_node.children[i].id; | |
381 browser_id_map_[browser_child_node->id] = browser_child_node; | |
382 browser_node->children.push_back(browser_child_node); | |
383 UpdateBrowserTree(renderer_node.children[i]); | |
384 } | |
385 } | |
386 | |
387 void RendererAccessibilityComplete::ClearBrowserTreeNode( | |
388 BrowserTreeNode* browser_node) { | |
389 for (size_t i = 0; i < browser_node->children.size(); ++i) { | |
390 browser_id_map_.erase(browser_node->children[i]->id); | |
391 ClearBrowserTreeNode(browser_node->children[i]); | |
392 delete browser_node->children[i]; | |
393 } | |
394 browser_node->children.clear(); | |
395 } | |
396 | |
397 void RendererAccessibilityComplete::OnDoDefaultAction(int acc_obj_id) { | |
398 const WebDocument& document = GetMainDocument(); | |
399 if (document.isNull()) | |
400 return; | |
401 | |
402 WebAccessibilityObject obj = document.accessibilityObjectFromID(acc_obj_id); | |
403 if (obj.isDetached()) { | |
404 #ifndef NDEBUG | |
405 if (logging_) | |
406 LOG(WARNING) << "DoDefaultAction on invalid object id " << acc_obj_id; | |
407 #endif | |
408 return; | |
409 } | |
410 | |
411 obj.performDefaultAction(); | |
412 } | |
413 | |
414 void RendererAccessibilityComplete::OnScrollToMakeVisible( | |
415 int acc_obj_id, gfx::Rect subfocus) { | |
416 const WebDocument& document = GetMainDocument(); | |
417 if (document.isNull()) | |
418 return; | |
419 | |
420 WebAccessibilityObject obj = document.accessibilityObjectFromID(acc_obj_id); | |
421 if (obj.isDetached()) { | |
422 #ifndef NDEBUG | |
423 if (logging_) | |
424 LOG(WARNING) << "ScrollToMakeVisible on invalid object id " << acc_obj_id; | |
425 #endif | |
426 return; | |
427 } | |
428 | |
429 obj.scrollToMakeVisibleWithSubFocus( | |
430 WebRect(subfocus.x(), subfocus.y(), | |
431 subfocus.width(), subfocus.height())); | |
432 | |
433 // Make sure the browser gets a notification 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 HandleAccessibilityNotification( | |
438 document.accessibilityObject(), | |
439 AccessibilityNotificationLayoutComplete); | |
440 } | |
441 | |
442 void RendererAccessibilityComplete::OnScrollToPoint( | |
443 int acc_obj_id, gfx::Point point) { | |
444 const WebDocument& document = GetMainDocument(); | |
445 if (document.isNull()) | |
446 return; | |
447 | |
448 WebAccessibilityObject obj = document.accessibilityObjectFromID(acc_obj_id); | |
449 if (obj.isDetached()) { | |
450 #ifndef NDEBUG | |
451 if (logging_) | |
452 LOG(WARNING) << "ScrollToPoint on invalid object id " << acc_obj_id; | |
453 #endif | |
454 return; | |
455 } | |
456 | |
457 obj.scrollToGlobalPoint(WebPoint(point.x(), point.y())); | |
458 | |
459 // Make sure the browser gets a notification when the scroll | |
460 // position actually changes. | |
461 // TODO(dmazzoni): remove this once this bug is fixed: | |
462 // https://bugs.webkit.org/show_bug.cgi?id=73460 | |
463 HandleAccessibilityNotification( | |
464 document.accessibilityObject(), | |
465 AccessibilityNotificationLayoutComplete); | |
466 } | |
467 | |
468 void RendererAccessibilityComplete::OnSetTextSelection( | |
469 int acc_obj_id, int start_offset, int end_offset) { | |
470 const WebDocument& document = GetMainDocument(); | |
471 if (document.isNull()) | |
472 return; | |
473 | |
474 WebAccessibilityObject obj = document.accessibilityObjectFromID(acc_obj_id); | |
475 if (obj.isDetached()) { | |
476 #ifndef NDEBUG | |
477 if (logging_) | |
478 LOG(WARNING) << "SetTextSelection on invalid object id " << acc_obj_id; | |
479 #endif | |
480 return; | |
481 } | |
482 | |
483 // TODO(dmazzoni): support elements other than <input>. | |
484 WebKit::WebNode node = obj.node(); | |
485 if (!node.isNull() && node.isElementNode()) { | |
486 WebKit::WebElement element = node.to<WebKit::WebElement>(); | |
487 WebKit::WebInputElement* input_element = | |
488 WebKit::toWebInputElement(&element); | |
489 if (input_element && input_element->isTextField()) | |
490 input_element->setSelectionRange(start_offset, end_offset); | |
491 } | |
492 } | |
493 | |
494 void RendererAccessibilityComplete::OnNotificationsAck() { | |
495 DCHECK(ack_pending_); | |
496 ack_pending_ = false; | |
497 SendPendingAccessibilityNotifications(); | |
498 } | |
499 | |
500 void RendererAccessibilityComplete::OnSetFocus(int acc_obj_id) { | |
501 const WebDocument& document = GetMainDocument(); | |
502 if (document.isNull()) | |
503 return; | |
504 | |
505 WebAccessibilityObject obj = document.accessibilityObjectFromID(acc_obj_id); | |
506 if (obj.isDetached()) { | |
507 #ifndef NDEBUG | |
508 if (logging_) { | |
509 LOG(WARNING) << "OnSetAccessibilityFocus on invalid object id " | |
510 << acc_obj_id; | |
511 } | |
512 #endif | |
513 return; | |
514 } | |
515 | |
516 WebAccessibilityObject root = document.accessibilityObject(); | |
517 if (root.isDetached()) { | |
518 #ifndef NDEBUG | |
519 if (logging_) { | |
520 LOG(WARNING) << "OnSetAccessibilityFocus but root is invalid"; | |
521 } | |
522 #endif | |
523 return; | |
524 } | |
525 | |
526 // By convention, calling SetFocus on the root of the tree should clear the | |
527 // current focus. Otherwise set the focus to the new node. | |
528 if (acc_obj_id == root.axID()) | |
529 render_view()->GetWebView()->clearFocusedNode(); | |
530 else | |
531 obj.setFocused(true); | |
532 } | |
533 | |
534 bool RendererAccessibilityComplete::ShouldIncludeChildren( | |
535 const AccessibilityHostMsg_NotificationParams& notification) { | |
536 AccessibilityNotification type = notification.notification_type; | |
537 if (type == AccessibilityNotificationChildrenChanged || | |
538 type == AccessibilityNotificationLoadComplete || | |
539 type == AccessibilityNotificationLiveRegionChanged || | |
540 type == AccessibilityNotificationSelectedChildrenChanged) { | |
541 return true; | |
542 } | |
543 return false; | |
544 } | |
545 | |
546 } // namespace content | |
OLD | NEW |