blob: 70ac5ffad5d5ffda8a71e84f9e130a3f728caa10 [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/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 "wtf/PassRefPtr.h"
#include "wtf/PtrUtil.h"
namespace blink {
using namespace HTMLNames;
// static
AXObjectCache* AXObjectCacheImpl::create(Document& document) {
return new AXObjectCacheImpl(document);
}
AXObjectCacheImpl::AXObjectCacheImpl(Document& document)
: m_document(document),
m_modificationCount(0),
m_notificationPostTimer(
TaskRunnerHelper::get(TaskType::UnspecedTimer, &document),
this,
&AXObjectCacheImpl::notificationPostTimerFired) {}
AXObjectCacheImpl::~AXObjectCacheImpl() {
#if DCHECK_IS_ON()
DCHECK(m_hasBeenDisposed);
#endif
}
void AXObjectCacheImpl::dispose() {
m_notificationPostTimer.stop();
for (auto& entry : m_objects) {
AXObject* obj = entry.value;
obj->detach();
removeAXID(obj);
}
#if DCHECK_IS_ON()
m_hasBeenDisposed = true;
#endif
}
AXObject* AXObjectCacheImpl::root() {
return getOrCreate(m_document);
}
AXObject* AXObjectCacheImpl::focusedImageMapUIElement(
HTMLAreaElement* areaElement) {
// Find the corresponding accessibility object for the HTMLAreaElement. This
// should be in the list of children for its corresponding image.
if (!areaElement)
return 0;
HTMLImageElement* imageElement = areaElement->imageElement();
if (!imageElement)
return 0;
AXObject* axLayoutImage = getOrCreate(imageElement);
if (!axLayoutImage)
return 0;
const AXObject::AXObjectVector& imageChildren = axLayoutImage->children();
unsigned count = imageChildren.size();
for (unsigned k = 0; k < count; ++k) {
AXObject* child = imageChildren[k];
if (!child->isImageMapLink())
continue;
if (toAXImageMapLink(child)->areaElement() == areaElement)
return child;
}
return 0;
}
AXObject* AXObjectCacheImpl::focusedObject() {
if (!accessibilityEnabled())
return nullptr;
Node* focusedNode = m_document->focusedElement();
if (!focusedNode)
focusedNode = m_document;
// If it's an image map, get the focused link within the image map.
if (isHTMLAreaElement(focusedNode))
return focusedImageMapUIElement(toHTMLAreaElement(focusedNode));
// See if there's a page popup, for example a calendar picker.
Element* adjustedFocusedElement = m_document->adjustedFocusedElement();
if (isHTMLInputElement(adjustedFocusedElement)) {
if (AXObject* axPopup =
toHTMLInputElement(adjustedFocusedElement)->popupRootAXObject()) {
if (Element* focusedElementInPopup =
axPopup->getDocument()->focusedElement())
focusedNode = focusedElementInPopup;
}
}
AXObject* obj = getOrCreate(focusedNode);
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;
}
AXObject* AXObjectCacheImpl::get(LayoutObject* layoutObject) {
if (!layoutObject)
return 0;
AXID axID = m_layoutObjectMapping.at(layoutObject);
DCHECK(!HashTraits<AXID>::isDeletedValue(axID));
if (!axID)
return 0;
return m_objects.at(axID);
}
// 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* layoutObject = select->layoutObject();
return layoutObject && layoutObject->isMenuList();
}
AXObject* 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* layoutObject = node->layoutObject();
if (isMenuListOption(node) || isHTMLAreaElement(node))
layoutObject = nullptr;
AXID layoutID = layoutObject ? m_layoutObjectMapping.at(layoutObject) : 0;
DCHECK(!HashTraits<AXID>::isDeletedValue(layoutID));
AXID nodeID = m_nodeObjectMapping.at(node);
DCHECK(!HashTraits<AXID>::isDeletedValue(nodeID));
if (layoutObject && nodeID && !layoutID) {
// 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(nodeID);
return 0;
}
if (layoutID)
return m_objects.at(layoutID);
if (!nodeID)
return 0;
return m_objects.at(nodeID);
}
AXObject* AXObjectCacheImpl::get(AbstractInlineTextBox* inlineTextBox) {
if (!inlineTextBox)
return 0;
AXID axID = m_inlineTextBoxObjectMapping.at(inlineTextBox);
DCHECK(!HashTraits<AXID>::isDeletedValue(axID));
if (!axID)
return 0;
return m_objects.at(axID);
}
// 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 equalIgnoringCase(toElement(node)->getAttribute(roleAttr), role);
}
AXObject* AXObjectCacheImpl::createFromRenderer(LayoutObject* layoutObject) {
// FIXME: How could layoutObject->node() ever not be an Element?
Node* node = layoutObject->node();
// 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, nullAtom) &&
(isHTMLUListElement(node) || isHTMLOListElement(node) ||
isHTMLDListElement(node))))
return AXList::create(layoutObject, *this);
// aria tables
if (nodeHasRole(node, "grid") || nodeHasRole(node, "treegrid"))
return AXARIAGrid::create(layoutObject, *this);
if (nodeHasRole(node, "row"))
return AXARIAGridRow::create(layoutObject, *this);
if (nodeHasRole(node, "gridcell") || nodeHasRole(node, "columnheader") ||
nodeHasRole(node, "rowheader"))
return AXARIAGridCell::create(layoutObject, *this);
// media controls
if (node && node->isMediaControlElement())
return AccessibilityMediaControl::create(layoutObject, *this);
if (isHTMLOptionElement(node))
return AXListBoxOption::create(layoutObject, *this);
if (isHTMLInputElement(node) &&
toHTMLInputElement(node)->type() == InputTypeNames::radio)
return AXRadioInput::create(layoutObject, *this);
if (layoutObject->isSVGRoot())
return AXSVGRoot::create(layoutObject, *this);
if (layoutObject->isBoxModelObject()) {
LayoutBoxModelObject* cssBox = toLayoutBoxModelObject(layoutObject);
if (cssBox->isListBox())
return AXListBox::create(toLayoutListBox(cssBox), *this);
if (cssBox->isMenuList())
return AXMenuList::create(toLayoutMenuList(cssBox), *this);
// standard tables
if (cssBox->isTable())
return AXTable::create(toLayoutTable(cssBox), *this);
if (cssBox->isTableRow())
return AXTableRow::create(toLayoutTableRow(cssBox), *this);
if (cssBox->isTableCell())
return AXTableCell::create(toLayoutTableCell(cssBox), *this);
// progress bar
if (cssBox->isProgress())
return AXProgressIndicator::create(toLayoutProgress(cssBox), *this);
// input type=range
if (cssBox->isSlider())
return AXSlider::create(toLayoutSlider(cssBox), *this);
}
return AXLayoutObject::create(layoutObject, *this);
}
AXObject* 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);
}
AXObject* AXObjectCacheImpl::createFromInlineTextBox(
AbstractInlineTextBox* inlineTextBox) {
return AXInlineTextBox::create(inlineTextBox, *this);
}
AXObject* AXObjectCacheImpl::getOrCreate(Node* node) {
if (!node)
return 0;
if (AXObject* obj = get(node))
return obj;
// If the node has a layout object, prefer using that as the primary key for
// the AXObject, with the exception of an HTMLAreaElement, which is created
// based on its node.
if (node->layoutObject() && !isHTMLAreaElement(node))
return getOrCreate(node->layoutObject());
if (!node->parentElement())
return 0;
if (isHTMLHeadElement(node))
return 0;
AXObject* newObj = createFromNode(node);
// Will crash later if we have two objects for the same node.
DCHECK(!get(node));
const AXID axID = getOrCreateAXID(newObj);
m_nodeObjectMapping.set(node, axID);
newObj->init();
newObj->setLastKnownIsIgnoredValue(newObj->accessibilityIsIgnored());
if (node->isElementNode())
updateTreeIfElementIdIsAriaOwned(toElement(node));
return newObj;
}
AXObject* AXObjectCacheImpl::getOrCreate(LayoutObject* layoutObject) {
if (!layoutObject)
return 0;
if (AXObject* obj = get(layoutObject))
return obj;
AXObject* newObj = createFromRenderer(layoutObject);
// Will crash later if we have two objects for the same layoutObject.
DCHECK(!get(layoutObject));
const AXID axid = getOrCreateAXID(newObj);
m_layoutObjectMapping.set(layoutObject, axid);
newObj->init();
newObj->setLastKnownIsIgnoredValue(newObj->accessibilityIsIgnored());
return newObj;
}
AXObject* AXObjectCacheImpl::getOrCreate(AbstractInlineTextBox* inlineTextBox) {
if (!inlineTextBox)
return 0;
if (AXObject* obj = get(inlineTextBox))
return obj;
AXObject* newObj = createFromInlineTextBox(inlineTextBox);
// Will crash later if we have two objects for the same inlineTextBox.
DCHECK(!get(inlineTextBox));
const AXID axid = getOrCreateAXID(newObj);
m_inlineTextBoxObjectMapping.set(inlineTextBox, axid);
newObj->init();
newObj->setLastKnownIsIgnoredValue(newObj->accessibilityIsIgnored());
return newObj;
}
AXObject* AXObjectCacheImpl::getOrCreate(AccessibilityRole role) {
AXObject* obj = nullptr;
// will be filled in...
switch (role) {
case ColumnRole:
obj = AXTableColumn::create(*this);
break;
case TableHeaderContainerRole:
obj = AXTableHeaderContainer::create(*this);
break;
case SliderThumbRole:
obj = AXSliderThumb::create(*this);
break;
case MenuListPopupRole:
obj = AXMenuListPopup::create(*this);
break;
case SpinButtonRole:
obj = AXSpinButton::create(*this);
break;
case SpinButtonPartRole:
obj = AXSpinButtonPart::create(*this);
break;
default:
obj = nullptr;
}
if (!obj)
return 0;
getOrCreateAXID(obj);
obj->init();
return obj;
}
void AXObjectCacheImpl::remove(AXID axID) {
if (!axID)
return;
// first fetch object to operate some cleanup functions on it
AXObject* obj = m_objects.at(axID);
if (!obj)
return;
obj->detach();
removeAXID(obj);
// finally remove the object
if (!m_objects.take(axID))
return;
DCHECK(m_objects.size() >= m_idsInUse.size());
}
void AXObjectCacheImpl::remove(LayoutObject* layoutObject) {
if (!layoutObject)
return;
AXID axID = m_layoutObjectMapping.at(layoutObject);
remove(axID);
m_layoutObjectMapping.erase(layoutObject);
}
void AXObjectCacheImpl::remove(Node* node) {
if (!node)
return;
// This is all safe even if we didn't have a mapping.
AXID axID = m_nodeObjectMapping.at(node);
remove(axID);
m_nodeObjectMapping.erase(node);
if (node->layoutObject()) {
remove(node->layoutObject());
return;
}
}
void AXObjectCacheImpl::remove(AbstractInlineTextBox* inlineTextBox) {
if (!inlineTextBox)
return;
AXID axID = m_inlineTextBoxObjectMapping.at(inlineTextBox);
remove(axID);
m_inlineTextBoxObjectMapping.erase(inlineTextBox);
}
AXID AXObjectCacheImpl::generateAXID() const {
static AXID lastUsedID = 0;
// Generate a new ID.
AXID objID = lastUsedID;
do {
++objID;
} while (!objID || HashTraits<AXID>::isDeletedValue(objID) ||
m_idsInUse.contains(objID));
lastUsedID = objID;
return objID;
}
AXID AXObjectCacheImpl::getOrCreateAXID(AXObject* obj) {
// check for already-assigned ID
const AXID existingAXID = obj->axObjectID();
if (existingAXID) {
DCHECK(m_idsInUse.contains(existingAXID));
return existingAXID;
}
const AXID newAXID = generateAXID();
m_idsInUse.insert(newAXID);
obj->setAXObjectID(newAXID);
m_objects.set(newAXID, obj);
return newAXID;
}
void AXObjectCacheImpl::removeAXID(AXObject* object) {
if (!object)
return;
AXID objID = object->axObjectID();
if (!objID)
return;
DCHECK(!HashTraits<AXID>::isDeletedValue(objID));
DCHECK(m_idsInUse.contains(objID));
object->setAXObjectID(0);
m_idsInUse.erase(objID);
if (m_ariaOwnerToChildrenMapping.contains(objID)) {
Vector<AXID> childAXIDs = m_ariaOwnerToChildrenMapping.at(objID);
for (size_t i = 0; i < childAXIDs.size(); ++i)
m_ariaOwnedChildToOwnerMapping.erase(childAXIDs[i]);
m_ariaOwnerToChildrenMapping.erase(objID);
}
m_ariaOwnedChildToOwnerMapping.erase(objID);
m_ariaOwnedChildToRealParentMapping.erase(objID);
m_ariaOwnerToIdsMapping.erase(objID);
}
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 (AXObject* obj = get(node)) {
obj->selectionChanged();
return;
}
node = node->parentNode();
}
}
void AXObjectCacheImpl::textChanged(Node* node) {
textChanged(getOrCreate(node));
}
void AXObjectCacheImpl::textChanged(LayoutObject* layoutObject) {
textChanged(getOrCreate(layoutObject));
}
void AXObjectCacheImpl::textChanged(AXObject* obj) {
if (!obj)
return;
bool parentAlreadyExists = obj->parentObjectIfExists();
obj->textChanged();
postNotification(obj, AXObjectCacheImpl::AXTextChanged);
if (parentAlreadyExists)
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* layoutObject) {
childrenChanged(get(layoutObject));
}
void AXObjectCacheImpl::childrenChanged(AXObject* obj) {
if (!obj)
return;
obj->childrenChanged();
}
void AXObjectCacheImpl::notificationPostTimerFired(TimerBase*) {
m_notificationPostTimer.stop();
unsigned i = 0, count = m_notificationsToPost.size();
for (i = 0; i < count; ++i) {
AXObject* obj = m_notificationsToPost[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* layoutObj = toAXLayoutObject(obj);
LayoutObject* layoutObject = layoutObj->getLayoutObject();
if (layoutObject && layoutObject->view())
DCHECK(!layoutObject->view()->layoutState());
}
#endif
AXNotification notification = m_notificationsToPost[i].second;
postPlatformNotification(obj, notification);
if (notification == AXChildrenChanged && obj->parentObjectIfExists() &&
obj->lastKnownIsIgnoredValue() != obj->accessibilityIsIgnored())
childrenChanged(obj->parentObject());
}
m_notificationsToPost.clear();
}
void AXObjectCacheImpl::postNotification(LayoutObject* layoutObject,
AXNotification notification) {
if (!layoutObject)
return;
postNotification(get(layoutObject), notification);
}
void AXObjectCacheImpl::postNotification(Node* node,
AXNotification notification) {
if (!node)
return;
postNotification(get(node), notification);
}
void AXObjectCacheImpl::postNotification(AXObject* object,
AXNotification notification) {
if (!object)
return;
m_modificationCount++;
m_notificationsToPost.push_back(std::make_pair(object, notification));
if (!m_notificationPostTimer.isActive())
m_notificationPostTimer.startOneShot(0, BLINK_FROM_HERE);
}
bool AXObjectCacheImpl::isAriaOwned(const AXObject* child) const {
return m_ariaOwnedChildToOwnerMapping.contains(child->axObjectID());
}
AXObject* AXObjectCacheImpl::getAriaOwnedParent(const AXObject* child) const {
return objectFromAXID(m_ariaOwnedChildToOwnerMapping.at(child->axObjectID()));
}
void AXObjectCacheImpl::updateAriaOwns(
const AXObject* owner,
const Vector<String>& idVector,
HeapVector<Member<AXObject>>& ownedChildren) {
//
// 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> currentIds = m_ariaOwnerToIdsMapping.at(owner->axObjectID());
HashSet<String> newIds;
bool idsChanged = false;
for (const String& id : idVector) {
newIds.insert(id);
if (!currentIds.contains(id)) {
idsChanged = true;
HashSet<AXID>* owners = m_idToAriaOwnersMapping.at(id);
if (!owners) {
owners = new HashSet<AXID>();
m_idToAriaOwnersMapping.set(id, WTF::wrapUnique(owners));
}
owners->insert(owner->axObjectID());
}
}
for (const String& id : currentIds) {
if (!newIds.contains(id)) {
idsChanged = true;
HashSet<AXID>* owners = m_idToAriaOwnersMapping.at(id);
if (owners) {
owners->erase(owner->axObjectID());
if (owners->isEmpty())
m_idToAriaOwnersMapping.erase(id);
}
}
}
if (idsChanged)
m_ariaOwnerToIdsMapping.set(owner->axObjectID(), newIds);
//
// 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()->treeScope();
Vector<AXID> newChildAXIDs;
for (const String& idName : idVector) {
Element* element = scope.getElementById(AtomicString(idName));
if (!element)
continue;
AXObject* 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 foundCycle = false;
for (AXObject* parent = owner->parentObject(); parent && !foundCycle;
parent = parent->parentObject()) {
if (parent == child)
foundCycle = true;
}
if (foundCycle)
continue;
newChildAXIDs.push_back(child->axObjectID());
ownedChildren.push_back(child);
}
// Compare this to the current list of owned children, and exit early if there
// are no changes.
Vector<AXID> currentChildAXIDs =
m_ariaOwnerToChildrenMapping.at(owner->axObjectID());
bool same = true;
if (currentChildAXIDs.size() != newChildAXIDs.size()) {
same = false;
} else {
for (size_t i = 0; i < currentChildAXIDs.size() && same; ++i) {
if (currentChildAXIDs[i] != newChildAXIDs[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 < currentChildAXIDs.size(); ++i) {
// Find the AXObject for the child that this owner no longer owns.
AXID removedChildID = currentChildAXIDs[i];
AXObject* removedChild = objectFromAXID(removedChildID);
// 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 (removedChild && getAriaOwnedParent(removedChild) != owner)
continue;
// Remove it from the child -> owner mapping so it's not owned by this owner
// anymore.
m_ariaOwnedChildToOwnerMapping.erase(removedChildID);
if (removedChild) {
// 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.
removedChild->detachFromParent();
AXID realParentID =
m_ariaOwnedChildToRealParentMapping.at(removedChildID);
AXObject* realParent = objectFromAXID(realParentID);
childrenChanged(realParent);
}
// Remove the child -> original parent mapping too since this object has now
// been reparented back to its original parent.
m_ariaOwnedChildToRealParentMapping.erase(removedChildID);
}
for (size_t i = 0; i < newChildAXIDs.size(); ++i) {
// Find the AXObject for the child that will now be a child of this owner.
AXID addedChildID = newChildAXIDs[i];
AXObject* addedChild = objectFromAXID(addedChildID);
// Add this child to the mapping from child to owner.
m_ariaOwnedChildToOwnerMapping.set(addedChildID, 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.
AXObject* originalParent = addedChild->parentObject();
m_ariaOwnedChildToRealParentMapping.set(addedChildID,
originalParent->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.
addedChild->detachFromParent();
childrenChanged(originalParent);
}
// Finally, update the mapping from the owner to the list of child IDs.
m_ariaOwnerToChildrenMapping.set(owner->axObjectID(), newChildAXIDs);
}
void AXObjectCacheImpl::updateTreeIfElementIdIsAriaOwned(Element* element) {
if (!element->hasID())
return;
String id = element->getIdAttribute();
HashSet<AXID>* owners = m_idToAriaOwnersMapping.at(id);
if (!owners)
return;
AXObject* axElement = getOrCreate(element);
if (!axElement)
return;
// If it's already owned, call childrenChanged on the owner to make sure it's
// still an owner.
if (isAriaOwned(axElement)) {
AXObject* ownedParent = getAriaOwnedParent(axElement);
DCHECK(ownedParent);
childrenChanged(ownedParent);
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& axID : *owners) {
AXObject* owner = objectFromAXID(axID);
if (owner)
childrenChanged(owner);
}
}
void AXObjectCacheImpl::checkedStateChanged(Node* node) {
postNotification(node, AXObjectCacheImpl::AXCheckedStateChanged);
}
void AXObjectCacheImpl::listboxOptionStateChanged(HTMLOptionElement* option) {
postNotification(option, AXCheckedStateChanged);
}
void AXObjectCacheImpl::listboxSelectedChildrenChanged(
HTMLSelectElement* select) {
postNotification(select, AXSelectedChildrenChanged);
}
void AXObjectCacheImpl::listboxActiveIndexChanged(HTMLSelectElement* select) {
AXObject* obj = get(select);
if (!obj || !obj->isAXListBox())
return;
toAXListBox(obj)->activeIndexChanged();
}
void AXObjectCacheImpl::radiobuttonRemovedFromGroup(
HTMLInputElement* groupMember) {
AXObject* obj = get(groupMember);
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* firstRadio =
toAXRadioInput(obj)->findFirstRadioButtonInGroup(groupMember);
AXObject* firstObj = get(firstRadio);
if (!firstObj || !firstObj->isAXRadioInput())
return;
toAXRadioInput(firstObj)->updatePosAndSetSize(1);
postNotification(firstObj, AXAriaAttributeChanged);
toAXRadioInput(firstObj)->requestUpdateToNextNode(true);
}
void AXObjectCacheImpl::handleLayoutComplete(LayoutObject* layoutObject) {
if (!layoutObject)
return;
m_modificationCount++;
// Create the AXObject 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 (AXObject* obj = getOrCreate(layoutObject))
postNotification(obj, AXLayoutComplete);
}
void AXObjectCacheImpl::handleClicked(Node* node) {
if (AXObject* obj = getOrCreate(node))
postNotification(obj, AXClicked);
}
void AXObjectCacheImpl::handleAriaExpandedChange(Node* node) {
if (AXObject* obj = getOrCreate(node))
obj->handleAriaExpandedChanged();
}
void AXObjectCacheImpl::handleAriaSelectedChanged(Node* node) {
AXObject* obj = get(node);
if (!obj)
return;
postNotification(obj, AXCheckedStateChanged);
AXObject* listbox = obj->parentObjectUnignored();
if (listbox && listbox->roleValue() == ListBoxRole)
postNotification(listbox, AXSelectedChildrenChanged);
}
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.
m_modificationCount++;
if (AXObject* obj = getOrCreate(node))
obj->handleActiveDescendantChanged();
}
void AXObjectCacheImpl::handleAriaRoleChanged(Node* node) {
if (AXObject* obj = getOrCreate(node)) {
obj->updateAccessibilityRole();
m_modificationCount++;
obj->notifyIfIgnoredValueChanged();
}
}
void AXObjectCacheImpl::handleAttributeChanged(const QualifiedName& attrName,
Element* element) {
if (attrName == roleAttr)
handleAriaRoleChanged(element);
else if (attrName == altAttr || attrName == titleAttr)
textChanged(element);
else if (attrName == forAttr && isHTMLLabelElement(*element))
labelChanged(element);
else if (attrName == idAttr)
updateTreeIfElementIdIsAriaOwned(element);
if (!attrName.localName().startsWith("aria-"))
return;
if (attrName == aria_activedescendantAttr)
handleActiveDescendantChanged(element);
else if (attrName == aria_valuenowAttr || attrName == aria_valuetextAttr)
postNotification(element, AXObjectCacheImpl::AXValueChanged);
else if (attrName == aria_labelAttr || attrName == aria_labeledbyAttr ||
attrName == aria_labelledbyAttr)
textChanged(element);
else if (attrName == aria_checkedAttr)
checkedStateChanged(element);
else if (attrName == aria_selectedAttr)
handleAriaSelectedChanged(element);
else if (attrName == aria_expandedAttr)
handleAriaExpandedChange(element);
else if (attrName == aria_hiddenAttr)
childrenChanged(element->parentNode());
else if (attrName == aria_invalidAttr)
postNotification(element, AXObjectCacheImpl::AXInvalidStatusChanged);
else if (attrName == aria_ownsAttr)
childrenChanged(element);
else
postNotification(element, AXObjectCacheImpl::AXAriaAttributeChanged);
}
void AXObjectCacheImpl::labelChanged(Element* element) {
textChanged(toHTMLLabelElement(element)->control());
}
void AXObjectCacheImpl::inlineTextBoxesUpdated(LineLayoutItem lineLayoutItem) {
if (!inlineTextBoxAccessibilityEnabled())
return;
LayoutObject* layoutObject =
LineLayoutAPIShim::layoutObjectFrom(lineLayoutItem);
// Only update if the accessibility object already exists and it's
// not already marked as dirty.
if (AXObject* obj = get(layoutObject)) {
if (!obj->needsToUpdateChildren()) {
obj->setNeedsToUpdateChildren();
postNotification(layoutObject, AXChildrenChanged);
}
}
}
Settings* AXObjectCacheImpl::settings() {
return m_document->settings();
}
bool AXObjectCacheImpl::accessibilityEnabled() {
Settings* settings = this->settings();
if (!settings)
return false;
return settings->getAccessibilityEnabled();
}
bool AXObjectCacheImpl::inlineTextBoxAccessibilityEnabled() {
Settings* settings = this->settings();
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;
}
AXObject* AXObjectCacheImpl::firstAccessibleObjectFromNode(const Node* node) {
if (!node)
return 0;
AXObject* accessibleObject = getOrCreate(node->layoutObject());
while (accessibleObject && accessibleObject->accessibilityIsIgnored()) {
node = NodeTraversal::next(*node);
while (node && !node->layoutObject())
node = NodeTraversal::nextSkippingChildren(*node);
if (!node)
return 0;
accessibleObject = getOrCreate(node->layoutObject());
}
return accessibleObject;
}
bool AXObjectCacheImpl::nodeIsTextControl(const Node* node) {
if (!node)
return false;
const AXObject* axObject = getOrCreate(const_cast<Node*>(node));
return axObject && axObject->isTextControl();
}
bool isNodeAriaVisible(Node* node) {
if (!node)
return false;
if (!node->isElementNode())
return false;
return equalIgnoringCase(toElement(node)->getAttribute(aria_hiddenAttr),
"false");
}
void AXObjectCacheImpl::postPlatformNotification(AXObject* obj,
AXNotification notification) {
if (!obj || !obj->getDocument() || !obj->documentFrameView() ||
!obj->documentFrameView()->frame().page())
return;
ChromeClient& client =
obj->getDocument()->axObjectCacheOwner().page()->chromeClient();
client.postAccessibilityNotification(obj, notification);
}
void AXObjectCacheImpl::handleFocusedUIElementChanged(Node* oldFocusedNode,
Node* newFocusedNode) {
if (!newFocusedNode)
return;
Page* page = newFocusedNode->document().page();
if (!page)
return;
AXObject* focusedObject = this->focusedObject();
if (!focusedObject)
return;
AXObject* oldFocusedObject = get(oldFocusedNode);
postPlatformNotification(oldFocusedObject, AXBlur);
postPlatformNotification(focusedObject, AXFocusedUIElementChanged);
}
void AXObjectCacheImpl::handleInitialFocus() {
postNotification(m_document, AXObjectCache::AXFocusedUIElementChanged);
}
void AXObjectCacheImpl::handleEditableTextContentChanged(Node* node) {
AXObject* obj = get(node);
while (obj && !obj->isNativeTextControl() && !obj->isNonNativeTextControl())
obj = obj->parentObject();
postNotification(obj, AXObjectCache::AXValueChanged);
}
void AXObjectCacheImpl::handleTextFormControlChanged(Node* node) {
handleEditableTextContentChanged(node);
}
void AXObjectCacheImpl::handleValueChanged(Node* node) {
postNotification(node, AXObjectCache::AXValueChanged);
}
void AXObjectCacheImpl::handleUpdateActiveMenuOption(LayoutMenuList* menuList,
int optionIndex) {
AXObject* obj = get(menuList);
if (!obj || !obj->isMenuList())
return;
toAXMenuList(obj)->didUpdateActiveOption(optionIndex);
}
void AXObjectCacheImpl::didShowMenuListPopup(LayoutMenuList* menuList) {
AXObject* obj = get(menuList);
if (!obj || !obj->isMenuList())
return;
toAXMenuList(obj)->didShowPopup();
}
void AXObjectCacheImpl::didHideMenuListPopup(LayoutMenuList* menuList) {
AXObject* obj = get(menuList);
if (!obj || !obj->isMenuList())
return;
toAXMenuList(obj)->didHidePopup();
}
void AXObjectCacheImpl::handleLoadComplete(Document* document) {
postNotification(getOrCreate(document), AXObjectCache::AXLoadComplete);
}
void AXObjectCacheImpl::handleLayoutComplete(Document* document) {
postNotification(getOrCreate(document), AXObjectCache::AXLayoutComplete);
}
void AXObjectCacheImpl::handleScrolledToAnchor(const Node* anchorNode) {
if (!anchorNode)
return;
AXObject* obj = getOrCreate(anchorNode->layoutObject());
if (!obj)
return;
if (obj->accessibilityIsIgnored())
obj = obj->parentObjectUnignored();
postPlatformNotification(obj, AXScrolledToAnchor);
}
void AXObjectCacheImpl::handleScrollPositionChanged(FrameView* frameView) {
AXObject* targetAXObject = getOrCreate(frameView->frame().document());
postPlatformNotification(targetAXObject, AXScrollPositionChanged);
}
void AXObjectCacheImpl::handleScrollPositionChanged(
LayoutObject* layoutObject) {
postPlatformNotification(getOrCreate(layoutObject), AXScrollPositionChanged);
}
const AtomicString& AXObjectCacheImpl::computedRoleForNode(Node* node) {
AXObject* obj = getOrCreate(node);
if (!obj)
return AXObject::roleName(UnknownRole);
return AXObject::roleName(obj->roleValue());
}
String AXObjectCacheImpl::computedNameForNode(Node* node) {
AXObject* obj = getOrCreate(node);
if (!obj)
return "";
return obj->computedName();
}
void AXObjectCacheImpl::onTouchAccessibilityHover(const IntPoint& location) {
AXObject* 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, AXHover);
}
}
void AXObjectCacheImpl::setCanvasObjectBounds(HTMLCanvasElement* canvas,
Element* element,
const LayoutRect& rect) {
AXObject* obj = getOrCreate(element);
if (!obj)
return;
AXObject* axCanvas = getOrCreate(canvas);
if (!axCanvas)
return;
obj->setElementRect(rect, axCanvas);
}
DEFINE_TRACE(AXObjectCacheImpl) {
visitor->trace(m_document);
visitor->trace(m_nodeObjectMapping);
visitor->trace(m_objects);
visitor->trace(m_notificationsToPost);
AXObjectCache::trace(visitor);
}
} // namespace blink