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_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