| /* |
| * Copyright (C) 2008-2025 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. |
| * 3. Neither the name of Apple 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 "config.h" |
| |
| #include "AXObjectCache.h" |
| |
| #include "AXImage.h" |
| #include "AXIsolatedObject.h" |
| #include "AXIsolatedTree.h" |
| #include "AXLogger.h" |
| #include "AXRemoteFrame.h" |
| #include "AXTextMarker.h" |
| #include "AccessibilityARIAGridCell.h" |
| #include "AccessibilityARIAGridRow.h" |
| #include "AccessibilityARIATable.h" |
| #include "AccessibilityAttachment.h" |
| #include "AccessibilityImageMapLink.h" |
| #include "AccessibilityLabel.h" |
| #include "AccessibilityList.h" |
| #include "AccessibilityListBox.h" |
| #include "AccessibilityListBoxOption.h" |
| #include "AccessibilityMathMLElement.h" |
| #include "AccessibilityMediaObject.h" |
| #include "AccessibilityMenuList.h" |
| #include "AccessibilityMenuListOption.h" |
| #include "AccessibilityMenuListPopup.h" |
| #include "AccessibilityProgressIndicator.h" |
| #include "AccessibilityRenderObject.h" |
| #include "AccessibilitySVGObject.h" |
| #include "AccessibilitySVGRoot.h" |
| #include "AccessibilityScrollView.h" |
| #include "AccessibilityScrollbar.h" |
| #include "AccessibilitySlider.h" |
| #include "AccessibilitySpinButton.h" |
| #include "AccessibilityTable.h" |
| #include "AccessibilityTableCell.h" |
| #include "AccessibilityTableColumn.h" |
| #include "AccessibilityTableHeaderContainer.h" |
| #include "AccessibilityTableRow.h" |
| #include "AccessibilityTree.h" |
| #include "AccessibilityTreeItem.h" |
| #include "CaretRectComputation.h" |
| #include "ContainerNodeInlines.h" |
| #include "CustomElementDefaultARIA.h" |
| #include "DeprecatedGlobalSettings.h" |
| #include "Document.h" |
| #include "Editing.h" |
| #include "Editor.h" |
| #include "ElementAncestorIteratorInlines.h" |
| #include "ElementRareData.h" |
| #include "EventNames.h" |
| #include "FocusController.h" |
| #include "HTMLAreaElement.h" |
| #include "HTMLButtonElement.h" |
| #include "HTMLCanvasElement.h" |
| #include "HTMLDetailsElement.h" |
| #include "HTMLDialogElement.h" |
| #include "HTMLImageElement.h" |
| #include "HTMLInputElement.h" |
| #include "HTMLLabelElement.h" |
| #include "HTMLMediaElement.h" |
| #include "HTMLMeterElement.h" |
| #include "HTMLNames.h" |
| #include "HTMLOptGroupElement.h" |
| #include "HTMLOptionElement.h" |
| #include "HTMLProgressElement.h" |
| #include "HTMLSelectElement.h" |
| #include "HTMLSummaryElement.h" |
| #include "HTMLTableElement.h" |
| #include "HTMLTablePartElement.h" |
| #include "HTMLTableRowElement.h" |
| #include "HTMLTableSectionElement.h" |
| #include "HTMLTextFormControlElement.h" |
| #include "HitTestSource.h" |
| #include "InlineIteratorLogicalOrderTraversal.h" |
| #include "InlineRunAndOffset.h" |
| #include "LocalFrame.h" |
| #include "MathMLElement.h" |
| #include "Page.h" |
| #include "ProgressTracker.h" |
| #include "Range.h" |
| #include "RenderAttachment.h" |
| #include "RenderImage.h" |
| #include "RenderLayer.h" |
| #include "RenderLineBreak.h" |
| #include "RenderListBox.h" |
| #include "RenderMathMLOperator.h" |
| #include "RenderMenuList.h" |
| #include "RenderMeter.h" |
| #include "RenderObjectInlines.h" |
| #include "RenderProgress.h" |
| #include "RenderSVGInlineText.h" |
| #include "RenderSlider.h" |
| #include "RenderStyleInlines.h" |
| #include "RenderTable.h" |
| #include "RenderTableCell.h" |
| #include "RenderTableRow.h" |
| #include "RenderView.h" |
| #include "SVGElement.h" |
| #include "ScriptDisallowedScope.h" |
| #include "ScrollView.h" |
| #include "ShadowRoot.h" |
| #include "TextBoundaries.h" |
| #include "TextControlInnerElements.h" |
| #include "TextIterator.h" |
| #include "TypedElementDescendantIteratorInlines.h" |
| #include <utility> |
| #include <wtf/DataLog.h> |
| #include <wtf/NeverDestroyed.h> |
| #include <wtf/SetForScope.h> |
| #include <wtf/TZoneMallocInlines.h> |
| #include <wtf/text/AtomString.h> |
| #include <wtf/text/MakeString.h> |
| |
| namespace WebCore { |
| |
| DEFINE_ALLOCATOR_WITH_HEAP_IDENTIFIER(AXComputedObjectAttributeCache); |
| DEFINE_ALLOCATOR_WITH_HEAP_IDENTIFIER(AXObjectCache); |
| WTF_MAKE_TZONE_ALLOCATED_IMPL(AXObjectCache); |
| |
| using namespace HTMLNames; |
| |
| #if PLATFORM(COCOA) |
| |
| // Post notifications for secure fields or elements contained in secure fields at a 40hz interval to thwart analysis of typing cadence. |
| static const Seconds accessibilityPasswordValueChangeNotificationInterval { 25_ms }; |
| |
| static bool isSecureFieldOrContainedBySecureField(AccessibilityObject& object) |
| { |
| return object.isSecureField() || object.isContainedBySecureField(); |
| } |
| |
| #endif // PLATFORM(COCOA) |
| |
| static bool rendererNeedsDeferredUpdate(const RenderObject& renderer) |
| { |
| ASSERT(!renderer.beingDestroyed()); |
| auto& document = renderer.document(); |
| return renderer.needsLayout() || document.needsStyleRecalc() || document.inRenderTreeUpdate() || (document.view() && document.view()->layoutContext().isInRenderTreeLayout()); |
| } |
| |
| static bool nodeRendererIsValid(Node& node) |
| { |
| auto* renderer = node.renderer(); |
| return renderer && !renderer->beingDestroyed(); |
| } |
| |
| static bool nodeAndRendererAreValid(Node* node) |
| { |
| return node ? nodeRendererIsValid(*node) : false; |
| } |
| |
| AccessibilityObjectInclusion AXComputedObjectAttributeCache::getIgnored(AXID id) const |
| { |
| auto it = m_idMapping.find(id); |
| return it != m_idMapping.end() ? it->value.ignored : AccessibilityObjectInclusion::DefaultBehavior; |
| } |
| |
| void AXComputedObjectAttributeCache::setIgnored(AXID id, AccessibilityObjectInclusion inclusion) |
| { |
| HashMap<AXID, CachedAXObjectAttributes>::iterator it = m_idMapping.find(id); |
| if (it != m_idMapping.end()) |
| it->value.ignored = inclusion; |
| else { |
| CachedAXObjectAttributes attributes; |
| attributes.ignored = inclusion; |
| m_idMapping.set(id, attributes); |
| } |
| } |
| |
| AccessibilityReplacedText::AccessibilityReplacedText(const VisibleSelection& selection) |
| { |
| if (AXObjectCache::accessibilityEnabled()) { |
| m_replacedRange.startIndex.value = indexForVisiblePosition(selection.visibleStart(), m_replacedRange.startIndex.scope); |
| if (selection.isRange()) { |
| m_replacedText = AccessibilityObject::stringForVisiblePositionRange(selection); |
| m_replacedRange.endIndex.value = indexForVisiblePosition(selection.visibleEnd(), m_replacedRange.endIndex.scope); |
| } else |
| m_replacedRange.endIndex = m_replacedRange.startIndex; |
| } |
| } |
| |
| void AccessibilityReplacedText::postTextStateChangeNotification(AXObjectCache* cache, AXTextEditType type, const String& text, const VisibleSelection& selection) |
| { |
| if (!cache) |
| return; |
| if (!AXObjectCache::accessibilityEnabled()) |
| return; |
| |
| VisiblePosition position = selection.start(); |
| auto node = highestEditableRoot(position.deepEquivalent(), HasEditableAXRole); |
| if (m_replacedText.length()) |
| cache->postTextReplacementNotification(node.get(), AXTextEditTypeDelete, m_replacedText, type, text, position); |
| else |
| cache->postTextStateChangeNotification(node.get(), type, text, position); |
| } |
| |
| std::atomic<bool> AXObjectCache::gAccessibilityEnabled = false; |
| bool AXObjectCache::gAccessibilityEnhancedUserInterfaceEnabled = false; |
| std::atomic<bool> AXObjectCache::gForceDeferredSpellChecking = false; |
| #if ENABLE(AX_THREAD_TEXT_APIS) |
| std::atomic<bool> AXObjectCache::gAccessibilityThreadTextApisEnabled = false; |
| #endif |
| std::atomic<bool> AXObjectCache::gForceInitialFrameCaching = false; |
| |
| bool AXObjectCache::accessibilityEnhancedUserInterfaceEnabled() |
| { |
| ASSERT(isMainThread()); |
| return gAccessibilityEnhancedUserInterfaceEnabled; |
| } |
| |
| void AXObjectCache::setEnhancedUserInterfaceAccessibility(bool flag) |
| { |
| ASSERT(isMainThread()); |
| gAccessibilityEnhancedUserInterfaceEnabled = flag; |
| #if PLATFORM(MAC) |
| if (flag) |
| enableAccessibility(); |
| #endif |
| } |
| |
| void AXObjectCache::setForceInitialFrameCaching(bool shouldForce) |
| { |
| gForceInitialFrameCaching = shouldForce; |
| } |
| |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| bool AXObjectCache::shouldServeInitialCachedFrame() |
| { |
| return !clientIsInTestMode() || forceInitialFrameCaching(); |
| } |
| |
| static const Seconds updateTreeSnapshotTimerInterval { 100_ms }; |
| #endif |
| |
| AXObjectCache::AXObjectCache(Page& page, Document* document) |
| : m_document(document) |
| , m_pageID(page.identifier()) |
| , m_notificationPostTimer(*this, &AXObjectCache::notificationPostTimerFired) |
| #if PLATFORM(COCOA) |
| , m_passwordNotificationTimer(*this, &AXObjectCache::passwordNotificationTimerFired) |
| #endif |
| , m_liveRegionChangedPostTimer(*this, &AXObjectCache::liveRegionChangedNotificationPostTimerFired) |
| , m_currentModalElement(nullptr) |
| , m_performCacheUpdateTimer(*this, &AXObjectCache::performCacheUpdateTimerFired) |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| , m_buildIsolatedTreeTimer(*this, &AXObjectCache::buildIsolatedTree) |
| , m_geometryManager(AXGeometryManager::create(*this)) |
| , m_selectedTextRangeTimer(*this, &AXObjectCache::selectedTextRangeTimerFired, platformSelectedTextRangeDebounceInterval()) |
| , m_updateTreeSnapshotTimer(*this, &AXObjectCache::updateTreeSnapshotTimerFired) |
| #endif |
| { |
| AXTRACE(makeString("AXObjectCache::AXObjectCache 0x"_s, hex(reinterpret_cast<uintptr_t>(this)))); |
| #ifndef NDEBUG |
| if (m_pageID) |
| AXLOG(makeString("pageID "_s, m_pageID->loggingString())); |
| else |
| AXLOG("No pageID."); |
| #endif |
| ASSERT(isMainThread()); |
| |
| #if ENABLE(AX_THREAD_TEXT_APIS) |
| gAccessibilityThreadTextApisEnabled = DeprecatedGlobalSettings::accessibilityThreadTextApisEnabled(); |
| #endif |
| |
| // If loading completed before the cache was created, loading progress will have been reset to zero. |
| // Consider loading progress to be 100% in this case. |
| m_loadingProgress = page.progress().estimatedProgress(); |
| if (m_loadingProgress <= 0) |
| m_loadingProgress = 1; |
| |
| if (m_pageID) |
| m_pageActivityState = page.activityState(); |
| AXTreeStore::add(m_id, WeakPtr { this }); |
| } |
| |
| AXObjectCache::~AXObjectCache() |
| { |
| AXTRACE(makeString("AXObjectCache::~AXObjectCache 0x"_s, hex(reinterpret_cast<uintptr_t>(this)))); |
| |
| m_notificationPostTimer.stop(); |
| m_liveRegionChangedPostTimer.stop(); |
| m_performCacheUpdateTimer.stop(); |
| |
| for (const auto& object : m_objects.values()) |
| object->detach(AccessibilityDetachmentType::CacheDestroyed); |
| |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| m_selectedTextRangeTimer.stop(); |
| m_updateTreeSnapshotTimer.stop(); |
| |
| if (m_pageID) { |
| if (auto tree = AXIsolatedTree::treeForPageID(*m_pageID)) |
| tree->setPageActivityState({ }); |
| AXIsolatedTree::removeTreeForPageID(*m_pageID); |
| } |
| #endif |
| |
| AXTreeStore::remove(m_id); |
| } |
| |
| bool AXObjectCache::isModalElement(Element& element) const |
| { |
| if (hasAnyRole(element, { "dialog"_s, "alertdialog"_s }) && equalLettersIgnoringASCIICase(element.attributeWithDefaultARIA(aria_modalAttr), "true"_s)) |
| return true; |
| |
| RefPtr dialog = dynamicDowncast<HTMLDialogElement>(element); |
| return dialog && dialog->isModal(); |
| } |
| |
| void AXObjectCache::findModalNodes() |
| { |
| // Traverse the DOM tree to look for the aria-modal=true nodes or modal <dialog> elements. |
| RefPtr document = this->document(); |
| for (Element* element = document ? ElementTraversal::firstWithin(document->rootNode()) : nullptr; element; element = ElementTraversal::nextIncludingPseudo(*element)) { |
| if (isModalElement(*element)) |
| m_modalElements.append(element); |
| } |
| |
| m_modalNodesInitialized = true; |
| } |
| |
| bool AXObjectCache::modalElementHasAccessibleContent(Element& element) |
| { |
| // Unless you're trying to compute the new modal node, determining whether an element |
| // has accessible content is as easy as !getOrCreate(element)->unignoredChildren().isEmpty(). |
| // So don't call this method on anything besides modal elements. |
| ASSERT(isModalElement(element)); |
| |
| // Because computing any object's unignored children is dependent on whether a modal is on the page, |
| // we'll need to walk the DOM and find non-ignored AX objects manually. |
| Vector<Node*> nodeStack = { element.firstChild() }; |
| while (!nodeStack.isEmpty()) { |
| for (auto* node = nodeStack.takeLast(); node; node = node->nextSibling()) { |
| if (auto* axObject = getOrCreate(*node)) { |
| if (!axObject->computeIsIgnored()) |
| return true; |
| |
| #if USE(ATSPI) |
| // When using ATSPI, an accessibility object with 'StaticText' role is ignored. |
| // Its content is exposed by its parent. |
| // Treat such elements as having accessible content. |
| // FIXME: This may not be sufficient for visibility:hidden or inert (https://bugs.webkit.org/show_bug.cgi?id=280914). |
| if (axObject->role() == AccessibilityRole::StaticText && !axObject->isAXHidden()) |
| return true; |
| #endif |
| } |
| |
| // Don't descend into subtrees for non-visible nodes. |
| if (isNodeVisible(node)) |
| nodeStack.append(node->firstChild()); |
| } |
| } |
| return false; |
| } |
| |
| void AXObjectCache::updateCurrentModalNode() |
| { |
| auto recomputeModalElement = [&] () -> Element* { |
| // There might be multiple modal dialog nodes. |
| // We use this function to pick the one we want. |
| if (m_modalElements.isEmpty()) |
| return nullptr; |
| |
| RefPtr document = this->document(); |
| if (!document) |
| return nullptr; |
| |
| // Pick the document active modal <dialog> element if it exists. |
| if (Element* activeModalDialog = document->activeModalDialog()) { |
| ASSERT(m_modalElements.contains(activeModalDialog)); |
| return activeModalDialog; |
| } |
| |
| SetForScope retrievingCurrentModalNode(m_isRetrievingCurrentModalNode, true); |
| // If any of the modal nodes contains the keyboard focus, we want to pick that one. |
| // If multiple contain the keyboard focus, we want the deepest. |
| // If no modal contains focus, we want to pick the last visible dialog in the DOM. |
| RefPtr<Element> focusedElement = document->focusedElement(); |
| RefPtr<Element> modalElementToReturn; |
| bool foundModalWithFocusInside = false; |
| for (auto& element : m_modalElements) { |
| // Elements in m_modalElementsSet may have become un-modal since we added them, but not yet removed |
| // as part of the asynchronous m_deferredModalChangedList handling. Skip these. |
| if (!element || !isModalElement(*element)) |
| continue; |
| |
| // To avoid trapping users in an empty modal, skip any non-visible element, or any element without accessible content. |
| if (!isNodeVisible(element.get()) || !modalElementHasAccessibleContent(*element)) |
| continue; |
| |
| bool focusIsInsideElement = focusedElement && focusedElement->isInclusiveDescendantOf(*element); |
| |
| // If the modal we found previously is a descendant of this one, prefer the descendant and skip this one. |
| if (modalElementToReturn && foundModalWithFocusInside && modalElementToReturn->isDescendantOf(*element)) |
| continue; |
| |
| // If we already found a modal that focus is inside, and this one doesn't have focus inside, skip in favor of the one with focus inside. |
| if (modalElementToReturn && foundModalWithFocusInside && !focusIsInsideElement) |
| continue; |
| |
| modalElementToReturn = element.get(); |
| if (focusIsInsideElement) |
| foundModalWithFocusInside = true; |
| } |
| |
| if (!focusedElement || !foundModalWithFocusInside) |
| return nullptr; |
| |
| RefPtr object = getOrCreate(modalElementToReturn.get()); |
| if (!object || object->isAXHidden()) |
| return nullptr; |
| return modalElementToReturn.get(); |
| }; |
| |
| auto* previousModal = m_currentModalElement.get(); |
| m_currentModalElement = recomputeModalElement(); |
| if (previousModal != m_currentModalElement.get()) { |
| childrenChanged(rootWebArea()); |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| // Because the presence of a modal affects every element on the page, |
| // regenerate the entire isolated tree with the next cache update. |
| m_deferredRegenerateIsolatedTree = true; |
| #endif |
| } |
| } |
| |
| bool AXObjectCache::isNodeVisible(const Node* node) const |
| { |
| RefPtr element = dynamicDowncast<Element>(node); |
| if (!element) |
| return false; |
| |
| auto* renderer = element->renderer(); |
| if (!renderer) |
| return false; |
| |
| const auto& style = renderer->style(); |
| if (style.display() == DisplayType::None) |
| return false; |
| |
| auto* renderLayer = renderer->enclosingLayer(); |
| if (isVisibilityHidden(style) && renderLayer && !renderLayer->hasVisibleContent()) |
| return false; |
| |
| // Check whether this object or any of its ancestors has opacity 0. |
| // The resulting opacity of a RenderObject is computed as the multiplication |
| // of its opacity times the opacities of its ancestors. |
| for (auto* ancestor = renderer; ancestor; ancestor = ancestor->parent()) { |
| if (!ancestor->style().opacity()) |
| return false; |
| } |
| |
| // We also need to consider aria hidden status. |
| return !equalLettersIgnoringASCIICase(element->attributeWithDefaultARIA(aria_hiddenAttr), "true"_s) || element->focused(); |
| } |
| |
| // This function returns the valid aria modal node. |
| Node* AXObjectCache::modalNode() |
| { |
| if (!m_modalNodesInitialized) |
| findModalNodes(); |
| |
| if (m_modalElements.isEmpty()) |
| return nullptr; |
| |
| // Check the cached current valid aria modal node first. |
| // Usually when one dialog sets aria-modal=true, that dialog is the one we want. |
| if (isNodeVisible(m_currentModalElement.get())) |
| return m_currentModalElement.get(); |
| |
| // Recompute the valid aria modal node when m_currentModalElement is null or hidden. |
| updateCurrentModalNode(); |
| return m_currentModalElement.get(); |
| } |
| |
| AccessibilityObject* AXObjectCache::focusedImageMapUIElement(HTMLAreaElement& areaElement) |
| { |
| // Find the corresponding accessibility object for the HTMLAreaElement. This should be |
| // in the list of children for its corresponding image. |
| RefPtr imageElement = areaElement.imageElement(); |
| if (!imageElement) |
| return nullptr; |
| |
| RefPtr axRenderImage = areaElement.protectedDocument()->axObjectCache()->getOrCreate(*imageElement); |
| if (!axRenderImage) |
| return nullptr; |
| |
| for (const auto& child : axRenderImage->unignoredChildren()) { |
| auto* imageMapLink = dynamicDowncast<AccessibilityImageMapLink>(child.get()); |
| if (imageMapLink && imageMapLink->node() == &areaElement) |
| return imageMapLink; |
| } |
| return nullptr; |
| } |
| |
| AccessibilityObject* AXObjectCache::focusedObjectForPage(const Page* page) |
| { |
| ASSERT(isMainThread()); |
| |
| if (!gAccessibilityEnabled) |
| return nullptr; |
| |
| // get the focused node in the page |
| RefPtr focusedOrMainFrame = page->checkedFocusController()->focusedOrMainFrame(); |
| if (!focusedOrMainFrame) |
| return nullptr; |
| RefPtr document = focusedOrMainFrame->document(); |
| if (!document) |
| return nullptr; |
| |
| document->updateStyleIfNeeded(); |
| if (RefPtr focusedElement = document->focusedElement()) |
| return focusedObjectForNode(focusedElement.get()); |
| return focusedObjectForNode(document.get()); |
| } |
| |
| AccessibilityObject* AXObjectCache::focusedObjectForNode(Node* focusedNode) |
| { |
| if (auto* area = dynamicDowncast<HTMLAreaElement>(focusedNode)) |
| return focusedImageMapUIElement(*area); |
| |
| auto* focus = getOrCreate(focusedNode); |
| if (!focus) |
| return nullptr; |
| |
| if (focus->shouldFocusActiveDescendant()) { |
| if (auto* descendant = focus->activeDescendant()) |
| return dynamicDowncast<AccessibilityObject>(descendant); |
| } |
| |
| if (focus->isIgnored()) |
| return focus->parentObjectUnignored(); |
| return focus; |
| } |
| |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| void AXObjectCache::setIsolatedTreeFocusedObject(AccessibilityObject* focus) |
| { |
| ASSERT(isMainThread()); |
| |
| if (auto tree = AXIsolatedTree::treeForPageID(m_pageID)) |
| tree->setFocusedNodeID(focus ? std::optional { focus->objectID() } : std::nullopt); |
| } |
| #endif |
| |
| AccessibilityObject* AXObjectCache::get(Node& node) const |
| { |
| auto* renderer = node.renderer(); |
| auto renderID = renderer ? m_renderObjectMapping.getOptional(*renderer) : std::nullopt; |
| if (renderID) |
| return m_objects.get(*renderID); |
| |
| auto nodeID = m_nodeObjectMapping.get(node); |
| return nodeID ? m_objects.get(*nodeID) : nullptr; |
| } |
| |
| ContainerNode* composedParentIgnoringDocumentFragments(Node& node) |
| { |
| RefPtr ancestor = node.parentInComposedTree(); |
| while (is<DocumentFragment>(ancestor.get())) |
| ancestor = ancestor->parentInComposedTree(); |
| return ancestor.get(); |
| } |
| |
| ContainerNode* composedParentIgnoringDocumentFragments(Node* node) |
| { |
| return node ? composedParentIgnoringDocumentFragments(*node) : nullptr; |
| } |
| |
| ElementName elementName(Node* node) |
| { |
| auto* element = dynamicDowncast<Element>(node); |
| return element ? element->elementName() : ElementName::Unknown; |
| } |
| |
| ElementName elementName(Node& node) |
| { |
| auto* element = dynamicDowncast<Element>(node); |
| return element ? element->elementName() : ElementName::Unknown; |
| } |
| |
| bool hasAccNameAttribute(Element& element) |
| { |
| auto trimmed = [&] (const auto& attribute) { |
| const auto& value = element.attributeWithDefaultARIA(attribute); |
| if (value.isEmpty()) |
| return emptyString(); |
| auto copy = value.string(); |
| return copy.trim(isASCIIWhitespace); |
| }; |
| |
| // Avoid calculating the actual description here (e.g. resolving aria-labelledby), as it's expensive. |
| // The spec is generally permissive in allowing user agents to not ensure complete validity of these attributes. |
| // For example, https://w3c.github.io/svg-aam/#include_elements: |
| // "It has an ‘aria-labelledby’ attribute or ‘aria-describedby’ attribute containing valid IDREF tokens. User agents MAY include elements with these attributes without checking for validity." |
| if (trimmed(aria_labelAttr).length() || trimmed(aria_labelledbyAttr).length() || trimmed(aria_labeledbyAttr).length() || trimmed(aria_descriptionAttr).length() || trimmed(aria_describedbyAttr).length()) |
| return true; |
| |
| return element.attributeWithoutSynchronization(titleAttr).length(); |
| } |
| |
| static RenderImage* toSimpleImage(RenderObject& renderer) |
| { |
| CheckedPtr renderImage = dynamicDowncast<RenderImage>(renderer); |
| if (!renderImage) |
| return nullptr; |
| |
| // Exclude ImageButtons because they are treated as buttons, not as images. |
| RefPtr node = renderer.node(); |
| if (is<HTMLInputElement>(node)) |
| return nullptr; |
| |
| // ImageMaps are not simple images. |
| if (renderImage->imageMap()) |
| return nullptr; |
| |
| if (RefPtr imgElement = dynamicDowncast<HTMLImageElement>(node); imgElement && imgElement->hasAttributeWithoutSynchronization(usemapAttr)) |
| return nullptr; |
| |
| #if ENABLE(VIDEO) |
| // Exclude video and audio elements. |
| if (is<HTMLMediaElement>(node)) |
| return nullptr; |
| #endif // ENABLE(VIDEO) |
| |
| return renderImage.get(); |
| } |
| |
| // FIXME: This probably belongs on Element. |
| bool hasRole(Element& element, StringView role) |
| { |
| auto roleValue = element.attributeWithDefaultARIA(roleAttr); |
| if (role.isNull()) |
| return roleValue.isEmpty(); |
| if (roleValue.isEmpty()) |
| return false; |
| |
| return SpaceSplitString::spaceSplitStringContainsValue(roleValue, role, SpaceSplitString::ShouldFoldCase::Yes); |
| } |
| |
| bool hasAnyRole(Element& element, Vector<StringView>&& roles) |
| { |
| auto roleValue = element.attributeWithDefaultARIA(roleAttr); |
| if (roleValue.isEmpty()) |
| return false; |
| |
| for (const auto& role : roles) { |
| ASSERT(!role.isEmpty()); |
| if (SpaceSplitString::spaceSplitStringContainsValue(roleValue, role, SpaceSplitString::ShouldFoldCase::Yes)) |
| return true; |
| } |
| return false; |
| } |
| |
| bool hasAnyRole(Element* element, Vector<StringView>&& roles) |
| { |
| return element ? hasAnyRole(*element, WTFMove(roles)) : false; |
| } |
| |
| bool hasTableRole(Element& element) |
| { |
| return hasAnyRole(element, { "grid"_s, "table"_s, "treegrid"_s }); |
| } |
| |
| bool hasCellARIARole(Element& element) |
| { |
| return hasAnyRole(element, { "gridcell"_s, "cell"_s, "columnheader"_s, "rowheader"_s }); |
| } |
| |
| bool hasPresentationRole(Element& element) |
| { |
| return hasAnyRole(element, { "presentation"_s, "none"_s }); |
| } |
| |
| bool isRowGroup(Element& element) |
| { |
| auto name = element.elementName(); |
| return name == ElementName::HTML_thead || name == ElementName::HTML_tbody || name == ElementName::HTML_tfoot || hasRole(element, "rowgroup"_s); |
| } |
| |
| bool isRowGroup(Node* node) |
| { |
| auto* element = dynamicDowncast<Element>(node); |
| return element && isRowGroup(*element); |
| } |
| |
| static bool isAccessibilityList(Element& element) |
| { |
| if (hasAnyRole(element, { "list"_s, "directory"_s })) |
| return true; |
| |
| // Call it a list if it has no ARIA role and a list tag. |
| auto name = element.elementName(); |
| return hasRole(element, nullAtom()) && (name == ElementName::HTML_ul || name == ElementName::HTML_ol || name == ElementName::HTML_dl || name == ElementName::HTML_menu); |
| } |
| |
| static bool isAccessibilityTree(Element& element) |
| { |
| return hasRole(element, "tree"_s); |
| } |
| |
| static bool isAccessibilityTreeItem(Element& element) |
| { |
| return hasRole(element, "treeitem"_s); |
| } |
| |
| static bool isAccessibilityTable(Node* node) |
| { |
| return is<HTMLTableElement>(node); |
| } |
| |
| static bool isAccessibilityTableRow(Node* node) |
| { |
| return is<HTMLTableRowElement>(node); |
| } |
| |
| static bool isAccessibilityTableCell(Node* node) |
| { |
| return is<HTMLTableCellElement>(node); |
| } |
| |
| static bool isAccessibilityARIATable(Element& element) |
| { |
| return hasTableRole(element); |
| } |
| |
| static bool isAccessibilityARIAGridRow(Element& element) |
| { |
| return hasRole(element, "row"_s); |
| } |
| |
| static bool isAccessibilityARIAGridCell(Element& element) |
| { |
| return hasCellARIARole(element); |
| } |
| |
| Ref<AccessibilityRenderObject> AXObjectCache::createObjectFromRenderer(RenderObject& renderer) |
| { |
| RefPtr node = renderer.node(); |
| if (auto* element = dynamicDowncast<Element>(node.get())) { |
| if (isAccessibilityList(*element)) |
| return AccessibilityList::create(AXID::generate(), renderer); |
| |
| if (isAccessibilityARIATable(*element)) |
| return AccessibilityARIATable::create(AXID::generate(), renderer); |
| if (isAccessibilityARIAGridRow(*element)) |
| return AccessibilityARIAGridRow::create(AXID::generate(), renderer); |
| if (isAccessibilityARIAGridCell(*element)) |
| return AccessibilityARIAGridCell::create(AXID::generate(), renderer); |
| |
| if (isAccessibilityTree(*element)) |
| return AccessibilityTree::create(AXID::generate(), renderer); |
| if (isAccessibilityTreeItem(*element)) |
| return AccessibilityTreeItem::create(AXID::generate(), renderer); |
| |
| if (is<HTMLLabelElement>(*element) && hasRole(*element, nullAtom())) |
| return AccessibilityLabel::create(AXID::generate(), renderer); |
| |
| #if PLATFORM(IOS_FAMILY) |
| if (is<HTMLMediaElement>(*element) && hasRole(*element, nullAtom())) |
| return AccessibilityMediaObject::create(AXID::generate(), renderer); |
| #endif |
| } |
| |
| if (renderer.isRenderOrLegacyRenderSVGRoot()) |
| return AccessibilitySVGRoot::create(AXID::generate(), renderer, this); |
| |
| if (is<SVGElement>(node) || is<RenderSVGInlineText>(renderer)) |
| return AccessibilitySVGObject::create(AXID::generate(), renderer, this); |
| |
| if (auto* renderImage = toSimpleImage(renderer)) |
| return AXImage::create(AXID::generate(), *renderImage); |
| |
| #if ENABLE(MATHML) |
| // The mfenced element creates anonymous RenderMathMLOperators which should be treated |
| // as MathML elements and assigned the MathElementRole so that platform logic regarding |
| // inclusion and role mapping is not bypassed. |
| bool isAnonymousOperator = renderer.isAnonymous() && is<RenderMathMLOperator>(renderer); |
| if (isAnonymousOperator || is<MathMLElement>(node)) |
| return AccessibilityMathMLElement::create(AXID::generate(), renderer, isAnonymousOperator); |
| #endif |
| |
| if (is<RenderListBox>(renderer)) |
| return AccessibilityListBox::create(AXID::generate(), renderer); |
| if (CheckedPtr renderMenuList = dynamicDowncast<RenderMenuList>(renderer)) |
| return AccessibilityMenuList::create(AXID::generate(), *renderMenuList, *this); |
| |
| bool isAnonymous = false; |
| #if USE(ATSPI) |
| // This branch is only necessary because ATSPI walks the render tree rather than the DOM to build the accessibility tree. |
| // FIXME: Consider removing this with https://bugs.webkit.org/show_bug.cgi?id=282117. |
| isAnonymous = renderer.isAnonymous(); |
| #endif |
| // Some websites put display:table on tbody / thead / tfoot, resulting in a RenderTable being generated. |
| // We don't want to consider these tables (since they are typically wrapped by an actual <table> element), |
| // so only create an AccessibilityTable when !is<HTMLTableSectionElement>. |
| if ((is<RenderTable>(renderer) && !isAnonymous && !is<HTMLTableSectionElement>(node.get())) || isAccessibilityTable(node.get())) |
| return AccessibilityTable::create(AXID::generate(), renderer); |
| if ((is<RenderTableRow>(renderer) && !isAnonymous) || isAccessibilityTableRow(node.get())) |
| return AccessibilityTableRow::create(AXID::generate(), renderer); |
| if ((is<RenderTableCell>(renderer) && !isAnonymous) || isAccessibilityTableCell(node.get())) |
| return AccessibilityTableCell::create(AXID::generate(), renderer); |
| |
| // Progress indicator. |
| if (is<RenderProgress>(renderer) || is<RenderMeter>(renderer) |
| || is<HTMLProgressElement>(node) || is<HTMLMeterElement>(node)) |
| return AccessibilityProgressIndicator::create(AXID::generate(), renderer); |
| |
| #if ENABLE(ATTACHMENT_ELEMENT) |
| if (auto* renderAttachment = dynamicDowncast<RenderAttachment>(renderer)) |
| return AccessibilityAttachment::create(AXID::generate(), *renderAttachment); |
| #endif |
| |
| // input type=range |
| if (is<RenderSlider>(renderer)) |
| return AccessibilitySlider::create(AXID::generate(), renderer); |
| |
| return AccessibilityRenderObject::create(AXID::generate(), renderer); |
| } |
| |
| Ref<AccessibilityNodeObject> AXObjectCache::createFromNode(Node& node) |
| { |
| if (auto* element = dynamicDowncast<Element>(node)) { |
| if (isAccessibilityList(*element)) |
| return AccessibilityList::create(AXID::generate(), *element); |
| if (isAccessibilityTable(element)) |
| return AccessibilityTable::create(AXID::generate(), *element); |
| if (isAccessibilityTableRow(element)) |
| return AccessibilityTableRow::create(AXID::generate(), *element); |
| if (isAccessibilityTableCell(element)) |
| return AccessibilityTableCell::create(AXID::generate(), *element); |
| if (isAccessibilityTree(*element)) |
| return AccessibilityTree::create(AXID::generate(), *element); |
| if (isAccessibilityTreeItem(*element)) |
| return AccessibilityTreeItem::create(AXID::generate(), *element); |
| if (isAccessibilityARIATable(*element)) |
| return AccessibilityARIATable::create(AXID::generate(), *element); |
| if (isAccessibilityARIAGridRow(*element)) |
| return AccessibilityARIAGridRow::create(AXID::generate(), *element); |
| if (isAccessibilityARIAGridCell(*element)) |
| return AccessibilityARIAGridCell::create(AXID::generate(), *element); |
| if (auto* areaElement = dynamicDowncast<HTMLAreaElement>(*element)) |
| return AccessibilityImageMapLink::create(AXID::generate(), *areaElement); |
| } |
| return AccessibilityNodeObject::create(AXID::generate(), node); |
| } |
| |
| void AXObjectCache::cacheAndInitializeWrapper(AccessibilityObject& newObject, DOMObjectVariant domObject) |
| { |
| AXID axID = newObject.objectID(); |
| |
| WTF::switchOn(domObject, |
| [&] (RenderObject* typedValue) { |
| m_renderObjectMapping.set(*typedValue, axID); |
| if (auto* node = typedValue->node()) { |
| // If this node existed in the m_nodeObjectMapping (ie. it is replacing an old object), we should |
| // update the object mapping so it is up to date the next time it is replaced. |
| auto objectMapIterator = m_nodeObjectMapping.find(*node); |
| if (objectMapIterator != m_nodeObjectMapping.end()) |
| objectMapIterator->value = axID; |
| } |
| }, |
| [&] (Node* typedValue) { m_nodeObjectMapping.set(*typedValue, axID); }, |
| [&] (Widget* typedValue) { m_widgetObjectMapping.set(*typedValue, axID); }, |
| [] (auto&) { } |
| ); |
| |
| m_objects.set(axID, newObject); |
| newObject.init(); |
| attachWrapper(newObject); |
| } |
| |
| AccessibilityObject* AXObjectCache::getOrCreate(Widget& widget) |
| { |
| if (auto* object = get(widget)) |
| return object; |
| |
| RefPtr<AccessibilityObject> newObject; |
| if (auto* scrollView = dynamicDowncast<ScrollView>(widget)) |
| newObject = AccessibilityScrollView::create(AXID::generate(), *scrollView); |
| else if (auto* scrollbar = dynamicDowncast<Scrollbar>(widget)) |
| newObject = AccessibilityScrollbar::create(AXID::generate(), *scrollbar); |
| |
| // Will crash later if we have two objects for the same widget. |
| ASSERT(!get(widget)); |
| |
| // Ensure we weren't given an unsupported widget type. |
| ASSERT(newObject); |
| if (!newObject) |
| return nullptr; |
| |
| cacheAndInitializeWrapper(*newObject, &widget); |
| return newObject.get(); |
| } |
| |
| AccessibilityObject* AXObjectCache::getOrCreate(Node& node, IsPartOfRelation isPartOfRelation) |
| { |
| if (auto* object = get(node)) |
| return object; |
| |
| if (auto* renderer = node.renderer()) |
| return getOrCreate(*renderer); |
| |
| RefPtr composedParent = node.parentElementInComposedTree(); |
| if (!composedParent) |
| return nullptr; |
| |
| Ref protectedNode { node }; |
| |
| auto* optionElement = dynamicDowncast<HTMLOptionElement>(node); |
| auto* optGroupElement = dynamicDowncast<HTMLOptGroupElement>(node); |
| if (optionElement || optGroupElement) { |
| auto* select = optionElement |
| ? optionElement->ownerSelectElement() |
| : optGroupElement->ownerSelectElement(); |
| if (!select) |
| return nullptr; |
| RefPtr<AccessibilityObject> object; |
| if (select->usesMenuList()) { |
| if (!optionElement || !select->renderer()) |
| return nullptr; |
| object = AccessibilityMenuListOption::create(AXID::generate(), *optionElement); |
| } else |
| object = AccessibilityListBoxOption::create(AXID::generate(), downcast<HTMLElement>(node)); |
| cacheAndInitializeWrapper(*object, &node); |
| return object.get(); |
| } |
| |
| bool inCanvasSubtree = lineageOfType<HTMLCanvasElement>(*composedParent).first(); |
| if (inCanvasSubtree) { |
| // Don't include objects that are descendants of user agent shadow trees. For example, in this HTML: |
| // <canvas><input type="text"></canvas> |
| // <input type="text"> generates a user agent shadow root to host a <div contenteditable>. |
| // We already handle elements that host user agent shadows specially, so don't include their |
| // descendants (like this <div>) just because they happen to be within <canvas>. |
| if (auto* shadowRoot = composedParent->shadowRoot()) |
| inCanvasSubtree = !shadowRoot->isUserAgentShadowRoot(); |
| } |
| // If node is the target of a relationship or a descendant of one, create an AX object unconditionally. |
| if (isPartOfRelation == IsPartOfRelation::No && !isDescendantOfRelatedNode(node)) { |
| bool insideMeterElement = is<HTMLMeterElement>(*composedParent); |
| auto* element = dynamicDowncast<Element>(node); |
| bool hasDisplayContents = element && element->hasDisplayContents(); |
| bool isPopover = element && element->hasAttributeWithoutSynchronization(popoverAttr); |
| bool isAreaElement = is<HTMLAreaElement>(element); |
| if (!inCanvasSubtree && !insideMeterElement && !hasDisplayContents && !isPopover && !isNodeFocused(node) && !isAreaElement) |
| return nullptr; |
| } |
| |
| // The object may have already been created during relations update. |
| if (auto* object = get(node)) |
| return object; |
| |
| // Fallback content is only focusable as long as the canvas is displayed and visible. |
| // Update the style before Element::isFocusable() gets called. |
| if (inCanvasSubtree) |
| node.protectedDocument()->updateStyleIfNeeded(); |
| |
| RefPtr newObject = createFromNode(node); |
| |
| // Will crash later if we have two objects for the same node. |
| ASSERT(!get(node)); |
| cacheAndInitializeWrapper(*newObject, &node); |
| // Compute the object's initial ignored status. |
| newObject->recomputeIsIgnored(); |
| // Sometimes asking isIgnored() will cause the newObject to be deallocated, and then |
| // it will disappear when this function is finished, leading to a use-after-free. |
| if (newObject->isDetached()) |
| return nullptr; |
| return newObject.get(); |
| } |
| |
| AccessibilityObject* AXObjectCache::getOrCreate(RenderObject& renderer) |
| { |
| if (auto* object = get(renderer)) |
| return object; |
| |
| // Don't create an object for this renderer if it's being destroyed. |
| if (renderer.beingDestroyed()) |
| return nullptr; |
| |
| Ref object = createObjectFromRenderer(renderer); |
| |
| // Will crash later if we have two objects for the same renderer. |
| ASSERT(!get(renderer)); |
| |
| cacheAndInitializeWrapper(object.get(), &renderer); |
| // Compute the object's initial ignored status. |
| object->recomputeIsIgnored(); |
| // Sometimes asking isIgnored() will cause the newObject to be deallocated, and then |
| // it will disappear when this function is finished, leading to a use-after-free. |
| if (object->isDetached()) |
| return nullptr; |
| |
| return object.ptr(); |
| } |
| |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| RefPtr<AXIsolatedTree> AXObjectCache::getOrCreateIsolatedTree() |
| { |
| AXTRACE(makeString("AXObjectCache::getOrCreateIsolatedTree 0x"_s, hex(reinterpret_cast<uintptr_t>(this)))); |
| ASSERT(isMainThread()); |
| |
| if (!m_pageID) |
| return nullptr; |
| |
| RefPtr tree = AXIsolatedTree::treeForPageID(m_pageID); |
| if (tree) |
| return tree; |
| |
| // A new isolated tree needs to be created. Initialize the GeometryManager primary screen rect to be ready when needed. |
| m_geometryManager->initializePrimaryScreenRect(); |
| // Schedule a paint to cache the rects for the objects in this new isolated tree. |
| scheduleObjectRegionsUpdate(true /* scheduleImmediately */); |
| |
| // This method can be called as the result of a client request. Since creating the isolated tree can take long, |
| // especially for large documents, for real clients we build a temporary "empty" isolated tree consisting only of the ScrollView and the WebArea objects. |
| // Then we schedule building the entire isolated tree on a Timer. |
| // For test clients, LayoutTests or XCTests, build the whole isolated tree. |
| if (!clientIsInTestMode()) [[likely]] { |
| tree = AXIsolatedTree::createEmpty(*this); |
| if (!m_buildIsolatedTreeTimer.isActive()) |
| m_buildIsolatedTreeTimer.startOneShot(0_s); |
| } else |
| tree = AXIsolatedTree::create(*this); |
| |
| initializeAXThreadIfNeeded(); |
| |
| return tree; |
| } |
| |
| void AXObjectCache::buildIsolatedTree() |
| { |
| m_buildIsolatedTreeTimer.stop(); |
| |
| if (!m_pageID) |
| return; |
| |
| auto tree = AXIsolatedTree::create(*this); |
| if (RefPtr webArea = rootWebArea()) { |
| postPlatformNotification(*webArea, AXNotification::LoadComplete); |
| postPlatformNotification(*webArea, AXNotification::FocusedUIElementChanged); |
| } |
| } |
| |
| void AXObjectCache::setIsolatedTree(Ref<AXIsolatedTree> tree) |
| { |
| ASSERT(isMainThread()); |
| if (RefPtr frame = m_document ? m_document->frame() : nullptr) |
| frame->loader().client().setIsolatedTree(WTFMove(tree)); |
| } |
| #endif |
| |
| AXCoreObject* AXObjectCache::rootObjectForFrame(LocalFrame& frame) |
| { |
| if (!gAccessibilityEnabled) |
| return nullptr; |
| |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| if (isIsolatedTreeEnabled()) { |
| RefPtr tree = getOrCreateIsolatedTree(); |
| if (!isMainThread()) { |
| tree->applyPendingChanges(); |
| return tree->rootNode(); |
| } |
| } |
| #endif |
| |
| return getOrCreate(frame.view()); |
| } |
| |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| void AXObjectCache::buildIsolatedTreeIfNeeded() |
| { |
| if (!gAccessibilityEnabled) |
| return; |
| |
| if (isIsolatedTreeEnabled()) |
| getOrCreateIsolatedTree(); |
| } |
| #endif |
| |
| AccessibilityObject* AXObjectCache::create(AccessibilityRole role) |
| { |
| RefPtr<AccessibilityObject> object; |
| switch (role) { |
| case AccessibilityRole::Column: |
| object = AccessibilityTableColumn::create(AXID::generate()); |
| break; |
| case AccessibilityRole::TableHeaderContainer: |
| object = AccessibilityTableHeaderContainer::create(AXID::generate()); |
| break; |
| case AccessibilityRole::RemoteFrame: |
| object = AXRemoteFrame::create(AXID::generate()); |
| break; |
| case AccessibilityRole::SliderThumb: |
| object = AccessibilitySliderThumb::create(AXID::generate()); |
| break; |
| case AccessibilityRole::MenuListPopup: |
| object = AccessibilityMenuListPopup::create(AXID::generate()); |
| break; |
| case AccessibilityRole::SpinButton: |
| object = AccessibilitySpinButton::create(AXID::generate(), *this); |
| break; |
| case AccessibilityRole::SpinButtonPart: |
| object = AccessibilitySpinButtonPart::create(AXID::generate()); |
| break; |
| default: |
| break; |
| } |
| |
| if (!object) |
| return nullptr; |
| |
| cacheAndInitializeWrapper(*object); |
| return object.get(); |
| } |
| |
| void AXObjectCache::remove(std::optional<AXID> axID) |
| { |
| AXTRACE(makeString("AXObjectCache::remove 0x"_s, hex(reinterpret_cast<uintptr_t>(this)))); |
| AXLOG(makeString("AXID "_s, axID ? axID->loggingString() : ""_s)); |
| |
| if (!axID) |
| return; |
| |
| RefPtr object = m_objects.take(*axID); |
| if (!object) |
| return; |
| |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| unsigned liveRegionsRemoved = m_sortedLiveRegionIDs.removeAll(*axID); |
| unsigned webAreasRemoved = m_sortedNonRootWebAreaIDs.removeAll(*axID); |
| |
| if (RefPtr tree = AXIsolatedTree::treeForPageID(m_pageID)) { |
| tree->queueNodeRemoval(*object); |
| |
| if (liveRegionsRemoved) |
| tree->sortedLiveRegionsDidChange(m_sortedLiveRegionIDs); |
| else if (webAreasRemoved) |
| tree->sortedNonRootWebAreasDidChange(m_sortedNonRootWebAreaIDs); |
| } |
| #endif // ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| |
| removeAllRelations(*axID); |
| object->detach(AccessibilityDetachmentType::ElementDestroyed); |
| |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| m_geometryManager->remove(*axID); |
| #endif |
| } |
| |
| void AXObjectCache::remove(RenderObject& renderer) |
| { |
| AXTRACE(makeString("AXObjectCache::remove RenderObject* 0x"_s, hex(reinterpret_cast<uintptr_t>(this)))); |
| remove(m_renderObjectMapping.takeOptional(renderer)); |
| } |
| |
| void AXObjectCache::remove(Node& node) |
| { |
| AXTRACE(makeString("AXObjectCache::remove Node& 0x"_s, hex(reinterpret_cast<uintptr_t>(this)))); |
| |
| remove(m_nodeObjectMapping.take(node)); |
| if (auto* renderer = node.renderer()) |
| remove(*renderer); |
| |
| // If we're in the middle of a cache update, don't modify any of these vectors because we are currently |
| // iterating over them. They will be cleared at the end of the cache update, so not removing them here is fine. |
| if (m_performingDeferredCacheUpdate) { |
| AXLOG("Bailing out before removing node from m_deferred* vectors as we are in the middle of a cache update."); |
| return; |
| } |
| |
| // We cannot use RefPtr here as node's m_deletionHasBegun is true. |
| if (auto* nodeElement = dynamicDowncast<Element>(node)) { |
| m_deferredTextFormControlValue.remove(*nodeElement); |
| m_deferredAttributeChange.removeAllMatching([&node] (const auto& entry) { |
| return entry.element == &node; |
| }); |
| m_modalElements.removeAllMatching([&nodeElement] (const auto& element) { |
| return nodeElement == element.get(); |
| }); |
| m_deferredRecomputeIsIgnoredList.remove(*nodeElement); |
| m_deferredRecomputeTableIsExposedList.remove(*nodeElement); |
| m_deferredSelectedChildredChangedList.remove(*nodeElement); |
| m_deferredModalChangedList.remove(*nodeElement); |
| m_deferredMenuListChange.remove(*nodeElement); |
| m_deferredElementAddedOrRemovedList.remove(*nodeElement); |
| } |
| m_deferredTextChangedList.remove(node); |
| } |
| |
| void AXObjectCache::remove(Widget& view) |
| { |
| remove(m_widgetObjectMapping.takeOptional(view)); |
| if (auto* scrollView = dynamicDowncast<ScrollView>(view)) |
| m_deferredScrollbarUpdateChangeList.remove(*scrollView); |
| } |
| |
| void AXObjectCache::handleTextChanged(AccessibilityObject* object) |
| { |
| AXTRACE(makeString("AXObjectCache::handleTextChanged 0x"_s, hex(reinterpret_cast<uintptr_t>(this)))); |
| AXLOG(object); |
| |
| if (!object) |
| return; |
| |
| Ref<AccessibilityObject> protectedObject(*object); |
| |
| bool isText = object->isStaticText(); |
| // If this element supports ARIA live regions, or is part of a region with an ARIA editable role, |
| // then notify the AT of changes. |
| bool notifiedNonNativeTextControl = false; |
| for (RefPtr ancestor = object; ancestor; ancestor = ancestor->parentObject()) { |
| if (ancestor->supportsLiveRegion()) |
| postLiveRegionChangeNotification(*ancestor); |
| |
| if (!notifiedNonNativeTextControl && ancestor->isNonNativeTextControl()) { |
| postNotification(ancestor.get(), ancestor->protectedDocument().get(), AXNotification::ValueChanged); |
| notifiedNonNativeTextControl = true; |
| } |
| |
| if (isText) { |
| bool dependsOnTextUnderElement = ancestor->dependsOnTextUnderElement(); |
| auto role = ancestor->role(); |
| dependsOnTextUnderElement |= role == AccessibilityRole::Label || role == AccessibilityRole::TextField; |
| |
| // If the starting object is a static text, its underlying text has changed. |
| if (dependsOnTextUnderElement) { |
| // Inform this ancestor its textUnderElement-dependent data is now out-of-date. |
| postNotification(ancestor.get(), nullptr, AXNotification::TextUnderElementChanged); |
| } |
| |
| // Any objects this ancestor labeled now also need new AccessibilityText. |
| auto labeledObjects = ancestor->labelForObjects(); |
| for (const auto& labeledObject : labeledObjects) |
| postNotification(&downcast<AccessibilityObject>(labeledObject.get()), nullptr, AXNotification::TextChanged); |
| } |
| } |
| |
| postNotification(object, object->protectedDocument().get(), AXNotification::TextChanged); |
| object->recomputeIsIgnored(); |
| } |
| |
| void AXObjectCache::onRendererCreated(Element& element) |
| { |
| if (!element.renderer()) { |
| ASSERT_NOT_REACHED(); |
| return; |
| } |
| |
| // If there is already an AXObject that was created for this element, |
| // remove it since there will be a new AXRenderObject created using the renderer. |
| if (auto axID = m_nodeObjectMapping.get(element)) { |
| // The removal needs to be async because this is called during a RenderTree |
| // update and remove(AXID) updates the isolated tree, that in turn calls |
| // parentObjectUnignored() on the object being removed, that may result |
| // in a call to textUnderElement, that can not be called during a layout. |
| m_deferredReplacedObjects.add(*axID); |
| if (!m_performCacheUpdateTimer.isActive()) |
| m_performCacheUpdateTimer.startOneShot(0_s); |
| } |
| } |
| |
| void AXObjectCache::onRendererCreated(Text& textNode) |
| { |
| if (!textNode.renderer()) { |
| ASSERT_NOT_REACHED(); |
| return; |
| } |
| |
| // If we created an AccessibilityNodeObject for this Text, remove it since there should |
| // be a new AccessibilityRenderObject created using the renderer. |
| if (auto axID = m_nodeObjectMapping.get(textNode)) { |
| if (RefPtr nodeObject = get(textNode)) { |
| if (auto* parent = nodeObject->parentObject()) { |
| remove(textNode); |
| childrenChanged(parent); |
| return; |
| } |
| // We don't need to add nodeObject to m_deferredReplacedObjects, as that currently |
| // only serves to repair relationships for replaced objects, which text nodes cannot |
| // possibly be part of (because they are not elements). |
| } |
| } |
| } |
| |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| static bool isClickEvent(const AtomString& eventType) |
| { |
| return eventType == eventNames().clickEvent |
| || eventType == eventNames().mousedownEvent |
| || eventType == eventNames().mouseupEvent; |
| } |
| #endif // ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| |
| void AXObjectCache::onDragElementChanged(Element* oldElement, Element* newElement) |
| { |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| if (oldElement == newElement) |
| return; |
| |
| if (oldElement) |
| postNotification(get(*oldElement), AXNotification::GrabbedStateChanged); |
| |
| if (newElement) |
| postNotification(get(*newElement), AXNotification::GrabbedStateChanged); |
| #else |
| UNUSED_PARAM(oldElement); |
| UNUSED_PARAM(newElement); |
| #endif // ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| } |
| |
| void AXObjectCache::onEventListenerAdded(Node& node, const AtomString& eventType) |
| { |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| if (!isClickEvent(eventType)) |
| return; |
| |
| if (RefPtr tree = AXIsolatedTree::treeForPageID(m_pageID)) { |
| if (auto* object = get(node)) |
| tree->queueNodeUpdate(object->objectID(), { AXProperty::HasClickHandler }); |
| } |
| #else |
| UNUSED_PARAM(node); |
| UNUSED_PARAM(eventType); |
| #endif // ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| } |
| |
| void AXObjectCache::onEventListenerRemoved(Node& node, const AtomString& eventType) |
| { |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| if (!isClickEvent(eventType)) |
| return; |
| |
| if (RefPtr tree = AXIsolatedTree::treeForPageID(m_pageID)) { |
| if (auto* object = get(node)) |
| tree->queueNodeUpdate(object->objectID(), { AXProperty::HasClickHandler }); |
| } |
| #else |
| UNUSED_PARAM(node); |
| UNUSED_PARAM(eventType); |
| #endif // ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| } |
| |
| void AXObjectCache::updateLoadingProgress(double newProgressValue) |
| { |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| ASSERT_WITH_MESSAGE(newProgressValue >= 0 && newProgressValue <= 1, "unexpected loading progress value: %f", newProgressValue); |
| if (m_pageID) { |
| // Sometimes the isolated tree hasn't been created by the time we get loading progress updates, |
| // so cache this value in the AXObjectCache too so we can give it to the tree upon creation. |
| m_loadingProgress = newProgressValue; |
| if (auto tree = AXIsolatedTree::treeForPageID(*m_pageID)) |
| tree->updateLoadingProgress(newProgressValue); |
| } |
| #else |
| UNUSED_PARAM(newProgressValue); |
| #endif |
| } |
| |
| void AXObjectCache::handleAllDeferredChildrenChanged() |
| { |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| RefPtr<AXIsolatedTree> tree; |
| if (!m_deferredChildrenChangedList.isEmpty()) |
| tree = AXIsolatedTree::treeForPageID(m_pageID); |
| #endif |
| |
| // Because m_deferredChildrenChangedList can be appended to while we iterate, we have to process |
| // this list in two steps (repeatedly until empty) to ensure the isolated tree is updated correctly. |
| // Specifically, it's possible for an entry to be appended during AXIsolatedTree::updateChildren (e.g. |
| // due to an object re-computing is-ignored to a different value). |
| while (!m_deferredChildrenChangedList.isEmpty()) { |
| // Perform AXObjectCache::handleChildrenChanged on all objects first, then update the isolated tree |
| // afterwards. Doing it in two steps prevents thrashing m_subtreeDirty (i.e. AXObjectCache::handleChildrenChanged |
| // setting m_subtreeDirty on some high-in-the-tree object, clearing that during AXIsolatedTree::updateChildren, |
| // then having it set again by the next children-changed entry, repeat). |
| auto deferredChildrenChangedList = std::exchange(m_deferredChildrenChangedList, { }); |
| for (auto& object : deferredChildrenChangedList) |
| handleChildrenChanged(object); |
| |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| if (tree) |
| tree->updateChildrenForObjects(deferredChildrenChangedList); |
| #endif |
| |
| #if !PLATFORM(COCOA) |
| // Neither the MAC nor IOS_FAMILY ports map AXChildrenChanged to a platform notification. |
| for (auto& object : deferredChildrenChangedList) |
| postPlatformNotification(object, AXNotification::ChildrenChanged); |
| #endif |
| } |
| } |
| |
| void AXObjectCache::handleChildrenChanged(AccessibilityObject& object) |
| { |
| AXTRACE("AXObjectCache::handleChildrenChanged"_s); |
| AXLOG(object); |
| |
| // Handle MenuLists and MenuListPopups as special cases. |
| if (is<AccessibilityMenuList>(object)) { |
| const auto& children = object.unignoredChildren(/* updateChildrenIfNeeded */ false); |
| if (children.isEmpty()) |
| return; |
| |
| ASSERT(children.size() == 1); |
| handleChildrenChanged(downcast<AccessibilityObject>(children[0].get())); |
| } else if (auto* menuListPopup = dynamicDowncast<AccessibilityMenuListPopup>(object)) { |
| menuListPopup->handleChildrenChanged(); |
| return; |
| } else if (auto* axTable = dynamicDowncast<AccessibilityTable>(object)) |
| deferRecomputeTableCellSlots(*axTable); |
| else if (auto* axRow = dynamicDowncast<AccessibilityTableRow>(object)) { |
| if (auto* parentTable = axRow->parentTable()) |
| deferRecomputeTableCellSlots(*parentTable); |
| } else if (auto* scrollView = dynamicDowncast<AccessibilityScrollView>(object)) { |
| // When the children of an iframe change, e.g., because its visibility changes, |
| // then we need to dirty the web area's subtree since the scroll area doesn't |
| // have a node nor renderer, thus, failing the check below and returning early. |
| |
| // Only do this when the web area is not the root web area, as this indicates |
| // we are in an iframe. |
| if (RefPtr webArea = scrollView->webAreaObject(); webArea && webArea != rootWebArea()) { |
| webArea->setNeedsToUpdateSubtree(); |
| webArea->setNeedsToUpdateChildren(); |
| } |
| } |
| |
| if (!object.node() && !object.renderer()) |
| return; |
| |
| // Should make the subtree dirty so that everything below will be updated correctly. |
| object.setNeedsToUpdateSubtree(); |
| |
| object.recomputeIsIgnored(); |
| |
| // Go up the existing ancestors chain and fire the appropriate notifications. |
| bool shouldUpdateParent = true; |
| bool foundTableCaption = false; |
| for (RefPtr parent = &object; parent; parent = parent->parentObject()) { |
| if (shouldUpdateParent) |
| parent->setNeedsToUpdateChildren(); |
| |
| // If this object supports ARIA live regions, then notify AT of changes. |
| // This notification needs to be sent even when the screen reader has not accessed this live region since the last update. |
| // Sometimes this function can be called many times within a short period of time, leading to posting too many AXLiveRegionChanged notifications. |
| // To fix this, we use a timer to make sure we only post one notification for the children changes within a pre-defined time interval. |
| if (parent->supportsLiveRegion()) |
| postLiveRegionChangeNotification(*parent); |
| |
| // If this object is an ARIA text control, notify that its value changed. |
| if (parent->isNonNativeTextControl()) { |
| postNotification(parent.get(), parent->protectedDocument().get(), AXNotification::ValueChanged); |
| |
| // Do not let any ancestor of an editable object update its children. |
| shouldUpdateParent = false; |
| } |
| |
| if (parent->isLabel()) { |
| // A label's descendant was added or removed. Update its LabelFor relationships. |
| handleLabelChanged(parent.get()); |
| } |
| |
| for (const auto& describedObject : parent->descriptionForObjects()) |
| postNotification(&downcast<AccessibilityObject>(describedObject.get()), nullptr, AXNotification::ExtendedDescriptionChanged); |
| |
| if (parent->hasElementName(ElementName::HTML_caption)) |
| foundTableCaption = true; |
| else if (foundTableCaption && parent->isTable()) { |
| postNotification(parent.get(), nullptr, AXNotification::TextChanged); |
| foundTableCaption = false; |
| } |
| } |
| |
| // The role of list objects is dependent on their children, so we'll need to re-compute it here. |
| if (is<AccessibilityList>(object)) |
| object.updateRole(); |
| } |
| |
| void AXObjectCache::handleRecomputeCellSlots(AccessibilityTable& axTable) |
| { |
| axTable.setCellSlotsDirty(); |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| updateIsolatedTree(axTable, AXNotification::CellSlotsChanged); |
| #endif |
| } |
| |
| void AXObjectCache::onRemoteFrameInitialized(AXRemoteFrame& remoteFrame) |
| { |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| updateIsolatedTree(remoteFrame, AXProperty::RemoteFramePlatformElement); |
| #else |
| UNUSED_PARAM(remoteFrame); |
| #endif |
| } |
| |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| void AXObjectCache::handleRowspanChanged(AccessibilityTableCell& axCell) |
| { |
| updateIsolatedTree(axCell, AXNotification::RowSpanChanged); |
| } |
| #endif |
| |
| #if ENABLE(AX_THREAD_TEXT_APIS) |
| void AXObjectCache::onTextRunsChanged(const RenderObject& renderer) |
| { |
| updateIsolatedTree(get(const_cast<RenderObject&>(renderer)), AXNotification::TextRunsChanged); |
| } |
| #endif |
| |
| void AXObjectCache::handleMenuOpened(Element& element) |
| { |
| if (!element.renderer() || !hasRole(element, "menu"_s)) |
| return; |
| |
| postNotification(getOrCreate(element), protectedDocument().get(), AXNotification::MenuOpened); |
| } |
| |
| void AXObjectCache::handleLiveRegionCreated(Element& element) |
| { |
| if (!element.renderer()) |
| return; |
| |
| auto liveRegionStatus = element.attributeWithoutSynchronization(aria_liveAttr); |
| if (liveRegionStatus.isEmpty()) { |
| const AtomString& ariaRole = element.attributeWithoutSynchronization(roleAttr); |
| if (!ariaRole.isEmpty()) |
| liveRegionStatus = AtomString { AXCoreObject::defaultLiveRegionStatusForRole(AccessibilityObject::ariaRoleToWebCoreRole(ariaRole)) }; |
| } |
| |
| if (AXCoreObject::liveRegionStatusIsEnabled(liveRegionStatus)) { |
| RefPtr axObject = getOrCreate(element); |
| #if PLATFORM(MAC) |
| if (axObject) |
| addSortedObject(*axObject, PreSortedObjectType::LiveRegion); |
| #endif // PLATFORM(MAC) |
| |
| postNotification(axObject.get(), protectedDocument().get(), AXNotification::LiveRegionCreated); |
| } |
| } |
| |
| void AXObjectCache::deferElementAddedOrRemoved(Element* element) |
| { |
| if (!element) |
| return; |
| |
| m_deferredElementAddedOrRemovedList.add(*element); |
| if (isModalElement(*element)) |
| deferModalChange(*element); |
| |
| if (!m_performCacheUpdateTimer.isActive()) |
| m_performCacheUpdateTimer.startOneShot(0_s); |
| } |
| |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| void AXObjectCache::deferAddUnconnectedNode(AccessibilityObject& axObject) |
| { |
| m_deferredUnconnectedObjects.add(axObject); |
| |
| if (!m_performCacheUpdateTimer.isActive()) |
| m_performCacheUpdateTimer.startOneShot(0_s); |
| } |
| #endif |
| |
| AccessibilityObject* AXObjectCache::getIncludingAncestors(RenderObject& renderer) const |
| { |
| for (auto* current = &renderer; current; current = current->parent()) { |
| if (auto* object = get(*current)) |
| return object; |
| } |
| return nullptr; |
| } |
| |
| void AXObjectCache::childrenChanged(RenderObject& renderer, RenderObject* changedChild) |
| { |
| if (renderer.isAnonymous()) { |
| // Don't drop a children-changed event if we can't |get| the given renderer if the renderer is anonymous. |
| // Sometimes the only children-changed we get from the render tree for some subtrees is for an anonymous renderer, |
| // so if we just drop it, we can have a stale accessibility tree. This problem is specific to anonymous renderers |
| // because we walk the DOM when building the accessibility tree, so it's unlikely that we will have created |
| // an accessibility object for this renderer (since it only exists in the render tree). |
| // |
| // Children-changed events associated with a DOM node are safe to drop if |get| fails, since some other |
| // element that does have an accessibility object must also get a children-changed event. The same is |
| // not always true for anonymous renderers, hence this branch. |
| // |
| // This behavior comes from a bug on a real webpage that I unfortunately couldn't figure out how to distill |
| // into a layout test. The key seems to be anonymous continuation renderers destroyed and recreated as part |
| // of a call to Node::insertBefore(). dynamic-inline-continuation.html gets close to reproducing the issue, |
| // following many of the same codepaths, but unfortunately will still pass even without this branch. |
| childrenChanged(getIncludingAncestors(renderer)); |
| } else |
| childrenChanged(get(renderer)); |
| |
| if (changedChild) |
| deferElementAddedOrRemoved(dynamicDowncast<Element>(changedChild->node())); |
| } |
| |
| void AXObjectCache::childrenChanged(AccessibilityObject* object) |
| { |
| if (!object) |
| return; |
| m_deferredChildrenChangedList.add(*object); |
| |
| // Adding or removing rows from a table can cause it to change from layout table to AX data table and vice versa, so queue up recomputation of that for the parent table. |
| if (auto* tableElement = dynamicDowncast<HTMLTableElement>(object->element())) |
| deferRecomputeTableIsExposed(const_cast<HTMLTableElement*>(tableElement)); |
| else if (auto* tableSectionElement = dynamicDowncast<HTMLTableSectionElement>(object->element())) |
| deferRecomputeTableIsExposed(const_cast<HTMLTableElement*>(tableSectionElement->findParentTable().get())); |
| |
| if (!m_performCacheUpdateTimer.isActive()) |
| m_performCacheUpdateTimer.startOneShot(0_s); |
| } |
| |
| void AXObjectCache::valueChanged(Element& element) |
| { |
| postNotification(&element, AXNotification::ValueChanged); |
| } |
| |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| void AXObjectCache::columnIndexChanged(AccessibilityObject& object) |
| { |
| postNotification(object, AXNotification::ColumnIndexChanged); |
| } |
| |
| void AXObjectCache::rowIndexChanged(AccessibilityObject& object) |
| { |
| postNotification(object, AXNotification::RowIndexChanged); |
| } |
| #endif // ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| |
| void AXObjectCache::notificationPostTimerFired() |
| { |
| AXTRACE(makeString("AXObjectCache::notificationPostTimerFired 0x"_s, hex(reinterpret_cast<uintptr_t>(this)))); |
| // During LayoutTests, accessibility may be disabled between the time the notifications are queued and the timer fires. |
| // Thus check here and return if accessibility is disabled. |
| if (!accessibilityEnabled()) |
| return; |
| |
| RefPtr document = m_document.get(); |
| m_notificationPostTimer.stop(); |
| |
| if (!document || !document->hasLivingRenderTree()) |
| return; |
| |
| // In tests, posting notifications has a tendency to immediately queue up other notifications, which can lead to unexpected behavior |
| // when the notification list is cleared at the end. Instead copy this list at the start. |
| auto notifications = std::exchange(m_notificationsToPost, { }); |
| |
| // Filter out the notifications that are not going to be posted to platform clients. |
| Vector<std::pair<Ref<AccessibilityObject>, AXNotification>> notificationsToPost; |
| notificationsToPost.reserveInitialCapacity(notifications.size()); |
| for (auto& note : notifications) { |
| if (note.first->isDetached() || !note.first->axObjectCache()) |
| continue; |
| |
| #if ASSERT_ENABLED |
| // Make sure none of the render views are in the process of being layed out. |
| // Notifications should only be sent after the renderer has finished |
| if (auto* renderObject = dynamicDowncast<AccessibilityRenderObject>(note.first.get())) { |
| if (auto* renderer = renderObject->renderer()) |
| ASSERT(!renderer->view().frameView().layoutContext().layoutState()); |
| } |
| #endif |
| |
| if (note.second == AXNotification::MenuOpened) { |
| // Only notify if the object is in fact a menu. |
| note.first->updateChildrenIfNecessary(); |
| if (note.first->role() != AccessibilityRole::Menu) |
| continue; |
| } |
| |
| notificationsToPost.append(WTFMove(note)); |
| } |
| |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| updateIsolatedTree(notificationsToPost); |
| #endif |
| |
| for (const auto& note : notificationsToPost) |
| postPlatformNotification(note.first, note.second); |
| } |
| |
| void AXObjectCache::postNotification(RenderObject* renderer, AXNotification notification, PostTarget postTarget) |
| { |
| if (!renderer) |
| return; |
| |
| stopCachingComputedObjectAttributes(); |
| |
| // Get an accessibility object that already exists. One should not be created here |
| // because a render update may be in progress and creating an AX object can re-trigger a layout |
| RefPtr<AccessibilityObject> object = get(*renderer); |
| while (!object && renderer) { |
| renderer = renderer->parent(); |
| object = get(renderer); |
| } |
| |
| if (!renderer) |
| return; |
| |
| postNotification(object.get(), renderer->protectedDocument().ptr(), notification, postTarget); |
| } |
| |
| void AXObjectCache::postNotification(Node* node, AXNotification notification, PostTarget postTarget) |
| { |
| if (!node) |
| return; |
| |
| stopCachingComputedObjectAttributes(); |
| |
| // Get an accessibility object that already exists. One should not be created here |
| // because a render update may be in progress and creating an AX object can re-trigger a layout |
| RefPtr<AccessibilityObject> object = get(*node); |
| while (!object && node) { |
| node = node->parentNode(); |
| object = get(node); |
| } |
| |
| if (!node) |
| return; |
| |
| postNotification(object.get(), node->protectedDocument().ptr(), notification, postTarget); |
| } |
| |
| void AXObjectCache::postNotification(AccessibilityObject* object, Document* document, AXNotification notification, PostTarget postTarget) |
| { |
| AXTRACE(makeString("AXObjectCache::postNotification 0x"_s, hex(reinterpret_cast<uintptr_t>(this)))); |
| AXLOG(std::make_pair(object, notification)); |
| ASSERT(isMainThread()); |
| |
| stopCachingComputedObjectAttributes(); |
| |
| if (object && postTarget == PostTarget::ObservableParent) |
| object = object->observableObject(); |
| |
| if (!object && document) |
| object = get(document->renderView()); |
| |
| if (!object) |
| return; |
| |
| #if PLATFORM(COCOA) |
| if (notification == AXNotification::ValueChanged |
| && enqueuePasswordNotification(*object, { })) |
| return; |
| #endif |
| |
| m_notificationsToPost.append(std::make_pair(Ref { *object }, notification)); |
| if (!m_notificationPostTimer.isActive()) |
| m_notificationPostTimer.startOneShot(0_s); |
| } |
| |
| void AXObjectCache::postNotification(AccessibilityObject& object, AXNotification notification) |
| { |
| AXTRACE(makeString("AXObjectCache::postNotification 0x"_s, hex(reinterpret_cast<uintptr_t>(this)))); |
| AXLOG(std::make_pair(Ref { object }, notification)); |
| ASSERT(isMainThread()); |
| |
| stopCachingComputedObjectAttributes(); |
| |
| #if PLATFORM(COCOA) |
| if (notification == AXNotification::ValueChanged |
| && enqueuePasswordNotification(object, { })) |
| return; |
| #endif |
| |
| m_notificationsToPost.append(std::make_pair(Ref { object }, notification)); |
| if (!m_notificationPostTimer.isActive()) |
| m_notificationPostTimer.startOneShot(0_s); |
| } |
| |
| void AXObjectCache::checkedStateChanged(Element& element) |
| { |
| postNotification(&element, AXNotification::CheckedStateChanged); |
| } |
| |
| void AXObjectCache::autofillTypeChanged(HTMLInputElement& element) |
| { |
| postNotification(&element, AXNotification::AutofillTypeChanged); |
| } |
| |
| void AXObjectCache::handleMenuItemSelected(Element* element) |
| { |
| if (!element) |
| return; |
| |
| if (!hasAnyRole(*element, { "menuitem"_s, "menuitemradio"_s, "menuitemcheckbox"_s })) |
| return; |
| |
| if (!element->focused() && !equalLettersIgnoringASCIICase(element->attributeWithoutSynchronization(aria_selectedAttr), "true"_s)) |
| return; |
| |
| postNotification(getOrCreate(*element), protectedDocument().get(), AXNotification::MenuListItemSelected); |
| } |
| |
| void AXObjectCache::handleTabPanelSelected(Element* oldElement, Element* newElement) |
| { |
| auto updateTab = [this] (AccessibilityObject* controlPanel, Element& element) { |
| if (!controlPanel) |
| return; |
| |
| auto controllers = controlPanel->controllers(); |
| for (auto& controller : controllers) |
| postNotification(dynamicDowncast<AccessibilityObject>(controller.get()), element.protectedDocument().ptr(), AXNotification::SelectedStateChanged); |
| }; |
| |
| |
| RefPtr oldObject = get(oldElement); |
| RefPtr<AccessibilityObject> oldFocusedControlledPanel; |
| if (oldObject) { |
| oldFocusedControlledPanel = Accessibility::findAncestor<AccessibilityObject>(*oldObject, false, [] (auto& ancestor) { |
| return ancestor.role() == AccessibilityRole::TabPanel; |
| }); |
| |
| updateTab(oldFocusedControlledPanel.get(), *oldElement); |
| } |
| |
| RefPtr newObject = get(newElement); |
| if (!newObject) |
| return; |
| |
| RefPtr newFocusedControlledPanel = Accessibility::findAncestor<AccessibilityObject>(*newObject, false, [] (auto& ancestor) { |
| return ancestor.role() == AccessibilityRole::TabPanel; |
| }); |
| |
| if (oldFocusedControlledPanel != newFocusedControlledPanel) |
| updateTab(newFocusedControlledPanel.get(), *newElement); |
| } |
| |
| void AXObjectCache::handleRowCountChanged(AccessibilityObject* axObject, Document* document) |
| { |
| if (!axObject) |
| return; |
| |
| if (auto* axTable = dynamicDowncast<AccessibilityTable>(axObject)) |
| axTable->recomputeIsExposable(); |
| |
| postNotification(axObject, document, AXNotification::RowCountChanged); |
| } |
| |
| void AXObjectCache::onPageActivityStateChange(OptionSet<ActivityState> newState) |
| { |
| ASSERT(m_pageID); |
| |
| m_pageActivityState = newState; |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| if (auto tree = AXIsolatedTree::treeForPageID(m_pageID)) |
| tree->setPageActivityState(newState); |
| #endif |
| } |
| |
| static bool shouldDeferFocusChange(Element* element) |
| { |
| if (!element) |
| return false; |
| |
| auto* renderer = element->renderer(); |
| if (renderer && rendererNeedsDeferredUpdate(*renderer)) |
| return true; |
| |
| // We also want to defer handling focus changes for nodes that haven't yet attached their renderer. |
| if (const auto* style = element->existingComputedStyle()) |
| return !renderer && element->rendererIsNeeded(*style); |
| // No existing style, so we can't easily determine whether this element will need a renderer. |
| // Resolving style is expensive and we don't want to do it now, so make this decision assuming |
| // a renderer just hasn't been attached yet, indicated by it being nullptr. |
| return !renderer; |
| } |
| |
| void AXObjectCache::onFocusChange(Element* oldElement, Element* newElement) |
| { |
| if (shouldDeferFocusChange(newElement)) { |
| if (m_deferredFocusedNodeChange) { |
| // If we got a focus change notification but haven't committed a previously deferred focus change: |
| if (m_deferredFocusedNodeChange->first == newElement) { |
| // Cancel the focus change entirely if the new focused node will be the same as the old one (i.e. there is no effective focus change). |
| m_deferredFocusedNodeChange = std::nullopt; |
| return; |
| } |
| // Otherwise, recompute is-ignored for the node that was slated to become the AX focused element. |
| // This is important because we may have computed is-ignored for this node after it gained DOM focus, |
| // meaning it could be unignored solely because it was DOM focused. |
| recomputeIsIgnored(m_deferredFocusedNodeChange->second.get()); |
| // And now we can update the new deferred focus node to be |newNode|. |
| m_deferredFocusedNodeChange->second = newElement; |
| } else |
| m_deferredFocusedNodeChange = { oldElement, newElement }; |
| |
| // Don't start the timer if a layout is pending, as the layout will trigger a cache update. |
| bool needsLayout = newElement->renderer() && newElement->renderer()->needsLayout(); |
| if (!needsLayout && !m_performCacheUpdateTimer.isActive()) |
| m_performCacheUpdateTimer.startOneShot(0_s); |
| } else |
| handleFocusedUIElementChanged(oldElement, newElement); |
| } |
| |
| void AXObjectCache::onInertOrVisibilityChange(RenderElement& renderer) |
| { |
| #if ENABLE(INCLUDE_IGNORED_IN_CORE_AX_TREE) |
| RefPtr axObject = get(renderer); |
| if (!axObject) |
| return; |
| // Both of these change the is-ignored state of all descendants of `renderer`, so throw away |
| // the is-ignored cache. |
| stopCachingComputedObjectAttributes(); |
| |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| postNotification(*axObject, AXNotification::InertOrVisibilityChanged); |
| #endif // ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| |
| #else // !ENABLE(INCLUDE_IGNORED_IN_CORE_AX_TREE) |
| if (CheckedPtr parent = renderer.parent()) |
| childrenChanged(*parent, &renderer); |
| #endif // ENABLE(INCLUDE_IGNORED_IN_CORE_AX_TREE) |
| } |
| |
| void AXObjectCache::onPopoverToggle(const HTMLElement& popover) |
| { |
| RefPtr axPopover = get(const_cast<HTMLElement*>(&popover)); |
| if (!axPopover) |
| return; |
| // There may be multiple elements with popovertarget attributes that point at |popover|. |
| for (const auto& invoker : axPopover->controllers()) |
| postNotification(dynamicDowncast<AccessibilityObject>(invoker.get()), protectedDocument().get(), AXNotification::ExpandedChanged); |
| } |
| |
| void AXObjectCache::deferMenuListValueChange(Element* element) |
| { |
| if (!element) |
| return; |
| |
| m_deferredMenuListChange.add(*element); |
| if (!m_performCacheUpdateTimer.isActive()) |
| m_performCacheUpdateTimer.startOneShot(0_s); |
| } |
| |
| void AXObjectCache::deferModalChange(Element& element) |
| { |
| m_deferredModalChangedList.add(element); |
| |
| if (!m_performCacheUpdateTimer.isActive()) |
| m_performCacheUpdateTimer.startOneShot(0_s); |
| } |
| |
| void AXObjectCache::handleFocusedUIElementChanged(Element* oldElement, Element* newElement, UpdateModal updateModal) |
| { |
| if (updateModal == UpdateModal::Yes) |
| updateCurrentModalNode(); |
| handleMenuItemSelected(newElement); |
| |
| // FIXME: Consider creating a new ancestor flag to only do this work when |oldNode| or |newNode| have a tab panel ancestor (the only time it is necessary) |
| handleTabPanelSelected(oldElement, newElement); |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| setIsolatedTreeFocusedObject(focusedObjectForNode(newElement)); |
| #endif |
| platformHandleFocusedUIElementChanged(oldElement, newElement); |
| } |
| |
| void AXObjectCache::selectedChildrenChanged(Node* node) |
| { |
| postNotification(node, AXNotification::SelectedChildrenChanged); |
| } |
| |
| void AXObjectCache::selectedChildrenChanged(RenderObject* renderer) |
| { |
| if (renderer) |
| selectedChildrenChanged(renderer->node()); |
| } |
| |
| void AXObjectCache::onScrollbarFrameRectChange(const Scrollbar& scrollbar) |
| { |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| if (!m_pageID || !isIsolatedTreeEnabled()) |
| return; |
| |
| if (auto* axScrollbar = get(const_cast<Scrollbar*>(&scrollbar))) |
| m_geometryManager->cacheRect(axScrollbar->objectID(), enclosingIntRect(axScrollbar->relativeFrame())); |
| #else |
| UNUSED_PARAM(scrollbar); |
| #endif |
| } |
| |
| void AXObjectCache::onSelectedChanged(Element& element) |
| { |
| if (hasCellARIARole(element)) |
| postNotification(&element, AXNotification::SelectedCellsChanged); |
| else if (is<HTMLOptionElement>(element)) |
| postNotification(&element, AXNotification::SelectedStateChanged); |
| else if (auto* axObject = getOrCreate(element)) { |
| if (auto* ancestor = Accessibility::findAncestor<AccessibilityObject>(*axObject, false, [] (const auto& object) { |
| return object.canHaveSelectedChildren(); |
| })) { |
| selectedChildrenChanged(ancestor->node()); |
| postNotification(axObject, element.protectedDocument().ptr(), AXNotification::SelectedStateChanged); |
| } |
| } |
| |
| handleMenuItemSelected(&element); |
| handleTabPanelSelected(nullptr, &element); |
| } |
| |
| void AXObjectCache::onSlottedContentChange(const HTMLSlotElement& slot) |
| { |
| childrenChanged(get(const_cast<HTMLSlotElement&>(slot))); |
| } |
| |
| static bool isContentVisibilityHidden(const RenderStyle& style) |
| { |
| return style.usedContentVisibility() == ContentVisibility::Hidden; |
| } |
| |
| void AXObjectCache::onStyleChange(Element& element, OptionSet<Style::Change> change, const RenderStyle* oldStyle, const RenderStyle* newStyle) |
| { |
| if (!change || !oldStyle || !newStyle) |
| return; |
| |
| RefPtr object = get(element); |
| if (!object) |
| return; |
| |
| if (element.renderer()) { |
| // Unlike changes to `visibility`, changes to `content-visibility` do not provide accessibility |
| // children changed notifications via the render tree, so check for that here. |
| if (isContentVisibilityHidden(*oldStyle) != isContentVisibilityHidden(*newStyle)) |
| childrenChanged(object.get()); |
| } else if (isVisibilityHidden(*oldStyle) != isVisibilityHidden(*newStyle)) { |
| // We only need to do this when the given element doesn't have a renderer, as if it did, we would |
| // get a children-changed event through the render tree. |
| childrenChanged(object.get()); |
| } |
| |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| if (oldStyle->insideLink() != newStyle->insideLink()) |
| postNotification(*object, AXNotification::VisitedStateChanged); |
| |
| if (oldStyle->speakAs() != newStyle->speakAs()) |
| postNotification(*object, AXNotification::SpeakAsChanged); |
| #endif // ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| } |
| |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| bool AXObjectCache::onFontChange(Element& element, const RenderStyle* oldStyle, const RenderStyle* newStyle) |
| { |
| if (!oldStyle || !newStyle) |
| return false; |
| |
| RefPtr object = get(element); |
| if (!object) |
| return false; |
| |
| RefPtr tree = AXIsolatedTree::treeForPageID(m_pageID); |
| if (!tree) |
| return false; |
| |
| if (!oldStyle->fontCascadeEqual(*newStyle)) { |
| postNotification(*object, AXNotification::FontChanged); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| bool AXObjectCache::onTextColorChange(Element& element, const RenderStyle* oldStyle, const RenderStyle* newStyle) |
| { |
| if (!oldStyle || !newStyle) |
| return false; |
| |
| RefPtr object = get(element); |
| if (!object) |
| return false; |
| |
| RefPtr tree = AXIsolatedTree::treeForPageID(m_pageID); |
| if (!tree) |
| return false; |
| |
| if (oldStyle->visitedDependentColor(CSSPropertyColor) != newStyle->visitedDependentColor(CSSPropertyColor)) { |
| postNotification(*object, AXNotification::TextColorChanged); |
| return true; |
| } |
| |
| return false; |
| } |
| #endif // ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| |
| void AXObjectCache::onStyleChange(RenderText& renderText, StyleDifference difference, const RenderStyle* oldStyle, const RenderStyle& newStyle) |
| { |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| if (!oldStyle) |
| return; |
| |
| bool speakAsChanged = oldStyle->speakAs() != newStyle.speakAs(); |
| #if !ENABLE(AX_THREAD_TEXT_APIS) |
| // In !ENABLE(AX_THREAD_TEXT_APIS), we don't have anything to do if speak-as hasn't changed. |
| if (!speakAsChanged) [[likely]] |
| return; |
| #endif // !ENABLE(AX_THREAD_TEXT_APIS) |
| |
| bool diffIsEqual = difference == StyleDifference::Equal; |
| // When speak-as changes, style difference will be StyleDifference::Equal (so "equal" |
| // is not exactly accurate). So if the styles are "equal" and speak-as hasn't changed, |
| // we have nothing to do. |
| if (diffIsEqual) { |
| if (!speakAsChanged) [[likely]] |
| return; |
| } |
| |
| RefPtr tree = AXIsolatedTree::treeForPageID(m_pageID); |
| if (!tree) |
| return; |
| |
| RefPtr object = get(renderText); |
| if (!object) |
| return; |
| |
| if (speakAsChanged) [[unlikely]] |
| postNotification(*object, AXNotification::SpeakAsChanged); |
| |
| // The following style changes will not have a StyleDifference::Equal, so we can |
| // exit early if the diff is equal. |
| if (diffIsEqual) |
| return; |
| |
| #if ENABLE(AX_THREAD_TEXT_APIS) |
| if (oldStyle->visitedDependentColor(CSSPropertyBackgroundColor) != newStyle.visitedDependentColor(CSSPropertyBackgroundColor)) |
| tree->queueNodeUpdate(object->objectID(), { AXProperty::BackgroundColor }); |
| |
| if (oldStyle->verticalAlign() != newStyle.verticalAlign()) |
| tree->queueNodeUpdate(object->objectID(), { { AXProperty::IsSuperscript, AXProperty::IsSubscript } }); |
| |
| if (oldStyle->hasTextShadow() != newStyle.hasTextShadow()) |
| tree->queueNodeUpdate(object->objectID(), { AXProperty::HasTextShadow }); |
| |
| auto oldDecor = oldStyle->textDecorationLineInEffect(); |
| auto newDecor = newStyle.textDecorationLineInEffect(); |
| if ((oldDecor & TextDecorationLine::Underline) != (newDecor & TextDecorationLine::Underline)) |
| tree->queueNodeUpdate(object->objectID(), { AXProperty::HasUnderline }); |
| |
| if ((oldDecor & TextDecorationLine::LineThrough) != (newDecor & TextDecorationLine::LineThrough)) |
| tree->queueNodeUpdate(object->objectID(), { AXProperty::HasLinethrough }); |
| |
| if (oldStyle->textDecorationColor() != newStyle.textDecorationColor()) |
| tree->queueNodeUpdate(object->objectID(), { { AXProperty::LinethroughColor, AXProperty::UnderlineColor } }); |
| #endif // ENABLE(AX_THREAD_TEXT_APIS) |
| |
| #else |
| UNUSED_PARAM(renderText); |
| UNUSED_PARAM(difference); |
| UNUSED_PARAM(oldStyle); |
| UNUSED_PARAM(newStyle); |
| #endif // ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| } |
| |
| void AXObjectCache::onTextSecurityChanged(HTMLInputElement& inputElement) |
| { |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| postNotification(get(&inputElement), AXNotification::TextSecurityChanged); |
| #else |
| UNUSED_PARAM(inputElement); |
| #endif |
| } |
| |
| void AXObjectCache::onTitleChange(Document& document) |
| { |
| postNotification(get(&document), AXNotification::TextChanged); |
| } |
| |
| void AXObjectCache::onValidityChange(Element& element) |
| { |
| postNotification(get(&element), AXNotification::InvalidStatusChanged); |
| } |
| |
| void AXObjectCache::onTextCompositionChange(Node& node, CompositionState compositionState, bool valueChanged, const String& text, size_t position, bool handlingAcceptedCandidate) |
| { |
| #if HAVE(INLINE_PREDICTIONS) |
| auto* object = getOrCreate(node); |
| if (!object) |
| return; |
| |
| #if PLATFORM(IOS_FAMILY) |
| if (valueChanged) |
| object->setLastPresentedTextPrediction(node, compositionState, text, position, handlingAcceptedCandidate); |
| #endif |
| |
| if (auto* observableObject = object->observableObject()) |
| object = observableObject; |
| |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| updateIsolatedTree(object, AXNotification::TextCompositionChanged); |
| #endif // ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| |
| if (compositionState == CompositionState::Started) |
| postNotification(object, node.protectedDocument().ptr(), AXNotification::TextCompositionBegan); |
| |
| if (valueChanged) |
| postNotification(object, node.protectedDocument().ptr(), AXNotification::ValueChanged); |
| |
| if (compositionState == CompositionState::Ended) |
| postNotification(object, node.protectedDocument().ptr(), AXNotification::TextCompositionEnded); |
| #else |
| UNUSED_PARAM(node); |
| UNUSED_PARAM(compositionState); |
| UNUSED_PARAM(valueChanged); |
| #endif // HAVE(INLINE_PREDICTIONS) |
| |
| #if !PLATFORM(IOS_FAMILY) || !HAVE(INLINE_PREDICTIONS) |
| UNUSED_PARAM(text); |
| UNUSED_PARAM(position); |
| UNUSED_PARAM(handlingAcceptedCandidate); |
| #endif |
| } |
| |
| #ifndef NDEBUG |
| void AXObjectCache::showIntent(const AXTextStateChangeIntent &intent) |
| { |
| switch (intent.type) { |
| case AXTextStateChangeTypeUnknown: |
| dataLog("Unknown"); |
| break; |
| case AXTextStateChangeTypeEdit: |
| dataLog("Edit::"); |
| break; |
| case AXTextStateChangeTypeSelectionMove: |
| dataLog("Move::"); |
| break; |
| case AXTextStateChangeTypeSelectionExtend: |
| dataLog("Extend::"); |
| break; |
| case AXTextStateChangeTypeSelectionBoundary: |
| dataLog("Boundary::"); |
| break; |
| } |
| switch (intent.type) { |
| case AXTextStateChangeTypeUnknown: |
| break; |
| case AXTextStateChangeTypeEdit: |
| switch (intent.editType) { |
| case AXTextEditTypeUnknown: |
| dataLog("Unknown"); |
| break; |
| case AXTextEditTypeDelete: |
| dataLog("Delete"); |
| break; |
| case AXTextEditTypeInsert: |
| dataLog("Insert"); |
| break; |
| case AXTextEditTypeDictation: |
| dataLog("DictationInsert"); |
| break; |
| case AXTextEditTypeTyping: |
| dataLog("TypingInsert"); |
| break; |
| case AXTextEditTypeCut: |
| dataLog("Cut"); |
| break; |
| case AXTextEditTypePaste: |
| dataLog("Paste"); |
| break; |
| case AXTextEditTypeReplace: |
| dataLog("Replace"); |
| break; |
| case AXTextEditTypeAttributesChange: |
| dataLog("AttributesChange"); |
| break; |
| } |
| break; |
| case AXTextStateChangeTypeSelectionMove: |
| case AXTextStateChangeTypeSelectionExtend: |
| case AXTextStateChangeTypeSelectionBoundary: |
| switch (intent.selection.direction) { |
| case AXTextSelectionDirectionUnknown: |
| dataLog("Unknown::"); |
| break; |
| case AXTextSelectionDirectionBeginning: |
| dataLog("Beginning::"); |
| break; |
| case AXTextSelectionDirectionEnd: |
| dataLog("End::"); |
| break; |
| case AXTextSelectionDirectionPrevious: |
| dataLog("Previous::"); |
| break; |
| case AXTextSelectionDirectionNext: |
| dataLog("Next::"); |
| break; |
| case AXTextSelectionDirectionDiscontiguous: |
| dataLog("Discontiguous::"); |
| break; |
| } |
| switch (intent.selection.direction) { |
| case AXTextSelectionDirectionUnknown: |
| case AXTextSelectionDirectionBeginning: |
| case AXTextSelectionDirectionEnd: |
| case AXTextSelectionDirectionPrevious: |
| case AXTextSelectionDirectionNext: |
| switch (intent.selection.granularity) { |
| case AXTextSelectionGranularityUnknown: |
| dataLog("Unknown"); |
| break; |
| case AXTextSelectionGranularityCharacter: |
| dataLog("Character"); |
| break; |
| case AXTextSelectionGranularityWord: |
| dataLog("Word"); |
| break; |
| case AXTextSelectionGranularityLine: |
| dataLog("Line"); |
| break; |
| case AXTextSelectionGranularitySentence: |
| dataLog("Sentence"); |
| break; |
| case AXTextSelectionGranularityParagraph: |
| dataLog("Paragraph"); |
| break; |
| case AXTextSelectionGranularityPage: |
| dataLog("Page"); |
| break; |
| case AXTextSelectionGranularityDocument: |
| dataLog("Document"); |
| break; |
| case AXTextSelectionGranularityAll: |
| dataLog("All"); |
| break; |
| } |
| break; |
| case AXTextSelectionDirectionDiscontiguous: |
| break; |
| } |
| break; |
| } |
| dataLog("\n"); |
| } |
| #endif |
| |
| void AXObjectCache::setTextSelectionIntent(const AXTextStateChangeIntent& intent) |
| { |
| m_textSelectionIntent = intent; |
| } |
| |
| void AXObjectCache::setIsSynchronizingSelection(bool isSynchronizing) |
| { |
| m_isSynchronizingSelection = isSynchronizing; |
| } |
| |
| void AXObjectCache::postTextStateChangeNotification(Node* node, const AXTextStateChangeIntent& intent, const VisibleSelection& selection) |
| { |
| if (!node) |
| return; |
| |
| #if PLATFORM(COCOA) || USE(ATSPI) |
| stopCachingComputedObjectAttributes(); |
| |
| postTextStateChangeNotification(getOrCreate(*node), intent, selection); |
| #else |
| postNotification(node->renderer(), AXNotification::SelectedTextChanged, PostTarget::ObservableParent); |
| UNUSED_PARAM(intent); |
| UNUSED_PARAM(selection); |
| #endif |
| } |
| |
| void AXObjectCache::postTextStateChangeNotification(const Position& position, const AXTextStateChangeIntent& intent, const VisibleSelection& selection) |
| { |
| RefPtr node = position.deprecatedNode(); |
| if (!node) |
| return; |
| |
| stopCachingComputedObjectAttributes(); |
| |
| #if PLATFORM(COCOA) || USE(ATSPI) |
| AccessibilityObject* object = getOrCreate(*node); |
| if (object && object->isIgnored()) { |
| #if PLATFORM(COCOA) |
| if (position.atLastEditingPositionForNode()) { |
| if (AccessibilityObject* nextSibling = object->nextSiblingUnignored(1)) |
| object = nextSibling; |
| } else if (position.atFirstEditingPositionForNode()) { |
| if (AccessibilityObject* previousSibling = object->previousSiblingUnignored(1)) |
| object = previousSibling; |
| } |
| #elif USE(ATSPI) |
| // ATSPI doesn't expose text nodes, so we need the parent |
| // object which is the one implementing the text interface. |
| object = object->parentObjectUnignored(); |
| #endif |
| } |
| |
| postTextStateChangeNotification(object, intent, selection); |
| #else |
| postTextStateChangeNotification(node.get(), intent, selection); |
| #endif |
| } |
| |
| void AXObjectCache::postTextStateChangeNotification(AccessibilityObject* object, const AXTextStateChangeIntent& intent, const VisibleSelection& selection) |
| { |
| AXTRACE(makeString("AXObjectCache::postTextStateChangeNotification 0x"_s, hex(reinterpret_cast<uintptr_t>(this)))); |
| stopCachingComputedObjectAttributes(); |
| |
| #if PLATFORM(COCOA) || USE(ATSPI) |
| if (object) { |
| if (auto observableObject = object->observableObject()) |
| object = observableObject; |
| } |
| |
| if (!object) |
| object = rootWebArea(); |
| |
| if (object) { |
| const AXTextStateChangeIntent& newIntent = (intent.type == AXTextStateChangeTypeUnknown || (m_isSynchronizingSelection && m_textSelectionIntent.type != AXTextStateChangeTypeUnknown)) ? m_textSelectionIntent : intent; |
| |
| #if PLATFORM(COCOA) |
| if (enqueuePasswordNotification(*object, { newIntent, { }, { }, selection })) |
| return; |
| #endif |
| |
| postTextSelectionChangePlatformNotification(object, newIntent, selection); |
| } |
| |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| onSelectedTextChanged(selection, object); |
| #endif |
| #else // PLATFORM(COCOA) || USE(ATSPI) |
| UNUSED_PARAM(object); |
| UNUSED_PARAM(intent); |
| UNUSED_PARAM(selection); |
| #endif |
| |
| setTextSelectionIntent(AXTextStateChangeIntent()); |
| setIsSynchronizingSelection(false); |
| } |
| |
| void AXObjectCache::postTextStateChangeNotification(Node* node, AXTextEditType type, const String& text, const VisiblePosition& position) |
| { |
| AXTRACE(makeString("AXObjectCache::postTextStateChangeNotification 0x"_s, hex(reinterpret_cast<uintptr_t>(this)))); |
| if (!node || type == AXTextEditTypeUnknown) |
| return; |
| |
| stopCachingComputedObjectAttributes(); |
| |
| AccessibilityObject* object = getOrCreate(node); |
| #if PLATFORM(COCOA) || USE(ATSPI) |
| if (object) |
| object = object->observableObject(); |
| |
| if (!object) |
| object = rootWebArea(); |
| |
| if (!object) |
| return; |
| |
| #if PLATFORM(COCOA) |
| if (enqueuePasswordNotification(*object, { { type }, { }, text, { position, position } })) |
| return; |
| #endif |
| |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| updateIsolatedTree(*object, AXNotification::ValueChanged); |
| #endif |
| |
| postTextStateChangePlatformNotification(object, type, text, position); |
| #else // PLATFORM(COCOA) || USE(ATSPI) |
| nodeTextChangePlatformNotification(object, textChangeForEditType(type), position.deepEquivalent().deprecatedEditingOffset(), text); |
| #endif |
| } |
| |
| void AXObjectCache::postTextReplacementNotification(Node* node, AXTextEditType deletionType, const String& deletedText, AXTextEditType insertionType, const String& insertedText, const VisiblePosition& position) |
| { |
| if (!node) |
| return; |
| if (deletionType != AXTextEditTypeDelete) |
| return; |
| if (!(insertionType == AXTextEditTypeInsert || insertionType == AXTextEditTypeTyping || insertionType == AXTextEditTypeDictation || insertionType == AXTextEditTypePaste)) |
| return; |
| |
| stopCachingComputedObjectAttributes(); |
| |
| AccessibilityObject* object = getOrCreate(*node); |
| #if PLATFORM(COCOA) || USE(ATSPI) |
| if (object) |
| object = object->observableObject(); |
| if (!object) |
| return; |
| |
| #if PLATFORM(COCOA) |
| if (enqueuePasswordNotification(*object, { { AXTextEditTypeReplace }, deletedText, insertedText, { position, position } })) |
| return; |
| #endif |
| |
| postTextReplacementPlatformNotification(object, deletionType, deletedText, insertionType, insertedText, position); |
| #else // PLATFORM(COCOA) || USE(ATSPI) |
| nodeTextChangePlatformNotification(object, textChangeForEditType(deletionType), position.deepEquivalent().deprecatedEditingOffset(), deletedText); |
| nodeTextChangePlatformNotification(object, textChangeForEditType(insertionType), position.deepEquivalent().deprecatedEditingOffset(), insertedText); |
| #endif |
| } |
| |
| void AXObjectCache::postTextReplacementNotificationForTextControl(HTMLTextFormControlElement& textControl, const String& deletedText, const String& insertedText) |
| { |
| stopCachingComputedObjectAttributes(); |
| |
| AccessibilityObject* object = getOrCreate(textControl); |
| #if PLATFORM(COCOA) || USE(ATSPI) |
| if (object) |
| object = object->observableObject(); |
| if (!object) |
| return; |
| |
| #if PLATFORM(COCOA) |
| if (enqueuePasswordNotification(*object, { { AXTextEditTypeReplace }, deletedText, insertedText, { } })) |
| return; |
| #endif |
| |
| postTextReplacementPlatformNotificationForTextControl(object, deletedText, insertedText); |
| #else // PLATFORM(COCOA) || USE(ATSPI) |
| nodeTextChangePlatformNotification(object, textChangeForEditType(AXTextEditTypeDelete), 0, deletedText); |
| nodeTextChangePlatformNotification(object, textChangeForEditType(AXTextEditTypeInsert), 0, insertedText); |
| #endif |
| } |
| |
| #if PLATFORM(COCOA) |
| |
| static AXTextChangeContext secureContext(AccessibilityObject& object, AXTextChangeContext& context) |
| { |
| ASSERT(isSecureFieldOrContainedBySecureField(object)); |
| |
| // FIXME: Add a better way to retrieve the maskingCharacter for secure fields. |
| String value = object.secureFieldValue(); |
| auto maskingCharacter = value.length() ? value[0] : bullet; |
| |
| const auto& secureString = [&maskingCharacter] (String& text) { |
| if (text.isEmpty()) |
| return; |
| |
| std::span<UChar> characters; |
| text = String::createUninitialized(text.length(), characters); |
| for (unsigned i = 0; i < text.length(); ++i) |
| characters[i] = maskingCharacter; |
| }; |
| |
| secureString(context.deletedText); |
| secureString(context.insertedText); |
| return context; |
| } |
| |
| bool AXObjectCache::enqueuePasswordNotification(AccessibilityObject& object, AXTextChangeContext&& context) |
| { |
| if (!isSecureFieldOrContainedBySecureField(object)) |
| return false; |
| |
| auto* observableObject = object.observableObject(); |
| if (!observableObject) { |
| ASSERT_NOT_REACHED(); |
| // Return true even though the enqueue didn't happen because this is a password field and caller shouldn't post a notification unless it is queued. |
| return true; |
| } |
| |
| m_passwordNotifications.append({ *observableObject, secureContext(*observableObject, context) }); |
| if (!m_passwordNotificationTimer.isActive()) |
| m_passwordNotificationTimer.startRepeating(accessibilityPasswordValueChangeNotificationInterval); |
| |
| return true; |
| } |
| |
| void AXObjectCache::passwordNotificationTimerFired() |
| { |
| if (m_passwordNotifications.isEmpty()) { |
| m_passwordNotificationTimer.stop(); |
| return; |
| } |
| |
| auto notification = m_passwordNotifications.takeFirst(); |
| auto& context = notification.second; |
| switch (context.intent.type) { |
| case AXTextStateChangeTypeEdit: |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| updateIsolatedTree(notification.first, AXNotification::ValueChanged); |
| #endif |
| if (context.intent.editType == AXTextEditTypeReplace) { |
| postTextReplacementPlatformNotification(notification.first.ptr(), |
| AXTextEditTypeDelete, context.deletedText, AXTextEditTypeInsert, context.insertedText, context.selection.start()); |
| } else { |
| postTextStateChangePlatformNotification(notification.first.ptr(), |
| context.intent.editType, context.insertedText, context.selection.start()); |
| } |
| break; |
| case AXTextStateChangeTypeSelectionMove: |
| case AXTextStateChangeTypeSelectionExtend: |
| case AXTextStateChangeTypeSelectionBoundary: |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| updateIsolatedTree(notification.first, AXNotification::SelectedTextChanged); |
| #endif |
| postTextSelectionChangePlatformNotification(notification.first.ptr(), |
| context.intent, context.selection); |
| break; |
| case AXTextStateChangeTypeUnknown: |
| // No additional context, fallback to a ValueChanged notification. |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| updateIsolatedTree(notification.first, AXNotification::ValueChanged); |
| #endif |
| postPlatformNotification(notification.first, AXNotification::ValueChanged); |
| break; |
| }; |
| } |
| |
| #endif // PLATFORM(COCOA) |
| |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| void AXObjectCache::onSelectedTextChanged(const VisiblePositionRange& selection, AccessibilityObject* object) |
| { |
| if (object) { |
| m_lastDebouncedTextRangeObject = object->objectID(); |
| if (!m_selectedTextRangeTimer.isActive()) |
| m_selectedTextRangeTimer.restart(); |
| } |
| |
| if (RefPtr tree = AXIsolatedTree::treeForPageID(m_pageID)) { |
| if (selection.isNull()) |
| tree->setSelectedTextMarkerRange({ }); |
| else { |
| auto startPosition = selection.start.deepEquivalent(); |
| auto endPosition = selection.end.deepEquivalent(); |
| |
| if (startPosition.isNull() || endPosition.isNull()) |
| tree->setSelectedTextMarkerRange({ }); |
| else { |
| if (auto* startObject = get(startPosition.anchorNode())) |
| createIsolatedObjectIfNeeded(*startObject); |
| if (auto* endObject = get(endPosition.anchorNode())) |
| createIsolatedObjectIfNeeded(*endObject); |
| |
| tree->setSelectedTextMarkerRange({ selection }); |
| } |
| } |
| } |
| } |
| #endif // ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| |
| void AXObjectCache::frameLoadingEventNotification(LocalFrame* frame, AXLoadingEvent loadingEvent) |
| { |
| if (!frame) |
| return; |
| |
| // Delegate on the right platform |
| frameLoadingEventPlatformNotification(getOrCreate(frame->contentRenderer()), loadingEvent); |
| } |
| |
| void AXObjectCache::postLiveRegionChangeNotification(AccessibilityObject& object) |
| { |
| if (m_liveRegionChangedPostTimer.isActive()) |
| m_liveRegionChangedPostTimer.stop(); |
| |
| Ref objectRef = object; |
| if (!m_changedLiveRegions.contains(objectRef)) |
| m_changedLiveRegions.add(WTFMove(objectRef)); |
| |
| m_liveRegionChangedPostTimer.startOneShot(0_s); |
| } |
| |
| void AXObjectCache::liveRegionChangedNotificationPostTimerFired() |
| { |
| m_liveRegionChangedPostTimer.stop(); |
| |
| if (m_changedLiveRegions.isEmpty()) |
| return; |
| |
| for (auto& object : m_changedLiveRegions) |
| postNotification(object.ptr(), object->protectedDocument().get(), AXNotification::LiveRegionChanged); |
| m_changedLiveRegions.clear(); |
| } |
| |
| void AXObjectCache::onScrollbarUpdate(ScrollView& view) |
| { |
| m_deferredScrollbarUpdateChangeList.add(view); |
| if (!m_performCacheUpdateTimer.isActive()) |
| m_performCacheUpdateTimer.startOneShot(0_s); |
| } |
| |
| void AXObjectCache::handleScrollbarUpdate(ScrollView& view) |
| { |
| // We don't want to create a scroll view from this method, only update an existing one. |
| if (auto* scrollViewObject = get(&view)) { |
| stopCachingComputedObjectAttributes(); |
| scrollViewObject->updateChildrenIfNecessary(); |
| } |
| } |
| |
| void AXObjectCache::handleAriaExpandedChange(Element& element) |
| { |
| // An aria-expanded change can cause two notifications to be posted: |
| // RowCountChanged for the tree or table ancestor of this object, and |
| // RowExpanded/Collapsed for this object. |
| if (RefPtr object = get(element)) { |
| // Find the ancestor that supports RowCountChanged if exists. |
| auto* ancestor = Accessibility::findAncestor<AccessibilityObject>(*object, false, [] (auto& candidate) { |
| return candidate.supportsRowCountChange(); |
| }); |
| |
| // Post that the ancestor's row count changed. |
| if (ancestor) |
| handleRowCountChanged(ancestor, protectedDocument().get()); |
| |
| // Post that the specific row either collapsed or expanded. |
| auto role = object->role(); |
| if (role == AccessibilityRole::Row || role == AccessibilityRole::TreeItem) |
| postNotification(object.get(), protectedDocument().get(), object->isExpanded() ? AXNotification::RowExpanded : AXNotification::RowCollapsed); |
| else |
| postNotification(object.get(), protectedDocument().get(), AXNotification::ExpandedChanged); |
| } |
| } |
| |
| void AXObjectCache::handleActiveDescendantChange(Element& element, const AtomString& oldValue, const AtomString& newValue) |
| { |
| AXTRACE("AXObjectCache::handleActiveDescendantChange"_s); |
| |
| // Use the element's document instead of the cache's document in case we're inside a frame that's managing focus. |
| if (!element.document().frame()->selection().isFocusedAndActive()) |
| return; |
| |
| RefPtr object = getOrCreate(element); |
| if (!object) |
| return; |
| |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| updateIsolatedTree(*object, AXNotification::ActiveDescendantChanged); |
| #endif |
| |
| // Notify active descendant changes only for the focused element. |
| if (element.document().focusedElement() != &element) |
| return; |
| |
| RefPtr activeDescendant = dynamicDowncast<AccessibilityObject>(object->activeDescendant()); |
| if (!activeDescendant) { |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| if (object->shouldFocusActiveDescendant() |
| && !oldValue.isEmpty() && newValue.isEmpty()) { |
| // The focused object just lost its active descendant, so set the IsolatedTree focused object back to it. |
| setIsolatedTreeFocusedObject(object.get()); |
| } |
| #else |
| UNUSED_PARAM(oldValue); |
| UNUSED_PARAM(newValue); |
| #endif |
| return; |
| } |
| |
| if (object->shouldFocusActiveDescendant()) { |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| setIsolatedTreeFocusedObject(activeDescendant.get()); |
| #endif |
| postPlatformNotification(*activeDescendant, AXNotification::FocusedUIElementChanged); |
| } |
| |
| // Handle active-descendant changes when the target allows for it, or the controlled object allows for it. |
| RefPtr<AccessibilityObject> target; |
| if (object->supportsActiveDescendant()) |
| target = object; |
| else { |
| // Check to see if the active descendant is a descendant of an object controlled by this object. |
| // In that case, the controlled object will be the target for the notification. |
| auto controlledObjects = object->relatedObjects(AXRelation::ControllerFor); |
| if (controlledObjects.size()) { |
| target = Accessibility::findAncestor(*activeDescendant, false, [&controlledObjects] (const auto& activeDescendantAncestor) { |
| return controlledObjects.contains(Ref { activeDescendantAncestor }); |
| }); |
| } |
| } |
| |
| if (!target) |
| return; |
| |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| if (target != object) |
| updateIsolatedTree(target.get(), AXNotification::ActiveDescendantChanged); |
| #endif |
| |
| postPlatformNotification(*target, AXNotification::ActiveDescendantChanged); |
| |
| // Table cell active descendant changes should trigger selected cell changes. |
| if (target->isTable() && activeDescendant->isExposedTableCell()) |
| postPlatformNotification(*target, AXNotification::SelectedCellsChanged); |
| } |
| |
| static bool isTableOrRowRole(const AtomString& attrValue) |
| { |
| return attrValue == "table"_s |
| || attrValue == "grid"_s |
| || attrValue == "treegrid"_s |
| || attrValue == "row"_s; |
| } |
| |
| void AXObjectCache::handleRoleChanged(Element& element, const AtomString& oldValue, const AtomString& newValue) |
| { |
| AXTRACE("AXObjectCache::handleRoleChanged"_s); |
| AXLOG(makeString("oldValue "_s, oldValue, " new value "_s, newValue)); |
| ASSERT(oldValue != newValue); |
| |
| RefPtr object = get(element); |
| if (!object) |
| return; |
| |
| // The class of an AX object created for an Element depends on the role attribute of that Element. |
| // Thus when the role changes, remove the existing AX object and force a ChildrenChanged on the parent so that the object is re-created. |
| // At the moment this is done only for table and row roles. Other roles may be added here if needed. |
| if (oldValue.isEmpty() || isTableOrRowRole(oldValue) |
| || newValue.isEmpty() || isTableOrRowRole(newValue)) { |
| if (auto* parent = object->parentObject()) { |
| remove(element); |
| childrenChanged(parent); |
| return; |
| } |
| } |
| |
| object->updateRole(); |
| } |
| |
| void AXObjectCache::handleRoleChanged(AccessibilityObject& axObject, AccessibilityRole oldRole) |
| { |
| stopCachingComputedObjectAttributes(); |
| axObject.recomputeIsIgnored(); |
| |
| #if PLATFORM(MAC) |
| if (axObject.supportsLiveRegion()) |
| addSortedObject(axObject, PreSortedObjectType::LiveRegion); |
| else if (AXCoreObject::liveRegionStatusIsEnabled(AtomString { AXCoreObject::defaultLiveRegionStatusForRole(oldRole) })) |
| removeLiveRegion(axObject); |
| #else |
| UNUSED_PARAM(oldRole); |
| #endif // PLATFORM(MAC) |
| |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| postNotification(axObject, AXNotification::RoleChanged); |
| #endif |
| } |
| |
| void AXObjectCache::handleARIARoleDescriptionChanged(Element& element) |
| { |
| RefPtr object = get(element); |
| if (!object) |
| return; |
| |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| updateIsolatedTree(object.get(), AXNotification::ARIARoleDescriptionChanged); |
| #endif |
| } |
| |
| void AXObjectCache::handleInputTypeChanged(Element& element) |
| { |
| RefPtr object = get(element); |
| if (!object) |
| return; |
| |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| updateIsolatedTree(object.get(), AXNotification::InputTypeChanged); |
| #endif |
| } |
| |
| |
| void AXObjectCache::deferAttributeChangeIfNeeded(Element& element, const QualifiedName& attrName, const AtomString& oldValue, const AtomString& newValue) |
| { |
| AXTRACE(makeString("AXObjectCache::deferAttributeChangeIfNeeded 0x"_s, hex(reinterpret_cast<uintptr_t>(this)))); |
| |
| if (nodeRendererIsValid(element) && rendererNeedsDeferredUpdate(*element.renderer())) { |
| m_deferredAttributeChange.append({ element, attrName, oldValue, newValue }); |
| if (!m_performCacheUpdateTimer.isActive()) |
| m_performCacheUpdateTimer.startOneShot(0_s); |
| AXLOG(makeString("Deferring handling of attribute "_s, attrName.localName().string(), " for element "_s, element.debugDescription())); |
| return; |
| } |
| Ref protectedElement { element }; |
| handleAttributeChange(protectedElement.ptr(), attrName, oldValue, newValue); |
| if (attrName == idAttr) |
| relationsNeedUpdate(true); |
| } |
| |
| void AXObjectCache::handleReferenceTargetChanged() |
| { |
| relationsNeedUpdate(true); |
| } |
| |
| void AXObjectCache::handlePageEditibilityChanged(Document& document) |
| { |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| postNotification(&document, AXNotification::IsEditableWebAreaChanged); |
| #else |
| UNUSED_PARAM(document); |
| #endif |
| } |
| |
| bool AXObjectCache::shouldProcessAttributeChange(Element* element, const QualifiedName& attrName) |
| { |
| if (!element) |
| return false; |
| |
| // aria-modal ends up affecting sub-trees that are being shown/hidden so it's likely that |
| // an AT would not have accessed this node yet. |
| if (attrName == aria_modalAttr) |
| return true; |
| |
| // If an AXObject has yet to be created, then there's no need to process attribute changes. |
| // Some of these notifications are processed on the parent, so allow that to proceed as well |
| return get(*element) || get(element->parentNode()); |
| } |
| |
| void AXObjectCache::handleAttributeChange(Element* element, const QualifiedName& attrName, const AtomString& oldValue, const AtomString& newValue) |
| { |
| AXTRACE(makeString("AXObjectCache::handleAttributeChange 0x"_s, hex(reinterpret_cast<uintptr_t>(this)))); |
| AXLOG(makeString("attribute "_s, attrName.localName(), " for element "_s, element ? element->debugDescription() : "nullptr"_str)); |
| AXLOG(makeString("old value: "_s, oldValue, " new value: "_s, newValue)); |
| |
| enum class TableProperty : uint8_t { Exposed = 1 << 0, CellSlots = 1 << 1 }; |
| auto recomputeParentTableProperties = [this] (Element* element, OptionSet<TableProperty> properties) { |
| ASSERT(!properties.isEmpty()); |
| |
| if (!properties.contains(TableProperty::CellSlots)) { |
| // If we're re-computing the exposed state of the table, we only need to do work for non-ARIA tables, allowing us to |
| // do a cheap dynamicDowncast check for an HTMLTablePartElement rather than calling AccessibilityTableCell::parentTable(). |
| // (ARIA tables are inherently always exposed). |
| if (auto* tablePartElement = dynamicDowncast<HTMLTablePartElement>(element)) |
| deferRecomputeTableIsExposed(const_cast<HTMLTableElement*>(tablePartElement->findParentTable().get())); |
| } else if (auto* axCell = dynamicDowncast<AccessibilityTableCell>(getOrCreate(element))) { |
| if (auto* parentTable = axCell->parentTable()) { |
| if (properties.contains(TableProperty::Exposed) && !parentTable->isAriaTable()) |
| deferRecomputeTableIsExposed(parentTable->element()); |
| if (properties.contains(TableProperty::CellSlots)) |
| deferRecomputeTableCellSlots(*parentTable); |
| } |
| } |
| }; |
| |
| if (!shouldProcessAttributeChange(element, attrName)) |
| return; |
| // The remaining code in this method relies on shouldProcessAttributeChange null-checking element. |
| ASSERT(element); |
| |
| if (relationAttributes().contains(attrName)) |
| updateRelations(*element, attrName); |
| |
| if (attrName == roleAttr) |
| handleRoleChanged(*element, oldValue, newValue); |
| else if (attrName == altAttr || attrName == titleAttr) |
| handleTextChanged(getOrCreate(*element)); |
| else if (attrName == contenteditableAttr) { |
| if (auto* axObject = get(*element)) |
| axObject->updateRole(); |
| } |
| else if (attrName == disabledAttr) |
| postNotification(element, AXNotification::DisabledStateChanged); |
| else if (attrName == forAttr) { |
| if (RefPtr label = dynamicDowncast<HTMLLabelElement>(element)) { |
| updateLabelFor(*label); |
| |
| if (RefPtr oldControl = element->treeScope().elementByIdResolvingReferenceTarget(oldValue)) |
| postNotification(oldControl.get(), AXNotification::TextChanged); |
| if (RefPtr newControl = element->treeScope().elementByIdResolvingReferenceTarget(newValue)) |
| postNotification(newControl.get(), AXNotification::TextChanged); |
| } |
| } else if (attrName == requiredAttr) |
| postNotification(element, AXNotification::RequiredStatusChanged); |
| else if (attrName == tabindexAttr) { |
| if (oldValue.isEmpty() || newValue.isEmpty()) { |
| RefPtr parent = element->parentNode(); |
| if (auto* renderer = parent ? parent->renderer() : nullptr) |
| childrenChanged(*renderer); |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| postNotification(element, AXNotification::FocusableStateChanged); |
| #endif |
| } |
| } |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| else if (attrName == draggableAttr) |
| postNotification(get(*element), AXNotification::DraggableStateChanged); |
| else if (attrName == langAttr) |
| updateIsolatedTree(get(*element), AXNotification::LanguageChanged); |
| else if (attrName == nameAttr) |
| postNotification(get(*element), AXNotification::NameChanged); |
| else if (attrName == placeholderAttr) |
| postNotification(element, AXNotification::PlaceholderChanged); |
| else if (attrName == hrefAttr || attrName == srcAttr) |
| postNotification(element, AXNotification::URLChanged); |
| else if (attrName == idAttr) { |
| #if !LOG_DISABLED |
| updateIsolatedTree(get(*element), AXNotification::IdAttributeChanged); |
| #endif |
| } else if (attrName == accesskeyAttr) |
| updateIsolatedTree(get(*element), AXNotification::AccessKeyChanged); |
| #endif // ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| else if (attrName == openAttr) { |
| if (is<HTMLDialogElement>(*element)) { |
| deferModalChange(*element); |
| recomputeIsIgnored(element->parentNode()); |
| } else if (is<HTMLDetailsElement>(*element)) { |
| if (RefPtr object = get(*element)) { |
| postNotification(*object, AXNotification::ExpandedChanged); |
| childrenChanged(object.get()); |
| } |
| |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| for (auto& summary : descendantsOfType<HTMLSummaryElement>(*element)) |
| updateIsolatedTree(get(summary), AXNotification::ExpandedChanged); |
| #endif // ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| } |
| } else if (attrName == rowspanAttr) { |
| deferRowspanChange(get(*element)); |
| recomputeParentTableProperties(element, TableProperty::CellSlots); |
| } else if (attrName == colspanAttr) { |
| postNotification(element, AXNotification::ColumnSpanChanged); |
| recomputeParentTableProperties(element, TableProperty::CellSlots); |
| } else if (attrName == commandAttr) |
| postNotification(element, AXNotification::CommandChanged); |
| else if (attrName == commandforAttr) |
| postNotification(element, AXNotification::CommandForChanged); |
| else if (attrName == popovertargetAttr) |
| postNotification(element, AXNotification::PopoverTargetChanged); |
| else if (attrName == scopeAttr) |
| postNotification(element, AXNotification::CellScopeChanged); |
| else if (attrName == datetimeAttr) |
| postNotification(element, AXNotification::DatetimeChanged); |
| |
| if (!attrName.localName().string().startsWith("aria-"_s)) |
| return; |
| |
| if (attrName == aria_activedescendantAttr) |
| handleActiveDescendantChange(*element, oldValue, newValue); |
| else if (attrName == aria_atomicAttr) |
| postNotification(element, AXNotification::IsAtomicChanged); |
| else if (attrName == aria_busyAttr) |
| postNotification(element, AXNotification::ElementBusyChanged); |
| else if (attrName == aria_controlsAttr) |
| postNotification(element, AXNotification::ControlledObjectsChanged); |
| else if (attrName == aria_valuenowAttr || attrName == aria_valuetextAttr) |
| postNotification(element, AXNotification::ValueChanged); |
| else if (attrName == aria_labelAttr && element->elementName() == ElementName::HTML_html) { |
| // When aria-label changes on an <html> element, it's the web area who needs to re-compute its accessibility text. |
| handleTextChanged(get(element->protectedDocument().ptr())); |
| } else if (attrName == aria_labelAttr || attrName == aria_labeledbyAttr || attrName == aria_labelledbyAttr) { |
| RefPtr axObject = get(*element); |
| if (!axObject) |
| return; |
| |
| if (hasAnyRole(*element, { "form"_s, "region"_s })) { |
| // https://w3c.github.io/aria/#document-handling_author-errors_roles |
| // The computed role of ARIA forms and regions is dependent on whether they have a label. |
| if (oldValue.isEmpty() || newValue.isEmpty()) |
| axObject->updateRole(); |
| } |
| handleTextChanged(axObject.get()); |
| } |
| else if (attrName == aria_checkedAttr) |
| checkedStateChanged(*element); |
| else if (attrName == aria_colcountAttr) { |
| postNotification(element, AXNotification::ColumnCountChanged); |
| deferRecomputeTableIsExposed(dynamicDowncast<HTMLTableElement>(element)); |
| } else if (attrName == aria_colindexAttr) { |
| postNotification(element, AXNotification::ARIAColumnIndexChanged); |
| recomputeParentTableProperties(element, TableProperty::Exposed); |
| } else if (attrName == aria_colspanAttr) { |
| postNotification(element, AXNotification::ColumnSpanChanged); |
| recomputeParentTableProperties(element, { TableProperty::CellSlots, TableProperty::Exposed }); |
| } |
| else if (attrName == aria_describedbyAttr) |
| postNotification(element, AXNotification::DescribedByChanged); |
| else if (attrName == aria_descriptionAttr) |
| postNotification(element, AXNotification::ExtendedDescriptionChanged); |
| else if (attrName == aria_dropeffectAttr) |
| postNotification(element, AXNotification::DropEffectChanged); |
| else if (attrName == aria_flowtoAttr) |
| postNotification(element, AXNotification::FlowToChanged); |
| else if (attrName == aria_grabbedAttr) |
| postNotification(element, AXNotification::GrabbedStateChanged); |
| else if (attrName == aria_keyshortcutsAttr) |
| postNotification(element, AXNotification::KeyShortcutsChanged); |
| else if (attrName == aria_levelAttr) |
| postNotification(element, AXNotification::LevelChanged); |
| else if (attrName == aria_liveAttr) { |
| postNotification(element, AXNotification::LiveRegionStatusChanged); |
| |
| #if PLATFORM(MAC) |
| if (RefPtr object = getOrCreate(element)) { |
| if (object->supportsLiveRegion()) |
| addSortedObject(*object, PreSortedObjectType::LiveRegion); |
| else |
| removeLiveRegion(*object); |
| } |
| #endif // PLATFORM(MAC) |
| } |
| else if (attrName == aria_placeholderAttr) |
| postNotification(element, AXNotification::PlaceholderChanged); |
| else if (attrName == aria_rowindexAttr) { |
| postNotification(element, AXNotification::ARIARowIndexChanged); |
| recomputeParentTableProperties(element, { TableProperty::CellSlots, TableProperty::Exposed }); |
| } |
| else if (attrName == aria_valuemaxAttr) |
| postNotification(element, AXNotification::MaximumValueChanged); |
| else if (attrName == aria_valueminAttr) |
| postNotification(element, AXNotification::MinimumValueChanged); |
| else if (attrName == aria_multilineAttr) { |
| if (auto* axObject = get(*element)) { |
| // The role of textarea and textfield objects is dependent on whether they can span multiple lines, so recompute it here. |
| if (axObject->role() == AccessibilityRole::TextArea || axObject->role() == AccessibilityRole::TextField) |
| axObject->updateRole(); |
| } |
| } |
| else if (attrName == aria_multiselectableAttr) |
| postNotification(element, AXNotification::MultiSelectableStateChanged); |
| else if (attrName == aria_orientationAttr) |
| postNotification(element, AXNotification::OrientationChanged); |
| else if (attrName == aria_posinsetAttr) |
| postNotification(element, AXNotification::PositionInSetChanged); |
| else if (attrName == aria_relevantAttr) |
| postNotification(element, AXNotification::LiveRegionRelevantChanged); |
| else if (attrName == aria_selectedAttr) |
| onSelectedChanged(*element); |
| else if (attrName == aria_setsizeAttr) |
| postNotification(element, AXNotification::SetSizeChanged); |
| else if (attrName == aria_expandedAttr) |
| handleAriaExpandedChange(*element); |
| else if (attrName == aria_haspopupAttr) |
| postNotification(element, AXNotification::HasPopupChanged); |
| else if (attrName == aria_hiddenAttr) { |
| #if ENABLE(INCLUDE_IGNORED_IN_CORE_AX_TREE) |
| if (RefPtr axObject = getOrCreate(*element)) { |
| Accessibility::enumerateDescendantsIncludingIgnored<AXCoreObject>(*axObject, /* includeSelf */ true, [] (auto& descendant) { |
| downcast<AccessibilityObject>(descendant).recomputeIsIgnored(); |
| }); |
| } |
| #else |
| if (RefPtr parent = get(element->parentNode())) |
| childrenChanged(parent.get()); |
| #endif // ENABLE(INCLUDE_IGNORED_IN_CORE_AX_TREE) |
| |
| if (m_currentModalElement && m_currentModalElement->isDescendantOf(element)) |
| deferModalChange(*m_currentModalElement); |
| } |
| else if (attrName == aria_invalidAttr) |
| postNotification(element, AXNotification::InvalidStatusChanged); |
| else if (attrName == aria_modalAttr) { |
| // aria-modal changed, so the element may have become modal or un-modal. |
| if (isModalElement(*element)) |
| m_modalElements.appendIfNotContains(element); |
| else |
| m_modalElements.removeAll(element); |
| deferModalChange(*element); |
| } |
| else if (attrName == aria_currentAttr) |
| postNotification(element, AXNotification::CurrentStateChanged); |
| else if (attrName == aria_disabledAttr) |
| postNotification(element, AXNotification::DisabledStateChanged); |
| else if (attrName == aria_pressedAttr) |
| postNotification(element, AXNotification::PressedStateChanged); |
| else if (attrName == aria_readonlyAttr) |
| postNotification(element, AXNotification::ReadOnlyStatusChanged); |
| else if (attrName == aria_requiredAttr) |
| postNotification(element, AXNotification::RequiredStatusChanged); |
| else if (attrName == aria_roledescriptionAttr) |
| handleARIARoleDescriptionChanged(*element); |
| else if (attrName == aria_rowcountAttr) |
| handleRowCountChanged(get(*element), element->protectedDocument().ptr()); |
| else if (attrName == aria_rowspanAttr) { |
| deferRowspanChange(get(*element)); |
| recomputeParentTableProperties(element, { TableProperty::CellSlots, TableProperty::Exposed }); |
| } else if (attrName == aria_sortAttr) |
| postNotification(element, AXNotification::SortDirectionChanged); |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| else if (attrName == aria_ownsAttr) { |
| if (oldValue.isEmpty() || newValue.isEmpty()) |
| updateIsolatedTree(get(*element), AXProperty::SupportsARIAOwns); |
| } else if (attrName == aria_braillelabelAttr) |
| postNotification(element, AXNotification::BrailleLabelChanged); |
| else if (attrName == aria_brailleroledescriptionAttr) |
| postNotification(element, AXNotification::BrailleRoleDescriptionChanged); |
| #endif // ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| else if (attrName == typeAttr) |
| handleInputTypeChanged(*element); |
| } |
| |
| void AXObjectCache::handleLabelChanged(AccessibilityObject* object) |
| { |
| AXTRACE("AXObjectCache::handleLabelChanged"_s); |
| |
| if (!object) |
| return; |
| |
| if (RefPtr label = dynamicDowncast<HTMLLabelElement>(object->element())) |
| updateLabelFor(*label); |
| else { |
| auto labeledObjects = object->labelForObjects(); |
| for (auto& labeledObject : labeledObjects) { |
| updateLabeledBy(RefPtr { labeledObject->element() }.get()); |
| postNotification(&downcast<AccessibilityObject>(labeledObject.get()), protectedDocument().get(), AXNotification::ValueChanged); |
| } |
| } |
| |
| postNotification(object, protectedDocument().get(), AXNotification::LabelChanged); |
| } |
| |
| void AXObjectCache::updateLabelFor(HTMLLabelElement& label) |
| { |
| removeRelation(label, AXRelation::LabelFor); |
| addLabelForRelation(label); |
| } |
| |
| void AXObjectCache::updateLabeledBy(Element* element) |
| { |
| if (!element) |
| return; |
| |
| bool changedRelation = removeRelation(*element, AXRelation::LabeledBy); |
| changedRelation |= addRelation(*element, aria_labelledbyAttr); |
| if (changedRelation) |
| dirtyIsolatedTreeRelations(); |
| } |
| |
| void AXObjectCache::dirtyIsolatedTreeRelations() |
| { |
| AXTRACE("AXObjectCache::dirtyIsolatedTreeRelations"_s); |
| |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| if (auto tree = AXIsolatedTree::treeForPageID(m_pageID)) |
| tree->relationsNeedUpdate(true); |
| startUpdateTreeSnapshotTimer(); |
| #endif |
| } |
| |
| void AXObjectCache::recomputeIsIgnored(RenderObject& renderer) |
| { |
| if (auto* object = get(renderer)) |
| object->recomputeIsIgnored(); |
| } |
| |
| void AXObjectCache::recomputeIsIgnored(Node* node) |
| { |
| if (auto* object = get(node)) |
| object->recomputeIsIgnored(); |
| } |
| |
| void AXObjectCache::startCachingComputedObjectAttributesUntilTreeMutates() |
| { |
| if (!m_computedObjectAttributeCache) |
| m_computedObjectAttributeCache = makeUnique<AXComputedObjectAttributeCache>(); |
| } |
| |
| void AXObjectCache::stopCachingComputedObjectAttributes() |
| { |
| m_computedObjectAttributeCache = nullptr; |
| } |
| |
| RefPtr<Document> AXObjectCache::protectedDocument() const |
| { |
| return document(); |
| } |
| |
| VisiblePosition AXObjectCache::visiblePositionForTextMarkerData(const TextMarkerData& textMarkerData) |
| { |
| RefPtr node = nodeForID(textMarkerData.axObjectID()); |
| if (!node) |
| return { }; |
| |
| if (node->isPseudoElement()) |
| return { }; |
| |
| auto visiblePosition = VisiblePosition({ node.get(), textMarkerData.offset, textMarkerData.anchorType }, textMarkerData.affinity); |
| auto deepPosition = visiblePosition.deepEquivalent(); |
| if (deepPosition.isNull()) |
| return { }; |
| |
| auto* renderer = deepPosition.deprecatedNode()->renderer(); |
| if (!renderer) |
| return { }; |
| |
| auto* cache = renderer->document().axObjectCache(); |
| // Return an empty position if the object associated with the text marker has been destroyed. |
| if (!cache || !cache->objectForID(*textMarkerData.axObjectID())) |
| return { }; |
| |
| return visiblePosition; |
| } |
| |
| CharacterOffset AXObjectCache::characterOffsetForTextMarkerData(const TextMarkerData& textMarkerData) |
| { |
| if (textMarkerData.ignored) |
| return { }; |
| |
| RefPtr node = nodeForID(textMarkerData.axObjectID()); |
| if (!node) |
| return { }; |
| |
| CharacterOffset result(node.get(), textMarkerData.characterStart, textMarkerData.characterOffset); |
| // When we are at a line wrap and the VisiblePosition is upstream, it means the text marker is at the end of the previous line. |
| // We use the previous CharacterOffset so that it will match the Range. |
| if (textMarkerData.affinity == Affinity::Upstream) |
| return previousCharacterOffset(result, false); |
| return result; |
| } |
| |
| CharacterOffset AXObjectCache::traverseToOffsetInRange(const SimpleRange& range, int offset, TraverseOption option, bool stayWithinRange) |
| { |
| bool toNodeEnd = option & TraverseOptionToNodeEnd; |
| bool validateOffset = option & TraverseOptionValidateOffset; |
| bool doNotEnterTextControls = option & TraverseOptionDoNotEnterTextControls; |
| |
| int offsetInCharacter = 0; |
| int cumulativeOffset = 0; |
| int remaining = 0; |
| int lastLength = 0; |
| Node* currentNode = nullptr; |
| bool finished = false; |
| int lastStartOffset = 0; |
| |
| TextIteratorBehaviors behaviors; |
| if (!doNotEnterTextControls) |
| behaviors.add(TextIteratorBehavior::EntersTextControls); |
| TextIterator iterator(range, behaviors); |
| |
| // Enable the cache here for isIgnored calls in replacedNodeNeedsCharacter. |
| AXAttributeCacheEnabler enableCache(this); |
| |
| // When the range has zero length, there might be replaced node or brTag that we need to increment the characterOffset. |
| if (iterator.atEnd()) { |
| currentNode = range.start.container.ptr(); |
| lastStartOffset = range.start.offset; |
| if (offset > 0 || toNodeEnd) { |
| if (AccessibilityObject::replacedNodeNeedsCharacter(*currentNode) || (currentNode->renderer() && currentNode->renderer()->isBR())) |
| cumulativeOffset++; |
| lastLength = cumulativeOffset; |
| |
| // When going backwards, stayWithinRange is false. |
| // Here when we don't have any character to move and we are going backwards, we traverse to the previous node. |
| if (!lastLength && toNodeEnd && !stayWithinRange) { |
| if (Node* preNode = previousNode(currentNode)) |
| return traverseToOffsetInRange(rangeForNodeContents(*preNode), offset, option); |
| return CharacterOffset(); |
| } |
| } |
| } |
| |
| // Sometimes text contents in a node are split into several iterations, so that iterator.range().startOffset() |
| // might not be the total character count. Here we use a previousNode object to keep track of that. |
| Node* previousNode = nullptr; |
| for (; !iterator.atEnd(); iterator.advance()) { |
| int currentLength = iterator.text().length(); |
| bool hasReplacedNodeOrBR = false; |
| |
| Node& node = iterator.range().start.container; |
| currentNode = &node; |
| |
| // When currentLength == 0, we check if there's any replaced node. |
| // If not, we skip the node with no length. |
| if (!currentLength) { |
| Node* childNode = iterator.node(); |
| if (AccessibilityObject::replacedNodeNeedsCharacter(*childNode)) { |
| cumulativeOffset++; |
| currentLength++; |
| currentNode = childNode; |
| hasReplacedNodeOrBR = true; |
| } else |
| continue; |
| } else { |
| // Ignore space, new line, tag node. |
| if (currentLength == 1) { |
| if (isASCIIWhitespace(iterator.text()[0])) { |
| // If the node has BR tag, we want to set the currentNode to it. |
| Node* childNode = iterator.node(); |
| if (childNode && childNode->renderer() && childNode->renderer()->isBR()) { |
| currentNode = childNode; |
| hasReplacedNodeOrBR = true; |
| } else if (auto* shadowHost = currentNode->shadowHost()) { |
| // Since we are entering text controls, we should set the currentNode |
| // to be the shadow host when there's no content. |
| if (elementIsTextControl(*shadowHost) && currentNode->isShadowRoot()) { |
| currentNode = shadowHost; |
| continue; |
| } |
| } else if (previousNode && previousNode->isTextNode() && previousNode->isDescendantOf(currentNode) && elementName(*currentNode) == ElementName::HTML_p) { |
| // TextIterator is emitting an extra newline after the <p> element. We should |
| // ignore that since the extra text node is not in the DOM tree. |
| currentNode = previousNode; |
| continue; |
| } else if (currentNode != previousNode) { |
| // We should set the start offset and length for the current node in case this is the last iteration. |
| lastStartOffset = 1; |
| lastLength = 0; |
| continue; |
| } |
| } |
| } |
| cumulativeOffset += currentLength; |
| } |
| |
| if (currentNode == previousNode) { |
| lastLength += currentLength; |
| lastStartOffset = iterator.range().end.offset - lastLength; |
| } else { |
| lastLength = currentLength; |
| lastStartOffset = hasReplacedNodeOrBR ? 0 : iterator.range().start.offset; |
| } |
| |
| // Break early if we have advanced enough characters. |
| bool offsetLimitReached = validateOffset ? cumulativeOffset + lastStartOffset >= offset : cumulativeOffset >= offset; |
| if (!toNodeEnd && offsetLimitReached) { |
| offsetInCharacter = validateOffset ? std::max(offset - lastStartOffset, 0) : offset - (cumulativeOffset - lastLength); |
| finished = true; |
| break; |
| } |
| previousNode = currentNode; |
| } |
| |
| if (!finished) { |
| offsetInCharacter = lastLength; |
| if (!toNodeEnd) |
| remaining = offset - cumulativeOffset; |
| } |
| |
| // Sometimes when we are getting the end CharacterOffset of a line range, the TextIterator will emit an extra space at the end |
| // and make the character count greater than the Range's end offset. |
| if (toNodeEnd && currentNode->isTextNode() && currentNode == range.end.container.ptr() && static_cast<int>(range.end.offset) < lastStartOffset + offsetInCharacter) |
| offsetInCharacter = range.end.offset - lastStartOffset; |
| |
| return CharacterOffset(currentNode, lastStartOffset, offsetInCharacter, remaining); |
| } |
| |
| unsigned AXObjectCache::lengthForRange(const SimpleRange& range) |
| { |
| unsigned length = 0; |
| for (TextIterator it(range); !it.atEnd(); it.advance()) { |
| // Non-zero length means textual node, zero length means replaced node (AKA "attachments" in AX). |
| if (it.text().length()) |
| length += it.text().length(); |
| else { |
| if (AccessibilityObject::replacedNodeNeedsCharacter(*it.node())) |
| ++length; |
| } |
| } |
| return length; |
| } |
| |
| SimpleRange AXObjectCache::rangeForNodeContents(Node& node) |
| { |
| if (AccessibilityObject::replacedNodeNeedsCharacter(node)) { |
| // For replaced nodes without children, the node itself is included in the range. |
| if (auto range = makeRangeSelectingNode(node)) |
| return *range; |
| } |
| return makeRangeSelectingNodeContents(node); |
| } |
| |
| std::optional<SimpleRange> AXObjectCache::rangeMatchesTextNearRange(const SimpleRange& originalRange, const String& matchText) |
| { |
| // Create a large enough range to find the text within it that's being searched for. |
| unsigned textLength = matchText.length(); |
| auto startPosition = VisiblePosition(makeContainerOffsetPosition(originalRange.start)); |
| for (unsigned k = 0; k < textLength; k++) { |
| auto testPosition = startPosition.previous(); |
| if (testPosition.isNull()) |
| break; |
| startPosition = testPosition; |
| } |
| |
| auto endPosition = VisiblePosition(makeContainerOffsetPosition(originalRange.end)); |
| for (unsigned k = 0; k < textLength; k++) { |
| auto testPosition = endPosition.next(); |
| if (testPosition.isNull()) |
| break; |
| endPosition = testPosition; |
| } |
| |
| auto searchRange = makeSimpleRange(startPosition, endPosition); |
| if (!searchRange || searchRange->collapsed()) |
| return std::nullopt; |
| |
| auto targetOffset = characterCount({ searchRange->start, originalRange.start }, TextIteratorBehavior::EmitsCharactersBetweenAllVisiblePositions); |
| return findClosestPlainText(*searchRange, matchText, { }, targetOffset); |
| } |
| |
| static bool isReplacedNodeOrBR(Node& node) |
| { |
| return AccessibilityObject::replacedNodeNeedsCharacter(node) || WebCore::elementName(node) == ElementName::HTML_br; |
| } |
| |
| static bool characterOffsetsInOrder(const CharacterOffset& characterOffset1, const CharacterOffset& characterOffset2) |
| { |
| // FIXME: Should just be able to call treeOrder without accessibility-specific logic. |
| // FIXME: Not clear why CharacterOffset needs to exist at all; we have both Position and BoundaryPoint to choose from. |
| |
| if (characterOffset1.isNull() || characterOffset2.isNull()) |
| return false; |
| |
| if (characterOffset1.node == characterOffset2.node) |
| return characterOffset1.offset <= characterOffset2.offset; |
| |
| RefPtr node1 = characterOffset1.node; |
| RefPtr node2 = characterOffset2.node; |
| if (!node1->isCharacterDataNode() && !isReplacedNodeOrBR(*node1) && node1->hasChildNodes()) |
| node1 = node1->traverseToChildAt(characterOffset1.offset); |
| if (!node2->isCharacterDataNode() && !isReplacedNodeOrBR(*node2) && node2->hasChildNodes()) |
| node2 = node2->traverseToChildAt(characterOffset2.offset); |
| if (!node1 || !node2) |
| return false; |
| |
| auto range1 = AXObjectCache::rangeForNodeContents(*node1); |
| auto range2 = AXObjectCache::rangeForNodeContents(*node2); |
| return is_lteq(treeOrder<ComposedTree>(range1.start, range2.start)); |
| } |
| |
| static Node* resetNodeAndOffsetForReplacedNode(Node& replacedNode, int& offset, int characterCount) |
| { |
| // Use this function to include the replaced node itself in the range we are creating. |
| auto nodeRange = AXObjectCache::rangeForNodeContents(replacedNode); |
| bool isInNode = static_cast<unsigned>(characterCount) <= WebCore::characterCount(nodeRange); |
| offset = replacedNode.computeNodeIndex() + (isInNode ? 0 : 1); |
| return replacedNode.parentNode(); |
| } |
| |
| static std::optional<BoundaryPoint> boundaryPoint(const CharacterOffset& characterOffset) |
| { |
| if (characterOffset.isNull()) |
| return std::nullopt; |
| |
| int offset = characterOffset.startIndex + characterOffset.offset; |
| // Guaranteed to be non-null by checking CharacterOffset::isNull. |
| RefPtr node = characterOffset.node; |
| if (isReplacedNodeOrBR(*node)) |
| node = resetNodeAndOffsetForReplacedNode(*node, offset, characterOffset.offset); |
| |
| if (!node) |
| return std::nullopt; |
| return { { *node, static_cast<unsigned>(offset) } }; |
| } |
| |
| static bool setRangeStartOrEndWithCharacterOffset(SimpleRange& range, const CharacterOffset& characterOffset, bool isStart) |
| { |
| auto point = boundaryPoint(characterOffset); |
| if (!point) |
| return false; |
| if (isStart) |
| range.start = *point; |
| else |
| range.end = *point; |
| return true; |
| } |
| |
| std::optional<SimpleRange> AXObjectCache::rangeForUnorderedCharacterOffsets(const CharacterOffset& characterOffset1, const CharacterOffset& characterOffset2) |
| { |
| bool alreadyInOrder = characterOffsetsInOrder(characterOffset1, characterOffset2); |
| auto start = boundaryPoint(alreadyInOrder ? characterOffset1 : characterOffset2); |
| auto end = boundaryPoint(alreadyInOrder ? characterOffset2 : characterOffset1); |
| if (!start || !end) |
| return std::nullopt; |
| return { { *start, *end } }; |
| } |
| |
| TextMarkerData AXObjectCache::textMarkerDataForCharacterOffset(const CharacterOffset& characterOffset, TextMarkerOrigin origin) |
| { |
| if (characterOffset.isNull()) |
| return { }; |
| |
| if (RefPtr input = dynamicDowncast<HTMLInputElement>(characterOffset.node.get()); input && input->isSecureField()) |
| return { *this, { }, true, origin }; |
| |
| return { *this, characterOffset, false, origin }; |
| } |
| |
| CharacterOffset AXObjectCache::startOrEndCharacterOffsetForRange(const SimpleRange& range, bool isStart, bool enterTextControls) |
| { |
| // When getting the end CharacterOffset at node boundary, we don't want to collapse to the previous node. |
| if (!isStart && !range.end.offset) |
| return characterOffsetForNodeAndOffset(range.end.container, 0, TraverseOptionIncludeStart); |
| |
| // If it's end text marker, we want to go to the end of the range, and stay within the range. |
| bool stayWithinRange = !isStart; |
| |
| Node& endNode = range.end.container; |
| if (endNode.isCharacterDataNode() && !isStart) |
| return traverseToOffsetInRange(rangeForNodeContents(endNode), range.end.offset, TraverseOptionValidateOffset); |
| |
| auto copyRange = range; |
| // Change the start of the range, so the character offset starts from node beginning. |
| int offset = 0; |
| auto& node = copyRange.start.container.get(); |
| if (node.isCharacterDataNode()) { |
| auto nodeStartOffset = traverseToOffsetInRange(rangeForNodeContents(node), range.start.offset, TraverseOptionValidateOffset); |
| if (isStart) |
| return nodeStartOffset; |
| copyRange.start.offset = 0; |
| offset += nodeStartOffset.offset; |
| } |
| |
| auto options = isStart ? TraverseOptionDefault : TraverseOptionToNodeEnd; |
| if (!enterTextControls) |
| options = static_cast<TraverseOption>(options | TraverseOptionDoNotEnterTextControls); |
| return traverseToOffsetInRange(copyRange, offset, options, stayWithinRange); |
| } |
| |
| TextMarkerData AXObjectCache::startOrEndTextMarkerDataForRange(const SimpleRange& range, bool isStart) |
| { |
| auto characterOffset = startOrEndCharacterOffsetForRange(range, isStart); |
| if (characterOffset.isNull()) |
| return { }; |
| return textMarkerDataForCharacterOffset(characterOffset); |
| } |
| |
| CharacterOffset AXObjectCache::characterOffsetForNodeAndOffset(Node& node, int offset, TraverseOption option) |
| { |
| Node* domNode = &node; |
| bool toNodeEnd = option & TraverseOptionToNodeEnd; |
| bool includeStart = option & TraverseOptionIncludeStart; |
| |
| // ignoreStart is used to determine if we should go to previous node or |
| // stay in current node when offset is 0. |
| if (!toNodeEnd && (offset < 0 || (!offset && !includeStart))) { |
| // Set the offset to the amount of characters we need to go backwards. |
| offset = - offset; |
| CharacterOffset charOffset = CharacterOffset(); |
| while (offset >= 0 && charOffset.offset <= offset) { |
| offset -= charOffset.offset; |
| domNode = previousNode(domNode); |
| if (domNode) { |
| charOffset = characterOffsetForNodeAndOffset(*domNode, 0, TraverseOptionToNodeEnd); |
| } else |
| return CharacterOffset(); |
| if (charOffset.offset == offset) |
| break; |
| } |
| if (offset > 0) |
| charOffset = characterOffsetForNodeAndOffset(Ref { *charOffset.node }, charOffset.offset - offset, TraverseOptionIncludeStart); |
| return charOffset; |
| } |
| |
| auto range = rangeForNodeContents(*domNode); |
| |
| // Traverse the offset amount of characters forward and see if there's remaining offsets. |
| // Keep traversing to the next node when there's remaining offsets. |
| CharacterOffset characterOffset = traverseToOffsetInRange(range, offset, option); |
| while (!characterOffset.isNull() && characterOffset.remaining() && !toNodeEnd) { |
| domNode = nextNode(domNode); |
| if (!domNode) |
| return CharacterOffset(); |
| range = rangeForNodeContents(*domNode); |
| characterOffset = traverseToOffsetInRange(range, characterOffset.remaining(), option); |
| } |
| |
| return characterOffset; |
| } |
| |
| bool AXObjectCache::shouldSkipBoundary(const CharacterOffset& previous, const CharacterOffset& next) |
| { |
| // Match the behavior of VisiblePosition, we should skip the node boundary when there's no visual space or new line character. |
| if (previous.isNull() || next.isNull()) |
| return false; |
| |
| if (previous.node == next.node) |
| return false; |
| |
| if (next.startIndex > 0 || next.offset > 0) |
| return false; |
| |
| CharacterOffset newLine = startCharacterOffsetOfLine(next); |
| if (next.isEqual(newLine)) |
| return false; |
| |
| return true; |
| } |
| |
| TextMarkerData AXObjectCache::textMarkerDataForNextCharacterOffset(const CharacterOffset& characterOffset) |
| { |
| if (characterOffset.isNull()) |
| return { }; |
| |
| TextMarkerData data; |
| auto next = characterOffset; |
| auto previous = characterOffset; |
| bool shouldContinue; |
| do { |
| shouldContinue = false; |
| next = nextCharacterOffset(next, false); |
| if (next.isNull()) |
| return { }; |
| |
| if (shouldSkipBoundary(previous, next)) |
| next = nextCharacterOffset(next, false); |
| if (next.isNull() || next.isEqual(previous)) |
| return { }; |
| |
| data = textMarkerDataForCharacterOffset(next); |
| |
| // We should skip next CharacterOffset if it's visually the same. |
| auto range = rangeForUnorderedCharacterOffsets(previous, next); |
| if (!range || !lengthForRange(*range)) |
| shouldContinue = true; |
| previous = next; |
| } while (data.ignored || shouldContinue); |
| return data; |
| } |
| |
| AXTextMarker AXObjectCache::nextTextMarker(const AXTextMarker& marker) |
| { |
| ASSERT(m_id == marker.treeID()); |
| return textMarkerDataForNextCharacterOffset(marker); |
| } |
| |
| TextMarkerData AXObjectCache::textMarkerDataForPreviousCharacterOffset(const CharacterOffset& characterOffset) |
| { |
| if (characterOffset.isNull()) |
| return { }; |
| |
| TextMarkerData data; |
| auto previous = characterOffset; |
| auto next = characterOffset; |
| bool shouldContinue; |
| do { |
| shouldContinue = false; |
| previous = previousCharacterOffset(previous, false); |
| if (previous.isNull() || previous.isEqual(next)) |
| return { }; |
| |
| data = textMarkerDataForCharacterOffset(previous); |
| |
| // We should skip previous CharacterOffset if it's visually the same. |
| auto range = rangeForUnorderedCharacterOffsets(previous, next); |
| if (!range || !lengthForRange(*range)) |
| shouldContinue = true; |
| next = previous; |
| } while (data.ignored || shouldContinue); |
| return data; |
| } |
| |
| AXTextMarker AXObjectCache::previousTextMarker(const AXTextMarker& marker) |
| { |
| ASSERT(m_id == marker.treeID()); |
| return textMarkerDataForPreviousCharacterOffset(marker); |
| } |
| |
| Node* AXObjectCache::nextNode(Node* node) const |
| { |
| if (!node) |
| return nullptr; |
| |
| return NodeTraversal::nextSkippingChildren(*node); |
| } |
| |
| Node* AXObjectCache::previousNode(Node* node) const |
| { |
| if (!node) |
| return nullptr; |
| |
| // First child of body shouldn't have previous node. |
| if (node->parentNode() && node->parentNode()->renderer() && node->parentNode()->renderer()->isBody() && !node->previousSibling()) |
| return nullptr; |
| |
| return NodeTraversal::previousSkippingChildren(*node); |
| } |
| |
| VisiblePosition AXObjectCache::visiblePositionFromCharacterOffset(const CharacterOffset& characterOffset) |
| { |
| if (characterOffset.isNull()) |
| return VisiblePosition(); |
| |
| // Create a collapsed range and use that to form a VisiblePosition, so that the case with |
| // composed characters will be covered. |
| auto range = rangeForUnorderedCharacterOffsets(characterOffset, characterOffset); |
| if (!range) |
| return { }; |
| return makeContainerOffsetPosition(range->start); |
| } |
| |
| CharacterOffset AXObjectCache::characterOffsetFromVisiblePosition(const VisiblePosition& targetVisiblePosition) |
| { |
| if (targetVisiblePosition.isNull()) |
| return { }; |
| |
| Position targetDeepPosition = targetVisiblePosition.deepEquivalent(); |
| // Dereferencing deprecatedNode is safe because VisiblePosition::isNull returns true if the deepEquivalent position has a null node. |
| Ref targetNode = *targetDeepPosition.deprecatedNode(); |
| if (targetNode->isCharacterDataNode()) |
| return traverseToOffsetInRange(rangeForNodeContents(targetNode.get()), targetDeepPosition.deprecatedEditingOffset(), TraverseOptionValidateOffset); |
| |
| RefPtr object = getOrCreate(targetNode.get()); |
| if (!object) |
| return { }; |
| |
| // Use nextVisiblePosition to calculate how many characters we need to traverse to the current position. |
| auto visiblePosition = object->visiblePositionRange().start; |
| int characterOffset = 0; |
| auto currentPosition = visiblePosition.deepEquivalent(); |
| while (!currentPosition.isNull() && !targetDeepPosition.equals(currentPosition)) { |
| auto previousPosition = currentPosition; |
| // Note that we explicitly _do not_ want currentPosition to be derived from visiblePositon.deepEquivalent(), |
| // as creating a VisiblePosition with a Position calls |canonicalPosition| on said Position, which critically |
| // can return a previous position, resulting in us looping infinitely. Iterating solely through |
| // |nextVisuallyDistinctCandidate|s should guarantee forward progress. |
| currentPosition = nextVisuallyDistinctCandidate(currentPosition, SkipDisplayContents::No); |
| visiblePosition = VisiblePosition(currentPosition, visiblePosition.affinity()); |
| |
| characterOffset++; |
| // When VisiblePostion moves to next node, it will count the leading line break as |
| // 1 offset, which we shouldn't include in CharacterOffset. |
| if (currentPosition.deprecatedNode() != previousPosition.deprecatedNode()) { |
| if (visiblePosition.characterBefore() == '\n') |
| characterOffset--; |
| } else if (currentPosition.deprecatedNode()->isCharacterDataNode()) { |
| // Sometimes VisiblePosition will move multiple characters, like emoji. |
| characterOffset += currentPosition.offsetInContainerNode() - previousPosition.offsetInContainerNode() - 1; |
| } |
| } |
| |
| // Sometimes when the node is a replaced node and is ignored in accessibility, we get a wrong CharacterOffset from it. |
| CharacterOffset result = traverseToOffsetInRange(rangeForNodeContents(targetNode.get()), characterOffset); |
| if (result.remainingOffset > 0 && !result.isNull() && isRendererReplacedElement(result.node->renderer())) |
| result.offset += result.remainingOffset; |
| return result; |
| } |
| |
| AccessibilityObject* AXObjectCache::objectForTextMarkerData(const TextMarkerData& textMarkerData) |
| { |
| if (textMarkerData.ignored) |
| return nullptr; |
| |
| RefPtr object = m_objects.get(*textMarkerData.axObjectID()); |
| if (!object) |
| return nullptr; |
| |
| Node* node = object->node(); |
| if (!node) |
| return nullptr; |
| ASSERT(object.get() == getOrCreate(*node)); |
| return getOrCreate(*node); |
| } |
| |
| std::optional<TextMarkerData> AXObjectCache::textMarkerDataForVisiblePosition(const VisiblePosition& visiblePosition, TextMarkerOrigin origin) |
| { |
| if (visiblePosition.isNull()) |
| return std::nullopt; |
| |
| Position position = visiblePosition.deepEquivalent(); |
| RefPtr node = position.anchorNode(); |
| ASSERT(node); |
| if (!node) |
| return std::nullopt; |
| |
| if (auto* input = dynamicDowncast<HTMLInputElement>(node.get()); input && input->isSecureField()) |
| return std::nullopt; |
| |
| #if ENABLE(AX_THREAD_TEXT_APIS) |
| if (shouldCreateAXThreadCompatibleMarkers()) { |
| // We need to convert the DOM offset (which is offset into pre-whitespace-collapse text) into an offset into |
| // the rendered, post-whitespace-collapse text. |
| unsigned domOffset = position.deprecatedEditingOffset(); |
| |
| auto createFromRendererAndOffset = [&origin, &visiblePosition] (RenderObject& renderer, unsigned offset) -> std::optional<TextMarkerData> { |
| CheckedPtr cache = renderer.document().axObjectCache(); |
| RefPtr object = cache ? cache->getOrCreate(renderer) : nullptr; |
| if (!object) |
| return std::nullopt; |
| |
| return std::optional(TextMarkerData { |
| cache->treeID(), |
| object->objectID(), |
| offset, |
| Position::PositionIsOffsetInAnchor, |
| visiblePosition.affinity(), |
| 0, |
| offset, |
| object->isIgnored(), |
| origin |
| }); |
| |
| }; |
| |
| if (isRendererReplacedElement(node->renderer()) || is<RenderLineBreak>(node->renderer())) |
| return createFromRendererAndOffset(*node->renderer(), domOffset); |
| |
| CheckedPtr<const RenderText> renderText = dynamicDowncast<RenderText>(node ? node->renderer() : nullptr); |
| |
| if (!renderText) { |
| auto boxAndOffset = visiblePosition.inlineBoxAndOffset(); |
| if (!boxAndOffset.box) |
| return std::nullopt; |
| |
| renderText = dynamicDowncast<RenderText>(boxAndOffset.box->renderer()); |
| if (!renderText) |
| return std::nullopt; |
| domOffset = boxAndOffset.offset; |
| } |
| |
| auto [textBox, orderCache] = InlineIterator::firstTextBoxInLogicalOrderFor(*renderText); |
| if (!textBox) |
| return std::nullopt; |
| |
| unsigned differenceBetweenDomAndRenderedOffsets = textBox->minimumCaretOffset(); |
| unsigned previousEndDomOffset = textBox->maximumCaretOffset(); |
| size_t previousLineIndex = textBox->lineIndex(); |
| |
| while (domOffset > textBox->maximumCaretOffset()) { |
| textBox = InlineIterator::nextTextBoxInLogicalOrder(textBox, orderCache); |
| size_t newLineIndex = textBox->lineIndex(); |
| unsigned differenceToPrevious = textBox->minimumCaretOffset() - previousEndDomOffset; |
| // Just like when building AXTextRuns, we need to consider trimmed spaces between lines. So, if we find |
| // a gap between runs, subtract one from that gap to account for a trimmed space. |
| unsigned trimmedCharacterAdjustment = newLineIndex != previousLineIndex && differenceToPrevious ? 1 : 0; |
| differenceBetweenDomAndRenderedOffsets += differenceToPrevious - trimmedCharacterAdjustment; |
| previousEndDomOffset = textBox->maximumCaretOffset(); |
| previousLineIndex = newLineIndex; |
| } |
| RELEASE_ASSERT(domOffset >= differenceBetweenDomAndRenderedOffsets); |
| unsigned renderedOffset = domOffset - differenceBetweenDomAndRenderedOffsets; |
| return createFromRendererAndOffset(const_cast<RenderText&>(*renderText), renderedOffset); |
| } |
| #endif // ENABLE(AX_THREAD_TEXT_APIS) |
| |
| // If the visible position has an anchor type referring to a node other than the anchored node, we should |
| // set the text marker data with CharacterOffset so that the offset will correspond to the node. |
| auto characterOffset = characterOffsetFromVisiblePosition(visiblePosition); |
| if (position.anchorType() == Position::PositionIsAfterAnchor || position.anchorType() == Position::PositionIsAfterChildren) |
| return textMarkerDataForCharacterOffset(characterOffset); |
| |
| CheckedPtr cache = node->document().axObjectCache(); |
| if (!cache) |
| return std::nullopt; |
| return { { *cache, visiblePosition, |
| characterOffset.startIndex, characterOffset.offset, false, origin } }; |
| } |
| |
| CharacterOffset AXObjectCache::nextCharacterOffset(const CharacterOffset& characterOffset, bool ignoreNextNodeStart) |
| { |
| if (characterOffset.isNull()) |
| return CharacterOffset(); |
| |
| RefPtr node = characterOffset.node; |
| // We don't always move one 'character' at a time since there might be composed characters. |
| unsigned nextOffset = Position::uncheckedNextOffset(node.get(), characterOffset.offset); |
| CharacterOffset next = characterOffsetForNodeAndOffset(*node, nextOffset); |
| |
| // To be consistent with VisiblePosition, we should consider the case that current node end to next node start counts 1 offset. |
| RefPtr nextNode = next.node; |
| if (!ignoreNextNodeStart && !next.isNull() && !isReplacedNodeOrBR(*nextNode) && nextNode != node) { |
| if (auto range = rangeForUnorderedCharacterOffsets(characterOffset, next)) { |
| auto length = characterCount(*range); |
| if (length > nextOffset - characterOffset.offset) |
| next = characterOffsetForNodeAndOffset(*nextNode, 0, TraverseOptionIncludeStart); |
| } |
| } |
| |
| return next; |
| } |
| |
| CharacterOffset AXObjectCache::previousCharacterOffset(const CharacterOffset& characterOffset, bool ignorePreviousNodeEnd) |
| { |
| if (characterOffset.isNull()) |
| return CharacterOffset(); |
| |
| // To be consistent with VisiblePosition, we should consider the case that current node start to previous node end counts 1 offset. |
| if (!ignorePreviousNodeEnd && !characterOffset.offset) |
| return characterOffsetForNodeAndOffset(Ref { *characterOffset.node }, 0); |
| |
| // We don't always move one 'character' a time since there might be composed characters. |
| RefPtr characterOffsetNode = characterOffset.node; |
| int previousOffset = Position::uncheckedPreviousOffset(characterOffsetNode.get(), characterOffset.offset); |
| return characterOffsetForNodeAndOffset(*characterOffsetNode, previousOffset, TraverseOptionIncludeStart); |
| } |
| |
| CharacterOffset AXObjectCache::startCharacterOffsetOfWord(const CharacterOffset& characterOffset, WordSide side) |
| { |
| if (characterOffset.isNull()) |
| return CharacterOffset(); |
| |
| CharacterOffset c = characterOffset; |
| if (side == WordSide::RightWordIfOnBoundary) { |
| CharacterOffset endOfParagraph = endCharacterOffsetOfParagraph(c); |
| if (c.isEqual(endOfParagraph)) |
| return c; |
| |
| // We should consider the node boundary that splits words. Otherwise VoiceOver won't see it as space. |
| c = nextCharacterOffset(characterOffset, false); |
| if (shouldSkipBoundary(characterOffset, c)) |
| c = nextCharacterOffset(c, false); |
| if (c.isNull()) |
| return characterOffset; |
| } |
| |
| return previousBoundary(c, startWordBoundary); |
| } |
| |
| CharacterOffset AXObjectCache::endCharacterOffsetOfWord(const CharacterOffset& characterOffset, WordSide side) |
| { |
| if (characterOffset.isNull()) |
| return CharacterOffset(); |
| |
| CharacterOffset c = characterOffset; |
| if (side == WordSide::LeftWordIfOnBoundary) { |
| CharacterOffset startOfParagraph = startCharacterOffsetOfParagraph(c); |
| if (c.isEqual(startOfParagraph)) |
| return c; |
| |
| c = previousCharacterOffset(characterOffset); |
| if (c.isNull()) |
| return characterOffset; |
| } else { |
| CharacterOffset endOfParagraph = endCharacterOffsetOfParagraph(characterOffset); |
| if (characterOffset.isEqual(endOfParagraph)) |
| return characterOffset; |
| } |
| |
| return nextBoundary(c, endWordBoundary); |
| } |
| |
| CharacterOffset AXObjectCache::previousWordStartCharacterOffset(const CharacterOffset& characterOffset) |
| { |
| if (characterOffset.isNull()) |
| return CharacterOffset(); |
| |
| CharacterOffset previousOffset = previousCharacterOffset(characterOffset); |
| if (previousOffset.isNull()) |
| return CharacterOffset(); |
| |
| return startCharacterOffsetOfWord(previousOffset, WordSide::RightWordIfOnBoundary); |
| } |
| |
| CharacterOffset AXObjectCache::nextWordEndCharacterOffset(const CharacterOffset& characterOffset) |
| { |
| if (characterOffset.isNull()) |
| return CharacterOffset(); |
| |
| CharacterOffset nextOffset = nextCharacterOffset(characterOffset); |
| if (nextOffset.isNull()) |
| return CharacterOffset(); |
| |
| return endCharacterOffsetOfWord(nextOffset, WordSide::LeftWordIfOnBoundary); |
| } |
| |
| std::optional<SimpleRange> AXObjectCache::leftWordRange(const CharacterOffset& characterOffset) |
| { |
| CharacterOffset start = startCharacterOffsetOfWord(characterOffset, WordSide::LeftWordIfOnBoundary); |
| CharacterOffset end = endCharacterOffsetOfWord(start); |
| return rangeForUnorderedCharacterOffsets(start, end); |
| } |
| |
| std::optional<SimpleRange> AXObjectCache::rightWordRange(const CharacterOffset& characterOffset) |
| { |
| CharacterOffset start = startCharacterOffsetOfWord(characterOffset, WordSide::RightWordIfOnBoundary); |
| CharacterOffset end = endCharacterOffsetOfWord(start); |
| return rangeForUnorderedCharacterOffsets(start, end); |
| } |
| |
| static char32_t characterForCharacterOffset(const CharacterOffset& characterOffset) |
| { |
| if (characterOffset.isNull() || !characterOffset.node->isTextNode()) |
| return 0; |
| |
| char32_t ch = 0; |
| unsigned offset = characterOffset.startIndex + characterOffset.offset; |
| if (offset < characterOffset.node->textContent().length()) { |
| // FIXME: Remove IGNORE_CLANG_WARNINGS macros once one of <rdar://problem/58615489&58615391> is fixed. |
| IGNORE_CLANG_WARNINGS_BEGIN("conditional-uninitialized") |
| U16_NEXT(characterOffset.node->textContent(), offset, characterOffset.node->textContent().length(), ch); |
| IGNORE_CLANG_WARNINGS_END |
| } |
| return ch; |
| } |
| |
| char32_t AXObjectCache::characterAfter(const CharacterOffset& characterOffset) |
| { |
| return characterForCharacterOffset(nextCharacterOffset(characterOffset)); |
| } |
| |
| char32_t AXObjectCache::characterBefore(const CharacterOffset& characterOffset) |
| { |
| return characterForCharacterOffset(characterOffset); |
| } |
| |
| static bool characterOffsetNodeIsBR(const CharacterOffset& characterOffset) |
| { |
| if (characterOffset.isNull()) |
| return false; |
| |
| return WebCore::elementName(*characterOffset.node) == ElementName::HTML_br; |
| } |
| |
| static Node* parentEditingBoundary(Node* node) |
| { |
| if (!node) |
| return nullptr; |
| |
| Node* documentElement = node->document().documentElement(); |
| if (!documentElement) |
| return nullptr; |
| |
| Node* boundary = node; |
| while (boundary != documentElement && boundary->nonShadowBoundaryParentNode() && node->hasEditableStyle() == boundary->parentNode()->hasEditableStyle()) |
| boundary = boundary->nonShadowBoundaryParentNode(); |
| |
| return boundary; |
| } |
| |
| CharacterOffset AXObjectCache::nextBoundary(const CharacterOffset& characterOffset, BoundarySearchFunction searchFunction) |
| { |
| if (characterOffset.isNull()) |
| return { }; |
| |
| RefPtr boundary = parentEditingBoundary(RefPtr { characterOffset.node }.get()); |
| if (!boundary) |
| return { }; |
| |
| auto searchRange = rangeForNodeContents(*boundary); |
| |
| Vector<UChar, 1024> string; |
| unsigned prefixLength = 0; |
| |
| if (requiresContextForWordBoundary(characterAfter(characterOffset))) { |
| auto backwardsScanRange = makeRangeSelectingNodeContents(boundary->document()); |
| if (!setRangeStartOrEndWithCharacterOffset(backwardsScanRange, characterOffset, false)) |
| return { }; |
| prefixLength = prefixLengthForRange(backwardsScanRange, string); |
| } |
| |
| if (!setRangeStartOrEndWithCharacterOffset(searchRange, characterOffset, true)) |
| return { }; |
| CharacterOffset end = startOrEndCharacterOffsetForRange(searchRange, false); |
| |
| TextIterator it(searchRange, TextIteratorBehavior::EmitsObjectReplacementCharacters); |
| unsigned next = forwardSearchForBoundaryWithTextIterator(it, string, prefixLength, searchFunction); |
| |
| if (it.atEnd() && next == string.size()) |
| return end; |
| |
| // We should consider the node boundary that splits words. |
| if (searchFunction == endWordBoundary && next - prefixLength == 1) |
| return nextCharacterOffset(characterOffset, false); |
| |
| // The endSentenceBoundary function will include a line break at the end of the sentence. |
| if (searchFunction == endSentenceBoundary && string[next - 1] == '\n') |
| next--; |
| |
| if (next > prefixLength) |
| return characterOffsetForNodeAndOffset(Ref { *characterOffset.node }, characterOffset.offset + next - prefixLength); |
| |
| return characterOffset; |
| } |
| |
| // FIXME: Share code with the one in VisibleUnits.cpp. |
| CharacterOffset AXObjectCache::previousBoundary(const CharacterOffset& characterOffset, BoundarySearchFunction searchFunction, NeedsContextAtParagraphStart needsContextAtParagraphStart) |
| { |
| if (characterOffset.isNull()) |
| return CharacterOffset(); |
| |
| RefPtr boundary = parentEditingBoundary(RefPtr { characterOffset.node.get() }.get()); |
| if (!boundary) |
| return CharacterOffset(); |
| |
| auto searchRange = rangeForNodeContents(*boundary); |
| Vector<UChar, 1024> string; |
| unsigned suffixLength = 0; |
| |
| if (needsContextAtParagraphStart == NeedsContextAtParagraphStart::Yes && startCharacterOffsetOfParagraph(characterOffset).isEqual(characterOffset)) { |
| auto forwardsScanRange = makeRangeSelectingNodeContents(boundary->protectedDocument()); |
| auto endOfCurrentParagraph = endCharacterOffsetOfParagraph(characterOffset); |
| if (!setRangeStartOrEndWithCharacterOffset(forwardsScanRange, characterOffset, true)) |
| return { }; |
| if (!setRangeStartOrEndWithCharacterOffset(forwardsScanRange, endOfCurrentParagraph, false)) |
| return { }; |
| for (TextIterator forwardsIterator(forwardsScanRange); !forwardsIterator.atEnd(); forwardsIterator.advance()) |
| append(string, forwardsIterator.text()); |
| suffixLength = string.size(); |
| } else if (requiresContextForWordBoundary(characterBefore(characterOffset))) { |
| auto forwardsScanRange = makeRangeSelectingNodeContents(boundary->protectedDocument()); |
| auto afterBoundary = makeBoundaryPointAfterNode(*boundary); |
| if (!afterBoundary) |
| return { }; |
| forwardsScanRange.start = *afterBoundary; |
| if (!setRangeStartOrEndWithCharacterOffset(forwardsScanRange, characterOffset, true)) |
| return { }; |
| suffixLength = suffixLengthForRange(forwardsScanRange, string); |
| } |
| |
| if (!setRangeStartOrEndWithCharacterOffset(searchRange, characterOffset, false)) |
| return { }; |
| CharacterOffset start = startOrEndCharacterOffsetForRange(searchRange, true); |
| |
| SimplifiedBackwardsTextIterator it(searchRange); |
| unsigned next = backwardSearchForBoundaryWithTextIterator(it, string, suffixLength, searchFunction); |
| |
| if (!next) |
| return it.atEnd() ? start : characterOffset; |
| |
| auto& node = (it.atEnd() ? searchRange : it.range()).start.container.get(); |
| |
| // SimplifiedBackwardsTextIterator ignores replaced elements. |
| // Subsequent *characterOffset.node dereferences are safe because we called CharacterOffset.isNull() |
| // at the top of the method. |
| if (AccessibilityObject::replacedNodeNeedsCharacter(*characterOffset.node)) |
| return characterOffsetForNodeAndOffset(*characterOffset.node, 0); |
| Node* nextSibling = node.nextSibling(); |
| if (&node != characterOffset.node.get() && nextSibling && AccessibilityObject::replacedNodeNeedsCharacter(*nextSibling)) |
| return startOrEndCharacterOffsetForRange(rangeForNodeContents(*nextSibling), false); |
| |
| if ((!suffixLength && node.isTextNode() && next <= node.length()) || (node.renderer() && node.renderer()->isBR() && !next)) { |
| // The next variable contains a usable index into a text node |
| if (node.isTextNode()) |
| return traverseToOffsetInRange(rangeForNodeContents(node), next, TraverseOptionValidateOffset); |
| return characterOffsetForNodeAndOffset(node, next, TraverseOptionIncludeStart); |
| } |
| |
| int characterCount = characterOffset.offset; |
| if (next < string.size() - suffixLength) |
| characterCount -= string.size() - suffixLength - next; |
| // We don't want to go to the previous node if the node is at the start of a new line. |
| if (characterCount < 0 && (characterOffsetNodeIsBR(characterOffset) || string[string.size() - suffixLength - 1] == '\n')) |
| characterCount = 0; |
| return characterOffsetForNodeAndOffset(*characterOffset.node, characterCount, TraverseOptionIncludeStart); |
| } |
| |
| CharacterOffset AXObjectCache::startCharacterOffsetOfParagraph(const CharacterOffset& characterOffset, EditingBoundaryCrossingRule boundaryCrossingRule) |
| { |
| if (characterOffset.isNull()) |
| return CharacterOffset(); |
| |
| Ref startNode = *characterOffset.node; |
| |
| if (isRenderedAsNonInlineTableImageOrHR(startNode.ptr())) |
| return startOrEndCharacterOffsetForRange(rangeForNodeContents(startNode), true); |
| |
| auto startBlock = enclosingBlock(startNode.ptr()); |
| int offset = characterOffset.startIndex + characterOffset.offset; |
| auto highestRoot = highestEditableRoot(firstPositionInOrBeforeNode(startNode.ptr())); |
| Position::AnchorType type = Position::PositionIsOffsetInAnchor; |
| |
| Ref node = *findStartOfParagraph(startNode.ptr(), highestRoot.get(), startBlock.get(), offset, type, boundaryCrossingRule); |
| |
| if (type == Position::PositionIsOffsetInAnchor) |
| return characterOffsetForNodeAndOffset(node, offset, TraverseOptionIncludeStart); |
| |
| return startOrEndCharacterOffsetForRange(rangeForNodeContents(node), true); |
| } |
| |
| CharacterOffset AXObjectCache::endCharacterOffsetOfParagraph(const CharacterOffset& characterOffset, EditingBoundaryCrossingRule boundaryCrossingRule) |
| { |
| if (characterOffset.isNull()) |
| return CharacterOffset(); |
| |
| RefPtr startNode = characterOffset.node; |
| if (isRenderedAsNonInlineTableImageOrHR(startNode.get())) |
| return startOrEndCharacterOffsetForRange(rangeForNodeContents(*startNode), false); |
| |
| auto stayInsideBlock = enclosingBlock(startNode.get()); |
| int offset = characterOffset.startIndex + characterOffset.offset; |
| auto highestRoot = highestEditableRoot(firstPositionInOrBeforeNode(startNode.get())); |
| Position::AnchorType type = Position::PositionIsOffsetInAnchor; |
| |
| Ref node = *findEndOfParagraph(startNode.get(), highestRoot.get(), stayInsideBlock.get(), offset, type, boundaryCrossingRule); |
| if (type == Position::PositionIsOffsetInAnchor) { |
| if (node->isTextNode()) { |
| CharacterOffset startOffset = startOrEndCharacterOffsetForRange(rangeForNodeContents(node), true); |
| offset -= startOffset.startIndex; |
| } |
| return characterOffsetForNodeAndOffset(node, offset, TraverseOptionIncludeStart); |
| } |
| |
| return startOrEndCharacterOffsetForRange(rangeForNodeContents(node), false); |
| } |
| |
| std::optional<SimpleRange> AXObjectCache::paragraphForCharacterOffset(const CharacterOffset& characterOffset) |
| { |
| CharacterOffset start = startCharacterOffsetOfParagraph(characterOffset); |
| CharacterOffset end = endCharacterOffsetOfParagraph(start); |
| return rangeForUnorderedCharacterOffsets(start, end); |
| } |
| |
| CharacterOffset AXObjectCache::nextParagraphEndCharacterOffset(const CharacterOffset& characterOffset) |
| { |
| // make sure we move off of a paragraph end |
| CharacterOffset next = nextCharacterOffset(characterOffset); |
| |
| // We should skip the following BR node. |
| if (characterOffsetNodeIsBR(next) && !characterOffsetNodeIsBR(characterOffset)) |
| next = nextCharacterOffset(next); |
| |
| return endCharacterOffsetOfParagraph(next); |
| } |
| |
| CharacterOffset AXObjectCache::previousParagraphStartCharacterOffset(const CharacterOffset& characterOffset) |
| { |
| // make sure we move off of a paragraph start |
| CharacterOffset previous = previousCharacterOffset(characterOffset); |
| |
| // We should skip the preceding BR node. |
| if (characterOffsetNodeIsBR(previous) && !characterOffsetNodeIsBR(characterOffset)) |
| previous = previousCharacterOffset(previous); |
| |
| return startCharacterOffsetOfParagraph(previous); |
| } |
| |
| CharacterOffset AXObjectCache::startCharacterOffsetOfSentence(const CharacterOffset& characterOffset) |
| { |
| return previousBoundary(characterOffset, startSentenceBoundary, NeedsContextAtParagraphStart::Yes); |
| } |
| |
| CharacterOffset AXObjectCache::endCharacterOffsetOfSentence(const CharacterOffset& characterOffset) |
| { |
| return nextBoundary(characterOffset, endSentenceBoundary); |
| } |
| |
| std::optional<SimpleRange> AXObjectCache::sentenceForCharacterOffset(const CharacterOffset& characterOffset) |
| { |
| CharacterOffset start = startCharacterOffsetOfSentence(characterOffset); |
| CharacterOffset end = endCharacterOffsetOfSentence(start); |
| return rangeForUnorderedCharacterOffsets(start, end); |
| } |
| |
| CharacterOffset AXObjectCache::nextSentenceEndCharacterOffset(const CharacterOffset& characterOffset) |
| { |
| // Make sure we move off of a sentence end. |
| return endCharacterOffsetOfSentence(nextCharacterOffset(characterOffset)); |
| } |
| |
| CharacterOffset AXObjectCache::previousSentenceStartCharacterOffset(const CharacterOffset& characterOffset) |
| { |
| // Make sure we move off of a sentence start. |
| CharacterOffset previous = previousCharacterOffset(characterOffset); |
| |
| // We should skip the preceding BR node. |
| if (characterOffsetNodeIsBR(previous) && !characterOffsetNodeIsBR(characterOffset)) |
| previous = previousCharacterOffset(previous); |
| |
| return startCharacterOffsetOfSentence(previous); |
| } |
| |
| LayoutRect AXObjectCache::localCaretRectForCharacterOffset(RenderObject*& renderer, const CharacterOffset& characterOffset) |
| { |
| if (characterOffset.isNull()) { |
| renderer = nullptr; |
| return IntRect(); |
| } |
| |
| renderer = characterOffset.node->renderer(); |
| if (!renderer) |
| return LayoutRect(); |
| |
| // Use a collapsed range to get the position. |
| auto range = rangeForUnorderedCharacterOffsets(characterOffset, characterOffset); |
| if (!range) |
| return IntRect(); |
| |
| auto boxAndOffset = makeContainerOffsetPosition(range->start).inlineBoxAndOffset(Affinity::Downstream); |
| if (boxAndOffset.box) |
| renderer = const_cast<RenderObject*>(&boxAndOffset.box->renderer()); |
| |
| if (auto* renderLineBreak = dynamicDowncast<RenderLineBreak>(renderer); renderLineBreak && InlineIterator::boxFor(*renderLineBreak) != boxAndOffset.box) |
| return IntRect(); |
| |
| return computeLocalCaretRect(*renderer, boxAndOffset); |
| } |
| |
| IntRect AXObjectCache::absoluteCaretBoundsForCharacterOffset(const CharacterOffset& characterOffset) |
| { |
| RenderBlock* caretPainter = nullptr; |
| |
| // First compute a rect local to the renderer at the selection start. |
| RenderObject* renderer = nullptr; |
| LayoutRect localRect = localCaretRectForCharacterOffset(renderer, characterOffset); |
| |
| localRect = localCaretRectInRendererForRect(localRect, RefPtr { characterOffset.node }.get(), renderer, caretPainter); |
| return absoluteBoundsForLocalCaretRect(caretPainter, localRect); |
| } |
| |
| CharacterOffset AXObjectCache::characterOffsetForPoint(const IntPoint& point, AXCoreObject* object) |
| { |
| if (!object) |
| return { }; |
| auto range = makeSimpleRange(object->visiblePositionForPoint(point)); |
| if (!range) |
| return { }; |
| return startOrEndCharacterOffsetForRange(*range, true); |
| } |
| |
| CharacterOffset AXObjectCache::characterOffsetForPoint(const IntPoint& point) |
| { |
| if (!m_document) |
| return { }; |
| auto range = makeSimpleRange(m_document->caretPositionFromPoint(point, HitTestSource::User)); |
| if (!range) |
| return { }; |
| return startOrEndCharacterOffsetForRange(*range, true); |
| } |
| |
| CharacterOffset AXObjectCache::characterOffsetForBounds(const IntRect& rect, bool first) |
| { |
| if (rect.isEmpty()) |
| return CharacterOffset(); |
| |
| IntPoint corner = first ? rect.minXMinYCorner() : rect.maxXMaxYCorner(); |
| CharacterOffset characterOffset = characterOffsetForPoint(corner); |
| |
| if (rect.contains(absoluteCaretBoundsForCharacterOffset(characterOffset).center())) |
| return characterOffset; |
| |
| // If the initial position is located outside the bounds adjust it incrementally as needed. |
| CharacterOffset nextCharOffset = nextCharacterOffset(characterOffset, false); |
| CharacterOffset previousCharOffset = previousCharacterOffset(characterOffset, false); |
| while (!nextCharOffset.isNull() || !previousCharOffset.isNull()) { |
| if (rect.contains(absoluteCaretBoundsForCharacterOffset(nextCharOffset).center())) |
| return nextCharOffset; |
| if (rect.contains(absoluteCaretBoundsForCharacterOffset(previousCharOffset).center())) |
| return previousCharOffset; |
| |
| nextCharOffset = nextCharacterOffset(nextCharOffset, false); |
| previousCharOffset = previousCharacterOffset(previousCharOffset, false); |
| } |
| |
| return CharacterOffset(); |
| } |
| |
| // FIXME: Remove VisiblePosition code after implementing this using CharacterOffset. |
| CharacterOffset AXObjectCache::endCharacterOffsetOfLine(const CharacterOffset& characterOffset) |
| { |
| if (characterOffset.isNull()) |
| return CharacterOffset(); |
| |
| VisiblePosition vp = visiblePositionFromCharacterOffset(characterOffset); |
| VisiblePosition endLine = endOfLine(vp); |
| |
| return characterOffsetFromVisiblePosition(endLine); |
| } |
| |
| CharacterOffset AXObjectCache::startCharacterOffsetOfLine(const CharacterOffset& characterOffset) |
| { |
| if (characterOffset.isNull()) |
| return CharacterOffset(); |
| |
| VisiblePosition vp = visiblePositionFromCharacterOffset(characterOffset); |
| VisiblePosition startLine = startOfLine(vp); |
| |
| return characterOffsetFromVisiblePosition(startLine); |
| } |
| |
| CharacterOffset AXObjectCache::characterOffsetForIndex(int index, const AXCoreObject* object) |
| { |
| if (!object) |
| return { }; |
| |
| auto visiblePosition = object->visiblePositionForIndex(index); |
| auto characterOffset = characterOffsetFromVisiblePosition(visiblePosition); |
| // In text control, VisiblePosition always gives the before position of a |
| // BR node, while CharacterOffset will do the opposite. |
| if (object->isTextControl() && characterOffsetNodeIsBR(characterOffset)) |
| characterOffset.offset = 1; |
| |
| auto range = object->simpleRange(); |
| if (!range) |
| return { }; |
| |
| auto start = startOrEndCharacterOffsetForRange(*range, true, true); |
| auto end = startOrEndCharacterOffsetForRange(*range, false, true); |
| auto result = start; |
| for (int i = 0; i < index; i++) { |
| if (result.isEqual(characterOffset)) { |
| // Do not include the new line character, always move the offset to the start of next node. |
| if ((characterOffset.node->isTextNode() || characterOffsetNodeIsBR(characterOffset))) { |
| auto next = nextCharacterOffset(characterOffset, false); |
| if (!next.isNull() && !next.offset && rootAXEditableElement(RefPtr { next.node }.get()) == rootAXEditableElement(RefPtr { characterOffset.node }.get())) |
| result = next; |
| } |
| break; |
| } |
| |
| result = nextCharacterOffset(result, false); |
| if (result.isEqual(end)) |
| break; |
| } |
| |
| return result; |
| } |
| |
| const Element* AXObjectCache::rootAXEditableElement(const Node* node) |
| { |
| const auto* result = node->rootEditableElement(); |
| const auto* element = dynamicDowncast<Element>(*node); |
| if (!element) |
| element = node->parentElement(); |
| |
| for (; element; element = element->parentElement()) { |
| if (elementIsTextControl(*element)) |
| result = element; |
| } |
| |
| return result; |
| } |
| |
| static void conditionallyAddNodeToFilterList(Node* node, const Document& document, HashSet<Ref<Node>>& nodesToRemove) |
| { |
| if (node && (!node->isConnected() || &node->document() == &document)) |
| nodesToRemove.add(*node); |
| } |
| |
| template<typename T> |
| static void filterVectorPairForRemoval(const Vector<std::pair<T, T>>& list, const Document& document, HashSet<Ref<Node>>& nodesToRemove) |
| { |
| for (auto& entry : list) { |
| conditionallyAddNodeToFilterList(entry.first, document, nodesToRemove); |
| conditionallyAddNodeToFilterList(entry.second, document, nodesToRemove); |
| } |
| } |
| |
| template<typename T, typename U> |
| static void filterMapForRemoval(const HashMap<T, U>& list, const Document& document, HashSet<Ref<Node>>& nodesToRemove) |
| { |
| for (auto& entry : list) |
| conditionallyAddNodeToFilterList(entry.key, document, nodesToRemove); |
| } |
| |
| template<typename T> |
| static void filterListForRemoval(const ListHashSet<T>& list, const Document& document, HashSet<Ref<Node>>& nodesToRemove) |
| { |
| for (auto& node : list) |
| conditionallyAddNodeToFilterList(node.ptr(), document, nodesToRemove); |
| } |
| |
| template<typename WeakHashSet> |
| static void filterWeakHashSetForRemoval(WeakHashSet& weakHashSet, const Document& document, HashSet<Ref<Node>>& nodesToRemove) |
| { |
| weakHashSet.forEach([&] (auto& element) { |
| conditionallyAddNodeToFilterList(&element, document, nodesToRemove); |
| }); |
| } |
| |
| template<typename WeakListHashSetType> |
| static void filterWeakListHashSetForRemoval(WeakListHashSetType& list, const Document& document, HashSet<Ref<Node>>& nodesToRemove) |
| { |
| for (auto& node : list) |
| conditionallyAddNodeToFilterList(&node, document, nodesToRemove); |
| } |
| |
| template<typename WeakHashMapType> |
| static void filterWeakHashMapForRemoval(WeakHashMapType& map, const Document& document, HashSet<Ref<Node>>& nodesToRemove) |
| { |
| for (auto elementEntry : map) |
| conditionallyAddNodeToFilterList(&elementEntry.key, document, nodesToRemove); |
| } |
| |
| void AXObjectCache::prepareForDocumentDestruction(const Document& document) |
| { |
| HashSet<Ref<Node>> nodesToRemove; |
| filterWeakListHashSetForRemoval(m_deferredTextChangedList, document, nodesToRemove); |
| filterWeakListHashSetForRemoval(m_deferredElementAddedOrRemovedList, document, nodesToRemove); |
| filterWeakHashSetForRemoval(m_deferredRecomputeIsIgnoredList, document, nodesToRemove); |
| filterWeakHashSetForRemoval(m_deferredRecomputeTableIsExposedList, document, nodesToRemove); |
| filterWeakHashSetForRemoval(m_deferredSelectedChildredChangedList, document, nodesToRemove); |
| filterWeakHashSetForRemoval(m_deferredModalChangedList, document, nodesToRemove); |
| filterWeakHashSetForRemoval(m_deferredMenuListChange, document, nodesToRemove); |
| filterWeakHashMapForRemoval(m_deferredTextFormControlValue, document, nodesToRemove); |
| m_deferredFocusedNodeChange = std::nullopt; |
| |
| for (const auto& entry : m_deferredAttributeChange) { |
| if (entry.element && (!entry.element->isConnected() || &entry.element->document() == &document)) |
| nodesToRemove.add(*entry.element); |
| } |
| |
| for (const auto& element : m_modalElements) { |
| if (element && (!element->isConnected() || &element->document() == &document)) |
| nodesToRemove.add(*element); |
| } |
| |
| for (auto& node : nodesToRemove) |
| remove(node); |
| } |
| |
| bool AXObjectCache::elementIsTextControl(const Element& element) |
| { |
| const auto* axObject = getOrCreate(const_cast<Element&>(element)); |
| return axObject && axObject->isTextControl(); |
| } |
| |
| static bool documentNeedsLayoutOrStyleRecalc(Document& document) |
| { |
| if (RefPtr frameView = document.view()) { |
| if (frameView->needsLayout() || frameView->layoutContext().isLayoutPending()) |
| return true; |
| } |
| |
| return document.hasPendingStyleRecalc(); |
| } |
| |
| void AXObjectCache::performDeferredCacheUpdate(ForceLayout forceLayout) |
| { |
| AXTRACE(makeString("AXObjectCache::performDeferredCacheUpdate 0x"_s, hex(reinterpret_cast<uintptr_t>(this)))); |
| if (m_performingDeferredCacheUpdate) { |
| AXLOG("Bailing out due to reentrant call."); |
| return; |
| } |
| SetForScope performingDeferredCacheUpdate(m_performingDeferredCacheUpdate, true); |
| |
| RefPtr document = this->document(); |
| if (!document) |
| return; |
| |
| // It's unexpected for this function to run in the middle of a render tree or style update. |
| ASSERT(!Accessibility::inRenderTreeOrStyleUpdate(*document)); |
| |
| if (!document->view()) |
| return; |
| |
| if (documentNeedsLayoutOrStyleRecalc(*document)) { |
| // Layout became dirty while waiting to performDeferredCacheUpdate, and we require clean layout |
| // to update the accessibility tree correctly in this function. |
| if ((m_cacheUpdateDeferredCount >= 3 || forceLayout == ForceLayout::Yes) && !Accessibility::inRenderTreeOrStyleUpdate(*document)) { |
| // Layout is being thrashed before we get a chance to update, so stop waiting and just force it. |
| m_cacheUpdateDeferredCount = 0; |
| document->updateLayoutIgnorePendingStylesheets(); |
| } else { |
| // Wait for layout to trigger another async cache update. |
| ++m_cacheUpdateDeferredCount; |
| return; |
| } |
| } |
| |
| RefPtr<Frame> frame = document->frame(); |
| if (frame && frame->isMainFrame()) { |
| // The layout of subframes must also be clean (assuming we're processing objects from those subframes), so force it here if necessary. |
| for (; frame; frame = frame->tree().traverseNext()) { |
| auto* localFrame = dynamicDowncast<LocalFrame>(frame.get()); |
| RefPtr subDocument = localFrame ? localFrame->document() : nullptr; |
| if (subDocument && documentNeedsLayoutOrStyleRecalc(*subDocument)) |
| subDocument->updateLayoutIgnorePendingStylesheets(); |
| } |
| } |
| |
| bool markedRelationsDirty = false; |
| auto markRelationsDirty = [&] () { |
| if (!markedRelationsDirty) { |
| relationsNeedUpdate(true); |
| markedRelationsDirty = true; |
| } |
| }; |
| AXLOGDeferredCollection("ReplacedObjectsList"_s, m_deferredReplacedObjects); |
| bool anyRelationsDirty = false; |
| for (AXID axID : m_deferredReplacedObjects) { |
| // If the replaced object was part of any relation, we need to make sure the relations are updated. |
| // Relations for this object may have been removed already (via the renderer being destroyed), so |
| // we should check if this axID was recently removed so we can dirty relations. |
| if (m_relations.contains(axID) || m_recentlyRemovedRelations.contains(axID)) |
| anyRelationsDirty = true; |
| remove(axID); |
| } |
| m_deferredReplacedObjects.clear(); |
| if (anyRelationsDirty) |
| markRelationsDirty(); |
| |
| AXLOGDeferredCollection("RecomputeTableIsExposedList"_s, m_deferredRecomputeTableIsExposedList); |
| m_deferredRecomputeTableIsExposedList.forEach([this] (auto& tableElement) { |
| if (auto* axTable = dynamicDowncast<AccessibilityTable>(get(&tableElement))) |
| axTable->recomputeIsExposable(); |
| }); |
| m_deferredRecomputeTableIsExposedList.clear(); |
| |
| AXLOGDeferredCollection("ChildrenChangedList"_s, m_deferredChildrenChangedList); |
| handleAllDeferredChildrenChanged(); |
| |
| AXLOGDeferredCollection("ElementAddedOrRemovedList"_s, m_deferredElementAddedOrRemovedList); |
| auto nodeAddedOrRemovedList = copyToVector(m_deferredElementAddedOrRemovedList); |
| for (auto& weakNode : nodeAddedOrRemovedList) { |
| if (RefPtr node = weakNode.get()) { |
| handleMenuOpened(*node); |
| handleLiveRegionCreated(*node); |
| |
| if (RefPtr label = dynamicDowncast<HTMLLabelElement>(node.get())) { |
| // A label was added or removed. Update its LabelFor relationships. |
| handleLabelChanged(getOrCreate(*label)); |
| } |
| } |
| } |
| m_deferredElementAddedOrRemovedList.clear(); |
| |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| AXLOGDeferredCollection("UnconnectedObjects"_s, m_deferredUnconnectedObjects); |
| if (auto tree = AXIsolatedTree::treeForPageID(m_pageID)) { |
| m_deferredUnconnectedObjects.forEach([&tree] (auto& object) { |
| tree->addUnconnectedNode(object); |
| }); |
| m_deferredUnconnectedObjects.clear(); |
| } |
| #endif |
| |
| AXLOGDeferredCollection("RecomputeTableCellSlotsList"_s, m_deferredRecomputeTableCellSlotsList); |
| m_deferredRecomputeTableCellSlotsList.forEach([this] (auto& axTable) { |
| handleRecomputeCellSlots(axTable); |
| }); |
| m_deferredRecomputeTableCellSlotsList.clear(); |
| |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| AXLOGDeferredCollection("RowspanChangesList"_s, m_deferredRowspanChanges); |
| m_deferredRowspanChanges.forEach([this] (auto& axCell) { |
| handleRowspanChanged(axCell); |
| }); |
| m_deferredRowspanChanges.clear(); |
| #endif |
| |
| AXLOGDeferredCollection("TextChangedList"_s, m_deferredTextChangedList); |
| auto textChangedList = copyToVector(m_deferredTextChangedList); |
| for (auto& weakNode : textChangedList) { |
| if (RefPtr node = weakNode.get()) |
| handleTextChanged(getOrCreate(*node)); |
| } |
| m_deferredTextChangedList.clear(); |
| |
| AXLOGDeferredCollection("RecomputeIsIgnoredList"_s, m_deferredRecomputeIsIgnoredList); |
| m_deferredRecomputeIsIgnoredList.forEach([this] (auto& element) { |
| if (auto* renderer = element.renderer()) |
| recomputeIsIgnored(*renderer); |
| }); |
| m_deferredRecomputeIsIgnoredList.clear(); |
| |
| AXLOGDeferredCollection("SelectedChildredChangedList"_s, m_deferredSelectedChildredChangedList); |
| m_deferredSelectedChildredChangedList.forEach([this] (auto& selectElement) { |
| selectedChildrenChanged(&selectElement); |
| }); |
| m_deferredSelectedChildredChangedList.clear(); |
| |
| AXLOGDeferredCollection("TextFormControlValue"_s, m_deferredTextFormControlValue); |
| for (auto deferredFormControlContext : m_deferredTextFormControlValue) { |
| auto& textFormControlElement = downcast<HTMLTextFormControlElement>(deferredFormControlContext.key); |
| postTextReplacementNotificationForTextControl(textFormControlElement, deferredFormControlContext.value, textFormControlElement.innerTextValue()); |
| } |
| m_deferredTextFormControlValue.clear(); |
| |
| AXLOGDeferredCollection("AttributeChange"_s, m_deferredAttributeChange); |
| for (const auto& attributeChange : m_deferredAttributeChange) { |
| handleAttributeChange(attributeChange.element.get(), attributeChange.attrName, attributeChange.oldValue, attributeChange.newValue); |
| if (attributeChange.attrName == idAttr) |
| markRelationsDirty(); |
| } |
| m_deferredAttributeChange.clear(); |
| |
| if (m_deferredFocusedNodeChange) { |
| AXLOG(makeString( |
| "Processing deferred focused node change. Old node "_s, |
| m_deferredFocusedNodeChange->first ? m_deferredFocusedNodeChange->first->debugDescription() : "nullptr"_s, |
| ", new node "_s, |
| m_deferredFocusedNodeChange->second ? m_deferredFocusedNodeChange->second->debugDescription() : "nullptr"_s |
| )); |
| // Don't update the modal with this focus change since it may need to be updated again as a result of processing m_deferredModalChangedList below. |
| handleFocusedUIElementChanged(m_deferredFocusedNodeChange->first.get(), m_deferredFocusedNodeChange->second.get(), UpdateModal::No); |
| // Recompute isIgnored after a focus change in case that altered visibility. |
| recomputeIsIgnored(m_deferredFocusedNodeChange->first.get()); |
| recomputeIsIgnored(m_deferredFocusedNodeChange->second.get()); |
| } |
| // If we changed the focused element, that could affect what modal should be active, so recompute it. |
| bool shouldRecomputeModal = m_deferredFocusedNodeChange.has_value(); |
| RefPtr newFocusElement = m_deferredFocusedNodeChange ? m_deferredFocusedNodeChange->second.get() : nullptr; |
| m_deferredFocusedNodeChange = std::nullopt; |
| |
| AXLOGDeferredCollection("ModalChangedList"_s, m_deferredModalChangedList); |
| for (auto& element : m_deferredModalChangedList) { |
| if (!is<HTMLDialogElement>(element) && !hasAnyRole(element, { "dialog"_s, "alertdialog"_s })) |
| continue; |
| shouldRecomputeModal = true; |
| |
| if (!m_modalNodesInitialized) [[unlikely]] |
| findModalNodes(); |
| |
| if (isModalElement(element)) { |
| // Add the newly modified node to the modal nodes set. |
| // We will recompute the current valid aria modal node in modalNode() when this node is not visible. |
| m_modalElements.append(&element); |
| } else { |
| m_modalElements.removeAllMatching([&element] (const auto& modalElement) { |
| return &element == modalElement.get(); |
| }); |
| } |
| } |
| m_deferredModalChangedList.clear(); |
| |
| if (shouldRecomputeModal) |
| updateCurrentModalNode(); |
| |
| AXLOGDeferredCollection("MenuListChange"_s, m_deferredMenuListChange); |
| m_deferredMenuListChange.forEach([this] (auto& element) { |
| handleMenuListValueChanged(element); |
| }); |
| m_deferredMenuListChange.clear(); |
| |
| m_deferredScrollbarUpdateChangeList.forEach([this] (auto& scrollView) { |
| handleScrollbarUpdate(scrollView); |
| }); |
| m_deferredScrollbarUpdateChangeList.clear(); |
| |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| if (m_deferredRegenerateIsolatedTree) { |
| if (auto tree = AXIsolatedTree::treeForPageID(m_pageID)) { |
| // Re-generate the subtree rooted at the webarea. |
| if (RefPtr webArea = rootWebArea()) { |
| AXLOG("Regenerating isolated tree from AXObjectCache::performDeferredCacheUpdate()."); |
| #if ENABLE(INCLUDE_IGNORED_IN_CORE_AX_TREE) |
| // m_deferredRegenerateIsolatedTree is only set when we change the active modal, which effects the ignored |
| // status of every object on the page. With ENABLE(INCLUDE_IGNORED_IN_CORE_AX_TREE), this means we only have |
| // to re-compute is-ignored for every object, rather than re-compute all objects entirely as when this flag is off. |
| tree->updatePropertiesForSelfAndDescendants(*webArea, { AXProperty::IsIgnored }); |
| #else |
| tree->generateSubtree(*webArea); |
| #endif // ENABLE(INCLUDE_IGNORED_IN_CORE_AX_TREE) |
| |
| // In some cases, the ID of the focus after a dialog pops up doesn't match the ID in the last focus change notification, creating a mismatch between the isolated tree cached focused object ID and the actual focused object ID. |
| // For this reason, reset the focused object ID. |
| if (auto* focus = focusedObjectForPage(document->protectedPage().get())) |
| tree->setFocusedNodeID(focus->objectID()); |
| } |
| } |
| } |
| m_deferredRegenerateIsolatedTree = false; |
| #endif // ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| |
| platformPerformDeferredCacheUpdate(); |
| } |
| |
| void AXObjectCache::handleMenuListValueChanged(Element& element) |
| { |
| RefPtr<AccessibilityObject> object = get(&element); |
| if (!object) |
| return; |
| |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| updateIsolatedTree(*object, AXNotification::MenuListValueChanged); |
| #endif |
| |
| postPlatformNotification(*object, AXNotification::MenuListValueChanged); |
| } |
| |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| void AXObjectCache::updateIsolatedTree(AccessibilityObject* object, AXNotification notification) |
| { |
| if (object) |
| updateIsolatedTree(*object, notification); |
| } |
| |
| void AXObjectCache::updateIsolatedTree(AccessibilityObject& object, AXNotification notification) |
| { |
| updateIsolatedTree({ std::make_pair(Ref { object }, notification) }); |
| } |
| |
| void AXObjectCache::updateIsolatedTree(const Vector<std::pair<Ref<AccessibilityObject>, AXNotification>>& notifications) |
| { |
| AXTRACE(makeString("AXObjectCache::updateIsolatedTree 0x"_s, hex(reinterpret_cast<uintptr_t>(this)))); |
| |
| if (!m_pageID) { |
| AXLOG("No pageID."); |
| return; |
| } |
| |
| auto tree = AXIsolatedTree::treeForPageID(*m_pageID); |
| if (!tree) { |
| AXLOG("No isolated tree for m_pageID"); |
| return; |
| } |
| |
| enum class Field : uint8_t { |
| Children = 1 << 0, |
| DependentProperties = 1 << 1, |
| Node = 1 << 2, |
| }; |
| HashMap<AXID, OptionSet<Field>> updatedObjects; |
| auto updateChildren = [&] (const Ref<AccessibilityObject>& axObject) { |
| auto updatedFields = updatedObjects.get(axObject->objectID()); |
| if (!updatedFields.contains(Field::Children)) { |
| updatedFields.add(Field::Children); |
| updatedObjects.set(axObject->objectID(), updatedFields); |
| tree->queueNodeUpdate(axObject->objectID(), NodeUpdateOptions::childrenUpdate()); |
| } |
| }; |
| auto updateDependentProperties = [&] (const Ref<AccessibilityObject>& axObject) { |
| auto updatedFields = updatedObjects.get(axObject->objectID()); |
| if (!updatedFields.contains(Field::DependentProperties)) { |
| updatedFields.add(Field::DependentProperties); |
| updatedObjects.set(axObject->objectID(), updatedFields); |
| tree->updateDependentProperties(axObject.get()); |
| } |
| }; |
| auto updateNode = [&] (const Ref<AccessibilityObject>& axObject) { |
| auto updatedFields = updatedObjects.get(axObject->objectID()); |
| if (!updatedFields.contains(Field::Node)) { |
| updatedFields.add(Field::Node); |
| updatedObjects.set(axObject->objectID(), updatedFields); |
| tree->queueNodeUpdate(axObject->objectID(), NodeUpdateOptions::nodeUpdate()); |
| } |
| }; |
| |
| for (const auto& notification : notifications) { |
| AXLOG(notification); |
| if (notification.first->isDetached()) |
| continue; |
| |
| switch (notification.second) { |
| case AXNotification::AccessKeyChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { AXProperty::AccessKey }); |
| break; |
| case AXNotification::AutofillTypeChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { AXProperty::ValueAutofillButtonType }); |
| break; |
| case AXNotification::ARIAColumnIndexChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { AXProperty::AXColumnIndex }); |
| break; |
| case AXNotification::ARIARoleDescriptionChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { AXProperty::ARIARoleDescription }); |
| break; |
| case AXNotification::ARIARowIndexChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { AXProperty::AXRowIndex }); |
| break; |
| case AXNotification::BrailleLabelChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { AXProperty::BrailleLabel }); |
| break; |
| case AXNotification::BrailleRoleDescriptionChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { AXProperty::BrailleRoleDescription }); |
| break; |
| case AXNotification::CellSlotsChanged: |
| ASSERT(notification.first->isTable()); |
| tree->queueNodeUpdate(notification.first->objectID(), { AXProperty::CellSlots }); |
| break; |
| case AXNotification::CheckedStateChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { AXProperty::IsChecked }); |
| break; |
| case AXNotification::CurrentStateChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { AXProperty::CurrentState }); |
| break; |
| case AXNotification::ColumnCountChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { AXProperty::AXColumnCount }); |
| break; |
| case AXNotification::ColumnIndexChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { { AXProperty::ColumnIndexRange, AXProperty::ColumnIndex } }); |
| break; |
| case AXNotification::ColumnSpanChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { AXProperty::ColumnIndexRange }); |
| break; |
| case AXNotification::CommandChanged: |
| case AXNotification::CommandForChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { { AXProperty::SupportsExpanded, AXProperty::IsExpanded } }); |
| break; |
| case AXNotification::DatetimeChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { AXProperty::DatetimeAttributeValue }); |
| break; |
| case AXNotification::DisabledStateChanged: |
| tree->updatePropertiesForSelfAndDescendants(notification.first.get(), { { AXProperty::CanSetFocusAttribute, AXProperty::CanSetSelectedAttribute, AXProperty::IsEnabled } }); |
| break; |
| case AXNotification::DraggableStateChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { AXProperty::SupportsDragging }); |
| break; |
| case AXNotification::ExpandedChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { AXProperty::IsExpanded }); |
| break; |
| case AXNotification::ExtendedDescriptionChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { { AXProperty::AccessibilityText, AXProperty::ExtendedDescription } }); |
| break; |
| case AXNotification::FocusableStateChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { AXProperty::CanSetFocusAttribute }); |
| break; |
| case AXNotification::FontChanged: |
| tree->updatePropertiesForSelfAndDescendants(notification.first.get(), { AXProperty::Font }); |
| break; |
| case AXNotification::InertOrVisibilityChanged: |
| tree->updatePropertiesForSelfAndDescendants(notification.first.get(), { AXProperty::IsIgnored }); |
| break; |
| case AXNotification::InputTypeChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { AXProperty::InputType }); |
| break; |
| case AXNotification::IsEditableWebAreaChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { AXProperty::IsEditableWebArea }); |
| break; |
| case AXNotification::LevelChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { AXProperty::ARIALevel }); |
| break; |
| case AXNotification::MaximumValueChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { { AXProperty::MaxValueForRange, AXProperty::ValueForRange } }); |
| break; |
| case AXNotification::MenuListItemSelected: { |
| tree->queueNodeUpdate(notification.first->objectID(), { AXProperty::IsSelected }); |
| break; |
| } |
| case AXNotification::MinimumValueChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { { AXProperty::MinValueForRange, AXProperty::ValueForRange } }); |
| break; |
| case AXNotification::NameChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { AXProperty::NameAttribute }); |
| break; |
| case AXNotification::OrientationChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { AXProperty::ExplicitOrientation }); |
| break; |
| case AXNotification::PositionInSetChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { { AXProperty::PosInSet, AXProperty::SupportsPosInSet } }); |
| break; |
| case AXNotification::PopoverTargetChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { { AXProperty::SupportsExpanded, AXProperty::IsExpanded } }); |
| break; |
| case AXNotification::SelectedTextChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { AXProperty::SelectedTextRange }); |
| break; |
| case AXNotification::SortDirectionChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { AXProperty::SortDirection }); |
| break; |
| case AXNotification::IdAttributeChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { AXProperty::IdentifierAttribute }); |
| break; |
| case AXNotification::ReadOnlyStatusChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { AXProperty::CanSetValueAttribute }); |
| break; |
| case AXNotification::RequiredStatusChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { AXProperty::IsRequired }); |
| break; |
| case AXNotification::RowIndexChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { { AXProperty::RowIndexRange, AXProperty::RowIndex } }); |
| break; |
| case AXNotification::RowSpanChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { AXProperty::RowIndexRange }); |
| break; |
| case AXNotification::CellScopeChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { { AXProperty::CellScope, AXProperty::IsColumnHeader, AXProperty::IsRowHeader } }); |
| break; |
| // FIXME: Contrary to the name "AXSelectedCellsChanged", this notification can be posted on a cell |
| // who has changed selected state, not just on table or grid who has changed its selected cells. |
| case AXNotification::SelectedCellsChanged: |
| case AXNotification::SelectedStateChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { AXProperty::IsSelected }); |
| break; |
| case AXNotification::SetSizeChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { { AXProperty::SetSize, AXProperty::SupportsSetSize } }); |
| break; |
| case AXNotification::SpeakAsChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { AXProperty::SpeakAs }); |
| break; |
| case AXNotification::TextColorChanged: |
| tree->updatePropertiesForSelfAndDescendants(notification.first.get(), { AXProperty::TextColor }); |
| break; |
| case AXNotification::TextCompositionChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { AXProperty::TextInputMarkedTextMarkerRange }); |
| break; |
| case AXNotification::TextUnderElementChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { { AXProperty::AccessibilityText, AXProperty::Title } }); |
| if (notification.first->isAccessibilityLabelInstance() || notification.first->role() == AccessibilityRole::TextField) |
| tree->queueNodeUpdate(notification.first->objectID(), { AXProperty::StringValue }); |
| break; |
| #if ENABLE(AX_THREAD_TEXT_APIS) |
| case AXNotification::TextRunsChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { AXProperty::TextRuns }); |
| break; |
| #endif |
| case AXNotification::URLChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { { AXProperty::URL, AXProperty::InternalLinkElement } }); |
| break; |
| case AXNotification::KeyShortcutsChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { AXProperty::KeyShortcuts }); |
| break; |
| case AXNotification::VisibilityChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { AXProperty::IsVisible }); |
| break; |
| case AXNotification::VisitedStateChanged: |
| tree->queueNodeUpdate(notification.first->objectID(), { AXProperty::IsVisited }); |
| break; |
| case AXNotification::ActiveDescendantChanged: |
| case AXNotification::RoleChanged: |
| case AXNotification::ControlledObjectsChanged: |
| case AXNotification::DescribedByChanged: |
| case AXNotification::DropEffectChanged: |
| case AXNotification::ElementBusyChanged: |
| case AXNotification::FlowToChanged: |
| case AXNotification::GrabbedStateChanged: |
| case AXNotification::HasPopupChanged: |
| case AXNotification::InvalidStatusChanged: |
| case AXNotification::IsAtomicChanged: |
| case AXNotification::LiveRegionStatusChanged: |
| case AXNotification::LiveRegionRelevantChanged: |
| case AXNotification::PlaceholderChanged: |
| case AXNotification::MenuListValueChanged: |
| case AXNotification::MultiSelectableStateChanged: |
| case AXNotification::PressedStateChanged: |
| case AXNotification::TextChanged: |
| case AXNotification::TextSecurityChanged: |
| case AXNotification::ValueChanged: |
| updateNode(notification.first); |
| break; |
| case AXNotification::LabelChanged: { |
| updateNode(notification.first); |
| updateDependentProperties(notification.first); |
| break; |
| } |
| case AXNotification::LanguageChanged: |
| case AXNotification::RowCountChanged: |
| updateNode(notification.first); |
| [[fallthrough]]; |
| case AXNotification::RowCollapsed: |
| case AXNotification::RowExpanded: |
| updateChildren(notification.first); |
| break; |
| default: |
| break; |
| } |
| } |
| } |
| |
| void AXObjectCache::updateIsolatedTree(AccessibilityObject* axObject, AXProperty property) const |
| { |
| if (axObject) |
| updateIsolatedTree(*axObject, property); |
| } |
| |
| void AXObjectCache::updateIsolatedTree(AccessibilityObject& axObject, AXProperty property) const |
| { |
| if (RefPtr tree = AXIsolatedTree::treeForPageID(m_pageID)) |
| tree->queueNodeUpdate(axObject.objectID(), { property }); |
| } |
| |
| void AXObjectCache::startUpdateTreeSnapshotTimer() |
| { |
| if (!m_updateTreeSnapshotTimer.isActive()) |
| m_updateTreeSnapshotTimer.startOneShot(updateTreeSnapshotTimerInterval); |
| } |
| |
| void AXObjectCache::onPaint(const RenderObject& renderer, IntRect&& paintRect) const |
| { |
| if (!m_pageID) |
| return; |
| m_geometryManager->cacheRect(m_renderObjectMapping.getOptional(const_cast<RenderObject&>(renderer)), WTFMove(paintRect)); |
| } |
| |
| void AXObjectCache::onPaint(const Widget& widget, IntRect&& paintRect) const |
| { |
| if (!m_pageID) |
| return; |
| m_geometryManager->cacheRect(m_widgetObjectMapping.getOptional(const_cast<Widget&>(widget)), WTFMove(paintRect)); |
| } |
| #endif // ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| |
| void AXObjectCache::deferRecomputeIsIgnoredIfNeeded(Element* element) |
| { |
| if (!nodeAndRendererAreValid(element)) |
| return; |
| |
| SingleThreadWeakRef<RenderObject> renderer = *element->renderer(); |
| if (rendererNeedsDeferredUpdate(renderer.get())) { |
| m_deferredRecomputeIsIgnoredList.add(*element); |
| return; |
| } |
| recomputeIsIgnored(renderer.get()); |
| } |
| |
| void AXObjectCache::deferRecomputeIsIgnored(Element* element) |
| { |
| if (!nodeAndRendererAreValid(element)) |
| return; |
| |
| m_deferredRecomputeIsIgnoredList.add(*element); |
| } |
| |
| void AXObjectCache::deferRecomputeTableIsExposed(Element* element) |
| { |
| auto* tableElement = dynamicDowncast<HTMLTableElement>(element); |
| if (!tableElement) |
| return; |
| |
| m_deferredRecomputeTableIsExposedList.add(*tableElement); |
| if (!m_performCacheUpdateTimer.isActive()) |
| m_performCacheUpdateTimer.startOneShot(0_s); |
| } |
| |
| void AXObjectCache::deferRecomputeTableCellSlots(AccessibilityTable& axTable) |
| { |
| m_deferredRecomputeTableCellSlotsList.add(axTable); |
| if (!m_performCacheUpdateTimer.isActive()) |
| m_performCacheUpdateTimer.startOneShot(0_s); |
| } |
| |
| void AXObjectCache::deferRowspanChange(AccessibilityObject* axObject) |
| { |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| auto* axTableCell = dynamicDowncast<AccessibilityTableCell>(axObject); |
| if (!axTableCell) |
| return; |
| |
| m_deferredRowspanChanges.add(*axTableCell); |
| if (!m_performCacheUpdateTimer.isActive()) |
| m_performCacheUpdateTimer.startOneShot(0_s); |
| #else |
| UNUSED_PARAM(axObject); |
| #endif |
| } |
| |
| void AXObjectCache::deferTextChangedIfNeeded(Node* node) |
| { |
| if (!nodeAndRendererAreValid(node)) |
| return; |
| |
| if (rendererNeedsDeferredUpdate(*node->renderer())) { |
| m_deferredTextChangedList.add(*node); |
| return; |
| } |
| handleTextChanged(getOrCreate(*node)); |
| } |
| |
| void AXObjectCache::deferSelectedChildrenChangedIfNeeded(Element& selectElement) |
| { |
| if (!nodeRendererIsValid(selectElement)) |
| return; |
| |
| if (rendererNeedsDeferredUpdate(*selectElement.renderer())) { |
| m_deferredSelectedChildredChangedList.add(selectElement); |
| if (!m_performCacheUpdateTimer.isActive()) |
| m_performCacheUpdateTimer.startOneShot(0_s); |
| return; |
| } |
| selectedChildrenChanged(&selectElement); |
| } |
| |
| void AXObjectCache::deferTextReplacementNotificationForTextControl(HTMLTextFormControlElement& formControlElement, const String& previousValue) |
| { |
| auto* renderer = formControlElement.renderer(); |
| if (!renderer) |
| return; |
| m_deferredTextFormControlValue.add(formControlElement, previousValue); |
| } |
| |
| bool isNodeFocused(Node& node) |
| { |
| return is<Element>(node) && uncheckedDowncast<Element>(node).focused(); |
| } |
| |
| bool isVisibilityHidden(const RenderStyle& style) |
| { |
| return style.usedVisibility() != Visibility::Visible || isContentVisibilityHidden(style); |
| } |
| |
| // DOM component of hidden definition. |
| // https://www.w3.org/TR/wai-aria/#dfn-hidden |
| bool isRenderHidden(const RenderStyle& style) |
| { |
| return style.display() == DisplayType::None || isVisibilityHidden(style); |
| } |
| |
| bool isRenderHidden(const RenderStyle* style) |
| { |
| return style ? isRenderHidden(*style) : true; |
| } |
| |
| AccessibilityObject* AXObjectCache::rootWebArea() |
| { |
| if (!m_document) |
| return nullptr; |
| auto* root = getOrCreate(m_document->view()); |
| if (!root || !root->isScrollView()) |
| return nullptr; |
| return root->webAreaObject(); |
| } |
| |
| AXTreeData AXObjectCache::treeData(std::optional<OptionSet<AXStreamOptions>> additionalOptions) |
| { |
| ASSERT(isMainThread()); |
| |
| AXTreeData data; |
| TextStream stream(TextStream::LineMode::MultipleLine); |
| |
| stream << "\nAXObjectTree:\n"; |
| RefPtr document = this->document(); |
| if (RefPtr root = document ? get(document->view()) : nullptr) { |
| OptionSet<AXStreamOptions> options = { AXStreamOptions::ObjectID, AXStreamOptions::ParentID, AXStreamOptions::Role, AXStreamOptions::IdentifierAttribute, AXStreamOptions::OuterHTML }; |
| if (additionalOptions) |
| options |= additionalOptions.value(); |
| streamSubtree(stream, *root, options); |
| } else |
| stream << "No root!"; |
| data.liveTree = stream.release(); |
| |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| if (isIsolatedTreeEnabled()) { |
| stream << "\nAXIsolatedTree:\n"; |
| RefPtr tree = getOrCreateIsolatedTree(); |
| if (RefPtr root = tree ? tree->rootNode() : nullptr) { |
| OptionSet<AXStreamOptions> options = { AXStreamOptions::ObjectID, AXStreamOptions::ParentID }; |
| if (additionalOptions) |
| options |= additionalOptions.value(); |
| streamSubtree(stream, root.releaseNonNull(), options); |
| } else |
| stream << "No isolated tree!"; |
| data.isolatedTree = stream.release(); |
| } |
| #endif |
| |
| return data; |
| } |
| |
| Vector<QualifiedName>& AXObjectCache::relationAttributes() |
| { |
| static NeverDestroyed<Vector<QualifiedName>> relationAttributes = Vector<QualifiedName> { |
| aria_activedescendantAttr, |
| aria_controlsAttr, |
| aria_describedbyAttr, |
| aria_detailsAttr, |
| aria_errormessageAttr, |
| aria_flowtoAttr, |
| aria_labelledbyAttr, |
| aria_labeledbyAttr, |
| aria_ownsAttr, |
| commandforAttr, |
| headersAttr, |
| popovertargetAttr, |
| }; |
| return relationAttributes; |
| } |
| |
| AXRelation AXObjectCache::symmetricRelation(AXRelation relation) |
| { |
| switch (relation) { |
| case AXRelation::ActiveDescendant: |
| return AXRelation::ActiveDescendantOf; |
| case AXRelation::ActiveDescendantOf: |
| return AXRelation::ActiveDescendant; |
| case AXRelation::ControlledBy: |
| return AXRelation::ControllerFor; |
| case AXRelation::ControllerFor: |
| return AXRelation::ControlledBy; |
| case AXRelation::DescribedBy: |
| return AXRelation::DescriptionFor; |
| case AXRelation::DescriptionFor: |
| return AXRelation::DescribedBy; |
| case AXRelation::Details: |
| return AXRelation::DetailsFor; |
| case AXRelation::DetailsFor: |
| return AXRelation::Details; |
| case AXRelation::ErrorMessage: |
| return AXRelation::ErrorMessageFor; |
| case AXRelation::ErrorMessageFor: |
| return AXRelation::ErrorMessage; |
| case AXRelation::FlowsFrom: |
| return AXRelation::FlowsTo; |
| case AXRelation::FlowsTo: |
| return AXRelation::FlowsFrom; |
| case AXRelation::Headers: |
| return AXRelation::HeaderFor; |
| case AXRelation::HeaderFor: |
| return AXRelation::Headers; |
| case AXRelation::LabeledBy: |
| return AXRelation::LabelFor; |
| case AXRelation::LabelFor: |
| return AXRelation::LabeledBy; |
| case AXRelation::OwnedBy: |
| return AXRelation::OwnerFor; |
| case AXRelation::OwnerFor: |
| return AXRelation::OwnedBy; |
| case AXRelation::None: |
| return AXRelation::None; |
| } |
| RELEASE_ASSERT_NOT_REACHED(); |
| } |
| |
| AXRelation AXObjectCache::attributeToRelationType(const QualifiedName& attribute) |
| { |
| if (attribute == aria_activedescendantAttr) |
| return AXRelation::ActiveDescendant; |
| if (attribute == aria_controlsAttr || attribute == commandforAttr || attribute == popovertargetAttr) |
| return AXRelation::ControllerFor; |
| if (attribute == aria_describedbyAttr) |
| return AXRelation::DescribedBy; |
| if (attribute == aria_detailsAttr) |
| return AXRelation::Details; |
| if (attribute == aria_errormessageAttr) |
| return AXRelation::ErrorMessage; |
| if (attribute == aria_flowtoAttr) |
| return AXRelation::FlowsTo; |
| if (attribute == aria_labelledbyAttr || attribute == aria_labeledbyAttr) |
| return AXRelation::LabeledBy; |
| if (attribute == aria_ownsAttr) |
| return AXRelation::OwnerFor; |
| if (attribute == headersAttr) |
| return AXRelation::Headers; |
| return AXRelation::None; |
| } |
| |
| static bool validRelation(void* origin, void* target, AXRelation relation) |
| { |
| if (!origin || !target || relation == AXRelation::None) |
| return false; |
| return origin != target || relation == AXRelation::LabeledBy; |
| } |
| |
| static bool validRelation(Element& origin, Element& target, AXRelation relation) |
| { |
| if (relation == AXRelation::None) |
| return false; |
| return &origin != &target || relation == AXRelation::LabeledBy; |
| } |
| |
| bool AXObjectCache::addRelation(Element& origin, Element& target, AXRelation relation) |
| { |
| AXTRACE("AXObjectCache::addRelation"_s); |
| AXLOG(makeString("origin: "_s, origin.debugDescription(), " target: "_s, target.debugDescription(), " relation "_s, static_cast<uint8_t>(relation))); |
| |
| if (!validRelation(origin, target, relation)) { |
| ASSERT_NOT_REACHED(); |
| return false; |
| } |
| |
| if (relation == AXRelation::LabelFor) { |
| // Add a LabelFor relation if the target doesn't have an ARIA label which should take precedence. |
| if (target.hasAttributeWithoutSynchronization(aria_labelAttr) |
| || target.hasAttributeWithoutSynchronization(aria_labelledbyAttr) |
| || target.hasAttributeWithoutSynchronization(aria_labeledbyAttr)) |
| return false; |
| } |
| |
| return addRelation(RefPtr { getOrCreate(origin, IsPartOfRelation::Yes) }.get(), RefPtr { getOrCreate(target, IsPartOfRelation::Yes) }.get(), relation); |
| } |
| |
| static bool canHaveRelations(Element& element) |
| { |
| auto elementName = element.elementName(); |
| return !(elementName == ElementName::HTML_meta || elementName == ElementName::HTML_head || elementName == ElementName::HTML_script || elementName == ElementName::HTML_html || elementName == ElementName::HTML_style); |
| } |
| |
| static bool relationCausesCycle(AccessibilityObject* origin, AccessibilityObject* target, AXRelation relation) |
| { |
| // Validate that we're not creating an aria-owns cycle. |
| if (relation == AXRelation::OwnerFor) { |
| for (auto* verifyOrigin = origin; verifyOrigin; verifyOrigin = verifyOrigin->parentObject()) { |
| if (verifyOrigin == target) |
| return true; |
| } |
| } else if (relation == AXRelation::OwnedBy) { |
| for (auto* verifyTarget = target; verifyTarget; verifyTarget = verifyTarget->parentObject()) { |
| if (verifyTarget == origin) |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| bool AXObjectCache::addRelation(AccessibilityObject* origin, AccessibilityObject* target, AXRelation relation, AddSymmetricRelation addSymmetricRelation) |
| { |
| AXTRACE("AXObjectCache::addRelation"_s); |
| AXLOG(origin); |
| AXLOG(target); |
| AXLOG(relation); |
| |
| if (!validRelation(origin, target, relation)) |
| return false; |
| |
| if (relationCausesCycle(origin, target, relation)) |
| return false; |
| |
| if (relation == AXRelation::OwnerFor) { |
| // Before adding the new OwnerFor relationship, that alters the AX parent-child hierarchy, notify the current target-s parent that one child is being removed. |
| // FIXME: This kicks off children-changed notifications every time relations are dirtied and cleaned, |
| // even if this specific relationship existed before, incurring unnecessary work. |
| RefPtr targetParent = target->parentObject(); |
| if (targetParent && targetParent.get() != origin) |
| childrenChanged(targetParent.get()); |
| } |
| |
| AXID originID = origin->objectID(); |
| AXID targetID = target->objectID(); |
| auto relationsIterator = m_relations.find(originID); |
| if (relationsIterator == m_relations.end()) { |
| // No relations for this object, add the first one. |
| m_relations.add(originID, AXRelations { { enumToUnderlyingType(relation), { targetID } } }); |
| } else if (auto targetsIterator = relationsIterator->value.find(enumToUnderlyingType(relation)); targetsIterator == relationsIterator->value.end()) { |
| // No relation of this type for this object, add the first one. |
| relationsIterator->value.add(enumToUnderlyingType(relation), ListHashSet { targetID }); |
| } else { |
| // There are already relations of this type for the object. Add the new relation. |
| if (relation == AXRelation::ActiveDescendant |
| || relation == AXRelation::OwnedBy) { |
| // There should be only one active descendant and only one owner. Enforce that by removing any existing targets. |
| targetsIterator->value.clear(); |
| } |
| targetsIterator->value.add(targetID); |
| } |
| m_relationTargets.add(targetID); |
| |
| if (relation == AXRelation::OwnerFor) { |
| // First find and clear the old owner. |
| for (auto oldOwnerIterator = m_relations.begin(); oldOwnerIterator != m_relations.end(); ++oldOwnerIterator) { |
| if (oldOwnerIterator->key == originID) |
| continue; |
| |
| removeRelationByID(oldOwnerIterator->key, targetID, AXRelation::OwnerFor); |
| } |
| |
| childrenChanged(origin); |
| } else if (relation == AXRelation::OwnedBy) { |
| if (auto* parentObject = origin->parentObjectUnignored()) |
| childrenChanged(parentObject); |
| } |
| |
| if (addSymmetricRelation == AddSymmetricRelation::Yes |
| && m_objects.contains(originID) && m_objects.contains(targetID)) { |
| // If the IDs are still in the m_objects map, the objects should be still alive. |
| if (auto symmetric = symmetricRelation(relation); symmetric != AXRelation::None) |
| addRelation(target, origin, symmetric, AddSymmetricRelation::No); |
| |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| if (auto tree = AXIsolatedTree::treeForPageID(m_pageID)) { |
| if (origin && origin->isIgnored()) |
| deferAddUnconnectedNode(*origin); |
| if (target && target->isIgnored()) |
| deferAddUnconnectedNode(*target); |
| } |
| #endif |
| } |
| |
| return true; |
| } |
| |
| void AXObjectCache::removeAllRelations(AXID axID) |
| { |
| AXTRACE(makeString("AXObjectCache::removeRelations for axID "_s, axID.loggingString())); |
| |
| auto it = m_relations.find(axID); |
| if (it == m_relations.end()) |
| return; |
| |
| m_recentlyRemovedRelations.add(axID, it->value); |
| |
| for (auto relation : it->value.keys()) { |
| auto symmetric = symmetricRelation(static_cast<AXRelation>(relation)); |
| if (symmetric == AXRelation::None) |
| continue; |
| |
| auto targetIDs = it->value.get(static_cast<uint8_t>(relation)); |
| for (AXID targetID : targetIDs) |
| removeRelationByID(targetID, axID, symmetric); |
| } |
| |
| m_relations.remove(it); |
| dirtyIsolatedTreeRelations(); |
| } |
| |
| bool AXObjectCache::removeRelation(Element& origin, AXRelation relation) |
| { |
| AXTRACE(makeString("AXObjectCache::removeRelations for "_s, origin.debugDescription())); |
| AXLOG(relation); |
| |
| auto* object = get(&origin); |
| if (!object) |
| return false; |
| |
| auto relationsIterator = m_relations.find(object->objectID()); |
| if (relationsIterator == m_relations.end()) |
| return false; |
| |
| auto targetIDs = relationsIterator->value.take(enumToUnderlyingType(relation)); |
| bool removedRelation = !targetIDs.isEmpty(); |
| |
| auto symmetric = symmetricRelation(relation); |
| if (symmetric != AXRelation::None) { |
| for (AXID targetID : targetIDs) |
| removeRelationByID(targetID, object->objectID(), symmetric); |
| } |
| |
| if (removedRelation && relation == AXRelation::OwnerFor) |
| childrenChanged(object); |
| |
| return removedRelation; |
| } |
| |
| void AXObjectCache::removeRelationByID(AXID originID, AXID targetID, AXRelation relation) |
| { |
| AXTRACE("AXObjectCache::removeRelationByID"_s); |
| AXLOG(makeString("originID "_s, originID.loggingString(), " targetID "_s, targetID.loggingString())); |
| AXLOG(relation); |
| |
| auto relationsIterator = m_relations.find(originID); |
| if (relationsIterator == m_relations.end()) |
| return; |
| |
| auto targetsIterator = relationsIterator->value.find(enumToUnderlyingType(relation)); |
| if (targetsIterator == relationsIterator->value.end()) |
| return; |
| targetsIterator->value.remove(targetID); |
| } |
| |
| void AXObjectCache::updateRelationsIfNeeded() |
| { |
| if (!m_relationsNeedUpdate) |
| return; |
| relationsNeedUpdate(false); |
| m_relations.clear(); |
| m_recentlyRemovedRelations.clear(); |
| m_relationTargets.clear(); |
| if (m_document) |
| updateRelationsForTree(m_document->rootNode()); |
| } |
| |
| void AXObjectCache::updateRelationsForTree(ContainerNode& rootNode) |
| { |
| ASSERT(!rootNode.parentNode()); |
| for (auto& element : descendantsOfType<Element>(rootNode)) { |
| if (!canHaveRelations(element)) |
| continue; |
| |
| if (RefPtr shadowRoot = element.shadowRoot(); shadowRoot && shadowRoot->mode() != ShadowRootMode::UserAgent) |
| updateRelationsForTree(*shadowRoot); |
| if (RefPtr frameOwnerElement = dynamicDowncast<HTMLFrameOwnerElement>(element)) { |
| if (RefPtr document = frameOwnerElement->contentDocument()) |
| updateRelationsForTree(*document); |
| } |
| |
| for (const auto& attribute : relationAttributes()) |
| addRelation(element, attribute); |
| |
| // In addition to ARIA specified relations, there may be other relevant relations. |
| // For instance, LabelFor in HTMLLabelElements. |
| addLabelForRelation(element); |
| } |
| } |
| |
| bool AXObjectCache::addRelation(Element& origin, const QualifiedName& attribute) |
| { |
| if (attribute == aria_labeledbyAttr && origin.hasAttribute(aria_labelledbyAttr)) { |
| // The attribute name with British spelling should override the one with American spelling. |
| return false; |
| } |
| |
| bool addedRelation = false; |
| auto relation = attributeToRelationType(attribute); |
| if (!m_document) |
| return false; |
| if (Element::isElementReflectionAttribute(Ref { m_document->settings() }, attribute)) { |
| if (auto reflectedElement = origin.elementForAttributeInternal(attribute)) |
| return addRelation(origin, *reflectedElement, relation); |
| } else if (Element::isElementsArrayReflectionAttribute(attribute)) { |
| if (auto reflectedElements = origin.elementsArrayForAttributeInternal(attribute)) { |
| for (auto reflectedElement : reflectedElements.value()) { |
| if (addRelation(origin, reflectedElement, relation)) |
| addedRelation = true; |
| } |
| return addedRelation; |
| } |
| } |
| |
| auto& value = origin.attributeWithoutSynchronization(attribute); |
| if (value.isNull()) { |
| if (auto* defaultARIA = origin.customElementDefaultARIAIfExists()) { |
| for (auto& target : defaultARIA->elementsForAttribute(origin, attribute)) { |
| if (addRelation(origin, target, relation)) |
| addedRelation = true; |
| } |
| } |
| return addedRelation; |
| } |
| |
| SpaceSplitString ids(value, SpaceSplitString::ShouldFoldCase::No); |
| for (auto& id : ids) { |
| RefPtr target = origin.treeScope().elementByIdResolvingReferenceTarget(id); |
| if (!target || target == &origin) |
| continue; |
| |
| if (addRelation(origin, *target, relation)) |
| addedRelation = true; |
| } |
| |
| return addedRelation; |
| } |
| |
| void AXObjectCache::addLabelForRelation(Element& origin) |
| { |
| bool addedRelation = false; |
| |
| // LabelFor relations are established for <label for=...>. |
| if (RefPtr label = dynamicDowncast<HTMLLabelElement>(origin)) { |
| if (RefPtr control = Accessibility::controlForLabelElement(*label)) |
| addedRelation = addRelation(origin, *control, AXRelation::LabelFor); |
| } |
| |
| if (addedRelation) |
| dirtyIsolatedTreeRelations(); |
| } |
| |
| void AXObjectCache::updateRelations(Element& origin, const QualifiedName& attribute) |
| { |
| if (!canHaveRelations(origin)) |
| return; |
| |
| auto relation = attributeToRelationType(attribute); |
| if (relation == AXRelation::None) { |
| ASSERT_NOT_REACHED(); |
| return; |
| } |
| |
| // `commandfor` is only valid for button elements. |
| if (attribute == commandforAttr) [[unlikely]] { |
| if (!is<HTMLButtonElement>(origin)) |
| return; |
| } |
| |
| // `popovertarget` is only valid for input and button elements. |
| if (attribute == popovertargetAttr) [[unlikely]] { |
| if (!is<HTMLInputElement>(origin) && !is<HTMLButtonElement>(origin)) |
| return; |
| } |
| |
| bool changedRelation = removeRelation(origin, relation); |
| changedRelation |= addRelation(origin, attribute); |
| if (changedRelation) |
| dirtyIsolatedTreeRelations(); |
| } |
| |
| void AXObjectCache::relationsNeedUpdate(bool needUpdate) |
| { |
| m_relationsNeedUpdate = needUpdate; |
| if (m_relationsNeedUpdate) |
| dirtyIsolatedTreeRelations(); |
| } |
| |
| HashMap<AXID, AXRelations> AXObjectCache::relations() |
| { |
| updateRelationsIfNeeded(); |
| return m_relations; |
| } |
| |
| const HashSet<AXID>& AXObjectCache::relationTargetIDs() |
| { |
| updateRelationsIfNeeded(); |
| return m_relationTargets; |
| } |
| |
| bool AXObjectCache::isDescendantOfRelatedNode(Node& node) |
| { |
| auto& targetIDs = relationTargetIDs(); |
| for (auto* parent = node.parentNode(); parent; parent = parent->parentNode()) { |
| auto* object = get(*parent); |
| if (object && (m_relations.contains(object->objectID()) || targetIDs.contains(object->objectID()))) |
| return true; |
| } |
| return false; |
| } |
| |
| std::optional<ListHashSet<AXID>> AXObjectCache::relatedObjectIDsFor(const AXCoreObject& object, AXRelation relation, UpdateRelations updateRelations) |
| { |
| if (updateRelations == UpdateRelations::Yes) |
| updateRelationsIfNeeded(); |
| |
| auto relationsIterator = m_relations.find(object.objectID()); |
| if (relationsIterator == m_relations.end()) |
| return std::nullopt; |
| |
| auto targetsIterator = relationsIterator->value.find(enumToUnderlyingType(relation)); |
| if (targetsIterator == relationsIterator->value.end()) |
| return std::nullopt; |
| return targetsIterator->value; |
| } |
| |
| #if PLATFORM(COCOA) |
| void AXObjectCache::announce(const String& message) |
| { |
| postPlatformAnnouncementNotification(message); |
| } |
| #else |
| void AXObjectCache::announce(const String&) |
| { |
| // FIXME: implement in other platforms. |
| } |
| #endif |
| |
| AXAttributeCacheEnabler::AXAttributeCacheEnabler(AXObjectCache* cache) |
| : m_cache(cache) |
| { |
| if (m_cache) { |
| if (m_cache->computedObjectAttributeCache()) |
| m_wasAlreadyCaching = true; |
| else |
| m_cache->startCachingComputedObjectAttributesUntilTreeMutates(); |
| } |
| } |
| |
| AXAttributeCacheEnabler::~AXAttributeCacheEnabler() |
| { |
| if (m_cache && !m_wasAlreadyCaching) |
| m_cache->stopCachingComputedObjectAttributes(); |
| } |
| |
| #if !PLATFORM(COCOA) && !USE(ATSPI) |
| AXTextChange AXObjectCache::textChangeForEditType(AXTextEditType type) |
| { |
| switch (type) { |
| case AXTextEditTypeCut: |
| case AXTextEditTypeDelete: |
| return AXTextChange::Deleted; |
| case AXTextEditTypeInsert: |
| case AXTextEditTypeDictation: |
| case AXTextEditTypeTyping: |
| case AXTextEditTypePaste: |
| return AXTextChange::Inserted; |
| case AXTextEditTypeReplace: |
| return AXTextChange::Replaced; |
| case AXTextEditTypeAttributesChange: |
| return AXTextChange::AttributesChanged; |
| case AXTextEditTypeUnknown: |
| ASSERT_NOT_REACHED(); |
| return AXTextChange::Inserted; |
| } |
| } |
| #endif |
| |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| void AXObjectCache::selectedTextRangeTimerFired() |
| { |
| if (!accessibilityEnabled()) |
| return; |
| |
| m_selectedTextRangeTimer.stop(); |
| |
| if (m_lastDebouncedTextRangeObject) { |
| for (auto* axObject = objectForID(*m_lastDebouncedTextRangeObject); axObject; axObject = axObject->parentObject()) { |
| if (axObject->isTextControl()) |
| postNotification(*axObject, AXNotification::SelectedTextChanged); |
| } |
| } |
| |
| m_lastDebouncedTextRangeObject = std::nullopt; |
| } |
| |
| void AXObjectCache::updateTreeSnapshotTimerFired() |
| { |
| m_updateTreeSnapshotTimer.stop(); |
| processQueuedIsolatedNodeUpdates(); |
| } |
| |
| void AXObjectCache::processQueuedIsolatedNodeUpdates() |
| { |
| if (auto tree = AXIsolatedTree::treeForPageID(m_pageID)) |
| tree->processQueuedNodeUpdates(); |
| } |
| #endif |
| |
| void AXObjectCache::onWidgetVisibilityChanged(RenderWidget& widget) |
| { |
| #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) |
| postNotification(get(widget), AXNotification::VisibilityChanged); |
| #else |
| UNUSED_PARAM(widget); |
| #endif |
| } |
| |
| } // namespace WebCore |