OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2011 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 "base/command_line.h" |
| 6 #include "content/common/content_switches.h" |
| 7 #include "content/common/view_messages.h" |
| 8 #include "content/renderer/render_view.h" |
| 9 #include "content/renderer/renderer_accessibility.h" |
| 10 #include "third_party/WebKit/Source/WebKit/chromium/public/WebAccessibilityObjec
t.h" |
| 11 #include "third_party/WebKit/Source/WebKit/chromium/public/WebDocument.h" |
| 12 #include "third_party/WebKit/Source/WebKit/chromium/public/WebFrame.h" |
| 13 #include "third_party/WebKit/Source/WebKit/chromium/public/WebView.h" |
| 14 #include "webkit/glue/webaccessibility.h" |
| 15 |
| 16 using WebKit::WebAccessibilityNotification; |
| 17 using WebKit::WebAccessibilityObject; |
| 18 using WebKit::WebDocument; |
| 19 using WebKit::WebFrame; |
| 20 using WebKit::WebNode; |
| 21 using WebKit::WebView; |
| 22 using webkit_glue::WebAccessibility; |
| 23 |
| 24 bool WebAccessibilityNotificationToViewHostMsg( |
| 25 WebAccessibilityNotification notification, |
| 26 ViewHostMsg_AccEvent::Value* type) { |
| 27 switch (notification) { |
| 28 case WebKit::WebAccessibilityNotificationActiveDescendantChanged: |
| 29 *type = ViewHostMsg_AccEvent::ACTIVE_DESCENDANT_CHANGED; |
| 30 break; |
| 31 case WebKit::WebAccessibilityNotificationCheckedStateChanged: |
| 32 *type = ViewHostMsg_AccEvent::CHECK_STATE_CHANGED; |
| 33 break; |
| 34 case WebKit::WebAccessibilityNotificationChildrenChanged: |
| 35 *type = ViewHostMsg_AccEvent::CHILDREN_CHANGED; |
| 36 break; |
| 37 case WebKit::WebAccessibilityNotificationFocusedUIElementChanged: |
| 38 *type = ViewHostMsg_AccEvent::FOCUS_CHANGED; |
| 39 break; |
| 40 case WebKit::WebAccessibilityNotificationLayoutComplete: |
| 41 *type = ViewHostMsg_AccEvent::LAYOUT_COMPLETE; |
| 42 break; |
| 43 case WebKit::WebAccessibilityNotificationLiveRegionChanged: |
| 44 *type = ViewHostMsg_AccEvent::LIVE_REGION_CHANGED; |
| 45 break; |
| 46 case WebKit::WebAccessibilityNotificationLoadComplete: |
| 47 *type = ViewHostMsg_AccEvent::LOAD_COMPLETE; |
| 48 break; |
| 49 case WebKit::WebAccessibilityNotificationMenuListValueChanged: |
| 50 *type = ViewHostMsg_AccEvent::MENU_LIST_VALUE_CHANGED; |
| 51 break; |
| 52 case WebKit::WebAccessibilityNotificationRowCollapsed: |
| 53 *type = ViewHostMsg_AccEvent::ROW_COLLAPSED; |
| 54 break; |
| 55 case WebKit::WebAccessibilityNotificationRowCountChanged: |
| 56 *type = ViewHostMsg_AccEvent::ROW_COUNT_CHANGED; |
| 57 break; |
| 58 case WebKit::WebAccessibilityNotificationRowExpanded: |
| 59 *type = ViewHostMsg_AccEvent::ROW_EXPANDED; |
| 60 break; |
| 61 case WebKit::WebAccessibilityNotificationScrolledToAnchor: |
| 62 *type = ViewHostMsg_AccEvent::SCROLLED_TO_ANCHOR; |
| 63 break; |
| 64 case WebKit::WebAccessibilityNotificationSelectedChildrenChanged: |
| 65 *type = ViewHostMsg_AccEvent::SELECTED_CHILDREN_CHANGED; |
| 66 break; |
| 67 case WebKit::WebAccessibilityNotificationSelectedTextChanged: |
| 68 *type = ViewHostMsg_AccEvent::SELECTED_TEXT_CHANGED; |
| 69 break; |
| 70 case WebKit::WebAccessibilityNotificationValueChanged: |
| 71 *type = ViewHostMsg_AccEvent::VALUE_CHANGED; |
| 72 break; |
| 73 default: |
| 74 NOTREACHED(); |
| 75 return false; |
| 76 } |
| 77 return true; |
| 78 } |
| 79 |
| 80 RendererAccessibility::RendererAccessibility(RenderView* render_view) |
| 81 : RenderViewObserver(render_view), |
| 82 ALLOW_THIS_IN_INITIALIZER_LIST(method_factory_(this)), |
| 83 browser_root_(NULL), |
| 84 ack_pending_(false), |
| 85 logging_(false), |
| 86 sent_load_complete_(false) { |
| 87 const CommandLine& command_line = *CommandLine::ForCurrentProcess(); |
| 88 if (command_line.HasSwitch(switches::kEnableAccessibility)) |
| 89 WebAccessibilityObject::enableAccessibility(); |
| 90 if (command_line.HasSwitch(switches::kEnableAccessibilityLogging)) |
| 91 logging_ = true; |
| 92 } |
| 93 |
| 94 RendererAccessibility::~RendererAccessibility() { |
| 95 } |
| 96 |
| 97 bool RendererAccessibility::OnMessageReceived(const IPC::Message& message) { |
| 98 bool handled = true; |
| 99 IPC_BEGIN_MESSAGE_MAP(RendererAccessibility, message) |
| 100 IPC_MESSAGE_HANDLER(ViewMsg_EnableAccessibility, OnEnableAccessibility) |
| 101 IPC_MESSAGE_HANDLER(ViewMsg_SetAccessibilityFocus, OnSetAccessibilityFocus) |
| 102 IPC_MESSAGE_HANDLER(ViewMsg_AccessibilityDoDefaultAction, |
| 103 OnAccessibilityDoDefaultAction) |
| 104 IPC_MESSAGE_HANDLER(ViewMsg_AccessibilityNotifications_ACK, |
| 105 OnAccessibilityNotificationsAck) |
| 106 IPC_MESSAGE_UNHANDLED(handled = false) |
| 107 IPC_END_MESSAGE_MAP() |
| 108 return handled; |
| 109 } |
| 110 |
| 111 void RendererAccessibility::FocusedNodeChanged(const WebNode& node) { |
| 112 if (!WebAccessibilityObject::accessibilityEnabled()) |
| 113 return; |
| 114 |
| 115 const WebDocument& document = GetMainDocument(); |
| 116 if (document.isNull()) |
| 117 return; |
| 118 |
| 119 if (node.isNull()) { |
| 120 // When focus is cleared, implicitly focus the document. |
| 121 // TODO(dmazzoni): Make WebKit send this notification instead. |
| 122 PostAccessibilityNotification( |
| 123 document.accessibilityObject(), |
| 124 WebKit::WebAccessibilityNotificationFocusedUIElementChanged); |
| 125 } |
| 126 } |
| 127 |
| 128 void RendererAccessibility::DidFinishLoad(WebKit::WebFrame* frame) { |
| 129 if (!WebAccessibilityObject::accessibilityEnabled()) |
| 130 return; |
| 131 |
| 132 const WebDocument& document = GetMainDocument(); |
| 133 if (document.isNull()) |
| 134 return; |
| 135 |
| 136 // Check to see if the root accessibility object has changed, to work |
| 137 // around WebKit bugs that cause AXObjectCache to be cleared |
| 138 // unnecessarily. |
| 139 // TODO(dmazzoni): remove this once rdar://5794454 is fixed. |
| 140 WebAccessibilityObject new_root = document.accessibilityObject(); |
| 141 if (!browser_root_ || new_root.axID() != browser_root_->id) { |
| 142 PostAccessibilityNotification( |
| 143 new_root, |
| 144 WebKit::WebAccessibilityNotificationLoadComplete); |
| 145 } |
| 146 } |
| 147 |
| 148 void RendererAccessibility::PostAccessibilityNotification( |
| 149 const WebAccessibilityObject& obj, |
| 150 WebAccessibilityNotification notification) { |
| 151 if (!WebAccessibilityObject::accessibilityEnabled()) |
| 152 return; |
| 153 |
| 154 const WebDocument& document = GetMainDocument(); |
| 155 if (document.isNull()) |
| 156 return; |
| 157 |
| 158 if (notification != WebKit::WebAccessibilityNotificationLoadComplete && |
| 159 !sent_load_complete_) { |
| 160 // Load complete should be our first notification sent. Send it manually |
| 161 // in cases where we don't get it first to avoid focus problems. |
| 162 PostAccessibilityNotification( |
| 163 document.accessibilityObject(), |
| 164 WebKit::WebAccessibilityNotificationLoadComplete); |
| 165 } |
| 166 |
| 167 if (notification == WebKit::WebAccessibilityNotificationLoadComplete) |
| 168 sent_load_complete_ = true; |
| 169 |
| 170 // Add the accessibility object to our cache and ensure it's valid. |
| 171 Notification acc_notification; |
| 172 acc_notification.id = obj.axID(); |
| 173 acc_notification.type = notification; |
| 174 |
| 175 ViewHostMsg_AccEvent::Value temp; |
| 176 if (!WebAccessibilityNotificationToViewHostMsg(notification, &temp)) |
| 177 return; |
| 178 |
| 179 // Discard duplicate accessibility notifications. |
| 180 for (uint32 i = 0; i < pending_notifications_.size(); ++i) { |
| 181 if (pending_notifications_[i].id == acc_notification.id && |
| 182 pending_notifications_[i].type == acc_notification.type) { |
| 183 return; |
| 184 } |
| 185 } |
| 186 pending_notifications_.push_back(acc_notification); |
| 187 |
| 188 if (!ack_pending_ && method_factory_.empty()) { |
| 189 // When no accessibility notifications are in-flight post a task to send |
| 190 // the notifications to the browser. We use PostTask so that we can queue |
| 191 // up additional notifications. |
| 192 MessageLoop::current()->PostTask( |
| 193 FROM_HERE, |
| 194 method_factory_.NewRunnableMethod( |
| 195 &RendererAccessibility::SendPendingAccessibilityNotifications)); |
| 196 } |
| 197 } |
| 198 |
| 199 void RendererAccessibility::SendPendingAccessibilityNotifications() { |
| 200 const WebDocument& document = GetMainDocument(); |
| 201 if (document.isNull()) |
| 202 return; |
| 203 |
| 204 if (pending_notifications_.empty()) |
| 205 return; |
| 206 |
| 207 // Send all pending accessibility notifications. |
| 208 std::vector<ViewHostMsg_AccessibilityNotification_Params> notifications; |
| 209 for (size_t i = 0; i < pending_notifications_.size(); ++i) { |
| 210 Notification& notification = pending_notifications_[i]; |
| 211 |
| 212 bool includes_children = ShouldIncludeChildren(notification); |
| 213 WebAccessibilityObject obj = document.accessibilityObjectFromID( |
| 214 notification.id); |
| 215 |
| 216 if (!obj.isValid()) { |
| 217 #ifndef NDEBUG |
| 218 if (logging_) |
| 219 LOG(WARNING) << "Got notification on invalid object id " << obj.axID(); |
| 220 #endif |
| 221 continue; |
| 222 } |
| 223 |
| 224 // The browser may not have this object yet, for example if we get a |
| 225 // notification on an object that was recently added, or if we get a |
| 226 // notification on a node before the page has loaded. Work our way |
| 227 // up the parent chain until we find a node the browser has, or until |
| 228 // we reach the root. |
| 229 int root_id = document.accessibilityObject().axID(); |
| 230 while (browser_id_map_.find(obj.axID()) == browser_id_map_.end() && |
| 231 obj.axID() != root_id) { |
| 232 obj = obj.parentObject(); |
| 233 includes_children = true; |
| 234 if (notification.type == |
| 235 WebKit::WebAccessibilityNotificationChildrenChanged) { |
| 236 notification.id = obj.axID(); |
| 237 } |
| 238 } |
| 239 |
| 240 // Another potential problem is that this notification may be on an |
| 241 // object that is detached from the tree. Determine if this node is not a |
| 242 // child of its parent, and if so move the notification to the parent. |
| 243 // TODO(dmazzoni): see if this can be removed after |
| 244 // https://bugs.webkit.org/show_bug.cgi?id=68466 is fixed. |
| 245 if (obj.axID() != root_id) { |
| 246 WebAccessibilityObject parent = obj.parentObject(); |
| 247 while (!parent.isNull() && parent.accessibilityIsIgnored()) |
| 248 parent = parent.parentObject(); |
| 249 if (parent.isNull()) { |
| 250 NOTREACHED(); |
| 251 } |
| 252 bool is_child_of_parent = false; |
| 253 for (unsigned int i = 0; i < parent.childCount(); ++i) { |
| 254 if (parent.childAt(i).equals(obj)) { |
| 255 is_child_of_parent = true; |
| 256 break; |
| 257 } |
| 258 } |
| 259 if (!is_child_of_parent) { |
| 260 obj = parent; |
| 261 notification.id = obj.axID(); |
| 262 includes_children = true; |
| 263 } |
| 264 } |
| 265 |
| 266 ViewHostMsg_AccessibilityNotification_Params param; |
| 267 WebAccessibilityNotificationToViewHostMsg( |
| 268 notification.type, ¶m.notification_type); |
| 269 param.id = notification.id; |
| 270 param.includes_children = includes_children; |
| 271 param.acc_tree = WebAccessibility(obj, includes_children); |
| 272 if (obj.axID() == root_id) { |
| 273 DCHECK_EQ(param.acc_tree.role, WebAccessibility::ROLE_WEB_AREA); |
| 274 param.acc_tree.role = WebAccessibility::ROLE_ROOT_WEB_AREA; |
| 275 } |
| 276 notifications.push_back(param); |
| 277 |
| 278 if (includes_children) |
| 279 UpdateBrowserTree(param.acc_tree); |
| 280 |
| 281 #ifndef NDEBUG |
| 282 if (logging_) { |
| 283 LOG(INFO) << "Accessibility update: " |
| 284 << param.acc_tree.DebugString(true, |
| 285 routing_id(), |
| 286 param.notification_type); |
| 287 } |
| 288 #endif |
| 289 } |
| 290 pending_notifications_.clear(); |
| 291 Send(new ViewHostMsg_AccessibilityNotifications(routing_id(), notifications)); |
| 292 ack_pending_ = true; |
| 293 } |
| 294 |
| 295 void RendererAccessibility::UpdateBrowserTree( |
| 296 const webkit_glue::WebAccessibility& renderer_node) { |
| 297 BrowserTreeNode* browser_node = NULL; |
| 298 base::hash_map<int32, BrowserTreeNode*>::iterator iter = |
| 299 browser_id_map_.find(renderer_node.id); |
| 300 if (iter != browser_id_map_.end()) { |
| 301 browser_node = iter->second; |
| 302 ClearBrowserTreeNode(browser_node); |
| 303 } else { |
| 304 DCHECK_EQ(renderer_node.role, WebAccessibility::ROLE_ROOT_WEB_AREA); |
| 305 if (browser_root_) { |
| 306 ClearBrowserTreeNode(browser_root_); |
| 307 browser_id_map_.erase(browser_root_->id); |
| 308 delete browser_root_; |
| 309 } |
| 310 browser_root_ = new BrowserTreeNode; |
| 311 browser_node = browser_root_; |
| 312 browser_node->id = renderer_node.id; |
| 313 browser_id_map_[browser_node->id] = browser_node; |
| 314 } |
| 315 browser_node->children.reserve(renderer_node.children.size()); |
| 316 for (size_t i = 0; i < renderer_node.children.size(); ++i) { |
| 317 BrowserTreeNode* browser_child_node = new BrowserTreeNode; |
| 318 browser_child_node->id = renderer_node.children[i].id; |
| 319 browser_id_map_[browser_child_node->id] = browser_child_node; |
| 320 browser_node->children.push_back(browser_child_node); |
| 321 UpdateBrowserTree(renderer_node.children[i]); |
| 322 } |
| 323 } |
| 324 |
| 325 void RendererAccessibility::ClearBrowserTreeNode( |
| 326 BrowserTreeNode* browser_node) { |
| 327 for (size_t i = 0; i < browser_node->children.size(); ++i) { |
| 328 browser_id_map_.erase(browser_node->children[i]->id); |
| 329 ClearBrowserTreeNode(browser_node->children[i]); |
| 330 delete browser_node->children[i]; |
| 331 } |
| 332 browser_node->children.clear(); |
| 333 } |
| 334 |
| 335 void RendererAccessibility::OnAccessibilityDoDefaultAction(int acc_obj_id) { |
| 336 if (!WebAccessibilityObject::accessibilityEnabled()) |
| 337 return; |
| 338 |
| 339 const WebDocument& document = GetMainDocument(); |
| 340 if (document.isNull()) |
| 341 return; |
| 342 |
| 343 WebAccessibilityObject obj = document.accessibilityObjectFromID(acc_obj_id); |
| 344 if (!obj.isValid()) { |
| 345 #ifndef NDEBUG |
| 346 if (logging_) |
| 347 LOG(WARNING) << "DoDefaultAction on invalid object id " << acc_obj_id; |
| 348 #endif |
| 349 return; |
| 350 } |
| 351 |
| 352 obj.performDefaultAction(); |
| 353 } |
| 354 |
| 355 void RendererAccessibility::OnAccessibilityNotificationsAck() { |
| 356 DCHECK(ack_pending_); |
| 357 ack_pending_ = false; |
| 358 SendPendingAccessibilityNotifications(); |
| 359 } |
| 360 |
| 361 void RendererAccessibility::OnEnableAccessibility() { |
| 362 if (WebAccessibilityObject::accessibilityEnabled()) |
| 363 return; |
| 364 |
| 365 WebAccessibilityObject::enableAccessibility(); |
| 366 |
| 367 const WebDocument& document = GetMainDocument(); |
| 368 if (!document.isNull()) { |
| 369 // It's possible that the webview has already loaded a webpage without |
| 370 // accessibility being enabled. Initialize the browser's cached |
| 371 // accessibility tree by sending it a 'load complete' notification. |
| 372 PostAccessibilityNotification( |
| 373 document.accessibilityObject(), |
| 374 WebKit::WebAccessibilityNotificationLoadComplete); |
| 375 } |
| 376 } |
| 377 |
| 378 void RendererAccessibility::OnSetAccessibilityFocus(int acc_obj_id) { |
| 379 if (!WebAccessibilityObject::accessibilityEnabled()) |
| 380 return; |
| 381 |
| 382 const WebDocument& document = GetMainDocument(); |
| 383 if (document.isNull()) |
| 384 return; |
| 385 |
| 386 WebAccessibilityObject obj = document.accessibilityObjectFromID(acc_obj_id); |
| 387 if (!obj.isValid()) { |
| 388 #ifndef NDEBUG |
| 389 if (logging_) { |
| 390 LOG(WARNING) << "OnSetAccessibilityFocus on invalid object id " |
| 391 << acc_obj_id; |
| 392 } |
| 393 #endif |
| 394 return; |
| 395 } |
| 396 |
| 397 WebAccessibilityObject root = document.accessibilityObject(); |
| 398 if (!root.isValid()) { |
| 399 #ifndef NDEBUG |
| 400 if (logging_) { |
| 401 LOG(WARNING) << "OnSetAccessibilityFocus but root is invalid"; |
| 402 } |
| 403 #endif |
| 404 return; |
| 405 } |
| 406 |
| 407 // By convention, calling SetFocus on the root of the tree should clear the |
| 408 // current focus. Otherwise set the focus to the new node. |
| 409 if (acc_obj_id == root.axID()) |
| 410 render_view()->webview()->clearFocusedNode(); |
| 411 else |
| 412 obj.setFocused(true); |
| 413 } |
| 414 |
| 415 bool RendererAccessibility::ShouldIncludeChildren( |
| 416 const RendererAccessibility::Notification& notification) { |
| 417 WebKit::WebAccessibilityNotification type = notification.type; |
| 418 if (type == WebKit::WebAccessibilityNotificationChildrenChanged || |
| 419 type == WebKit::WebAccessibilityNotificationLoadComplete || |
| 420 type == WebKit::WebAccessibilityNotificationLiveRegionChanged) { |
| 421 return true; |
| 422 } |
| 423 return false; |
| 424 } |
| 425 |
| 426 WebDocument RendererAccessibility::GetMainDocument() { |
| 427 WebView* view = render_view()->webview(); |
| 428 WebFrame* main_frame = view ? view->mainFrame() : NULL; |
| 429 |
| 430 if (main_frame) |
| 431 return main_frame->document(); |
| 432 else |
| 433 return WebDocument(); |
| 434 } |
OLD | NEW |