fune/accessible/base/Pivot.cpp
Eitan Isaacson f9fbfefda3 Bug 1801986 - Set pivot root to top remote document. r=Jamie
The pivot should never cross from remote into parent process local
containers. We need to explicitly set the root for the pivot, and
assert for this in the pivot traversal methods.

Differential Revision: https://phabricator.services.mozilla.com/D163285
2022-11-29 20:31:21 +00:00

688 lines
22 KiB
C++

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "Pivot.h"
#include "AccIterator.h"
#include "LocalAccessible.h"
#include "RemoteAccessible.h"
#include "DocAccessible.h"
#include "nsAccessibilityService.h"
#include "nsAccUtils.h"
#include "mozilla/a11y/Accessible.h"
#include "mozilla/a11y/HyperTextAccessibleBase.h"
#include "mozilla/dom/ChildIterator.h"
#include "mozilla/dom/Element.h"
#include "mozilla/StaticPrefs_accessibility.h"
using namespace mozilla;
using namespace mozilla::a11y;
////////////////////////////////////////////////////////////////////////////////
// Pivot
////////////////////////////////////////////////////////////////////////////////
Pivot::Pivot(Accessible* aRoot) : mRoot(aRoot) { MOZ_COUNT_CTOR(Pivot); }
Pivot::~Pivot() { MOZ_COUNT_DTOR(Pivot); }
Accessible* Pivot::AdjustStartPosition(Accessible* aAnchor, PivotRule& aRule,
uint16_t* aFilterResult) {
Accessible* matched = aAnchor;
*aFilterResult = aRule.Match(aAnchor);
if (aAnchor && aAnchor != mRoot) {
for (Accessible* temp = aAnchor->Parent(); temp && temp != mRoot;
temp = temp->Parent()) {
uint16_t filtered = aRule.Match(temp);
if (filtered & nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE) {
*aFilterResult = filtered;
matched = temp;
}
}
}
return matched;
}
Accessible* Pivot::SearchBackward(Accessible* aAnchor, PivotRule& aRule,
bool aSearchCurrent) {
// Initial position could be unset, in that case return null.
if (!aAnchor) {
return nullptr;
}
uint16_t filtered = nsIAccessibleTraversalRule::FILTER_IGNORE;
Accessible* acc = AdjustStartPosition(aAnchor, aRule, &filtered);
if (aSearchCurrent && (filtered & nsIAccessibleTraversalRule::FILTER_MATCH)) {
return acc;
}
while (acc && acc != mRoot) {
Accessible* parent = acc->Parent();
#if defined(ANDROID)
MOZ_ASSERT(
acc->IsLocal() || (acc->IsRemote() && parent->IsRemote()),
"Pivot::SearchBackward climbed out of remote subtree in Android!");
#endif
int32_t idxInParent = acc->IndexInParent();
while (idxInParent > 0 && parent) {
acc = parent->ChildAt(--idxInParent);
if (!acc) {
continue;
}
filtered = aRule.Match(acc);
Accessible* lastChild = acc->LastChild();
while (!(filtered & nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE) &&
lastChild) {
parent = acc;
acc = lastChild;
idxInParent = acc->IndexInParent();
filtered = aRule.Match(acc);
lastChild = acc->LastChild();
}
if (filtered & nsIAccessibleTraversalRule::FILTER_MATCH) {
return acc;
}
}
acc = parent;
if (!acc) {
break;
}
filtered = aRule.Match(acc);
if (filtered & nsIAccessibleTraversalRule::FILTER_MATCH) {
return acc;
}
}
return nullptr;
}
Accessible* Pivot::SearchForward(Accessible* aAnchor, PivotRule& aRule,
bool aSearchCurrent) {
// Initial position could be not set, in that case begin search from root.
Accessible* acc = aAnchor ? aAnchor : mRoot;
uint16_t filtered = nsIAccessibleTraversalRule::FILTER_IGNORE;
acc = AdjustStartPosition(acc, aRule, &filtered);
if (aSearchCurrent && (filtered & nsIAccessibleTraversalRule::FILTER_MATCH)) {
return acc;
}
while (acc) {
Accessible* firstChild = acc->FirstChild();
while (!(filtered & nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE) &&
firstChild) {
acc = firstChild;
filtered = aRule.Match(acc);
if (filtered & nsIAccessibleTraversalRule::FILTER_MATCH) {
return acc;
}
firstChild = acc->FirstChild();
}
Accessible* sibling = nullptr;
Accessible* temp = acc;
do {
if (temp == mRoot) {
break;
}
sibling = temp->NextSibling();
if (sibling) {
break;
}
temp = temp->Parent();
#if defined(ANDROID)
MOZ_ASSERT(
acc->IsLocal() || (acc->IsRemote() && temp->IsRemote()),
"Pivot::SearchForward climbed out of remote subtree in Android!");
#endif
} while (temp);
if (!sibling) {
break;
}
acc = sibling;
filtered = aRule.Match(acc);
if (filtered & nsIAccessibleTraversalRule::FILTER_MATCH) {
return acc;
}
}
return nullptr;
}
Accessible* Pivot::SearchForText(Accessible* aAnchor, bool aBackward) {
if (mRoot->IsRemote() &&
!StaticPrefs::accessibility_cache_enabled_AtStartup()) {
// Not supported for RemoteAccessible when the cache is disabled.
return nullptr;
}
Accessible* accessible = aAnchor;
while (true) {
Accessible* child = nullptr;
while ((child = (aBackward ? accessible->LastChild()
: accessible->FirstChild()))) {
accessible = child;
if (child->IsHyperText()) {
return child;
}
}
Accessible* sibling = nullptr;
Accessible* temp = accessible;
do {
if (temp == mRoot) {
break;
}
// Unlike traditional pre-order traversal we revisit the parent
// nodes when we go up the tree. This is because our starting point
// may be a subtree or a leaf. If it's parent matches, it should
// take precedent over a sibling.
if (temp != aAnchor && temp->IsHyperText()) {
return temp;
}
if (sibling) {
break;
}
sibling = aBackward ? temp->PrevSibling() : temp->NextSibling();
} while ((temp = temp->Parent()));
if (!sibling) {
break;
}
accessible = sibling;
if (accessible->IsHyperText()) {
return accessible;
}
}
return nullptr;
}
Accessible* Pivot::Next(Accessible* aAnchor, PivotRule& aRule,
bool aIncludeStart) {
return SearchForward(aAnchor, aRule, aIncludeStart);
}
Accessible* Pivot::Prev(Accessible* aAnchor, PivotRule& aRule,
bool aIncludeStart) {
return SearchBackward(aAnchor, aRule, aIncludeStart);
}
Accessible* Pivot::First(PivotRule& aRule) {
return SearchForward(mRoot, aRule, true);
}
Accessible* Pivot::Last(PivotRule& aRule) {
Accessible* lastAcc = mRoot;
// First go to the last accessible in pre-order
while (lastAcc && lastAcc->HasChildren()) {
lastAcc = lastAcc->LastChild();
}
// Search backwards from last accessible and find the last occurrence in the
// doc
return SearchBackward(lastAcc, aRule, true);
}
Accessible* Pivot::NextText(Accessible* aAnchor, int32_t* aStartOffset,
int32_t* aEndOffset, int32_t aBoundaryType) {
if (mRoot->IsRemote() &&
!StaticPrefs::accessibility_cache_enabled_AtStartup()) {
// Not supported for RemoteAccessible when the cache is disabled.
return nullptr;
}
int32_t tempStart = *aStartOffset, tempEnd = *aEndOffset;
Accessible* tempPosition = aAnchor;
// if we're starting on a text leaf, translate the offsets to the
// HyperTextAccessible parent and start from there.
if (aAnchor->IsTextLeaf() && aAnchor->Parent() &&
aAnchor->Parent()->IsHyperText()) {
tempPosition = aAnchor->Parent();
HyperTextAccessibleBase* text = tempPosition->AsHyperTextBase();
int32_t childOffset = text->GetChildOffset(aAnchor);
if (tempEnd == -1) {
tempStart = 0;
tempEnd = 0;
}
tempStart += childOffset;
tempEnd += childOffset;
}
while (true) {
MOZ_ASSERT(tempPosition);
Accessible* curPosition = tempPosition;
HyperTextAccessibleBase* text = nullptr;
// Find the nearest text node using a preorder traversal starting from
// the current node.
if (!(text = tempPosition->AsHyperTextBase())) {
tempPosition = SearchForText(tempPosition, false);
if (!tempPosition) {
return nullptr;
}
if (tempPosition != curPosition) {
tempStart = tempEnd = -1;
}
text = tempPosition->AsHyperTextBase();
}
// If the search led to the parent of the node we started on (e.g. when
// starting on a text leaf), start the text movement from the end of that
// node, otherwise we just default to 0.
if (tempEnd == -1) {
tempEnd = tempPosition == curPosition->Parent()
? text->GetChildOffset(curPosition)
: 0;
}
// If there's no more text on the current node, try to find the next text
// node; if there isn't one, bail out.
if (tempEnd == static_cast<int32_t>(text->CharacterCount())) {
if (tempPosition == mRoot) {
return nullptr;
}
// If we're currently sitting on a link, try move to either the next
// sibling or the parent, whichever is closer to the current end
// offset. Otherwise, do a forward search for the next node to land on
// (we don't do this in the first case because we don't want to go to the
// subtree).
Accessible* sibling = tempPosition->NextSibling();
if (tempPosition->IsLink()) {
if (sibling && sibling->IsLink()) {
tempStart = tempEnd = -1;
tempPosition = sibling;
} else {
tempStart = tempPosition->StartOffset();
tempEnd = tempPosition->EndOffset();
tempPosition = tempPosition->Parent();
}
} else {
tempPosition = SearchForText(tempPosition, false);
if (!tempPosition) {
return nullptr;
}
tempStart = tempEnd = -1;
}
continue;
}
AccessibleTextBoundary startBoundary, endBoundary;
switch (aBoundaryType) {
case nsIAccessiblePivot::CHAR_BOUNDARY:
startBoundary = nsIAccessibleText::BOUNDARY_CHAR;
endBoundary = nsIAccessibleText::BOUNDARY_CHAR;
break;
case nsIAccessiblePivot::WORD_BOUNDARY:
startBoundary = nsIAccessibleText::BOUNDARY_WORD_START;
endBoundary = nsIAccessibleText::BOUNDARY_WORD_END;
break;
case nsIAccessiblePivot::LINE_BOUNDARY:
startBoundary = nsIAccessibleText::BOUNDARY_LINE_START;
endBoundary = nsIAccessibleText::BOUNDARY_LINE_END;
break;
default:
return nullptr;
}
nsAutoString unusedText;
int32_t newStart = 0, newEnd = 0, currentEnd = tempEnd;
text->TextAtOffset(tempEnd, endBoundary, &newStart, &tempEnd, unusedText);
text->TextBeforeOffset(tempEnd, startBoundary, &newStart, &newEnd,
unusedText);
int32_t potentialStart = newEnd == tempEnd ? newStart : newEnd;
tempStart = potentialStart > tempStart ? potentialStart : currentEnd;
// The offset range we've obtained might have embedded characters in it,
// limit the range to the start of the first occurrence of an embedded
// character.
Accessible* childAtOffset = nullptr;
for (int32_t i = tempStart; i < tempEnd; i++) {
childAtOffset = text->GetChildAtOffset(i);
if (childAtOffset && childAtOffset->IsHyperText()) {
tempEnd = i;
break;
}
}
// If there's an embedded character at the very start of the range, we
// instead want to traverse into it. So restart the movement with
// the child as the starting point.
if (childAtOffset && childAtOffset->IsHyperText() &&
tempStart == static_cast<int32_t>(childAtOffset->StartOffset())) {
tempPosition = childAtOffset;
tempStart = tempEnd = -1;
continue;
}
*aStartOffset = tempStart;
*aEndOffset = tempEnd;
MOZ_ASSERT(tempPosition);
return tempPosition;
}
}
Accessible* Pivot::PrevText(Accessible* aAnchor, int32_t* aStartOffset,
int32_t* aEndOffset, int32_t aBoundaryType) {
if (mRoot->IsRemote() &&
!StaticPrefs::accessibility_cache_enabled_AtStartup()) {
// Not supported for RemoteAccessible when the cache is disabled.
return nullptr;
}
int32_t tempStart = *aStartOffset, tempEnd = *aEndOffset;
Accessible* tempPosition = aAnchor;
// if we're starting on a text leaf, translate the offsets to the
// HyperTextAccessible parent and start from there.
if (aAnchor->IsTextLeaf() && aAnchor->Parent() &&
aAnchor->Parent()->IsHyperText()) {
tempPosition = aAnchor->Parent();
HyperTextAccessibleBase* text = tempPosition->AsHyperTextBase();
int32_t childOffset = text->GetChildOffset(aAnchor);
if (tempStart == -1) {
tempStart = nsAccUtils::TextLength(aAnchor);
tempEnd = tempStart;
}
tempStart += childOffset;
tempEnd += childOffset;
}
while (true) {
MOZ_ASSERT(tempPosition);
Accessible* curPosition = tempPosition;
HyperTextAccessibleBase* text;
// Find the nearest text node using a reverse preorder traversal starting
// from the current node.
if (!(text = tempPosition->AsHyperTextBase())) {
tempPosition = SearchForText(tempPosition, true);
if (!tempPosition) {
return nullptr;
}
if (tempPosition != curPosition) {
tempStart = tempEnd = -1;
}
text = tempPosition->AsHyperTextBase();
}
// If the search led to the parent of the node we started on (e.g. when
// starting on a text leaf), start the text movement from the end offset
// of that node. Otherwise we just default to the last offset in the parent.
if (tempStart == -1) {
if (tempPosition != curPosition &&
tempPosition == curPosition->Parent()) {
tempStart = text->GetChildOffset(curPosition) +
nsAccUtils::TextLength(curPosition);
} else {
tempStart = text->CharacterCount();
}
}
// If there's no more text on the current node, try to find the previous
// text node; if there isn't one, bail out.
if (tempStart == 0) {
if (tempPosition == mRoot) {
return nullptr;
}
// If we're currently sitting on a link, try move to either the previous
// sibling or the parent, whichever is closer to the current end
// offset. Otherwise, do a forward search for the next node to land on
// (we don't do this in the first case because we don't want to go to the
// subtree).
Accessible* sibling = tempPosition->PrevSibling();
if (tempPosition->IsLink()) {
if (sibling && sibling->IsLink()) {
HyperTextAccessibleBase* siblingText = sibling->AsHyperTextBase();
tempStart = tempEnd =
siblingText ? siblingText->CharacterCount() : -1;
tempPosition = sibling;
} else {
tempStart = tempPosition->StartOffset();
tempEnd = tempPosition->EndOffset();
tempPosition = tempPosition->Parent();
}
} else {
tempPosition = SearchForText(tempPosition, true);
if (!tempPosition) {
return nullptr;
}
HyperTextAccessibleBase* tempText = tempPosition->AsHyperTextBase();
tempStart = tempEnd = tempText->CharacterCount();
}
continue;
}
AccessibleTextBoundary startBoundary, endBoundary;
switch (aBoundaryType) {
case nsIAccessiblePivot::CHAR_BOUNDARY:
startBoundary = nsIAccessibleText::BOUNDARY_CHAR;
endBoundary = nsIAccessibleText::BOUNDARY_CHAR;
break;
case nsIAccessiblePivot::WORD_BOUNDARY:
startBoundary = nsIAccessibleText::BOUNDARY_WORD_START;
endBoundary = nsIAccessibleText::BOUNDARY_WORD_END;
break;
case nsIAccessiblePivot::LINE_BOUNDARY:
startBoundary = nsIAccessibleText::BOUNDARY_LINE_START;
endBoundary = nsIAccessibleText::BOUNDARY_LINE_END;
break;
default:
return nullptr;
}
nsAutoString unusedText;
int32_t newStart = 0, newEnd = 0, currentStart = tempStart,
potentialEnd = 0;
text->TextBeforeOffset(tempStart, startBoundary, &newStart, &newEnd,
unusedText);
if (newStart < tempStart) {
tempStart = newEnd >= currentStart ? newStart : newEnd;
} else {
// XXX: In certain odd cases newStart is equal to tempStart
text->TextBeforeOffset(tempStart - 1, startBoundary, &newStart,
&tempStart, unusedText);
}
text->TextAtOffset(tempStart, endBoundary, &newStart, &potentialEnd,
unusedText);
tempEnd = potentialEnd < tempEnd ? potentialEnd : currentStart;
// The offset range we've obtained might have embedded characters in it,
// limit the range to the start of the last occurrence of an embedded
// character.
Accessible* childAtOffset = nullptr;
for (int32_t i = tempEnd - 1; i >= tempStart; i--) {
childAtOffset = text->GetChildAtOffset(i);
if (childAtOffset && !childAtOffset->IsText()) {
tempStart = childAtOffset->EndOffset();
break;
}
}
// If there's an embedded character at the very end of the range, we
// instead want to traverse into it. So restart the movement with
// the child as the starting point.
if (childAtOffset && !childAtOffset->IsText() &&
tempEnd == static_cast<int32_t>(childAtOffset->EndOffset())) {
tempPosition = childAtOffset;
tempStart = tempEnd = static_cast<int32_t>(
childAtOffset->AsHyperTextBase()->CharacterCount());
continue;
}
*aStartOffset = tempStart;
*aEndOffset = tempEnd;
MOZ_ASSERT(tempPosition);
return tempPosition;
}
}
Accessible* Pivot::AtPoint(int32_t aX, int32_t aY, PivotRule& aRule) {
Accessible* match = nullptr;
Accessible* child =
mRoot ? mRoot->ChildAtPoint(aX, aY,
Accessible::EWhichChildAtPoint::DeepestChild)
: nullptr;
while (child && (mRoot != child)) {
uint16_t filtered = aRule.Match(child);
// Ignore any matching nodes that were below this one
if (filtered & nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE) {
match = nullptr;
}
// Match if no node below this is a match
if ((filtered & nsIAccessibleTraversalRule::FILTER_MATCH) && !match) {
LayoutDeviceIntRect childRect = child->IsLocal()
? child->AsLocal()->Bounds()
: child->AsRemote()->Bounds();
// Double-check child's bounds since the deepest child may have been out
// of bounds. This assures we don't return a false positive.
if (childRect.Contains(aX, aY)) {
match = child;
}
}
child = child->Parent();
}
return match;
}
// Role Rule
PivotRoleRule::PivotRoleRule(mozilla::a11y::role aRole)
: mRole(aRole), mDirectDescendantsFrom(nullptr) {}
PivotRoleRule::PivotRoleRule(mozilla::a11y::role aRole,
Accessible* aDirectDescendantsFrom)
: mRole(aRole), mDirectDescendantsFrom(aDirectDescendantsFrom) {}
uint16_t PivotRoleRule::Match(Accessible* aAcc) {
uint16_t result = nsIAccessibleTraversalRule::FILTER_IGNORE;
if (nsAccUtils::MustPrune(aAcc)) {
result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
}
if (mDirectDescendantsFrom && (aAcc != mDirectDescendantsFrom)) {
// If we've specified mDirectDescendantsFrom, we should ignore
// non-direct descendants of from the specified AoP. Because
// pivot performs a preorder traversal, the first aAcc
// object(s) that don't equal mDirectDescendantsFrom will be
// mDirectDescendantsFrom's children. We'll process them, but ignore
// their subtrees thereby processing direct descendants of
// mDirectDescendantsFrom only.
result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
}
if (aAcc && aAcc->Role() == mRole) {
result |= nsIAccessibleTraversalRule::FILTER_MATCH;
}
return result;
}
// State Rule
PivotStateRule::PivotStateRule(uint64_t aState) : mState(aState) {}
uint16_t PivotStateRule::Match(Accessible* aAcc) {
uint16_t result = nsIAccessibleTraversalRule::FILTER_IGNORE;
if (nsAccUtils::MustPrune(aAcc)) {
result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
}
if (aAcc && (aAcc->State() & mState)) {
result = nsIAccessibleTraversalRule::FILTER_MATCH |
nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
}
return result;
}
// LocalAccInSameDocRule
uint16_t LocalAccInSameDocRule::Match(Accessible* aAcc) {
LocalAccessible* acc = aAcc ? aAcc->AsLocal() : nullptr;
if (!acc) {
return nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
}
if (acc->IsOuterDoc()) {
return nsIAccessibleTraversalRule::FILTER_MATCH |
nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
}
return nsIAccessibleTraversalRule::FILTER_MATCH;
}
// Radio Button Name Rule
PivotRadioNameRule::PivotRadioNameRule(const nsString& aName) : mName(aName) {}
uint16_t PivotRadioNameRule::Match(Accessible* aAcc) {
uint16_t result = nsIAccessibleTraversalRule::FILTER_IGNORE;
RemoteAccessible* remote = aAcc->AsRemote();
if (!remote || !StaticPrefs::accessibility_cache_enabled_AtStartup()) {
// We need the cache to be able to fetch the name attribute below.
return result;
}
if (nsAccUtils::MustPrune(aAcc) || aAcc->IsOuterDoc()) {
result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
}
if (remote->IsHTMLRadioButton()) {
nsString currName = remote->GetCachedHTMLNameAttribute();
if (!currName.IsEmpty() && mName.Equals(currName)) {
result |= nsIAccessibleTraversalRule::FILTER_MATCH;
}
}
return result;
}
// MustPruneSameDocRule
uint16_t MustPruneSameDocRule::Match(Accessible* aAcc) {
if (!aAcc) {
return nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
}
if (nsAccUtils::MustPrune(aAcc) || aAcc->IsOuterDoc()) {
return nsIAccessibleTraversalRule::FILTER_MATCH |
nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
}
return nsIAccessibleTraversalRule::FILTER_MATCH;
}