chromeos: dbus: add Bluetooth properties support
[chromium-blink-merge.git] / content / renderer / renderer_accessibility.cc
blobbfadd57172f768e7c9a63a5660f166c1845411f4
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 "base/bind.h"
6 #include "base/command_line.h"
7 #include "content/common/accessibility_messages.h"
8 #include "content/public/common/content_switches.h"
9 #include "content/renderer/render_view_impl.h"
10 #include "content/renderer/renderer_accessibility.h"
11 #include "third_party/WebKit/Source/WebKit/chromium/public/WebAccessibilityObject.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 #include "webkit/glue/webaccessibility.h"
19 using WebKit::WebAccessibilityNotification;
20 using WebKit::WebAccessibilityObject;
21 using WebKit::WebDocument;
22 using WebKit::WebFrame;
23 using WebKit::WebNode;
24 using WebKit::WebPoint;
25 using WebKit::WebRect;
26 using WebKit::WebSize;
27 using WebKit::WebView;
28 using webkit_glue::WebAccessibility;
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::WebAccessibilityNotificationMenuListValueChanged:
56 *type = AccessibilityNotificationMenuListValueChanged;
57 break;
58 case WebKit::WebAccessibilityNotificationRowCollapsed:
59 *type = AccessibilityNotificationRowCollapsed;
60 break;
61 case WebKit::WebAccessibilityNotificationRowCountChanged:
62 *type = AccessibilityNotificationRowCountChanged;
63 break;
64 case WebKit::WebAccessibilityNotificationRowExpanded:
65 *type = AccessibilityNotificationRowExpanded;
66 break;
67 case WebKit::WebAccessibilityNotificationScrolledToAnchor:
68 *type = AccessibilityNotificationScrolledToAnchor;
69 break;
70 case WebKit::WebAccessibilityNotificationSelectedChildrenChanged:
71 *type = AccessibilityNotificationSelectedChildrenChanged;
72 break;
73 case WebKit::WebAccessibilityNotificationSelectedTextChanged:
74 *type = AccessibilityNotificationSelectedTextChanged;
75 break;
76 case WebKit::WebAccessibilityNotificationValueChanged:
77 *type = AccessibilityNotificationValueChangedD;
78 break;
79 default:
80 NOTREACHED();
81 return false;
83 return true;
86 RendererAccessibility::RendererAccessibility(RenderViewImpl* render_view)
87 : content::RenderViewObserver(render_view),
88 ALLOW_THIS_IN_INITIALIZER_LIST(weak_factory_(this)),
89 browser_root_(NULL),
90 last_scroll_offset_(gfx::Size()),
91 ack_pending_(false),
92 logging_(false) {
93 const CommandLine& command_line = *CommandLine::ForCurrentProcess();
94 if (command_line.HasSwitch(switches::kEnableAccessibility))
95 WebAccessibilityObject::enableAccessibility();
96 if (command_line.HasSwitch(switches::kEnableAccessibilityLogging))
97 logging_ = true;
100 RendererAccessibility::~RendererAccessibility() {
103 bool RendererAccessibility::OnMessageReceived(const IPC::Message& message) {
104 bool handled = true;
105 IPC_BEGIN_MESSAGE_MAP(RendererAccessibility, message)
106 IPC_MESSAGE_HANDLER(AccessibilityMsg_Enable, OnEnable)
107 IPC_MESSAGE_HANDLER(AccessibilityMsg_SetFocus, OnSetFocus)
108 IPC_MESSAGE_HANDLER(AccessibilityMsg_DoDefaultAction,
109 OnDoDefaultAction)
110 IPC_MESSAGE_HANDLER(AccessibilityMsg_Notifications_ACK,
111 OnNotificationsAck)
112 IPC_MESSAGE_HANDLER(AccessibilityMsg_ScrollToMakeVisible,
113 OnScrollToMakeVisible)
114 IPC_MESSAGE_HANDLER(AccessibilityMsg_ScrollToPoint,
115 OnScrollToPoint)
116 IPC_MESSAGE_HANDLER(AccessibilityMsg_SetTextSelection,
117 OnSetTextSelection)
118 IPC_MESSAGE_UNHANDLED(handled = false)
119 IPC_END_MESSAGE_MAP()
120 return handled;
123 void RendererAccessibility::FocusedNodeChanged(const WebNode& node) {
124 if (!WebAccessibilityObject::accessibilityEnabled())
125 return;
127 const WebDocument& document = GetMainDocument();
128 if (document.isNull())
129 return;
131 if (node.isNull()) {
132 // When focus is cleared, implicitly focus the document.
133 // TODO(dmazzoni): Make WebKit send this notification instead.
134 PostAccessibilityNotification(
135 document.accessibilityObject(),
136 WebKit::WebAccessibilityNotificationFocusedUIElementChanged);
140 void RendererAccessibility::DidFinishLoad(WebKit::WebFrame* frame) {
141 if (!WebAccessibilityObject::accessibilityEnabled())
142 return;
144 const WebDocument& document = GetMainDocument();
145 if (document.isNull())
146 return;
148 // Check to see if the root accessibility object has changed, to work
149 // around WebKit bugs that cause AXObjectCache to be cleared
150 // unnecessarily.
151 // TODO(dmazzoni): remove this once rdar://5794454 is fixed.
152 WebAccessibilityObject new_root = document.accessibilityObject();
153 if (!browser_root_ || new_root.axID() != browser_root_->id) {
154 PostAccessibilityNotification(
155 new_root,
156 WebKit::WebAccessibilityNotificationLayoutComplete);
160 void RendererAccessibility::PostAccessibilityNotification(
161 const WebAccessibilityObject& obj,
162 WebAccessibilityNotification notification) {
163 if (!WebAccessibilityObject::accessibilityEnabled())
164 return;
166 const WebDocument& document = GetMainDocument();
167 if (document.isNull())
168 return;
170 gfx::Size scroll_offset = document.frame()->scrollOffset();
171 if (scroll_offset != last_scroll_offset_) {
172 // Make sure the browser is always aware of the scroll position of
173 // the root document element by posting a generic notification that
174 // will update it.
175 // TODO(dmazzoni): remove this as soon as
176 // https://bugs.webkit.org/show_bug.cgi?id=73460 is fixed.
177 last_scroll_offset_ = scroll_offset;
178 if (!obj.equals(document.accessibilityObject())) {
179 PostAccessibilityNotification(
180 document.accessibilityObject(),
181 WebKit::WebAccessibilityNotificationLayoutComplete);
185 // Add the accessibility object to our cache and ensure it's valid.
186 Notification acc_notification;
187 acc_notification.id = obj.axID();
188 acc_notification.type = notification;
190 AccessibilityNotification temp;
191 if (!WebAccessibilityNotificationToAccessibilityNotification(
192 notification, &temp)) {
193 return;
196 // Discard duplicate accessibility notifications.
197 for (uint32 i = 0; i < pending_notifications_.size(); ++i) {
198 if (pending_notifications_[i].id == acc_notification.id &&
199 pending_notifications_[i].type == acc_notification.type) {
200 return;
203 pending_notifications_.push_back(acc_notification);
205 if (!ack_pending_ && !weak_factory_.HasWeakPtrs()) {
206 // When no accessibility notifications are in-flight post a task to send
207 // the notifications to the browser. We use PostTask so that we can queue
208 // up additional notifications.
209 MessageLoop::current()->PostTask(
210 FROM_HERE,
211 base::Bind(
212 &RendererAccessibility::SendPendingAccessibilityNotifications,
213 weak_factory_.GetWeakPtr()));
217 void RendererAccessibility::SendPendingAccessibilityNotifications() {
218 const WebDocument& document = GetMainDocument();
219 if (document.isNull())
220 return;
222 if (pending_notifications_.empty())
223 return;
225 ack_pending_ = true;
227 // Make a copy of the notifications, because it's possible that
228 // actions inside this loop will cause more notifications to be
229 // queued up.
230 std::vector<Notification> src_notifications = pending_notifications_;
231 pending_notifications_.clear();
233 // Generate a notification message from each WebKit notification.
234 std::vector<AccessibilityHostMsg_NotificationParams> notification_msgs;
236 // Loop over each WebKit notification and generate a notification message
237 // from it.
238 for (size_t i = 0; i < src_notifications.size(); ++i) {
239 Notification& notification = src_notifications[i];
241 // TODO(dtseng): Come up with a cleaner way of deciding to include children.
242 int root_id = document.accessibilityObject().axID();
243 bool includes_children = ShouldIncludeChildren(notification) ||
244 root_id == notification.id;
245 WebAccessibilityObject obj = document.accessibilityObjectFromID(
246 notification.id);
248 // The browser may not have this object yet, for example if we get a
249 // notification on an object that was recently added, or if we get a
250 // notification on a node before the page has loaded. Work our way
251 // up the parent chain until we find a node the browser has, or until
252 // we reach the root.
253 while (browser_id_map_.find(obj.axID()) == browser_id_map_.end() &&
254 obj.isValid() &&
255 obj.axID() != root_id) {
256 obj = obj.parentObject();
257 includes_children = true;
258 if (notification.type ==
259 WebKit::WebAccessibilityNotificationChildrenChanged) {
260 notification.id = obj.axID();
264 if (!obj.isValid()) {
265 #ifndef NDEBUG
266 if (logging_)
267 LOG(WARNING) << "Got notification on object that is invalid or has"
268 << " invalid ancestor. Id: " << obj.axID();
269 #endif
270 continue;
273 // Another potential problem is that this notification may be on an
274 // object that is detached from the tree. Determine if this node is not a
275 // child of its parent, and if so move the notification to the parent.
276 // TODO(dmazzoni): see if this can be removed after
277 // https://bugs.webkit.org/show_bug.cgi?id=68466 is fixed.
278 if (obj.axID() != root_id) {
279 WebAccessibilityObject parent = obj.parentObject();
280 while (!parent.isNull() &&
281 parent.isValid() &&
282 parent.accessibilityIsIgnored()) {
283 parent = parent.parentObject();
286 if (parent.isNull() || !parent.isValid()) {
287 NOTREACHED();
288 continue;
290 bool is_child_of_parent = false;
291 for (unsigned int i = 0; i < parent.childCount(); ++i) {
292 if (parent.childAt(i).equals(obj)) {
293 is_child_of_parent = true;
294 break;
297 if (!is_child_of_parent) {
298 obj = parent;
299 notification.id = obj.axID();
300 includes_children = true;
304 AccessibilityHostMsg_NotificationParams notification_msg;
305 WebAccessibilityNotificationToAccessibilityNotification(
306 notification.type, &notification_msg.notification_type);
307 notification_msg.id = notification.id;
308 notification_msg.includes_children = includes_children;
309 notification_msg.acc_tree = WebAccessibility(obj, includes_children);
310 if (obj.axID() == root_id) {
311 DCHECK_EQ(notification_msg.acc_tree.role,
312 WebAccessibility::ROLE_WEB_AREA);
313 notification_msg.acc_tree.role = WebAccessibility::ROLE_ROOT_WEB_AREA;
315 notification_msgs.push_back(notification_msg);
317 if (includes_children)
318 UpdateBrowserTree(notification_msg.acc_tree);
320 #ifndef NDEBUG
321 if (logging_) {
322 LOG(INFO) << "Accessibility update: "
323 << notification_msg.acc_tree.DebugString(true,
324 routing_id(),
325 notification.type);
327 #endif
330 Send(new AccessibilityHostMsg_Notifications(routing_id(), notification_msgs));
333 void RendererAccessibility::UpdateBrowserTree(
334 const webkit_glue::WebAccessibility& renderer_node) {
335 BrowserTreeNode* browser_node = NULL;
336 base::hash_map<int32, BrowserTreeNode*>::iterator iter =
337 browser_id_map_.find(renderer_node.id);
338 if (iter != browser_id_map_.end()) {
339 browser_node = iter->second;
340 ClearBrowserTreeNode(browser_node);
341 } else {
342 DCHECK_EQ(renderer_node.role, WebAccessibility::ROLE_ROOT_WEB_AREA);
343 if (browser_root_) {
344 ClearBrowserTreeNode(browser_root_);
345 browser_id_map_.erase(browser_root_->id);
346 delete browser_root_;
348 browser_root_ = new BrowserTreeNode;
349 browser_node = browser_root_;
350 browser_node->id = renderer_node.id;
351 browser_id_map_[browser_node->id] = browser_node;
353 browser_node->children.reserve(renderer_node.children.size());
354 for (size_t i = 0; i < renderer_node.children.size(); ++i) {
355 BrowserTreeNode* browser_child_node = new BrowserTreeNode;
356 browser_child_node->id = renderer_node.children[i].id;
357 browser_id_map_[browser_child_node->id] = browser_child_node;
358 browser_node->children.push_back(browser_child_node);
359 UpdateBrowserTree(renderer_node.children[i]);
363 void RendererAccessibility::ClearBrowserTreeNode(
364 BrowserTreeNode* browser_node) {
365 for (size_t i = 0; i < browser_node->children.size(); ++i) {
366 browser_id_map_.erase(browser_node->children[i]->id);
367 ClearBrowserTreeNode(browser_node->children[i]);
368 delete browser_node->children[i];
370 browser_node->children.clear();
373 void RendererAccessibility::OnDoDefaultAction(int acc_obj_id) {
374 if (!WebAccessibilityObject::accessibilityEnabled())
375 return;
377 const WebDocument& document = GetMainDocument();
378 if (document.isNull())
379 return;
381 WebAccessibilityObject obj = document.accessibilityObjectFromID(acc_obj_id);
382 if (!obj.isValid()) {
383 #ifndef NDEBUG
384 if (logging_)
385 LOG(WARNING) << "DoDefaultAction on invalid object id " << acc_obj_id;
386 #endif
387 return;
390 obj.performDefaultAction();
393 void RendererAccessibility::OnScrollToMakeVisible(
394 int acc_obj_id, gfx::Rect subfocus) {
395 if (!WebAccessibilityObject::accessibilityEnabled())
396 return;
398 const WebDocument& document = GetMainDocument();
399 if (document.isNull())
400 return;
402 WebAccessibilityObject obj = document.accessibilityObjectFromID(acc_obj_id);
403 if (!obj.isValid()) {
404 #ifndef NDEBUG
405 if (logging_)
406 LOG(WARNING) << "ScrollToMakeVisible on invalid object id " << acc_obj_id;
407 #endif
408 return;
411 obj.scrollToMakeVisibleWithSubFocus(
412 WebRect(subfocus.x(), subfocus.y(),
413 subfocus.width(), subfocus.height()));
415 // Make sure the browser gets a notification when the scroll
416 // position actually changes.
417 // TODO(dmazzoni): remove this once this bug is fixed:
418 // https://bugs.webkit.org/show_bug.cgi?id=73460
419 PostAccessibilityNotification(
420 document.accessibilityObject(),
421 WebKit::WebAccessibilityNotificationLayoutComplete);
424 void RendererAccessibility::OnScrollToPoint(
425 int acc_obj_id, gfx::Point point) {
426 if (!WebAccessibilityObject::accessibilityEnabled())
427 return;
429 const WebDocument& document = GetMainDocument();
430 if (document.isNull())
431 return;
433 WebAccessibilityObject obj = document.accessibilityObjectFromID(acc_obj_id);
434 if (!obj.isValid()) {
435 #ifndef NDEBUG
436 if (logging_)
437 LOG(WARNING) << "ScrollToPoint on invalid object id " << acc_obj_id;
438 #endif
439 return;
442 obj.scrollToGlobalPoint(WebPoint(point.x(), point.y()));
444 // Make sure the browser gets a notification when the scroll
445 // position actually changes.
446 // TODO(dmazzoni): remove this once this bug is fixed:
447 // https://bugs.webkit.org/show_bug.cgi?id=73460
448 PostAccessibilityNotification(
449 document.accessibilityObject(),
450 WebKit::WebAccessibilityNotificationLayoutComplete);
453 void RendererAccessibility::OnSetTextSelection(
454 int acc_obj_id, int start_offset, int end_offset) {
455 if (!WebAccessibilityObject::accessibilityEnabled())
456 return;
458 const WebDocument& document = GetMainDocument();
459 if (document.isNull())
460 return;
462 WebAccessibilityObject obj = document.accessibilityObjectFromID(acc_obj_id);
463 if (!obj.isValid()) {
464 #ifndef NDEBUG
465 if (logging_)
466 LOG(WARNING) << "SetTextSelection on invalid object id " << acc_obj_id;
467 #endif
468 return;
471 // TODO(dmazzoni): support elements other than <input>.
472 WebKit::WebNode node = obj.node();
473 if (!node.isNull() && node.isElementNode()) {
474 WebKit::WebElement element = node.to<WebKit::WebElement>();
475 WebKit::WebInputElement* input_element =
476 WebKit::toWebInputElement(&element);
477 if (input_element && input_element->isTextField())
478 input_element->setSelectionRange(start_offset, end_offset);
482 void RendererAccessibility::OnNotificationsAck() {
483 DCHECK(ack_pending_);
484 ack_pending_ = false;
485 SendPendingAccessibilityNotifications();
488 void RendererAccessibility::OnEnable() {
489 if (WebAccessibilityObject::accessibilityEnabled())
490 return;
492 WebAccessibilityObject::enableAccessibility();
494 const WebDocument& document = GetMainDocument();
495 if (!document.isNull()) {
496 // It's possible that the webview has already loaded a webpage without
497 // accessibility being enabled. Initialize the browser's cached
498 // accessibility tree by sending it a 'load complete' notification.
499 PostAccessibilityNotification(
500 document.accessibilityObject(),
501 WebKit::WebAccessibilityNotificationLayoutComplete);
505 void RendererAccessibility::OnSetFocus(int acc_obj_id) {
506 if (!WebAccessibilityObject::accessibilityEnabled())
507 return;
509 const WebDocument& document = GetMainDocument();
510 if (document.isNull())
511 return;
513 WebAccessibilityObject obj = document.accessibilityObjectFromID(acc_obj_id);
514 if (!obj.isValid()) {
515 #ifndef NDEBUG
516 if (logging_) {
517 LOG(WARNING) << "OnSetAccessibilityFocus on invalid object id "
518 << acc_obj_id;
520 #endif
521 return;
524 WebAccessibilityObject root = document.accessibilityObject();
525 if (!root.isValid()) {
526 #ifndef NDEBUG
527 if (logging_) {
528 LOG(WARNING) << "OnSetAccessibilityFocus but root is invalid";
530 #endif
531 return;
534 // By convention, calling SetFocus on the root of the tree should clear the
535 // current focus. Otherwise set the focus to the new node.
536 if (acc_obj_id == root.axID())
537 render_view()->GetWebView()->clearFocusedNode();
538 else
539 obj.setFocused(true);
542 bool RendererAccessibility::ShouldIncludeChildren(
543 const RendererAccessibility::Notification& notification) {
544 WebKit::WebAccessibilityNotification type = notification.type;
545 if (type == WebKit::WebAccessibilityNotificationChildrenChanged ||
546 type == WebKit::WebAccessibilityNotificationLoadComplete ||
547 type == WebKit::WebAccessibilityNotificationLiveRegionChanged ||
548 type == WebKit::WebAccessibilityNotificationSelectedChildrenChanged) {
549 return true;
551 return false;
554 WebDocument RendererAccessibility::GetMainDocument() {
555 WebView* view = render_view()->GetWebView();
556 WebFrame* main_frame = view ? view->mainFrame() : NULL;
558 if (main_frame)
559 return main_frame->document();
560 else
561 return WebDocument();