blob: b26e162cf32bcbdfcb79f5c19abd3546458a93a0 [file] [log] [blame]
/*
* Copyright (C) 2019 Apple 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.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. 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 INC. 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 "config.h"
#if ENABLE(ACCESSIBILITY_ISOLATED_TREE)
#include "AXIsolatedTree.h"
#include "AXIsolatedObject.h"
#include "AXLogger.h"
#include "FrameView.h"
#include "Page.h"
#include <wtf/NeverDestroyed.h>
#include <wtf/SetForScope.h>
namespace WebCore {
Lock AXIsolatedTree::s_cacheLock;
static unsigned newTreeID()
{
static unsigned s_currentTreeID = 0;
return ++s_currentTreeID;
}
HashMap<PageIdentifier, Ref<AXIsolatedTree>>& AXIsolatedTree::treePageCache()
{
static NeverDestroyed<HashMap<PageIdentifier, Ref<AXIsolatedTree>>> map;
return map;
}
HashMap<AXIsolatedTreeID, Ref<AXIsolatedTree>>& AXIsolatedTree::treeIDCache()
{
static NeverDestroyed<HashMap<AXIsolatedTreeID, Ref<AXIsolatedTree>>> map;
return map;
}
AXIsolatedTree::AXIsolatedTree(AXObjectCache* axObjectCache)
: m_treeID(newTreeID())
, m_axObjectCache(axObjectCache)
, m_usedOnAXThread(axObjectCache->usedOnAXThread())
{
AXTRACE("AXIsolatedTree::AXIsolatedTree"_s);
ASSERT(isMainThread());
}
AXIsolatedTree::~AXIsolatedTree()
{
AXTRACE("AXIsolatedTree::~AXIsolatedTree"_s);
}
void AXIsolatedTree::clear()
{
AXTRACE("AXIsolatedTree::clear"_s);
ASSERT(isMainThread());
m_axObjectCache = nullptr;
m_nodeMap.clear();
Locker locker { m_changeLogLock };
m_pendingSubtreeRemovals.append(m_rootNode->objectID());
m_rootNode = nullptr;
}
RefPtr<AXIsolatedTree> AXIsolatedTree::treeForID(AXIsolatedTreeID treeID)
{
AXTRACE("AXIsolatedTree::treeForID"_s);
Locker locker { s_cacheLock };
return treeIDCache().get(treeID);
}
Ref<AXIsolatedTree> AXIsolatedTree::create(AXObjectCache* axObjectCache)
{
AXTRACE("AXIsolatedTree::create"_s);
ASSERT(isMainThread());
ASSERT(axObjectCache && axObjectCache->pageID());
auto tree = adoptRef(*new AXIsolatedTree(axObjectCache));
auto& document = axObjectCache->document();
if (!document.view()->layoutContext().isInRenderTreeLayout() && !document.inRenderTreeUpdate() && !document.inStyleRecalc())
document.updateLayoutIgnorePendingStylesheets();
tree->m_maxTreeDepth = document.settings().maximumHTMLParserDOMTreeDepth();
ASSERT(tree->m_maxTreeDepth);
// Generate the nodes of the tree and set its root and focused objects.
// For this, we need the root and focused objects of the AXObject tree.
auto* axRoot = axObjectCache->getOrCreate(axObjectCache->document().view());
if (axRoot)
tree->generateSubtree(*axRoot);
auto* axFocus = axObjectCache->focusedObjectForPage(axObjectCache->document().page());
if (axFocus)
tree->setFocusedNodeID(axFocus->objectID());
// Now that the tree is ready to take client requests, add it to the tree
// maps so that it can be found.
auto pageID = axObjectCache->pageID();
Locker locker { s_cacheLock };
ASSERT(!treePageCache().contains(*pageID));
treePageCache().set(*pageID, tree.copyRef());
treeIDCache().set(tree->treeID(), tree.copyRef());
tree->updateLoadingProgress(axObjectCache->loadingProgress());
return tree;
}
void AXIsolatedTree::removeTreeForPageID(PageIdentifier pageID)
{
AXTRACE("AXIsolatedTree::removeTreeForPageID"_s);
ASSERT(isMainThread());
Locker locker { s_cacheLock };
if (auto tree = treePageCache().take(pageID)) {
tree->clear();
treeIDCache().remove(tree->treeID());
}
}
RefPtr<AXIsolatedTree> AXIsolatedTree::treeForPageID(PageIdentifier pageID)
{
Locker locker { s_cacheLock };
if (auto tree = treePageCache().get(pageID))
return RefPtr { tree };
return nullptr;
}
RefPtr<AXIsolatedObject> AXIsolatedTree::nodeForID(const AXID& axID) const
{
// In isolated tree mode 2, only access m_readerThreadNodeMap on the AX thread.
ASSERT(m_usedOnAXThread ? !isMainThread() : isMainThread());
if (m_usedOnAXThread && isMainThread())
return nullptr;
return axID.isValid() ? m_readerThreadNodeMap.get(axID) : nullptr;
}
Vector<RefPtr<AXCoreObject>> AXIsolatedTree::objectsForIDs(const Vector<AXID>& axIDs)
{
AXTRACE("AXIsolatedTree::objectsForIDs"_s);
ASSERT(!isMainThread());
Vector<RefPtr<AXCoreObject>> result;
result.reserveInitialCapacity(axIDs.size());
for (auto& axID : axIDs) {
auto object = nodeForID(axID);
if (object) {
result.uncheckedAppend(object);
continue;
}
// There is no isolated object for this AXID. This can happen if the corresponding live object is ignored.
// If there is a live object for this ID and it is an ignored target of a relationship, create an isolated object for it.
object = Accessibility::retrieveValueFromMainThread<RefPtr<AXIsolatedObject>>([&axID, this] () -> RefPtr<AXIsolatedObject> {
auto* cache = axObjectCache();
if (!cache || !cache->relationTargetIDs().contains(axID))
return nullptr;
auto* axObject = cache->objectForID(axID);
if (!axObject || !axObject->accessibilityIsIgnored())
return nullptr;
auto object = AXIsolatedObject::create(*axObject, const_cast<AXIsolatedTree*>(this));
ASSERT(axObject->wrapper());
object->attachPlatformWrapper(axObject->wrapper());
return object;
});
if (object) {
m_readerThreadNodeMap.add(axID, *object);
result.uncheckedAppend(object);
}
}
result.shrinkToFit();
return result;
}
void AXIsolatedTree::generateSubtree(AXCoreObject& axObject)
{
AXTRACE("AXIsolatedTree::generateSubtree"_s);
ASSERT(isMainThread());
if (!axObject.objectID().isValid())
return;
collectNodeChangesForSubtree(axObject);
queueRemovalsAndUnresolvedChanges({ });
}
static bool shouldCreateNodeChange(AXCoreObject& axObject)
{
// We should never create an isolated object from an ignored object or one with an invalid ID.
return !axObject.accessibilityIsIgnored() && axObject.objectID().isValid();
}
std::optional<AXIsolatedTree::NodeChange> AXIsolatedTree::nodeChangeForObject(Ref<AXCoreObject> axObject, AttachWrapper attachWrapper)
{
ASSERT(isMainThread());
ASSERT(axObject->objectID().isValid());
if (!shouldCreateNodeChange(axObject.get()))
return std::nullopt;
auto object = AXIsolatedObject::create(axObject, this);
NodeChange nodeChange { object, nullptr };
ASSERT(axObject->wrapper());
if (attachWrapper == AttachWrapper::OnMainThread)
object->attachPlatformWrapper(axObject->wrapper());
else {
// Set the wrapper in the NodeChange so that it is set on the AX thread.
nodeChange.wrapper = axObject->wrapper();
}
m_nodeMap.set(axObject->objectID(), ParentChildrenIDs { nodeChange.isolatedObject->parent(), axObject->childrenIDs() });
if (!nodeChange.isolatedObject->parent().isValid()) {
Locker locker { m_changeLogLock };
setRootNode(nodeChange.isolatedObject.ptr());
}
return nodeChange;
}
void AXIsolatedTree::queueChange(const NodeChange& nodeChange)
{
ASSERT(isMainThread());
ASSERT(m_changeLogLock.isLocked());
m_pendingAppends.append(nodeChange);
AXID parentID = nodeChange.isolatedObject->parent();
if (parentID.isValid()) {
ASSERT_WITH_MESSAGE(m_nodeMap.contains(parentID), "node map should've contained parentID: %s", parentID.loggingString().utf8().data());
auto siblingsIDs = m_nodeMap.get(parentID).childrenIDs;
m_pendingChildrenUpdates.append({ parentID, WTFMove(siblingsIDs) });
}
AXID objectID = nodeChange.isolatedObject->objectID();
ASSERT_WITH_MESSAGE(objectID != parentID, "object ID was the same as its parent ID (%s) when queueing a node change", objectID.loggingString().utf8().data());
ASSERT_WITH_MESSAGE(m_nodeMap.contains(objectID), "node map should've contained objectID: %s", objectID.loggingString().utf8().data());
auto childrenIDs = m_nodeMap.get(objectID).childrenIDs;
m_pendingChildrenUpdates.append({ objectID, WTFMove(childrenIDs) });
}
void AXIsolatedTree::queueRemovals(const Vector<AXID>& subtreeRemovals)
{
ASSERT(isMainThread());
Locker locker { m_changeLogLock };
queueRemovalsLocked(subtreeRemovals);
}
void AXIsolatedTree::queueRemovalsLocked(const Vector<AXID>& subtreeRemovals)
{
ASSERT(isMainThread());
ASSERT(m_changeLogLock.isLocked());
for (const auto& axID : subtreeRemovals)
m_pendingSubtreeRemovals.append(axID);
}
void AXIsolatedTree::queueRemovalsAndUnresolvedChanges(const Vector<AXID>& subtreeRemovals)
{
ASSERT(isMainThread());
Vector<NodeChange> resolvedAppends;
if (!m_unresolvedPendingAppends.isEmpty()) {
if (auto* cache = axObjectCache()) {
resolvedAppends.reserveInitialCapacity(m_unresolvedPendingAppends.size());
for (const auto& unresolvedAppend : m_unresolvedPendingAppends) {
if (auto* axObject = cache->objectForID(unresolvedAppend.key)) {
if (auto nodeChange = nodeChangeForObject(*axObject, unresolvedAppend.value))
resolvedAppends.uncheckedAppend(WTFMove(*nodeChange));
}
}
m_unresolvedPendingAppends.clear();
}
}
Locker locker { m_changeLogLock };
for (const auto& resolvedAppend : resolvedAppends)
queueChange(resolvedAppend);
queueRemovalsLocked(subtreeRemovals);
}
void AXIsolatedTree::collectNodeChangesForSubtree(AXCoreObject& axObject)
{
AXTRACE("AXIsolatedTree::collectNodeChangesForSubtree"_s);
ASSERT(isMainThread());
if (!axObject.objectID().isValid()) {
// Bail out here, we can't build an isolated tree branch rooted at an object with no ID.
return;
}
SetForScope collectingAtTreeLevel(m_collectingNodeChangesAtTreeLevel, m_collectingNodeChangesAtTreeLevel + 1);
if (m_collectingNodeChangesAtTreeLevel >= m_maxTreeDepth)
return;
m_unresolvedPendingAppends.set(axObject.objectID(), AttachWrapper::OnMainThread);
auto axChildrenCopy = axObject.children();
Vector<AXID> axChildrenIDs;
axChildrenIDs.reserveInitialCapacity(axChildrenCopy.size());
for (const auto& axChild : axChildrenCopy) {
if (!axChild || axChild.get() == &axObject) {
ASSERT_NOT_REACHED();
continue;
}
axChildrenIDs.uncheckedAppend(axChild->objectID());
collectNodeChangesForSubtree(*axChild);
}
axChildrenIDs.shrinkToFit();
// Update the m_nodeMap.
auto* axParent = axObject.parentObjectUnignored();
m_nodeMap.set(axObject.objectID(), ParentChildrenIDs { axParent ? axParent->objectID() : AXID(), WTFMove(axChildrenIDs) });
}
void AXIsolatedTree::updateNode(AXCoreObject& axObject)
{
AXTRACE("AXIsolatedTree::updateNode"_s);
AXLOG(&axObject);
ASSERT(isMainThread());
// If we update a node as the result of some side effect while collecting node changes (e.g. a role change from
// AccessibilityRenderObject::updateRoleAfterChildrenCreation), queue the append up to be resolved with the rest
// of the collected changes. This prevents us from creating two node changes for the same object.
if (isCollectingNodeChanges()) {
m_unresolvedPendingAppends.set(axObject.objectID(), AttachWrapper::OnAXThread);
return;
}
// Otherwise, resolve the change immediately and queue it up.
// In both cases, we can't attach the wrapper immediately on the main thread, since the wrapper could be in use
// on the AX thread (because this function updates an existing node).
if (auto change = nodeChangeForObject(axObject, AttachWrapper::OnAXThread)) {
Locker locker { m_changeLogLock };
queueChange(WTFMove(*change));
}
}
void AXIsolatedTree::updateNodeProperty(AXCoreObject& axObject, AXPropertyName property)
{
AXTRACE("AXIsolatedTree::updateNodeProperty"_s);
ASSERT(isMainThread());
AXPropertyMap propertyMap;
switch (property) {
case AXPropertyName::ARIATreeItemContent:
propertyMap.set(AXPropertyName::ARIATreeItemContent, axIDs(axObject.ariaTreeItemContent()));
break;
case AXPropertyName::ARIATreeRows: {
AXCoreObject::AccessibilityChildrenVector ariaTreeRows;
axObject.ariaTreeRows(ariaTreeRows);
propertyMap.set(AXPropertyName::ARIATreeRows, axIDs(ariaTreeRows));
break;
}
case AXPropertyName::CanSetFocusAttribute:
propertyMap.set(AXPropertyName::CanSetFocusAttribute, axObject.canSetFocusAttribute());
break;
case AXPropertyName::CanSetValueAttribute:
propertyMap.set(AXPropertyName::CanSetValueAttribute, axObject.canSetValueAttribute());
break;
case AXPropertyName::CurrentValue:
propertyMap.set(AXPropertyName::CurrentValue, axObject.currentValue().isolatedCopy());
break;
case AXPropertyName::DisclosedRows:
propertyMap.set(AXPropertyName::DisclosedRows, axIDs(axObject.disclosedRows()));
break;
case AXPropertyName::IdentifierAttribute:
propertyMap.set(AXPropertyName::IdentifierAttribute, axObject.identifierAttribute().isolatedCopy());
break;
case AXPropertyName::IsChecked:
ASSERT(axObject.supportsCheckedState());
propertyMap.set(AXPropertyName::IsChecked, axObject.isChecked());
propertyMap.set(AXPropertyName::ButtonState, axObject.checkboxOrRadioValue());
break;
case AXPropertyName::IsEnabled:
propertyMap.set(AXPropertyName::IsEnabled, axObject.isEnabled());
break;
case AXPropertyName::IsExpanded:
propertyMap.set(AXPropertyName::IsExpanded, axObject.isExpanded());
break;
case AXPropertyName::IsRequired:
propertyMap.set(AXPropertyName::IsRequired, axObject.isRequired());
break;
case AXPropertyName::IsSelected:
propertyMap.set(AXPropertyName::IsSelected, axObject.isSelected());
break;
case AXPropertyName::MaxValueForRange:
propertyMap.set(AXPropertyName::MaxValueForRange, axObject.maxValueForRange());
break;
case AXPropertyName::MinValueForRange:
propertyMap.set(AXPropertyName::MinValueForRange, axObject.minValueForRange());
break;
case AXPropertyName::Orientation:
propertyMap.set(AXPropertyName::Orientation, static_cast<int>(axObject.orientation()));
break;
case AXPropertyName::PosInSet:
propertyMap.set(AXPropertyName::PosInSet, axObject.posInSet());
break;
case AXPropertyName::ReadOnlyValue:
propertyMap.set(AXPropertyName::ReadOnlyValue, axObject.readOnlyValue().isolatedCopy());
break;
case AXPropertyName::SetSize:
propertyMap.set(AXPropertyName::SetSize, axObject.setSize());
break;
case AXPropertyName::SortDirection:
propertyMap.set(AXPropertyName::SortDirection, static_cast<int>(axObject.sortDirection()));
break;
case AXPropertyName::SupportsPosInSet:
propertyMap.set(AXPropertyName::SupportsPosInSet, axObject.supportsPosInSet());
break;
case AXPropertyName::SupportsSetSize:
propertyMap.set(AXPropertyName::SupportsSetSize, axObject.supportsSetSize());
break;
case AXPropertyName::ValueForRange:
propertyMap.set(AXPropertyName::ValueForRange, axObject.valueForRange());
break;
default:
return;
}
Locker locker { m_changeLogLock };
m_pendingPropertyChanges.append({ axObject.objectID(), propertyMap });
}
void AXIsolatedTree::updateNodeAndDependentProperties(AXCoreObject& axObject)
{
ASSERT(isMainThread());
updateNode(axObject);
if (auto* treeAncestor = Accessibility::findAncestor(axObject, true, [] (const auto& object) { return object.isTree(); }))
updateNodeProperty(*treeAncestor, AXPropertyName::ARIATreeRows);
}
void AXIsolatedTree::updateChildren(AXCoreObject& axObject, ResolveNodeChanges resolveNodeChanges)
{
AXTRACE("AXIsolatedTree::updateChildren"_s);
AXLOG("For AXObject:");
AXLOG(&axObject);
ASSERT(isMainThread());
if (m_nodeMap.isEmpty()) {
ASSERT_NOT_REACHED();
return;
}
if (!axObject.document() || !axObject.document()->hasLivingRenderTree())
return;
// updateChildren may be called as the result of a children changed
// notification for an axObject that has no associated isolated object.
// An example of this is when an empty element such as a <canvas> or <div>
// has added a new child. So find the closest ancestor of axObject that has
// an associated isolated object and update its children.
auto* axAncestor = Accessibility::findAncestor(axObject, true, [this] (auto& ancestor) {
return m_nodeMap.find(ancestor.objectID()) != m_nodeMap.end();
});
if (!axAncestor || !axAncestor->objectID().isValid()) {
// This update was triggered before the isolated tree has been repopulated.
// Return here since there is nothing to update.
AXLOG("Bailing because no ancestor could be found, or ancestor had an invalid objectID");
return;
}
if (axAncestor != &axObject) {
AXLOG(makeString("Original object with ID ", axObject.objectID().loggingString(), " wasn't in the isolated tree, so instead updating the closest in-isolated-tree ancestor:"));
AXLOG(axAncestor);
for (auto& child : axObject.children()) {
auto* liveChild = dynamicDowncast<AccessibilityObject>(child.get());
if (!liveChild || liveChild->childrenInitialized())
continue;
if (!m_nodeMap.contains(liveChild->objectID())) {
if (!shouldCreateNodeChange(*liveChild))
continue;
// This child should be added to the isolated tree but hasn't been yet.
// Add it to the nodemap so the recursive call to updateChildren below properly builds the subtree for this object.
auto* parent = liveChild->parentObjectUnignored();
m_nodeMap.set(liveChild->objectID(), ParentChildrenIDs { parent ? parent->objectID() : AXID(), liveChild->childrenIDs() });
m_unresolvedPendingAppends.set(liveChild->objectID(), AttachWrapper::OnMainThread);
}
AXLOG(makeString(
"Child ID ", liveChild->objectID().loggingString(), " of original object ID ", axObject.objectID().loggingString(), " was found in the isolated tree with uninitialized live children. Updating its isolated children."
));
// Don't immediately resolve node changes in these recursive calls to updateChildren. This avoids duplicate node change creation in this scenario:
// 1. Some subtree is updated in the below call to updateChildren.
// 2. Later in this function, when updating axAncestor, we update some higher subtree that includes the updated subtree from step 1.
updateChildren(*liveChild, ResolveNodeChanges::No);
}
}
auto oldIDs = m_nodeMap.get(axAncestor->objectID());
auto& oldChildrenIDs = oldIDs.childrenIDs;
const auto& newChildren = axAncestor->children();
auto newChildrenIDs = axAncestor->childrenIDs(false);
for (size_t i = 0; i < newChildren.size(); ++i) {
ASSERT(newChildren[i]->objectID() == newChildrenIDs[i]);
ASSERT(newChildrenIDs[i].isValid());
size_t index = oldChildrenIDs.find(newChildrenIDs[i]);
if (index != notFound)
oldChildrenIDs.remove(index);
else {
// This is a new child, add it to the tree.
AXLOG(makeString("AXID ", axAncestor->objectID().loggingString(), " gaining new subtree, starting at ID ", newChildren[i]->objectID().loggingString(), ":"));
AXLOG(newChildren[i]);
collectNodeChangesForSubtree(*newChildren[i]);
}
}
m_nodeMap.set(axAncestor->objectID(), ParentChildrenIDs { oldIDs.parentID, WTFMove(newChildrenIDs) });
// What is left in oldChildrenIDs are the IDs that are no longer children of axAncestor.
// Thus, remove them from m_nodeMap and queue them to be removed from the tree.
for (const AXID& axID : oldChildrenIDs) {
// However, we don't want to remove subtrees from the nodemap that are part of the to-be-queued node changes (i.e those in `idsBeingChanged`).
// This is important when a node moves to a different part of the tree rather than being deleted -- for example:
// 1. Object 123 is slated to be a child of this object (i.e. in newChildren), and we collect node changes for it.
// 2. Object 123 is currently a member of a subtree of some other object in oldChildrenIDs.
// 3. Thus, we don't want to delete Object 123 from the nodemap, instead allowing it to be moved.
if (axID.isValid())
removeSubtreeFromNodeMap(axID, axAncestor);
}
if (resolveNodeChanges == ResolveNodeChanges::Yes)
queueRemovalsAndUnresolvedChanges(oldChildrenIDs);
else
queueRemovals(oldChildrenIDs);
// Also queue updates to the target node itself and any properties that depend on children().
updateNodeAndDependentProperties(*axAncestor);
}
RefPtr<AXIsolatedObject> AXIsolatedTree::focusedNode()
{
AXTRACE("AXIsolatedTree::focusedNode"_s);
RELEASE_ASSERT(!isMainThread());
// Apply pending changes in case focus has changed and hasn't been updated.
applyPendingChanges();
AXLOG(makeString("focusedNodeID ", m_focusedNodeID.loggingString()));
AXLOG("focused node:");
AXLOG(nodeForID(m_focusedNodeID));
return nodeForID(m_focusedNodeID);
}
RefPtr<AXIsolatedObject> AXIsolatedTree::rootNode()
{
AXTRACE("AXIsolatedTree::rootNode"_s);
Locker locker { m_changeLogLock };
return m_rootNode;
}
void AXIsolatedTree::setRootNode(AXIsolatedObject* root)
{
AXTRACE("AXIsolatedTree::setRootNode"_s);
ASSERT(isMainThread());
ASSERT(m_changeLogLock.isLocked());
ASSERT(!m_rootNode);
ASSERT(root);
m_rootNode = root;
}
void AXIsolatedTree::setFocusedNodeID(AXID axID)
{
AXTRACE("AXIsolatedTree::setFocusedNodeID"_s);
AXLOG(makeString("axID ", axID.loggingString()));
ASSERT(isMainThread());
AXPropertyMap propertyMap;
propertyMap.set(AXPropertyName::IsFocused, true);
Locker locker { m_changeLogLock };
m_pendingFocusedNodeID = axID;
m_pendingPropertyChanges.append({ axID, propertyMap });
}
void AXIsolatedTree::updateLoadingProgress(double newProgressValue)
{
AXTRACE("AXIsolatedTree::updateLoadingProgress"_s);
AXLOG(makeString("Updating loading progress to ", newProgressValue, " for treeID ", treeID()));
ASSERT(isMainThread());
m_loadingProgress = newProgressValue;
}
void AXIsolatedTree::removeNode(const AXCoreObject& axObject)
{
AXTRACE("AXIsolatedTree::removeNode"_s);
AXLOG(makeString("objectID ", axObject.objectID().loggingString()));
ASSERT(isMainThread());
m_unresolvedPendingAppends.remove(axObject.objectID());
removeSubtreeFromNodeMap(axObject.objectID(), axObject.parentObjectUnignored());
queueRemovals({ axObject.objectID() });
}
void AXIsolatedTree::removeSubtreeFromNodeMap(AXID objectID, AXCoreObject* axParent)
{
AXTRACE("AXIsolatedTree::removeSubtreeFromNodeMap"_s);
AXLOG(makeString("Removing subtree for objectID ", objectID.loggingString()));
ASSERT(isMainThread());
if (!m_nodeMap.contains(objectID)) {
AXLOG(makeString("Tried to remove AXID ", objectID.loggingString(), " that is no longer in m_nodeMap."));
return;
}
AXID axParentID = axParent ? axParent->objectID() : AXID();
if (axParentID != m_nodeMap.get(objectID).parentID) {
AXLOG(makeString("Tried to remove object ID ", objectID.loggingString(), " from a different parent ", axParentID.loggingString(), ", actual parent ", m_nodeMap.get(objectID).parentID.loggingString(), ", bailing out."));
return;
}
Vector<AXID> removals = { objectID };
while (removals.size()) {
AXID axID = removals.takeLast();
if (!axID.isValid() || m_unresolvedPendingAppends.contains(axID))
continue;
auto it = m_nodeMap.find(axID);
if (it != m_nodeMap.end()) {
removals.appendVector(it->value.childrenIDs);
m_nodeMap.remove(axID);
}
}
// Update the childrenIDs of the parent since one of its children has been removed.
if (axParent) {
auto ids = m_nodeMap.get(axParentID);
ids.childrenIDs = axParent->childrenIDs();
m_nodeMap.set(axParentID, WTFMove(ids));
}
}
std::optional<Vector<AXID>> AXIsolatedTree::relatedObjectIDsFor(const AXCoreObject& object, AXRelationType relationType)
{
ASSERT(!isMainThread());
if (m_relationsNeedUpdate) {
m_relations = Accessibility::retrieveValueFromMainThread<HashMap<AXID, AXRelations>>([this] () -> HashMap<AXID, AXRelations> {
if (auto* cache = axObjectCache())
return cache->relations();
return { };
});
m_relationsNeedUpdate = false;
}
auto relationsIterator = m_relations.find(object.objectID());
if (relationsIterator == m_relations.end())
return std::nullopt;
auto targetsIterator = relationsIterator->value.find(static_cast<uint8_t>(relationType));
if (targetsIterator == relationsIterator->value.end())
return std::nullopt;
return targetsIterator->value;
}
void AXIsolatedTree::applyPendingChanges()
{
AXTRACE("AXIsolatedTree::applyPendingChanges"_s);
// In isolated tree mode 2, only apply pending changes on the AX thread.
ASSERT(m_usedOnAXThread ? !isMainThread() : isMainThread());
if (m_usedOnAXThread && isMainThread())
return;
Locker locker { m_changeLogLock };
if (m_pendingFocusedNodeID != m_focusedNodeID) {
AXLOG(makeString("focusedNodeID ", m_focusedNodeID.loggingString(), " pendingFocusedNodeID ", m_pendingFocusedNodeID.loggingString()));
if (m_focusedNodeID.isValid()) {
// Set the old focused object's IsFocused property to false.
AXPropertyMap propertyMap;
propertyMap.set(AXPropertyName::IsFocused, false);
m_pendingPropertyChanges.append({ m_focusedNodeID, propertyMap });
}
m_focusedNodeID = m_pendingFocusedNodeID;
}
while (m_pendingSubtreeRemovals.size()) {
auto axID = m_pendingSubtreeRemovals.takeLast();
AXLOG(makeString("removing subtree axID ", axID.loggingString()));
if (auto object = nodeForID(axID)) {
object->detach(AccessibilityDetachmentType::ElementDestroyed);
m_pendingSubtreeRemovals.appendVector(object->m_childrenIDs);
m_readerThreadNodeMap.remove(axID);
}
}
for (const auto& item : m_pendingAppends) {
AXID axID = item.isolatedObject->objectID();
AXLOG(makeString("appending axID ", axID.loggingString()));
if (!axID.isValid())
continue;
auto& wrapper = item.wrapper ? item.wrapper : item.isolatedObject->wrapper();
if (!wrapper)
continue;
if (auto existingObject = m_readerThreadNodeMap.get(axID)) {
if (existingObject != &item.isolatedObject.get()
&& existingObject->wrapper() == wrapper.get()) {
// The new IsolatedObject is a replacement for an existing object
// as the result of an update. Thus detach the existing object
// and attach the wrapper to the new one.
existingObject->detach(AccessibilityDetachmentType::ElementChanged);
item.isolatedObject->attachPlatformWrapper(wrapper.get());
}
m_readerThreadNodeMap.remove(axID);
}
// If the new object hasn't been attached to a wrapper yet, or if it was detached from
// the wrapper when processing removals above, we must attach / re-attach it.
if (item.isolatedObject->isDetached())
item.isolatedObject->attachPlatformWrapper(wrapper.get());
auto addResult = m_readerThreadNodeMap.add(axID, item.isolatedObject.get());
// The newly added object must have a wrapper.
ASSERT_UNUSED(addResult, addResult.iterator->value->wrapper());
// The reference count of the just added IsolatedObject must be 2
// because it is referenced by m_readerThreadNodeMap and m_pendingAppends.
// When m_pendingAppends is cleared, the object will be held only by m_readerThreadNodeMap. The exception is the root node whose reference count is 3.
ASSERT_WITH_MESSAGE(
addResult.iterator->value->refCount() == 2 || (addResult.iterator->value.ptr() == m_rootNode.get() && m_rootNode->refCount() == 3),
"unexpected ref count after adding object to m_readerThreadNodeMap: %d", addResult.iterator->value->refCount()
);
}
m_pendingAppends.clear();
for (auto& update : m_pendingChildrenUpdates) {
AXLOG(makeString("updating children for axID ", update.first.loggingString()));
if (auto object = nodeForID(update.first))
object->m_childrenIDs = WTFMove(update.second);
}
m_pendingChildrenUpdates.clear();
for (auto& change : m_pendingPropertyChanges) {
if (auto object = nodeForID(change.axID)) {
for (auto& property : change.properties)
object->setProperty(property.key, WTFMove(property.value));
}
}
m_pendingPropertyChanges.clear();
}
} // namespace WebCore
#endif // ENABLE(ACCESSIBILITY_ISOLATED_TREE)