blob: fb07f28b4abfafacda7581fc879b9c700d00c7a9 [file] [log] [blame]
/*
* Copyright (C) 2014, Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "modules/accessibility/AXObjectCacheImpl.h"
#include "core/HTMLNames.h"
#include "core/InputTypeNames.h"
#include "core/dom/AccessibleNode.h"
#include "core/dom/Document.h"
#include "core/dom/TaskRunnerHelper.h"
#include "core/editing/EditingUtilities.h"
#include "core/frame/FrameView.h"
#include "core/frame/LocalFrame.h"
#include "core/frame/Settings.h"
#include "core/html/HTMLAreaElement.h"
#include "core/html/HTMLCanvasElement.h"
#include "core/html/HTMLImageElement.h"
#include "core/html/HTMLInputElement.h"
#include "core/html/HTMLLabelElement.h"
#include "core/html/HTMLOptionElement.h"
#include "core/html/HTMLSelectElement.h"
#include "core/layout/LayoutListBox.h"
#include "core/layout/LayoutMenuList.h"
#include "core/layout/LayoutProgress.h"
#include "core/layout/LayoutSlider.h"
#include "core/layout/LayoutTable.h"
#include "core/layout/LayoutTableCell.h"
#include "core/layout/LayoutTableRow.h"
#include "core/layout/LayoutView.h"
#include "core/layout/api/LineLayoutAPIShim.h"
#include "core/layout/line/AbstractInlineTextBox.h"
#include "core/page/ChromeClient.h"
#include "core/page/FocusController.h"
#include "core/page/Page.h"
#include "modules/accessibility/AXARIAGrid.h"
#include "modules/accessibility/AXARIAGridCell.h"
#include "modules/accessibility/AXARIAGridRow.h"
#include "modules/accessibility/AXImageMapLink.h"
#include "modules/accessibility/AXInlineTextBox.h"
#include "modules/accessibility/AXLayoutObject.h"
#include "modules/accessibility/AXList.h"
#include "modules/accessibility/AXListBox.h"
#include "modules/accessibility/AXListBoxOption.h"
#include "modules/accessibility/AXMediaControls.h"
#include "modules/accessibility/AXMenuList.h"
#include "modules/accessibility/AXMenuListOption.h"
#include "modules/accessibility/AXMenuListPopup.h"
#include "modules/accessibility/AXProgressIndicator.h"
#include "modules/accessibility/AXRadioInput.h"
#include "modules/accessibility/AXSVGRoot.h"
#include "modules/accessibility/AXSlider.h"
#include "modules/accessibility/AXSpinButton.h"
#include "modules/accessibility/AXTable.h"
#include "modules/accessibility/AXTableCell.h"
#include "modules/accessibility/AXTableColumn.h"
#include "modules/accessibility/AXTableHeaderContainer.h"
#include "modules/accessibility/AXTableRow.h"
#include "platform/wtf/PassRefPtr.h"
#include "platform/wtf/PtrUtil.h"
namespace blink {
using namespace HTMLNames;
// static
AXObjectCache* AXObjectCacheImpl::Create(Document& document) {
return new AXObjectCacheImpl(document);
}
AXObjectCacheImpl::AXObjectCacheImpl(Document& document)
: document_(document),
modification_count_(0),
notification_post_timer_(
TaskRunnerHelper::Get(TaskType::kUnspecedTimer, &document),
this,
&AXObjectCacheImpl::NotificationPostTimerFired) {}
AXObjectCacheImpl::~AXObjectCacheImpl() {
#if DCHECK_IS_ON()
DCHECK(has_been_disposed_);
#endif
}
void AXObjectCacheImpl::Dispose() {
notification_post_timer_.Stop();
for (auto& entry : objects_) {
AXObjectImpl* obj = entry.value;
obj->Detach();
RemoveAXID(obj);
}
#if DCHECK_IS_ON()
has_been_disposed_ = true;
#endif
}
AXObjectImpl* AXObjectCacheImpl::Root() {
return GetOrCreate(document_);
}
AXObjectImpl* AXObjectCacheImpl::FocusedImageMapUIElement(
HTMLAreaElement* area_element) {
// Find the corresponding accessibility object for the HTMLAreaElement. This
// should be in the list of children for its corresponding image.
if (!area_element)
return 0;
HTMLImageElement* image_element = area_element->ImageElement();
if (!image_element)
return 0;
AXObjectImpl* ax_layout_image = GetOrCreate(image_element);
if (!ax_layout_image)
return 0;
const AXObjectImpl::AXObjectVector& image_children =
ax_layout_image->Children();
unsigned count = image_children.size();
for (unsigned k = 0; k < count; ++k) {
AXObjectImpl* child = image_children[k];
if (!child->IsImageMapLink())
continue;
if (ToAXImageMapLink(child)->AreaElement() == area_element)
return child;
}
return 0;
}
AXObjectImpl* AXObjectCacheImpl::FocusedObject() {
if (!AccessibilityEnabled())
return nullptr;
Node* focused_node = document_->FocusedElement();
if (!focused_node)
focused_node = document_;
// If it's an image map, get the focused link within the image map.
if (isHTMLAreaElement(focused_node))
return FocusedImageMapUIElement(toHTMLAreaElement(focused_node));
// See if there's a page popup, for example a calendar picker.
Element* adjusted_focused_element = document_->AdjustedFocusedElement();
if (isHTMLInputElement(adjusted_focused_element)) {
if (AXObject* ax_popup =
toHTMLInputElement(adjusted_focused_element)->PopupRootAXObject()) {
if (Element* focused_element_in_popup =
ToAXObjectImpl(ax_popup)->GetDocument()->FocusedElement())
focused_node = focused_element_in_popup;
}
}
AXObjectImpl* obj = GetOrCreate(focused_node);
if (!obj)
return nullptr;
// the HTML element, for example, is focusable but has an AX object that is
// ignored
if (obj->AccessibilityIsIgnored())
obj = obj->ParentObjectUnignored();
return obj;
}
AXObjectImpl* AXObjectCacheImpl::Get(LayoutObject* layout_object) {
if (!layout_object)
return 0;
AXID ax_id = layout_object_mapping_.at(layout_object);
DCHECK(!HashTraits<AXID>::IsDeletedValue(ax_id));
if (!ax_id)
return 0;
return objects_.at(ax_id);
}
// Returns true if |node| is an <option> element and its parent <select>
// is a menu list (not a list box).
static bool IsMenuListOption(Node* node) {
if (!isHTMLOptionElement(node))
return false;
HTMLSelectElement* select = toHTMLOptionElement(node)->OwnerSelectElement();
if (!select)
return false;
LayoutObject* layout_object = select->GetLayoutObject();
return layout_object && layout_object->IsMenuList();
}
AXObjectImpl* AXObjectCacheImpl::Get(Node* node) {
if (!node)
return 0;
// Menu list option and HTML area elements are indexed by DOM node, never by
// layout object.
LayoutObject* layout_object = node->GetLayoutObject();
if (IsMenuListOption(node) || isHTMLAreaElement(node))
layout_object = nullptr;
AXID layout_id = layout_object ? layout_object_mapping_.at(layout_object) : 0;
DCHECK(!HashTraits<AXID>::IsDeletedValue(layout_id));
AXID node_id = node_object_mapping_.at(node);
DCHECK(!HashTraits<AXID>::IsDeletedValue(node_id));
if (layout_object && node_id && !layout_id) {
// This can happen if an AXNodeObject is created for a node that's not
// laid out, but later something changes and it gets a layoutObject (like if
// it's reparented).
Remove(node_id);
return 0;
}
if (layout_id)
return objects_.at(layout_id);
if (!node_id)
return 0;
return objects_.at(node_id);
}
AXObjectImpl* AXObjectCacheImpl::Get(AbstractInlineTextBox* inline_text_box) {
if (!inline_text_box)
return 0;
AXID ax_id = inline_text_box_object_mapping_.at(inline_text_box);
DCHECK(!HashTraits<AXID>::IsDeletedValue(ax_id));
if (!ax_id)
return 0;
return objects_.at(ax_id);
}
// FIXME: This probably belongs on Node.
// FIXME: This should take a const char*, but one caller passes nullAtom.
bool NodeHasRole(Node* node, const String& role) {
if (!node || !node->IsElementNode())
return false;
return EqualIgnoringASCIICase(ToElement(node)->getAttribute(roleAttr), role);
}
AXObjectImpl* AXObjectCacheImpl::CreateFromRenderer(
LayoutObject* layout_object) {
// FIXME: How could layoutObject->node() ever not be an Element?
Node* node = layout_object->GetNode();
// If the node is aria role="list" or the aria role is empty and its a
// ul/ol/dl type (it shouldn't be a list if aria says otherwise).
if (NodeHasRole(node, "list") || NodeHasRole(node, "directory") ||
(NodeHasRole(node, g_null_atom) &&
(isHTMLUListElement(node) || isHTMLOListElement(node) ||
isHTMLDListElement(node))))
return AXList::Create(layout_object, *this);
// aria tables
if (NodeHasRole(node, "grid") || NodeHasRole(node, "treegrid"))
return AXARIAGrid::Create(layout_object, *this);
if (NodeHasRole(node, "row"))
return AXARIAGridRow::Create(layout_object, *this);
if (NodeHasRole(node, "gridcell") || NodeHasRole(node, "columnheader") ||
NodeHasRole(node, "rowheader"))
return AXARIAGridCell::Create(layout_object, *this);
// media controls
if (node && node->IsMediaControlElement())
return AccessibilityMediaControl::Create(layout_object, *this);
if (isHTMLOptionElement(node))
return AXListBoxOption::Create(layout_object, *this);
if (isHTMLInputElement(node) &&
toHTMLInputElement(node)->type() == InputTypeNames::radio)
return AXRadioInput::Create(layout_object, *this);
if (layout_object->IsSVGRoot())
return AXSVGRoot::Create(layout_object, *this);
if (layout_object->IsBoxModelObject()) {
LayoutBoxModelObject* css_box = ToLayoutBoxModelObject(layout_object);
if (css_box->IsListBox())
return AXListBox::Create(ToLayoutListBox(css_box), *this);
if (css_box->IsMenuList())
return AXMenuList::Create(ToLayoutMenuList(css_box), *this);
// standard tables
if (css_box->IsTable())
return AXTable::Create(ToLayoutTable(css_box), *this);
if (css_box->IsTableRow())
return AXTableRow::Create(ToLayoutTableRow(css_box), *this);
if (css_box->IsTableCell())
return AXTableCell::Create(ToLayoutTableCell(css_box), *this);
// progress bar
if (css_box->IsProgress())
return AXProgressIndicator::Create(ToLayoutProgress(css_box), *this);
// input type=range
if (css_box->IsSlider())
return AXSlider::Create(ToLayoutSlider(css_box), *this);
}
return AXLayoutObject::Create(layout_object, *this);
}
AXObjectImpl* AXObjectCacheImpl::CreateFromNode(Node* node) {
if (IsMenuListOption(node))
return AXMenuListOption::Create(toHTMLOptionElement(node), *this);
if (isHTMLAreaElement(node))
return AXImageMapLink::Create(toHTMLAreaElement(node), *this);
return AXNodeObject::Create(node, *this);
}
AXObjectImpl* AXObjectCacheImpl::CreateFromInlineTextBox(
AbstractInlineTextBox* inline_text_box) {
return AXInlineTextBox::Create(inline_text_box, *this);
}
AXObjectImpl* AXObjectCacheImpl::GetOrCreate(Node* node) {
if (!node)
return 0;
if (AXObjectImpl* obj = Get(node))
return obj;
// If the node has a layout object, prefer using that as the primary key for
// the AXObjectImpl, with the exception of an HTMLAreaElement, which is
// created based on its node.
if (node->GetLayoutObject() && !isHTMLAreaElement(node))
return GetOrCreate(node->GetLayoutObject());
if (!node->parentElement())
return 0;
if (isHTMLHeadElement(node))
return 0;
AXObjectImpl* new_obj = CreateFromNode(node);
// Will crash later if we have two objects for the same node.
DCHECK(!Get(node));
const AXID ax_id = GetOrCreateAXID(new_obj);
node_object_mapping_.Set(node, ax_id);
new_obj->Init();
new_obj->SetLastKnownIsIgnoredValue(new_obj->AccessibilityIsIgnored());
if (node->IsElementNode())
UpdateTreeIfElementIdIsAriaOwned(ToElement(node));
return new_obj;
}
AXObjectImpl* AXObjectCacheImpl::GetOrCreate(LayoutObject* layout_object) {
if (!layout_object)
return 0;
if (AXObjectImpl* obj = Get(layout_object))
return obj;
AXObjectImpl* new_obj = CreateFromRenderer(layout_object);
// Will crash later if we have two objects for the same layoutObject.
DCHECK(!Get(layout_object));
const AXID axid = GetOrCreateAXID(new_obj);
layout_object_mapping_.Set(layout_object, axid);
new_obj->Init();
new_obj->SetLastKnownIsIgnoredValue(new_obj->AccessibilityIsIgnored());
return new_obj;
}
AXObjectImpl* AXObjectCacheImpl::GetOrCreate(
AbstractInlineTextBox* inline_text_box) {
if (!inline_text_box)
return 0;
if (AXObjectImpl* obj = Get(inline_text_box))
return obj;
AXObjectImpl* new_obj = CreateFromInlineTextBox(inline_text_box);
// Will crash later if we have two objects for the same inlineTextBox.
DCHECK(!Get(inline_text_box));
const AXID axid = GetOrCreateAXID(new_obj);
inline_text_box_object_mapping_.Set(inline_text_box, axid);
new_obj->Init();
new_obj->SetLastKnownIsIgnoredValue(new_obj->AccessibilityIsIgnored());
return new_obj;
}
AXObjectImpl* AXObjectCacheImpl::GetOrCreate(AccessibilityRole role) {
AXObjectImpl* obj = nullptr;
// will be filled in...
switch (role) {
case kColumnRole:
obj = AXTableColumn::Create(*this);
break;
case kTableHeaderContainerRole:
obj = AXTableHeaderContainer::Create(*this);
break;
case kSliderThumbRole:
obj = AXSliderThumb::Create(*this);
break;
case kMenuListPopupRole:
obj = AXMenuListPopup::Create(*this);
break;
case kSpinButtonRole:
obj = AXSpinButton::Create(*this);
break;
case kSpinButtonPartRole:
obj = AXSpinButtonPart::Create(*this);
break;
default:
obj = nullptr;
}
if (!obj)
return 0;
GetOrCreateAXID(obj);
obj->Init();
return obj;
}
void AXObjectCacheImpl::Remove(AXID ax_id) {
if (!ax_id)
return;
// first fetch object to operate some cleanup functions on it
AXObjectImpl* obj = objects_.at(ax_id);
if (!obj)
return;
obj->Detach();
RemoveAXID(obj);
// finally remove the object
if (!objects_.Take(ax_id))
return;
DCHECK_GE(objects_.size(), ids_in_use_.size());
}
void AXObjectCacheImpl::Remove(LayoutObject* layout_object) {
if (!layout_object)
return;
AXID ax_id = layout_object_mapping_.at(layout_object);
Remove(ax_id);
layout_object_mapping_.erase(layout_object);
}
void AXObjectCacheImpl::Remove(Node* node) {
if (!node)
return;
// This is all safe even if we didn't have a mapping.
AXID ax_id = node_object_mapping_.at(node);
Remove(ax_id);
node_object_mapping_.erase(node);
if (node->GetLayoutObject()) {
Remove(node->GetLayoutObject());
return;
}
}
void AXObjectCacheImpl::Remove(AbstractInlineTextBox* inline_text_box) {
if (!inline_text_box)
return;
AXID ax_id = inline_text_box_object_mapping_.at(inline_text_box);
Remove(ax_id);
inline_text_box_object_mapping_.erase(inline_text_box);
}
AXID AXObjectCacheImpl::GenerateAXID() const {
static AXID last_used_id = 0;
// Generate a new ID.
AXID obj_id = last_used_id;
do {
++obj_id;
} while (!obj_id || HashTraits<AXID>::IsDeletedValue(obj_id) ||
ids_in_use_.Contains(obj_id));
last_used_id = obj_id;
return obj_id;
}
AXID AXObjectCacheImpl::GetOrCreateAXID(AXObjectImpl* obj) {
// check for already-assigned ID
const AXID existing_axid = obj->AxObjectID();
if (existing_axid) {
DCHECK(ids_in_use_.Contains(existing_axid));
return existing_axid;
}
const AXID new_axid = GenerateAXID();
ids_in_use_.insert(new_axid);
obj->SetAXObjectID(new_axid);
objects_.Set(new_axid, obj);
return new_axid;
}
void AXObjectCacheImpl::RemoveAXID(AXObjectImpl* object) {
if (!object)
return;
AXID obj_id = object->AxObjectID();
if (!obj_id)
return;
DCHECK(!HashTraits<AXID>::IsDeletedValue(obj_id));
DCHECK(ids_in_use_.Contains(obj_id));
object->SetAXObjectID(0);
ids_in_use_.erase(obj_id);
if (aria_owner_to_children_mapping_.Contains(obj_id)) {
Vector<AXID> child_axi_ds = aria_owner_to_children_mapping_.at(obj_id);
for (size_t i = 0; i < child_axi_ds.size(); ++i)
aria_owned_child_to_owner_mapping_.erase(child_axi_ds[i]);
aria_owner_to_children_mapping_.erase(obj_id);
}
aria_owned_child_to_owner_mapping_.erase(obj_id);
aria_owned_child_to_real_parent_mapping_.erase(obj_id);
aria_owner_to_ids_mapping_.erase(obj_id);
}
void AXObjectCacheImpl::SelectionChanged(Node* node) {
// Find the nearest ancestor that already has an accessibility object, since
// we might be in the middle of a layout.
while (node) {
if (AXObjectImpl* obj = Get(node)) {
obj->SelectionChanged();
return;
}
node = node->parentNode();
}
}
void AXObjectCacheImpl::TextChanged(Node* node) {
TextChanged(GetOrCreate(node));
}
void AXObjectCacheImpl::TextChanged(LayoutObject* layout_object) {
TextChanged(GetOrCreate(layout_object));
}
void AXObjectCacheImpl::TextChanged(AXObjectImpl* obj) {
if (!obj)
return;
bool parent_already_exists = obj->ParentObjectIfExists();
obj->TextChanged();
PostNotification(obj, AXObjectCacheImpl::kAXTextChanged);
if (parent_already_exists)
obj->NotifyIfIgnoredValueChanged();
}
void AXObjectCacheImpl::UpdateCacheAfterNodeIsAttached(Node* node) {
// Calling get() will update the AX object if we had an AXNodeObject but now
// we need an AXLayoutObject, because it was reparented to a location outside
// of a canvas.
Get(node);
if (node->IsElementNode())
UpdateTreeIfElementIdIsAriaOwned(ToElement(node));
}
void AXObjectCacheImpl::ChildrenChanged(Node* node) {
ChildrenChanged(Get(node));
}
void AXObjectCacheImpl::ChildrenChanged(LayoutObject* layout_object) {
ChildrenChanged(Get(layout_object));
}
void AXObjectCacheImpl::ChildrenChanged(AXObjectImpl* obj) {
if (!obj)
return;
obj->ChildrenChanged();
}
void AXObjectCacheImpl::NotificationPostTimerFired(TimerBase*) {
notification_post_timer_.Stop();
unsigned i = 0, count = notifications_to_post_.size();
for (i = 0; i < count; ++i) {
AXObjectImpl* obj = notifications_to_post_[i].first;
if (!obj->AxObjectID())
continue;
if (obj->IsDetached())
continue;
#if DCHECK_IS_ON()
// Make sure none of the layout views are in the process of being layed out.
// Notifications should only be sent after the layoutObject has finished
if (obj->IsAXLayoutObject()) {
AXLayoutObject* layout_obj = ToAXLayoutObject(obj);
LayoutObject* layout_object = layout_obj->GetLayoutObject();
if (layout_object && layout_object->View())
DCHECK(!layout_object->View()->GetLayoutState());
}
#endif
AXNotification notification = notifications_to_post_[i].second;
PostPlatformNotification(obj, notification);
if (notification == kAXChildrenChanged && obj->ParentObjectIfExists() &&
obj->LastKnownIsIgnoredValue() != obj->AccessibilityIsIgnored())
ChildrenChanged(obj->ParentObject());
}
notifications_to_post_.clear();
}
void AXObjectCacheImpl::PostNotification(LayoutObject* layout_object,
AXNotification notification) {
if (!layout_object)
return;
PostNotification(Get(layout_object), notification);
}
void AXObjectCacheImpl::PostNotification(Node* node,
AXNotification notification) {
if (!node)
return;
PostNotification(Get(node), notification);
}
void AXObjectCacheImpl::PostNotification(AXObjectImpl* object,
AXNotification notification) {
if (!object)
return;
modification_count_++;
notifications_to_post_.push_back(std::make_pair(object, notification));
if (!notification_post_timer_.IsActive())
notification_post_timer_.StartOneShot(0, BLINK_FROM_HERE);
}
bool AXObjectCacheImpl::IsAriaOwned(const AXObjectImpl* child) const {
return aria_owned_child_to_owner_mapping_.Contains(child->AxObjectID());
}
AXObjectImpl* AXObjectCacheImpl::GetAriaOwnedParent(
const AXObjectImpl* child) const {
return ObjectFromAXID(
aria_owned_child_to_owner_mapping_.at(child->AxObjectID()));
}
void AXObjectCacheImpl::UpdateAriaOwns(
const AXObjectImpl* owner,
const Vector<String>& id_vector,
HeapVector<Member<AXObjectImpl>>& owned_children) {
//
// Update the map from the AXID of this element to the ids of the owned
// children, and the reverse map from ids to possible AXID owners.
//
HashSet<String> current_ids =
aria_owner_to_ids_mapping_.at(owner->AxObjectID());
HashSet<String> new_ids;
bool ids_changed = false;
for (const String& id : id_vector) {
new_ids.insert(id);
if (!current_ids.Contains(id)) {
ids_changed = true;
HashSet<AXID>* owners = id_to_aria_owners_mapping_.at(id);
if (!owners) {
owners = new HashSet<AXID>();
id_to_aria_owners_mapping_.Set(id, WTF::WrapUnique(owners));
}
owners->insert(owner->AxObjectID());
}
}
for (const String& id : current_ids) {
if (!new_ids.Contains(id)) {
ids_changed = true;
HashSet<AXID>* owners = id_to_aria_owners_mapping_.at(id);
if (owners) {
owners->erase(owner->AxObjectID());
if (owners->IsEmpty())
id_to_aria_owners_mapping_.erase(id);
}
}
}
if (ids_changed)
aria_owner_to_ids_mapping_.Set(owner->AxObjectID(), new_ids);
//
// Now figure out the ids that actually correspond to children that exist and
// that we can legally own (not cyclical, not already owned, etc.) and update
// the maps and |ownedChildren| based on that.
//
// Figure out the children that are owned by this object and are in the tree.
TreeScope& scope = owner->GetNode()->GetTreeScope();
Vector<AXID> new_child_axi_ds;
for (const String& id_name : id_vector) {
Element* element = scope.getElementById(AtomicString(id_name));
if (!element)
continue;
AXObjectImpl* child = GetOrCreate(element);
if (!child)
continue;
// If this child is already aria-owned by a different owner, continue.
// It's an author error if this happens and we don't worry about which of
// the two owners wins ownership of the child, as long as only one of them
// does.
if (IsAriaOwned(child) && GetAriaOwnedParent(child) != owner)
continue;
// You can't own yourself!
if (child == owner)
continue;
// Walk up the parents of the owner object, make sure that this child
// doesn't appear there, as that would create a cycle.
bool found_cycle = false;
for (AXObjectImpl* parent = owner->ParentObject(); parent && !found_cycle;
parent = parent->ParentObject()) {
if (parent == child)
found_cycle = true;
}
if (found_cycle)
continue;
new_child_axi_ds.push_back(child->AxObjectID());
owned_children.push_back(child);
}
// Compare this to the current list of owned children, and exit early if there
// are no changes.
Vector<AXID> current_child_axi_ds =
aria_owner_to_children_mapping_.at(owner->AxObjectID());
bool same = true;
if (current_child_axi_ds.size() != new_child_axi_ds.size()) {
same = false;
} else {
for (size_t i = 0; i < current_child_axi_ds.size() && same; ++i) {
if (current_child_axi_ds[i] != new_child_axi_ds[i])
same = false;
}
}
if (same)
return;
// The list of owned children has changed. Even if they were just reordered,
// to be safe and handle all cases we remove all of the current owned children
// and add the new list of owned children.
for (size_t i = 0; i < current_child_axi_ds.size(); ++i) {
// Find the AXObjectImpl for the child that this owner no longer owns.
AXID removed_child_id = current_child_axi_ds[i];
AXObjectImpl* removed_child = ObjectFromAXID(removed_child_id);
// It's possible that this child has already been owned by some other owner,
// in which case we don't need to do anything.
if (removed_child && GetAriaOwnedParent(removed_child) != owner)
continue;
// Remove it from the child -> owner mapping so it's not owned by this owner
// anymore.
aria_owned_child_to_owner_mapping_.erase(removed_child_id);
if (removed_child) {
// If the child still exists, find its "real" parent, and reparent it back
// to its real parent in the tree by detaching it from its current parent
// and calling childrenChanged on its real parent.
removed_child->DetachFromParent();
AXID real_parent_id =
aria_owned_child_to_real_parent_mapping_.at(removed_child_id);
AXObjectImpl* real_parent = ObjectFromAXID(real_parent_id);
ChildrenChanged(real_parent);
}
// Remove the child -> original parent mapping too since this object has now
// been reparented back to its original parent.
aria_owned_child_to_real_parent_mapping_.erase(removed_child_id);
}
for (size_t i = 0; i < new_child_axi_ds.size(); ++i) {
// Find the AXObjectImpl for the child that will now be a child of this
// owner.
AXID added_child_id = new_child_axi_ds[i];
AXObjectImpl* added_child = ObjectFromAXID(added_child_id);
// Add this child to the mapping from child to owner.
aria_owned_child_to_owner_mapping_.Set(added_child_id, owner->AxObjectID());
// Add its parent object to a mapping from child to real parent. If later
// this owner doesn't own this child anymore, we need to return it to its
// original parent.
AXObjectImpl* original_parent = added_child->ParentObject();
aria_owned_child_to_real_parent_mapping_.Set(added_child_id,
original_parent->AxObjectID());
// Now detach the object from its original parent and call childrenChanged
// on the original parent so that it can recompute its list of children.
added_child->DetachFromParent();
ChildrenChanged(original_parent);
}
// Finally, update the mapping from the owner to the list of child IDs.
aria_owner_to_children_mapping_.Set(owner->AxObjectID(), new_child_axi_ds);
}
void AXObjectCacheImpl::UpdateTreeIfElementIdIsAriaOwned(Element* element) {
if (!element->HasID())
return;
String id = element->GetIdAttribute();
HashSet<AXID>* owners = id_to_aria_owners_mapping_.at(id);
if (!owners)
return;
AXObjectImpl* ax_element = GetOrCreate(element);
if (!ax_element)
return;
// If it's already owned, call childrenChanged on the owner to make sure it's
// still an owner.
if (IsAriaOwned(ax_element)) {
AXObjectImpl* owned_parent = GetAriaOwnedParent(ax_element);
DCHECK(owned_parent);
ChildrenChanged(owned_parent);
return;
}
// If it's not already owned, check the possible owners based on our mapping
// from ids to elements that have that id listed in their aria-owns attribute.
for (const auto& ax_id : *owners) {
AXObjectImpl* owner = ObjectFromAXID(ax_id);
if (owner)
ChildrenChanged(owner);
}
}
void AXObjectCacheImpl::CheckedStateChanged(Node* node) {
PostNotification(node, AXObjectCacheImpl::kAXCheckedStateChanged);
}
void AXObjectCacheImpl::ListboxOptionStateChanged(HTMLOptionElement* option) {
PostNotification(option, kAXCheckedStateChanged);
}
void AXObjectCacheImpl::ListboxSelectedChildrenChanged(
HTMLSelectElement* select) {
PostNotification(select, kAXSelectedChildrenChanged);
}
void AXObjectCacheImpl::ListboxActiveIndexChanged(HTMLSelectElement* select) {
AXObjectImpl* obj = Get(select);
if (!obj || !obj->IsAXListBox())
return;
ToAXListBox(obj)->ActiveIndexChanged();
}
void AXObjectCacheImpl::RadiobuttonRemovedFromGroup(
HTMLInputElement* group_member) {
AXObjectImpl* obj = Get(group_member);
if (!obj || !obj->IsAXRadioInput())
return;
// The 'posInSet' and 'setSize' attributes should be updated from the first
// node, as the removed node is already detached from tree.
HTMLInputElement* first_radio =
ToAXRadioInput(obj)->FindFirstRadioButtonInGroup(group_member);
AXObjectImpl* first_obj = Get(first_radio);
if (!first_obj || !first_obj->IsAXRadioInput())
return;
ToAXRadioInput(first_obj)->UpdatePosAndSetSize(1);
PostNotification(first_obj, kAXAriaAttributeChanged);
ToAXRadioInput(first_obj)->RequestUpdateToNextNode(true);
}
void AXObjectCacheImpl::HandleLayoutComplete(LayoutObject* layout_object) {
if (!layout_object)
return;
modification_count_++;
// Create the AXObjectImpl if it didn't yet exist - that's always safe at the
// end of a layout, and it allows an AX notification to be sent when a page
// has its first layout, rather than when the document first loads.
if (AXObjectImpl* obj = GetOrCreate(layout_object))
PostNotification(obj, kAXLayoutComplete);
}
void AXObjectCacheImpl::HandleClicked(Node* node) {
if (AXObjectImpl* obj = GetOrCreate(node))
PostNotification(obj, kAXClicked);
}
void AXObjectCacheImpl::HandleAriaExpandedChange(Node* node) {
if (AXObjectImpl* obj = GetOrCreate(node))
obj->HandleAriaExpandedChanged();
}
void AXObjectCacheImpl::HandleAriaSelectedChanged(Node* node) {
AXObjectImpl* obj = Get(node);
if (!obj)
return;
PostNotification(obj, kAXCheckedStateChanged);
AXObjectImpl* listbox = obj->ParentObjectUnignored();
if (listbox && listbox->RoleValue() == kListBoxRole)
PostNotification(listbox, kAXSelectedChildrenChanged);
}
void AXObjectCacheImpl::HandleActiveDescendantChanged(Node* node) {
// Changing the active descendant should trigger recomputing all
// cached values even if it doesn't result in a notification, because
// it can affect what's focusable or not.
modification_count_++;
if (AXObjectImpl* obj = GetOrCreate(node))
obj->HandleActiveDescendantChanged();
}
void AXObjectCacheImpl::HandleAriaRoleChanged(Node* node) {
if (AXObjectImpl* obj = GetOrCreate(node)) {
obj->UpdateAccessibilityRole();
modification_count_++;
obj->NotifyIfIgnoredValueChanged();
}
}
void AXObjectCacheImpl::HandleAttributeChanged(const QualifiedName& attr_name,
Element* element) {
if (attr_name == roleAttr)
HandleAriaRoleChanged(element);
else if (attr_name == altAttr || attr_name == titleAttr)
TextChanged(element);
else if (attr_name == forAttr && isHTMLLabelElement(*element))
LabelChanged(element);
else if (attr_name == idAttr)
UpdateTreeIfElementIdIsAriaOwned(element);
if (!attr_name.LocalName().StartsWith("aria-"))
return;
if (attr_name == aria_activedescendantAttr)
HandleActiveDescendantChanged(element);
else if (attr_name == aria_valuenowAttr || attr_name == aria_valuetextAttr)
PostNotification(element, AXObjectCacheImpl::kAXValueChanged);
else if (attr_name == aria_labelAttr || attr_name == aria_labeledbyAttr ||
attr_name == aria_labelledbyAttr)
TextChanged(element);
else if (attr_name == aria_checkedAttr)
CheckedStateChanged(element);
else if (attr_name == aria_selectedAttr)
HandleAriaSelectedChanged(element);
else if (attr_name == aria_expandedAttr)
HandleAriaExpandedChange(element);
else if (attr_name == aria_hiddenAttr)
ChildrenChanged(element->parentNode());
else if (attr_name == aria_invalidAttr)
PostNotification(element, AXObjectCacheImpl::kAXInvalidStatusChanged);
else if (attr_name == aria_ownsAttr)
ChildrenChanged(element);
else
PostNotification(element, AXObjectCacheImpl::kAXAriaAttributeChanged);
}
void AXObjectCacheImpl::LabelChanged(Element* element) {
TextChanged(toHTMLLabelElement(element)->control());
}
void AXObjectCacheImpl::InlineTextBoxesUpdated(
LineLayoutItem line_layout_item) {
if (!InlineTextBoxAccessibilityEnabled())
return;
LayoutObject* layout_object =
LineLayoutAPIShim::LayoutObjectFrom(line_layout_item);
// Only update if the accessibility object already exists and it's
// not already marked as dirty.
if (AXObjectImpl* obj = Get(layout_object)) {
if (!obj->NeedsToUpdateChildren()) {
obj->SetNeedsToUpdateChildren();
PostNotification(layout_object, kAXChildrenChanged);
}
}
}
Settings* AXObjectCacheImpl::GetSettings() {
return document_->GetSettings();
}
bool AXObjectCacheImpl::AccessibilityEnabled() {
Settings* settings = this->GetSettings();
if (!settings)
return false;
return settings->GetAccessibilityEnabled();
}
bool AXObjectCacheImpl::InlineTextBoxAccessibilityEnabled() {
Settings* settings = this->GetSettings();
if (!settings)
return false;
return settings->GetInlineTextBoxAccessibilityEnabled();
}
const Element* AXObjectCacheImpl::RootAXEditableElement(const Node* node) {
const Element* result = RootEditableElement(*node);
const Element* element =
node->IsElementNode() ? ToElement(node) : node->parentElement();
for (; element; element = element->parentElement()) {
if (NodeIsTextControl(element))
result = element;
}
return result;
}
AXObjectImpl* AXObjectCacheImpl::FirstAccessibleObjectFromNode(
const Node* node) {
if (!node)
return 0;
AXObjectImpl* accessible_object = GetOrCreate(node->GetLayoutObject());
while (accessible_object && accessible_object->AccessibilityIsIgnored()) {
node = NodeTraversal::Next(*node);
while (node && !node->GetLayoutObject())
node = NodeTraversal::NextSkippingChildren(*node);
if (!node)
return 0;
accessible_object = GetOrCreate(node->GetLayoutObject());
}
return accessible_object;
}
bool AXObjectCacheImpl::NodeIsTextControl(const Node* node) {
if (!node)
return false;
const AXObjectImpl* ax_object = GetOrCreate(const_cast<Node*>(node));
return ax_object && ax_object->IsTextControl();
}
bool IsNodeAriaVisible(Node* node) {
if (!node)
return false;
if (!node->IsElementNode())
return false;
bool is_null = true;
bool hidden = AccessibleNode::GetPropertyOrARIAAttribute(
ToElement(node), AOMBooleanProperty::kHidden, is_null);
return !is_null && !hidden;
}
void AXObjectCacheImpl::PostPlatformNotification(AXObjectImpl* obj,
AXNotification notification) {
if (!obj || !obj->GetDocument() || !obj->DocumentFrameView() ||
!obj->DocumentFrameView()->GetFrame().GetPage())
return;
ChromeClient& client =
obj->GetDocument()->AxObjectCacheOwner().GetPage()->GetChromeClient();
client.PostAccessibilityNotification(obj, notification);
}
void AXObjectCacheImpl::HandleFocusedUIElementChanged(Node* old_focused_node,
Node* new_focused_node) {
if (!new_focused_node)
return;
Page* page = new_focused_node->GetDocument().GetPage();
if (!page)
return;
AXObjectImpl* focused_object = this->FocusedObject();
if (!focused_object)
return;
AXObjectImpl* old_focused_object = Get(old_focused_node);
PostPlatformNotification(old_focused_object, kAXBlur);
PostPlatformNotification(focused_object, kAXFocusedUIElementChanged);
}
void AXObjectCacheImpl::HandleInitialFocus() {
PostNotification(document_, AXObjectCache::kAXFocusedUIElementChanged);
}
void AXObjectCacheImpl::HandleEditableTextContentChanged(Node* node) {
AXObjectImpl* obj = Get(node);
while (obj && !obj->IsNativeTextControl() && !obj->IsNonNativeTextControl())
obj = obj->ParentObject();
PostNotification(obj, AXObjectCache::kAXValueChanged);
}
void AXObjectCacheImpl::HandleTextFormControlChanged(Node* node) {
HandleEditableTextContentChanged(node);
}
void AXObjectCacheImpl::HandleValueChanged(Node* node) {
PostNotification(node, AXObjectCache::kAXValueChanged);
}
void AXObjectCacheImpl::HandleUpdateActiveMenuOption(LayoutMenuList* menu_list,
int option_index) {
AXObjectImpl* obj = Get(menu_list);
if (!obj || !obj->IsMenuList())
return;
ToAXMenuList(obj)->DidUpdateActiveOption(option_index);
}
void AXObjectCacheImpl::DidShowMenuListPopup(LayoutMenuList* menu_list) {
AXObjectImpl* obj = Get(menu_list);
if (!obj || !obj->IsMenuList())
return;
ToAXMenuList(obj)->DidShowPopup();
}
void AXObjectCacheImpl::DidHideMenuListPopup(LayoutMenuList* menu_list) {
AXObjectImpl* obj = Get(menu_list);
if (!obj || !obj->IsMenuList())
return;
ToAXMenuList(obj)->DidHidePopup();
}
void AXObjectCacheImpl::HandleLoadComplete(Document* document) {
PostNotification(GetOrCreate(document), AXObjectCache::kAXLoadComplete);
}
void AXObjectCacheImpl::HandleLayoutComplete(Document* document) {
PostNotification(GetOrCreate(document), AXObjectCache::kAXLayoutComplete);
}
void AXObjectCacheImpl::HandleScrolledToAnchor(const Node* anchor_node) {
if (!anchor_node)
return;
AXObjectImpl* obj = GetOrCreate(anchor_node->GetLayoutObject());
if (!obj)
return;
if (obj->AccessibilityIsIgnored())
obj = obj->ParentObjectUnignored();
PostPlatformNotification(obj, kAXScrolledToAnchor);
}
void AXObjectCacheImpl::HandleScrollPositionChanged(FrameView* frame_view) {
AXObjectImpl* target_ax_object =
GetOrCreate(frame_view->GetFrame().GetDocument());
PostPlatformNotification(target_ax_object, kAXScrollPositionChanged);
}
void AXObjectCacheImpl::HandleScrollPositionChanged(
LayoutObject* layout_object) {
PostPlatformNotification(GetOrCreate(layout_object),
kAXScrollPositionChanged);
}
const AtomicString& AXObjectCacheImpl::ComputedRoleForNode(Node* node) {
AXObjectImpl* obj = GetOrCreate(node);
if (!obj)
return AXObjectImpl::RoleName(kUnknownRole);
return AXObjectImpl::RoleName(obj->RoleValue());
}
String AXObjectCacheImpl::ComputedNameForNode(Node* node) {
AXObjectImpl* obj = GetOrCreate(node);
if (!obj)
return "";
return obj->ComputedName();
}
void AXObjectCacheImpl::OnTouchAccessibilityHover(const IntPoint& location) {
AXObjectImpl* hit = Root()->AccessibilityHitTest(location);
if (hit) {
// Ignore events on a frame or plug-in, because the touch events
// will be re-targeted there and we don't want to fire duplicate
// accessibility events.
if (hit->GetLayoutObject() && hit->GetLayoutObject()->IsLayoutPart())
return;
PostPlatformNotification(hit, kAXHover);
}
}
void AXObjectCacheImpl::SetCanvasObjectBounds(HTMLCanvasElement* canvas,
Element* element,
const LayoutRect& rect) {
AXObjectImpl* obj = GetOrCreate(element);
if (!obj)
return;
AXObjectImpl* ax_canvas = GetOrCreate(canvas);
if (!ax_canvas)
return;
obj->SetElementRect(rect, ax_canvas);
}
DEFINE_TRACE(AXObjectCacheImpl) {
visitor->Trace(document_);
visitor->Trace(node_object_mapping_);
visitor->Trace(objects_);
visitor->Trace(notifications_to_post_);
AXObjectCache::Trace(visitor);
}
} // namespace blink