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