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/accessibility/renderer_accessibility_complete.h" 6 7 #include <queue> 8 9 #include "base/bind.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/WebFrame.h" 17 #include "third_party/WebKit/public/web/WebInputElement.h" 18 #include "third_party/WebKit/public/web/WebLocalFrame.h" 19 #include "third_party/WebKit/public/web/WebNode.h" 20 #include "third_party/WebKit/public/web/WebSettings.h" 21 #include "third_party/WebKit/public/web/WebView.h" 22 #include "ui/accessibility/ax_tree.h" 23 24 using blink::WebAXObject; 25 using blink::WebDocument; 26 using blink::WebFrame; 27 using blink::WebNode; 28 using blink::WebPoint; 29 using blink::WebRect; 30 using blink::WebSettings; 31 using blink::WebView; 32 33 namespace content { 34 35 RendererAccessibilityComplete::RendererAccessibilityComplete( 36 RenderFrameImpl* render_frame) 37 : RendererAccessibility(render_frame), 38 tree_source_(render_frame), 39 serializer_(&tree_source_), 40 last_scroll_offset_(gfx::Size()), 41 ack_pending_(false), 42 reset_token_(0), 43 weak_factory_(this) { 44 WebView* web_view = render_frame_->GetRenderView()->GetWebView(); 45 WebSettings* settings = web_view->settings(); 46 settings->setAccessibilityEnabled(true); 47 48 #if !defined(OS_ANDROID) 49 // Skip inline text boxes on Android - since there are no native Android 50 // APIs that compute the bounds of a range of text, it's a waste to 51 // include these in the AX tree. 52 settings->setInlineTextBoxAccessibilityEnabled(true); 53 #endif 54 55 const WebDocument& document = GetMainDocument(); 56 if (!document.isNull()) { 57 // It's possible that the webview has already loaded a webpage without 58 // accessibility being enabled. Initialize the browser's cached 59 // accessibility tree by sending it a notification. 60 HandleAXEvent(document.accessibilityObject(), 61 ui::AX_EVENT_LAYOUT_COMPLETE); 62 } 63 } 64 65 RendererAccessibilityComplete::~RendererAccessibilityComplete() { 66 } 67 68 bool RendererAccessibilityComplete::OnMessageReceived( 69 const IPC::Message& message) { 70 bool handled = true; 71 IPC_BEGIN_MESSAGE_MAP(RendererAccessibilityComplete, message) 72 IPC_MESSAGE_HANDLER(AccessibilityMsg_SetFocus, OnSetFocus) 73 IPC_MESSAGE_HANDLER(AccessibilityMsg_DoDefaultAction, 74 OnDoDefaultAction) 75 IPC_MESSAGE_HANDLER(AccessibilityMsg_Events_ACK, 76 OnEventsAck) 77 IPC_MESSAGE_HANDLER(AccessibilityMsg_ScrollToMakeVisible, 78 OnScrollToMakeVisible) 79 IPC_MESSAGE_HANDLER(AccessibilityMsg_ScrollToPoint, 80 OnScrollToPoint) 81 IPC_MESSAGE_HANDLER(AccessibilityMsg_SetTextSelection, 82 OnSetTextSelection) 83 IPC_MESSAGE_HANDLER(AccessibilityMsg_HitTest, OnHitTest) 84 IPC_MESSAGE_HANDLER(AccessibilityMsg_Reset, OnReset) 85 IPC_MESSAGE_HANDLER(AccessibilityMsg_FatalError, OnFatalError) 86 IPC_MESSAGE_UNHANDLED(handled = false) 87 IPC_END_MESSAGE_MAP() 88 return handled; 89 } 90 91 void RendererAccessibilityComplete::FocusedNodeChanged(const WebNode& node) { 92 const WebDocument& document = GetMainDocument(); 93 if (document.isNull()) 94 return; 95 96 if (node.isNull()) { 97 // When focus is cleared, implicitly focus the document. 98 // TODO(dmazzoni): Make Blink send this notification instead. 99 HandleAXEvent(document.accessibilityObject(), ui::AX_EVENT_BLUR); 100 } 101 } 102 103 void RendererAccessibilityComplete::HandleWebAccessibilityEvent( 104 const blink::WebAXObject& obj, blink::WebAXEvent event) { 105 HandleAXEvent(obj, AXEventFromBlink(event)); 106 } 107 108 void RendererAccessibilityComplete::HandleAXEvent( 109 const blink::WebAXObject& obj, ui::AXEvent event) { 110 const WebDocument& document = GetMainDocument(); 111 if (document.isNull()) 112 return; 113 114 gfx::Size scroll_offset = document.frame()->scrollOffset(); 115 if (scroll_offset != last_scroll_offset_) { 116 // Make sure the browser is always aware of the scroll position of 117 // the root document element by posting a generic notification that 118 // will update it. 119 // TODO(dmazzoni): remove this as soon as 120 // https://bugs.webkit.org/show_bug.cgi?id=73460 is fixed. 121 last_scroll_offset_ = scroll_offset; 122 if (!obj.equals(document.accessibilityObject())) { 123 HandleAXEvent(document.accessibilityObject(), 124 ui::AX_EVENT_LAYOUT_COMPLETE); 125 } 126 } 127 128 // Add the accessibility object to our cache and ensure it's valid. 129 AccessibilityHostMsg_EventParams acc_event; 130 acc_event.id = obj.axID(); 131 acc_event.event_type = event; 132 133 // Discard duplicate accessibility events. 134 for (uint32 i = 0; i < pending_events_.size(); ++i) { 135 if (pending_events_[i].id == acc_event.id && 136 pending_events_[i].event_type == 137 acc_event.event_type) { 138 return; 139 } 140 } 141 pending_events_.push_back(acc_event); 142 143 if (!ack_pending_ && !weak_factory_.HasWeakPtrs()) { 144 // When no accessibility events are in-flight post a task to send 145 // the events to the browser. We use PostTask so that we can queue 146 // up additional events. 147 base::MessageLoop::current()->PostTask( 148 FROM_HERE, 149 base::Bind(&RendererAccessibilityComplete:: 150 SendPendingAccessibilityEvents, 151 weak_factory_.GetWeakPtr())); 152 } 153 } 154 155 RendererAccessibilityType RendererAccessibilityComplete::GetType() { 156 return RendererAccessibilityTypeComplete; 157 } 158 159 void RendererAccessibilityComplete::SendPendingAccessibilityEvents() { 160 const WebDocument& document = GetMainDocument(); 161 if (document.isNull()) 162 return; 163 164 if (pending_events_.empty()) 165 return; 166 167 if (render_frame_->is_swapped_out()) 168 return; 169 170 ack_pending_ = true; 171 172 // Make a copy of the events, because it's possible that 173 // actions inside this loop will cause more events to be 174 // queued up. 175 std::vector<AccessibilityHostMsg_EventParams> src_events = 176 pending_events_; 177 pending_events_.clear(); 178 179 // Generate an event message from each Blink event. 180 std::vector<AccessibilityHostMsg_EventParams> event_msgs; 181 182 // If there's a layout complete message, we need to send location changes. 183 bool had_layout_complete_messages = false; 184 185 // Loop over each event and generate an updated event message. 186 for (size_t i = 0; i < src_events.size(); ++i) { 187 AccessibilityHostMsg_EventParams& event = src_events[i]; 188 if (event.event_type == ui::AX_EVENT_LAYOUT_COMPLETE) 189 had_layout_complete_messages = true; 190 191 WebAXObject obj = document.accessibilityObjectFromID(event.id); 192 193 // Make sure the object still exists. 194 if (!obj.updateBackingStoreAndCheckValidity()) 195 continue; 196 197 // Make sure it's a descendant of our root node - exceptions include the 198 // scroll area that's the parent of the main document (we ignore it), and 199 // possibly nodes attached to a different document. 200 if (!tree_source_.IsInTree(obj)) 201 continue; 202 203 // When we get a "selected children changed" event, Blink 204 // doesn't also send us events for each child that changed 205 // selection state, so make sure we re-send that whole subtree. 206 if (event.event_type == 207 ui::AX_EVENT_SELECTED_CHILDREN_CHANGED) { 208 serializer_.DeleteClientSubtree(obj); 209 } 210 211 AccessibilityHostMsg_EventParams event_msg; 212 tree_source_.CollectChildFrameIdMapping( 213 &event_msg.node_to_frame_routing_id_map, 214 &event_msg.node_to_browser_plugin_instance_id_map); 215 event_msg.event_type = event.event_type; 216 event_msg.id = event.id; 217 serializer_.SerializeChanges(obj, &event_msg.update); 218 event_msgs.push_back(event_msg); 219 220 // For each node in the update, set the location in our map from 221 // ids to locations. 222 for (size_t i = 0; i < event_msg.update.nodes.size(); ++i) { 223 locations_[event_msg.update.nodes[i].id] = 224 event_msg.update.nodes[i].location; 225 } 226 227 VLOG(0) << "Accessibility event: " << ui::ToString(event.event_type) 228 << " on node id " << event_msg.id 229 << "\n" << event_msg.update.ToString(); 230 } 231 232 Send(new AccessibilityHostMsg_Events(routing_id(), event_msgs, reset_token_)); 233 reset_token_ = 0; 234 235 if (had_layout_complete_messages) 236 SendLocationChanges(); 237 } 238 239 void RendererAccessibilityComplete::SendLocationChanges() { 240 std::vector<AccessibilityHostMsg_LocationChangeParams> messages; 241 242 // Do a breadth-first explore of the whole blink AX tree. 243 base::hash_map<int, gfx::Rect> new_locations; 244 std::queue<WebAXObject> objs_to_explore; 245 objs_to_explore.push(tree_source_.GetRoot()); 246 while (objs_to_explore.size()) { 247 WebAXObject obj = objs_to_explore.front(); 248 objs_to_explore.pop(); 249 250 // See if we had a previous location. If not, this whole subtree must 251 // be new, so don't continue to explore this branch. 252 int id = obj.axID(); 253 base::hash_map<int, gfx::Rect>::iterator iter = locations_.find(id); 254 if (iter == locations_.end()) 255 continue; 256 257 // If the location has changed, append it to the IPC message. 258 gfx::Rect new_location = obj.boundingBoxRect(); 259 if (iter != locations_.end() && iter->second != new_location) { 260 AccessibilityHostMsg_LocationChangeParams message; 261 message.id = id; 262 message.new_location = new_location; 263 messages.push_back(message); 264 } 265 266 // Save the new location. 267 new_locations[id] = new_location; 268 269 // Explore children of this object. 270 std::vector<blink::WebAXObject> children; 271 tree_source_.GetChildren(obj, &children); 272 for (size_t i = 0; i < children.size(); ++i) 273 objs_to_explore.push(children[i]); 274 } 275 locations_.swap(new_locations); 276 277 Send(new AccessibilityHostMsg_LocationChanges(routing_id(), messages)); 278 } 279 280 void RendererAccessibilityComplete::OnDoDefaultAction(int acc_obj_id) { 281 const WebDocument& document = GetMainDocument(); 282 if (document.isNull()) 283 return; 284 285 WebAXObject obj = document.accessibilityObjectFromID(acc_obj_id); 286 if (obj.isDetached()) { 287 #ifndef NDEBUG 288 LOG(WARNING) << "DoDefaultAction on invalid object id " << acc_obj_id; 289 #endif 290 return; 291 } 292 293 obj.performDefaultAction(); 294 } 295 296 void RendererAccessibilityComplete::OnScrollToMakeVisible( 297 int acc_obj_id, gfx::Rect subfocus) { 298 const WebDocument& document = GetMainDocument(); 299 if (document.isNull()) 300 return; 301 302 WebAXObject obj = document.accessibilityObjectFromID(acc_obj_id); 303 if (obj.isDetached()) { 304 #ifndef NDEBUG 305 LOG(WARNING) << "ScrollToMakeVisible on invalid object id " << acc_obj_id; 306 #endif 307 return; 308 } 309 310 obj.scrollToMakeVisibleWithSubFocus( 311 WebRect(subfocus.x(), subfocus.y(), 312 subfocus.width(), subfocus.height())); 313 314 // Make sure the browser gets an event when the scroll 315 // position actually changes. 316 // TODO(dmazzoni): remove this once this bug is fixed: 317 // https://bugs.webkit.org/show_bug.cgi?id=73460 318 HandleAXEvent(document.accessibilityObject(), 319 ui::AX_EVENT_LAYOUT_COMPLETE); 320 } 321 322 void RendererAccessibilityComplete::OnScrollToPoint( 323 int acc_obj_id, gfx::Point point) { 324 const WebDocument& document = GetMainDocument(); 325 if (document.isNull()) 326 return; 327 328 WebAXObject obj = document.accessibilityObjectFromID(acc_obj_id); 329 if (obj.isDetached()) { 330 #ifndef NDEBUG 331 LOG(WARNING) << "ScrollToPoint on invalid object id " << acc_obj_id; 332 #endif 333 return; 334 } 335 336 obj.scrollToGlobalPoint(WebPoint(point.x(), point.y())); 337 338 // Make sure the browser gets an event when the scroll 339 // position actually changes. 340 // TODO(dmazzoni): remove this once this bug is fixed: 341 // https://bugs.webkit.org/show_bug.cgi?id=73460 342 HandleAXEvent(document.accessibilityObject(), 343 ui::AX_EVENT_LAYOUT_COMPLETE); 344 } 345 346 void RendererAccessibilityComplete::OnSetTextSelection( 347 int acc_obj_id, int start_offset, int end_offset) { 348 const WebDocument& document = GetMainDocument(); 349 if (document.isNull()) 350 return; 351 352 WebAXObject obj = document.accessibilityObjectFromID(acc_obj_id); 353 if (obj.isDetached()) { 354 #ifndef NDEBUG 355 LOG(WARNING) << "SetTextSelection on invalid object id " << acc_obj_id; 356 #endif 357 return; 358 } 359 360 // TODO(dmazzoni): support elements other than <input>. 361 blink::WebNode node = obj.node(); 362 if (!node.isNull() && node.isElementNode()) { 363 blink::WebElement element = node.to<blink::WebElement>(); 364 blink::WebInputElement* input_element = 365 blink::toWebInputElement(&element); 366 if (input_element && input_element->isTextField()) 367 input_element->setSelectionRange(start_offset, end_offset); 368 } 369 } 370 371 void RendererAccessibilityComplete::OnHitTest(gfx::Point point) { 372 const WebDocument& document = GetMainDocument(); 373 if (document.isNull()) 374 return; 375 WebAXObject root_obj = document.accessibilityObject(); 376 if (!root_obj.updateBackingStoreAndCheckValidity()) 377 return; 378 379 WebAXObject obj = root_obj.hitTest(point); 380 if (!obj.isDetached()) 381 HandleAXEvent(obj, ui::AX_EVENT_HOVER); 382 } 383 384 void RendererAccessibilityComplete::OnEventsAck() { 385 DCHECK(ack_pending_); 386 ack_pending_ = false; 387 SendPendingAccessibilityEvents(); 388 } 389 390 void RendererAccessibilityComplete::OnSetFocus(int acc_obj_id) { 391 const WebDocument& document = GetMainDocument(); 392 if (document.isNull()) 393 return; 394 395 WebAXObject obj = document.accessibilityObjectFromID(acc_obj_id); 396 if (obj.isDetached()) { 397 #ifndef NDEBUG 398 LOG(WARNING) << "OnSetAccessibilityFocus on invalid object id " 399 << acc_obj_id; 400 #endif 401 return; 402 } 403 404 WebAXObject root = document.accessibilityObject(); 405 if (root.isDetached()) { 406 #ifndef NDEBUG 407 LOG(WARNING) << "OnSetAccessibilityFocus but root is invalid"; 408 #endif 409 return; 410 } 411 412 // By convention, calling SetFocus on the root of the tree should clear the 413 // current focus. Otherwise set the focus to the new node. 414 if (acc_obj_id == root.axID()) 415 render_frame_->GetRenderView()->GetWebView()->clearFocusedElement(); 416 else 417 obj.setFocused(true); 418 } 419 420 void RendererAccessibilityComplete::OnReset(int reset_token) { 421 reset_token_ = reset_token; 422 serializer_.Reset(); 423 pending_events_.clear(); 424 425 const WebDocument& document = GetMainDocument(); 426 if (!document.isNull()) 427 HandleAXEvent(document.accessibilityObject(), ui::AX_EVENT_LAYOUT_COMPLETE); 428 } 429 430 void RendererAccessibilityComplete::OnFatalError() { 431 CHECK(false) << "Invalid accessibility tree."; 432 } 433 434 } // namespace content 435