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