// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "content/browser/accessibility/browser_accessibility.h" #include #include "base/logging.h" #include "base/strings/string_number_conversions.h" #include "base/strings/string_util.h" #include "base/strings/utf_string_conversions.h" #include "content/browser/accessibility/browser_accessibility_manager.h" #include "content/common/accessibility_messages.h" #include "ui/accessibility/ax_text_utils.h" namespace content { #if !defined(OS_WIN) && \ !defined(OS_MACOSX) && \ !defined(OS_ANDROID) && \ !(defined(OS_LINUX) && !defined(OS_CHROMEOS) && defined(USE_X11)) // We have subclassess of BrowserAccessibility on some platforms. // On other platforms, instantiate the base class. // static BrowserAccessibility* BrowserAccessibility::Create() { return new BrowserAccessibility(); } #endif BrowserAccessibility::BrowserAccessibility() : manager_(NULL), node_(NULL) { } BrowserAccessibility::~BrowserAccessibility() { } void BrowserAccessibility::Init(BrowserAccessibilityManager* manager, ui::AXNode* node) { manager_ = manager; node_ = node; } bool BrowserAccessibility::PlatformIsLeaf() const { if (InternalChildCount() == 0) return true; // All of these roles may have children that we use as internal // implementation details, but we want to expose them as leaves // to platform accessibility APIs. switch (GetRole()) { case ui::AX_ROLE_LINE_BREAK: case ui::AX_ROLE_SLIDER: case ui::AX_ROLE_STATIC_TEXT: case ui::AX_ROLE_TEXT_FIELD: return true; default: return false; } } uint32 BrowserAccessibility::PlatformChildCount() const { if (HasIntAttribute(ui::AX_ATTR_CHILD_TREE_ID)) { BrowserAccessibilityManager* child_manager = BrowserAccessibilityManager::FromID( GetIntAttribute(ui::AX_ATTR_CHILD_TREE_ID)); if (child_manager) return 1; return 0; } return PlatformIsLeaf() ? 0 : InternalChildCount(); } bool BrowserAccessibility::IsNative() const { return false; } bool BrowserAccessibility::IsDescendantOf( const BrowserAccessibility* ancestor) const { if (this == ancestor) { return true; } else if (GetParent()) { return GetParent()->IsDescendantOf(ancestor); } return false; } bool BrowserAccessibility::IsTextOnlyObject() const { return GetRole() == ui::AX_ROLE_STATIC_TEXT || GetRole() == ui::AX_ROLE_LINE_BREAK; } BrowserAccessibility* BrowserAccessibility::PlatformGetChild( uint32 child_index) const { DCHECK(child_index < PlatformChildCount()); BrowserAccessibility* result = nullptr; if (HasIntAttribute(ui::AX_ATTR_CHILD_TREE_ID)) { BrowserAccessibilityManager* child_manager = BrowserAccessibilityManager::FromID( GetIntAttribute(ui::AX_ATTR_CHILD_TREE_ID)); if (child_manager) result = child_manager->GetRoot(); } else { result = InternalGetChild(child_index); } return result; } bool BrowserAccessibility::PlatformIsChildOfLeaf() const { BrowserAccessibility* ancestor = GetParent(); while (ancestor) { if (ancestor->PlatformIsLeaf()) return true; ancestor = ancestor->GetParent(); } return false; } BrowserAccessibility* BrowserAccessibility::GetPreviousSibling() { if (GetParent() && GetIndexInParent() > 0) return GetParent()->InternalGetChild(GetIndexInParent() - 1); return NULL; } BrowserAccessibility* BrowserAccessibility::GetNextSibling() { if (GetParent() && GetIndexInParent() >= 0 && GetIndexInParent() < static_cast( GetParent()->InternalChildCount() - 1)) { return GetParent()->InternalGetChild(GetIndexInParent() + 1); } return NULL; } uint32 BrowserAccessibility::InternalChildCount() const { if (!node_ || !manager_) return 0; return static_cast(node_->child_count()); } BrowserAccessibility* BrowserAccessibility::InternalGetChild( uint32 child_index) const { if (!node_ || !manager_) return NULL; return manager_->GetFromAXNode(node_->ChildAtIndex(child_index)); } BrowserAccessibility* BrowserAccessibility::GetParent() const { if (!node_ || !manager_) return NULL; ui::AXNode* parent = node_->parent(); if (parent) return manager_->GetFromAXNode(parent); return manager_->GetParentNodeFromParentTree(); } int32 BrowserAccessibility::GetIndexInParent() const { return node_ ? node_->index_in_parent() : -1; } int32 BrowserAccessibility::GetId() const { return node_ ? node_->id() : -1; } const ui::AXNodeData& BrowserAccessibility::GetData() const { CR_DEFINE_STATIC_LOCAL(ui::AXNodeData, empty_data, ()); if (node_) return node_->data(); else return empty_data; } gfx::Rect BrowserAccessibility::GetLocation() const { return GetData().location; } int32 BrowserAccessibility::GetRole() const { return GetData().role; } int32 BrowserAccessibility::GetState() const { return GetData().state; } const BrowserAccessibility::HtmlAttributes& BrowserAccessibility::GetHtmlAttributes() const { return GetData().html_attributes; } gfx::Rect BrowserAccessibility::GetLocalBoundsRect() const { gfx::Rect bounds = GetLocation(); FixEmptyBounds(&bounds); return ElementBoundsToLocalBounds(bounds); } gfx::Rect BrowserAccessibility::GetGlobalBoundsRect() const { gfx::Rect bounds = GetLocalBoundsRect(); // Adjust the bounds by the top left corner of the containing view's bounds // in screen coordinates. bounds.Offset(manager_->GetViewBounds().OffsetFromOrigin()); return bounds; } gfx::Rect BrowserAccessibility::GetLocalBoundsForRange(int start, int len) const { if (GetRole() != ui::AX_ROLE_STATIC_TEXT) { // Apply recursively to all static text descendants. For example, if // you call it on a div with two text node children, it just calls // GetLocalBoundsForRange on each of the two children (adjusting // |start| for each one) and unions the resulting rects. gfx::Rect bounds; for (size_t i = 0; i < InternalChildCount(); ++i) { BrowserAccessibility* child = InternalGetChild(i); int child_len = child->GetStaticTextLenRecursive(); if (start < child_len && start + len > 0) { gfx::Rect child_rect = child->GetLocalBoundsForRange(start, len); bounds.Union(child_rect); } start -= child_len; } return ElementBoundsToLocalBounds(bounds); } int end = start + len; int child_start = 0; int child_end = 0; gfx::Rect bounds; for (size_t i = 0; i < InternalChildCount() && child_end < start + len; ++i) { BrowserAccessibility* child = InternalGetChild(i); if (child->GetRole() != ui::AX_ROLE_INLINE_TEXT_BOX) { DLOG(WARNING) << "BrowserAccessibility objects with role STATIC_TEXT " << "should have children of role INLINE_TEXT_BOX."; continue; } std::string child_text; child->GetStringAttribute(ui::AX_ATTR_VALUE, &child_text); int child_len = static_cast(child_text.size()); child_start = child_end; child_end += child_len; if (child_end < start) continue; int overlap_start = std::max(start, child_start); int overlap_end = std::min(end, child_end); int local_start = overlap_start - child_start; int local_end = overlap_end - child_start; gfx::Rect child_rect = child->GetLocation(); int text_direction = child->GetIntAttribute( ui::AX_ATTR_TEXT_DIRECTION); const std::vector& character_offsets = child->GetIntListAttribute( ui::AX_ATTR_CHARACTER_OFFSETS); int start_pixel_offset = local_start > 0 ? character_offsets[local_start - 1] : 0; int end_pixel_offset = local_end > 0 ? character_offsets[local_end - 1] : 0; gfx::Rect child_overlap_rect; switch (text_direction) { case ui::AX_TEXT_DIRECTION_NONE: case ui::AX_TEXT_DIRECTION_LTR: { int left = child_rect.x() + start_pixel_offset; int right = child_rect.x() + end_pixel_offset; child_overlap_rect = gfx::Rect(left, child_rect.y(), right - left, child_rect.height()); break; } case ui::AX_TEXT_DIRECTION_RTL: { int right = child_rect.right() - start_pixel_offset; int left = child_rect.right() - end_pixel_offset; child_overlap_rect = gfx::Rect(left, child_rect.y(), right - left, child_rect.height()); break; } case ui::AX_TEXT_DIRECTION_TTB: { int top = child_rect.y() + start_pixel_offset; int bottom = child_rect.y() + end_pixel_offset; child_overlap_rect = gfx::Rect(child_rect.x(), top, child_rect.width(), bottom - top); break; } case ui::AX_TEXT_DIRECTION_BTT: { int bottom = child_rect.bottom() - start_pixel_offset; int top = child_rect.bottom() - end_pixel_offset; child_overlap_rect = gfx::Rect(child_rect.x(), top, child_rect.width(), bottom - top); break; } default: NOTREACHED(); } if (bounds.width() == 0 && bounds.height() == 0) bounds = child_overlap_rect; else bounds.Union(child_overlap_rect); } return ElementBoundsToLocalBounds(bounds); } gfx::Rect BrowserAccessibility::GetGlobalBoundsForRange(int start, int len) const { gfx::Rect bounds = GetLocalBoundsForRange(start, len); // Adjust the bounds by the top left corner of the containing view's bounds // in screen coordinates. bounds.Offset(manager_->GetViewBounds().OffsetFromOrigin()); return bounds; } int BrowserAccessibility::GetWordStartBoundary( int start, ui::TextBoundaryDirection direction) const { DCHECK_GE(start, -1); // Special offset that indicates that a word boundary has not been found. int word_start_not_found = GetStaticTextLenRecursive(); int word_start = word_start_not_found; switch (GetRole()) { case ui::AX_ROLE_STATIC_TEXT: { int prev_word_start = word_start_not_found; int child_start = 0; int child_end = 0; // Go through the inline text boxes. for (size_t i = 0; i < InternalChildCount(); ++i) { // The next child starts where the previous one ended. child_start = child_end; BrowserAccessibility* child = InternalGetChild(i); DCHECK_EQ(child->GetRole(), ui::AX_ROLE_INLINE_TEXT_BOX); const std::string& child_text = child->GetStringAttribute( ui::AX_ATTR_VALUE); int child_len = static_cast(child_text.size()); child_end += child_len; // End is one past the last character. const std::vector& word_starts = child->GetIntListAttribute( ui::AX_ATTR_WORD_STARTS); if (word_starts.empty()) { word_start = child_end; continue; } int local_start = start - child_start; std::vector::const_iterator iter = std::upper_bound( word_starts.begin(), word_starts.end(), local_start); if (iter != word_starts.end()) { if (direction == ui::FORWARDS_DIRECTION) { word_start = child_start + *iter; } else if (direction == ui::BACKWARDS_DIRECTION) { if (iter == word_starts.begin()) { // Return the position of the last word in the previous child. word_start = prev_word_start; } else { word_start = child_start + *(iter - 1); } } else { NOTREACHED(); } break; } // No word start that is greater than the requested offset has been // found. prev_word_start = child_start + *(iter - 1); if (direction == ui::FORWARDS_DIRECTION) { word_start = child_end; } else if (direction == ui::BACKWARDS_DIRECTION) { word_start = prev_word_start; } else { NOTREACHED(); } } return word_start; } case ui::AX_ROLE_LINE_BREAK: // Words never start at a line break. return word_start_not_found; default: // If there are no children, the word start boundary is still unknown or // found previously depending on the direction. if (!InternalChildCount()) return word_start_not_found; int child_start = 0; for (size_t i = 0; i < InternalChildCount(); ++i) { BrowserAccessibility* child = InternalGetChild(i); int child_len = child->GetStaticTextLenRecursive(); int child_word_start = child->GetWordStartBoundary(start, direction); if (child_word_start < child_len) { // We have found a possible word boundary. word_start = child_start + child_word_start; } // Decide when to stop searching. if ((word_start != word_start_not_found && direction == ui::FORWARDS_DIRECTION) || (start < child_len && direction == ui::BACKWARDS_DIRECTION)) { break; } child_start += child_len; if (start >= child_len) start -= child_len; else start = -1; } return word_start; } } BrowserAccessibility* BrowserAccessibility::BrowserAccessibilityForPoint( const gfx::Point& point) { // The best result found that's a child of this object. BrowserAccessibility* child_result = NULL; // The best result that's an indirect descendant like grandchild, etc. BrowserAccessibility* descendant_result = NULL; // Walk the children recursively looking for the BrowserAccessibility that // most tightly encloses the specified point. Walk backwards so that in // the absence of any other information, we assume the object that occurs // later in the tree is on top of one that comes before it. for (int i = static_cast(PlatformChildCount()) - 1; i >= 0; --i) { BrowserAccessibility* child = PlatformGetChild(i); // Skip table columns because cells are only contained in rows, // not columns. if (child->GetRole() == ui::AX_ROLE_COLUMN) continue; if (child->GetGlobalBoundsRect().Contains(point)) { BrowserAccessibility* result = child->BrowserAccessibilityForPoint(point); if (result == child && !child_result) child_result = result; if (result != child && !descendant_result) descendant_result = result; } if (child_result && descendant_result) break; } // Explanation of logic: it's possible that this point overlaps more than // one child of this object. If so, as a heuristic we prefer if the point // overlaps a descendant of one of the two children and not the other. // As an example, suppose you have two rows of buttons - the buttons don't // overlap, but the rows do. Without this heuristic, we'd greedily only // consider one of the containers. if (descendant_result) return descendant_result; if (child_result) return child_result; return this; } void BrowserAccessibility::Destroy() { // Allow the object to fire a TextRemoved notification. manager_->NotifyAccessibilityEvent(ui::AX_EVENT_HIDE, this); node_ = NULL; manager_ = NULL; NativeReleaseReference(); } void BrowserAccessibility::NativeReleaseReference() { delete this; } bool BrowserAccessibility::HasBoolAttribute( ui::AXBoolAttribute attribute) const { return GetData().HasBoolAttribute(attribute); } bool BrowserAccessibility::GetBoolAttribute( ui::AXBoolAttribute attribute) const { return GetData().GetBoolAttribute(attribute); } bool BrowserAccessibility::GetBoolAttribute( ui::AXBoolAttribute attribute, bool* value) const { return GetData().GetBoolAttribute(attribute, value); } bool BrowserAccessibility::HasFloatAttribute( ui::AXFloatAttribute attribute) const { return GetData().HasFloatAttribute(attribute); } float BrowserAccessibility::GetFloatAttribute( ui::AXFloatAttribute attribute) const { return GetData().GetFloatAttribute(attribute); } bool BrowserAccessibility::GetFloatAttribute( ui::AXFloatAttribute attribute, float* value) const { return GetData().GetFloatAttribute(attribute, value); } bool BrowserAccessibility::HasIntAttribute( ui::AXIntAttribute attribute) const { return GetData().HasIntAttribute(attribute); } int BrowserAccessibility::GetIntAttribute(ui::AXIntAttribute attribute) const { return GetData().GetIntAttribute(attribute); } bool BrowserAccessibility::GetIntAttribute( ui::AXIntAttribute attribute, int* value) const { return GetData().GetIntAttribute(attribute, value); } bool BrowserAccessibility::HasStringAttribute( ui::AXStringAttribute attribute) const { return GetData().HasStringAttribute(attribute); } const std::string& BrowserAccessibility::GetStringAttribute( ui::AXStringAttribute attribute) const { return GetData().GetStringAttribute(attribute); } bool BrowserAccessibility::GetStringAttribute( ui::AXStringAttribute attribute, std::string* value) const { return GetData().GetStringAttribute(attribute, value); } base::string16 BrowserAccessibility::GetString16Attribute( ui::AXStringAttribute attribute) const { return GetData().GetString16Attribute(attribute); } bool BrowserAccessibility::GetString16Attribute( ui::AXStringAttribute attribute, base::string16* value) const { return GetData().GetString16Attribute(attribute, value); } bool BrowserAccessibility::HasIntListAttribute( ui::AXIntListAttribute attribute) const { return GetData().HasIntListAttribute(attribute); } const std::vector& BrowserAccessibility::GetIntListAttribute( ui::AXIntListAttribute attribute) const { return GetData().GetIntListAttribute(attribute); } bool BrowserAccessibility::GetIntListAttribute( ui::AXIntListAttribute attribute, std::vector* value) const { return GetData().GetIntListAttribute(attribute, value); } bool BrowserAccessibility::GetHtmlAttribute( const char* html_attr, std::string* value) const { return GetData().GetHtmlAttribute(html_attr, value); } bool BrowserAccessibility::GetHtmlAttribute( const char* html_attr, base::string16* value) const { return GetData().GetHtmlAttribute(html_attr, value); } bool BrowserAccessibility::GetAriaTristate( const char* html_attr, bool* is_defined, bool* is_mixed) const { *is_defined = false; *is_mixed = false; base::string16 value; if (!GetHtmlAttribute(html_attr, &value) || value.empty() || base::EqualsASCII(value, "undefined")) { return false; // Not set (and *is_defined is also false) } *is_defined = true; if (base::EqualsASCII(value, "true")) return true; if (base::EqualsASCII(value, "mixed")) *is_mixed = true; return false; // Not set } bool BrowserAccessibility::HasState(ui::AXState state_enum) const { return (GetState() >> state_enum) & 1; } bool BrowserAccessibility::IsCellOrTableHeaderRole() const { return (GetRole() == ui::AX_ROLE_CELL || GetRole() == ui::AX_ROLE_COLUMN_HEADER || GetRole() == ui::AX_ROLE_ROW_HEADER); } bool BrowserAccessibility::IsEditableText() const { // These roles don't have readonly set, but they're not editable text. if (GetRole() == ui::AX_ROLE_SCROLL_AREA || GetRole() == ui::AX_ROLE_COLUMN || GetRole() == ui::AX_ROLE_TABLE_HEADER_CONTAINER) { return false; } // Note: WebAXStateReadonly being false means it's either a text control, // or contenteditable. We also check for the text field role to cover // elements that have role=textbox set on it. return (!HasState(ui::AX_STATE_READ_ONLY) || GetRole() == ui::AX_ROLE_TEXT_FIELD); } bool BrowserAccessibility::IsWebAreaForPresentationalIframe() const { if (GetRole() != ui::AX_ROLE_WEB_AREA && GetRole() != ui::AX_ROLE_ROOT_WEB_AREA) { return false; } BrowserAccessibility* parent = GetParent(); if (!parent) return false; BrowserAccessibility* grandparent = parent->GetParent(); if (!grandparent) return false; return grandparent->GetRole() == ui::AX_ROLE_IFRAME_PRESENTATIONAL; } bool BrowserAccessibility::IsControl() const { switch (GetRole()) { case ui::AX_ROLE_BUTTON: case ui::AX_ROLE_BUTTON_DROP_DOWN: case ui::AX_ROLE_CHECK_BOX: case ui::AX_ROLE_COLOR_WELL: case ui::AX_ROLE_COMBO_BOX: case ui::AX_ROLE_DISCLOSURE_TRIANGLE: case ui::AX_ROLE_LIST_BOX: case ui::AX_ROLE_MENU_BAR: case ui::AX_ROLE_MENU_BUTTON: case ui::AX_ROLE_MENU_ITEM: case ui::AX_ROLE_MENU_ITEM_CHECK_BOX: case ui::AX_ROLE_MENU_ITEM_RADIO: case ui::AX_ROLE_MENU: case ui::AX_ROLE_POP_UP_BUTTON: case ui::AX_ROLE_RADIO_BUTTON: case ui::AX_ROLE_SCROLL_BAR: case ui::AX_ROLE_SEARCH_BOX: case ui::AX_ROLE_SLIDER: case ui::AX_ROLE_SPIN_BUTTON: case ui::AX_ROLE_SWITCH: case ui::AX_ROLE_TAB: case ui::AX_ROLE_TEXT_FIELD: case ui::AX_ROLE_TOGGLE_BUTTON: case ui::AX_ROLE_TREE: return true; default: return false; } } int BrowserAccessibility::GetStaticTextLenRecursive() const { if (GetRole() == ui::AX_ROLE_STATIC_TEXT || GetRole() == ui::AX_ROLE_LINE_BREAK) { return static_cast(GetStringAttribute(ui::AX_ATTR_VALUE).size()); } int len = 0; for (size_t i = 0; i < InternalChildCount(); ++i) len += InternalGetChild(i)->GetStaticTextLenRecursive(); return len; } void BrowserAccessibility::FixEmptyBounds(gfx::Rect* bounds) const { if (bounds->width() > 0 && bounds->height() > 0) return; for (size_t i = 0; i < InternalChildCount(); ++i) { // Compute the bounds of each child - this calls FixEmptyBounds // recursively if necessary. BrowserAccessibility* child = InternalGetChild(i); gfx::Rect child_bounds = child->GetLocalBoundsRect(); // Ignore children that don't have valid bounds themselves. if (child_bounds.width() == 0 || child_bounds.height() == 0) continue; // For the first valid child, just set the bounds to that child's bounds. if (bounds->width() == 0 || bounds->height() == 0) { *bounds = child_bounds; continue; } // Union each additional child's bounds. bounds->Union(child_bounds); } } gfx::Rect BrowserAccessibility::ElementBoundsToLocalBounds(gfx::Rect bounds) const { // Walk up the parent chain. Every time we encounter a Web Area, offset // based on the scroll bars and then offset based on the origin of that // nested web area. BrowserAccessibility* parent = GetParent(); bool need_to_offset_web_area = (GetRole() == ui::AX_ROLE_WEB_AREA || GetRole() == ui::AX_ROLE_ROOT_WEB_AREA); while (parent) { if (need_to_offset_web_area && parent->GetLocation().width() > 0 && parent->GetLocation().height() > 0) { bounds.Offset(parent->GetLocation().x(), parent->GetLocation().y()); need_to_offset_web_area = false; } // On some platforms, we don't want to take the root scroll offsets // into account. if (parent->GetRole() == ui::AX_ROLE_ROOT_WEB_AREA && !manager()->UseRootScrollOffsetsWhenComputingBounds()) { break; } if (parent->GetRole() == ui::AX_ROLE_WEB_AREA || parent->GetRole() == ui::AX_ROLE_ROOT_WEB_AREA) { int sx = 0; int sy = 0; if (parent->GetIntAttribute(ui::AX_ATTR_SCROLL_X, &sx) && parent->GetIntAttribute(ui::AX_ATTR_SCROLL_Y, &sy)) { bounds.Offset(-sx, -sy); } need_to_offset_web_area = true; } parent = parent->GetParent(); } return bounds; } } // namespace content