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.
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
;
37 case WebKit::WebAccessibilityNotificationCheckedStateChanged
:
38 *type
= AccessibilityNotificationCheckStateChanged
;
40 case WebKit::WebAccessibilityNotificationChildrenChanged
:
41 *type
= AccessibilityNotificationChildrenChanged
;
43 case WebKit::WebAccessibilityNotificationFocusedUIElementChanged
:
44 *type
= AccessibilityNotificationFocusChanged
;
46 case WebKit::WebAccessibilityNotificationLayoutComplete
:
47 *type
= AccessibilityNotificationLayoutComplete
;
49 case WebKit::WebAccessibilityNotificationLiveRegionChanged
:
50 *type
= AccessibilityNotificationLiveRegionChanged
;
52 case WebKit::WebAccessibilityNotificationLoadComplete
:
53 *type
= AccessibilityNotificationLoadComplete
;
55 case WebKit::WebAccessibilityNotificationMenuListValueChanged
:
56 *type
= AccessibilityNotificationMenuListValueChanged
;
58 case WebKit::WebAccessibilityNotificationRowCollapsed
:
59 *type
= AccessibilityNotificationRowCollapsed
;
61 case WebKit::WebAccessibilityNotificationRowCountChanged
:
62 *type
= AccessibilityNotificationRowCountChanged
;
64 case WebKit::WebAccessibilityNotificationRowExpanded
:
65 *type
= AccessibilityNotificationRowExpanded
;
67 case WebKit::WebAccessibilityNotificationScrolledToAnchor
:
68 *type
= AccessibilityNotificationScrolledToAnchor
;
70 case WebKit::WebAccessibilityNotificationSelectedChildrenChanged
:
71 *type
= AccessibilityNotificationSelectedChildrenChanged
;
73 case WebKit::WebAccessibilityNotificationSelectedTextChanged
:
74 *type
= AccessibilityNotificationSelectedTextChanged
;
76 case WebKit::WebAccessibilityNotificationValueChanged
:
77 *type
= AccessibilityNotificationValueChangedD
;
86 RendererAccessibility::RendererAccessibility(RenderViewImpl
* render_view
)
87 : content::RenderViewObserver(render_view
),
88 ALLOW_THIS_IN_INITIALIZER_LIST(weak_factory_(this)),
90 last_scroll_offset_(gfx::Size()),
93 const CommandLine
& command_line
= *CommandLine::ForCurrentProcess();
94 if (command_line
.HasSwitch(switches::kEnableAccessibility
))
95 WebAccessibilityObject::enableAccessibility();
96 if (command_line
.HasSwitch(switches::kEnableAccessibilityLogging
))
100 RendererAccessibility::~RendererAccessibility() {
103 bool RendererAccessibility::OnMessageReceived(const IPC::Message
& message
) {
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
,
110 IPC_MESSAGE_HANDLER(AccessibilityMsg_Notifications_ACK
,
112 IPC_MESSAGE_HANDLER(AccessibilityMsg_ScrollToMakeVisible
,
113 OnScrollToMakeVisible
)
114 IPC_MESSAGE_HANDLER(AccessibilityMsg_ScrollToPoint
,
116 IPC_MESSAGE_HANDLER(AccessibilityMsg_SetTextSelection
,
118 IPC_MESSAGE_UNHANDLED(handled
= false)
119 IPC_END_MESSAGE_MAP()
123 void RendererAccessibility::FocusedNodeChanged(const WebNode
& node
) {
124 if (!WebAccessibilityObject::accessibilityEnabled())
127 const WebDocument
& document
= GetMainDocument();
128 if (document
.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())
144 const WebDocument
& document
= GetMainDocument();
145 if (document
.isNull())
148 // Check to see if the root accessibility object has changed, to work
149 // around WebKit bugs that cause AXObjectCache to be cleared
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(
156 WebKit::WebAccessibilityNotificationLayoutComplete
);
160 void RendererAccessibility::PostAccessibilityNotification(
161 const WebAccessibilityObject
& obj
,
162 WebAccessibilityNotification notification
) {
163 if (!WebAccessibilityObject::accessibilityEnabled())
166 const WebDocument
& document
= GetMainDocument();
167 if (document
.isNull())
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
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
)) {
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
) {
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(
212 &RendererAccessibility::SendPendingAccessibilityNotifications
,
213 weak_factory_
.GetWeakPtr()));
217 void RendererAccessibility::SendPendingAccessibilityNotifications() {
218 const WebDocument
& document
= GetMainDocument();
219 if (document
.isNull())
222 if (pending_notifications_
.empty())
227 // Make a copy of the notifications, because it's possible that
228 // actions inside this loop will cause more notifications to be
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
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(
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() &&
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()) {
267 LOG(WARNING
) << "Got notification on object that is invalid or has"
268 << " invalid ancestor. Id: " << obj
.axID();
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() &&
282 parent
.accessibilityIsIgnored()) {
283 parent
= parent
.parentObject();
286 if (parent
.isNull() || !parent
.isValid()) {
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;
297 if (!is_child_of_parent
) {
299 notification
.id
= obj
.axID();
300 includes_children
= true;
304 AccessibilityHostMsg_NotificationParams notification_msg
;
305 WebAccessibilityNotificationToAccessibilityNotification(
306 notification
.type
, ¬ification_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
);
322 LOG(INFO
) << "Accessibility update: "
323 << notification_msg
.acc_tree
.DebugString(true,
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
);
342 DCHECK_EQ(renderer_node
.role
, WebAccessibility::ROLE_ROOT_WEB_AREA
);
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())
377 const WebDocument
& document
= GetMainDocument();
378 if (document
.isNull())
381 WebAccessibilityObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
382 if (!obj
.isValid()) {
385 LOG(WARNING
) << "DoDefaultAction on invalid object id " << acc_obj_id
;
390 obj
.performDefaultAction();
393 void RendererAccessibility::OnScrollToMakeVisible(
394 int acc_obj_id
, gfx::Rect subfocus
) {
395 if (!WebAccessibilityObject::accessibilityEnabled())
398 const WebDocument
& document
= GetMainDocument();
399 if (document
.isNull())
402 WebAccessibilityObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
403 if (!obj
.isValid()) {
406 LOG(WARNING
) << "ScrollToMakeVisible on invalid object id " << acc_obj_id
;
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())
429 const WebDocument
& document
= GetMainDocument();
430 if (document
.isNull())
433 WebAccessibilityObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
434 if (!obj
.isValid()) {
437 LOG(WARNING
) << "ScrollToPoint on invalid object id " << acc_obj_id
;
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())
458 const WebDocument
& document
= GetMainDocument();
459 if (document
.isNull())
462 WebAccessibilityObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
463 if (!obj
.isValid()) {
466 LOG(WARNING
) << "SetTextSelection on invalid object id " << acc_obj_id
;
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())
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())
509 const WebDocument
& document
= GetMainDocument();
510 if (document
.isNull())
513 WebAccessibilityObject obj
= document
.accessibilityObjectFromID(acc_obj_id
);
514 if (!obj
.isValid()) {
517 LOG(WARNING
) << "OnSetAccessibilityFocus on invalid object id "
524 WebAccessibilityObject root
= document
.accessibilityObject();
525 if (!root
.isValid()) {
528 LOG(WARNING
) << "OnSetAccessibilityFocus but root is invalid";
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();
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
) {
554 WebDocument
RendererAccessibility::GetMainDocument() {
555 WebView
* view
= render_view()->GetWebView();
556 WebFrame
* main_frame
= view
? view
->mainFrame() : NULL
;
559 return main_frame
->document();
561 return WebDocument();