Home | History | Annotate | Download | only in accessibility
      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