Autogenerated with:
cp dom/.clang-format accessible/ && mach format accessible/**.{cpp,h,mm}
Manual fixes:
* Atk individual includes need to become atk/atk.h because otherwise some symbols incorrectly get hidden visibility.
Differential Revision: https://phabricator.services.mozilla.com/D311692
867 lines
31 KiB
C++
867 lines
31 KiB
C++
/* 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 "HyperTextAccessibleBase.h"
|
|
|
|
#include "TextLeafRange.h"
|
|
#include "TextRange.h"
|
|
#include "mozilla/a11y/Accessible.h"
|
|
#include "nsAccUtils.h"
|
|
|
|
namespace mozilla::a11y {
|
|
|
|
int32_t HyperTextAccessibleBase::GetChildIndexAtOffset(uint32_t aOffset) const {
|
|
auto& offsets =
|
|
const_cast<HyperTextAccessibleBase*>(this)->GetCachedHyperTextOffsets();
|
|
int32_t lastOffset = 0;
|
|
const uint32_t offsetCount = offsets.Length();
|
|
|
|
if (offsetCount > 0) {
|
|
lastOffset = offsets[offsetCount - 1];
|
|
if (static_cast<int32_t>(aOffset) < lastOffset) {
|
|
// We've cached up to aOffset.
|
|
size_t index;
|
|
if (BinarySearch(offsets, 0, offsetCount, static_cast<int32_t>(aOffset),
|
|
&index)) {
|
|
// aOffset is the exclusive end of a child, so return the child before
|
|
// it.
|
|
return static_cast<int32_t>((index < offsetCount - 1) ? index + 1
|
|
: index);
|
|
}
|
|
if (index == offsetCount) {
|
|
// aOffset is past the end of the text.
|
|
return -1;
|
|
}
|
|
// index points at the exclusive end after aOffset.
|
|
return static_cast<int32_t>(index);
|
|
}
|
|
}
|
|
|
|
// We haven't yet cached up to aOffset. Find it, caching as we go.
|
|
const Accessible* thisAcc = Acc();
|
|
uint32_t childCount = thisAcc->ChildCount();
|
|
// Even though we're only caching up to aOffset, it's likely that we'll
|
|
// eventually cache offsets for all children. Pre-allocate thus to minimize
|
|
// re-allocations.
|
|
offsets.SetCapacity(childCount);
|
|
while (offsets.Length() < childCount) {
|
|
Accessible* child = thisAcc->ChildAt(offsets.Length());
|
|
lastOffset += static_cast<int32_t>(nsAccUtils::TextLength(child));
|
|
offsets.AppendElement(lastOffset);
|
|
if (static_cast<int32_t>(aOffset) < lastOffset) {
|
|
return static_cast<int32_t>(offsets.Length() - 1);
|
|
}
|
|
}
|
|
|
|
if (static_cast<int32_t>(aOffset) == lastOffset) {
|
|
return static_cast<int32_t>(offsets.Length() - 1);
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
Accessible* HyperTextAccessibleBase::GetChildAtOffset(uint32_t aOffset) const {
|
|
const Accessible* thisAcc = Acc();
|
|
return thisAcc->ChildAt(GetChildIndexAtOffset(aOffset));
|
|
}
|
|
|
|
int32_t HyperTextAccessibleBase::GetChildOffset(const Accessible* aChild,
|
|
bool aInvalidateAfter) const {
|
|
const Accessible* thisAcc = Acc();
|
|
if (aChild->Parent() != thisAcc) {
|
|
return -1;
|
|
}
|
|
int32_t index = aChild->IndexInParent();
|
|
if (index == -1) {
|
|
return -1;
|
|
}
|
|
return GetChildOffset(index, aInvalidateAfter);
|
|
}
|
|
|
|
int32_t HyperTextAccessibleBase::GetChildOffset(uint32_t aChildIndex,
|
|
bool aInvalidateAfter) const {
|
|
auto& offsets =
|
|
const_cast<HyperTextAccessibleBase*>(this)->GetCachedHyperTextOffsets();
|
|
if (aChildIndex == 0) {
|
|
if (aInvalidateAfter) {
|
|
offsets.Clear();
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int32_t countCachedAfterChild = static_cast<int32_t>(offsets.Length()) -
|
|
static_cast<int32_t>(aChildIndex);
|
|
if (countCachedAfterChild > 0) {
|
|
// We've cached up to aChildIndex.
|
|
if (aInvalidateAfter) {
|
|
offsets.RemoveElementsAt(aChildIndex, countCachedAfterChild);
|
|
}
|
|
return offsets[aChildIndex - 1];
|
|
}
|
|
|
|
// We haven't yet cached up to aChildIndex. Find it, caching as we go.
|
|
const Accessible* thisAcc = Acc();
|
|
// Even though we're only caching up to aChildIndex, it's likely that we'll
|
|
// eventually cache offsets for all children. Pre-allocate thus to minimize
|
|
// re-allocations.
|
|
offsets.SetCapacity(thisAcc->ChildCount());
|
|
uint32_t lastOffset = offsets.IsEmpty() ? 0 : offsets[offsets.Length() - 1];
|
|
while (offsets.Length() < aChildIndex) {
|
|
Accessible* child = thisAcc->ChildAt(offsets.Length());
|
|
lastOffset += nsAccUtils::TextLength(child);
|
|
offsets.AppendElement(lastOffset);
|
|
}
|
|
|
|
return offsets[aChildIndex - 1];
|
|
}
|
|
|
|
uint32_t HyperTextAccessibleBase::CharacterCount() const {
|
|
return GetChildOffset(Acc()->ChildCount());
|
|
}
|
|
|
|
index_t HyperTextAccessibleBase::ConvertMagicOffset(int32_t aOffset) const {
|
|
if (aOffset == nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT) {
|
|
return CharacterCount();
|
|
}
|
|
|
|
if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) {
|
|
return CaretOffset();
|
|
}
|
|
|
|
return aOffset;
|
|
}
|
|
|
|
void HyperTextAccessibleBase::TextSubstring(int32_t aStartOffset,
|
|
int32_t aEndOffset,
|
|
nsAString& aText) const {
|
|
aText.Truncate();
|
|
|
|
index_t startOffset = ConvertMagicOffset(aStartOffset);
|
|
index_t endOffset = ConvertMagicOffset(aEndOffset);
|
|
if (!startOffset.IsValid() || !endOffset.IsValid() ||
|
|
startOffset > endOffset || endOffset > CharacterCount()) {
|
|
NS_ERROR("Wrong in offset");
|
|
return;
|
|
}
|
|
|
|
int32_t startChildIdx = GetChildIndexAtOffset(startOffset);
|
|
if (startChildIdx == -1) {
|
|
return;
|
|
}
|
|
|
|
int32_t endChildIdx = GetChildIndexAtOffset(endOffset);
|
|
if (endChildIdx == -1) {
|
|
return;
|
|
}
|
|
|
|
const Accessible* thisAcc = Acc();
|
|
if (startChildIdx == endChildIdx) {
|
|
int32_t childOffset = GetChildOffset(startChildIdx);
|
|
if (childOffset == -1) {
|
|
return;
|
|
}
|
|
|
|
Accessible* child = thisAcc->ChildAt(startChildIdx);
|
|
child->AppendTextTo(aText, startOffset - childOffset,
|
|
endOffset - startOffset);
|
|
return;
|
|
}
|
|
|
|
int32_t startChildOffset = GetChildOffset(startChildIdx);
|
|
if (startChildOffset == -1) {
|
|
return;
|
|
}
|
|
|
|
Accessible* startChild = thisAcc->ChildAt(startChildIdx);
|
|
startChild->AppendTextTo(aText, startOffset - startChildOffset);
|
|
|
|
for (int32_t childIdx = startChildIdx + 1; childIdx < endChildIdx;
|
|
childIdx++) {
|
|
Accessible* child = thisAcc->ChildAt(childIdx);
|
|
child->AppendTextTo(aText);
|
|
}
|
|
|
|
int32_t endChildOffset = GetChildOffset(endChildIdx);
|
|
if (endChildOffset == -1) {
|
|
return;
|
|
}
|
|
|
|
Accessible* endChild = thisAcc->ChildAt(endChildIdx);
|
|
endChild->AppendTextTo(aText, 0, endOffset - endChildOffset);
|
|
}
|
|
|
|
bool HyperTextAccessibleBase::CharAt(int32_t aOffset, nsAString& aChar,
|
|
int32_t* aStartOffset,
|
|
int32_t* aEndOffset) {
|
|
MOZ_ASSERT(!aStartOffset == !aEndOffset,
|
|
"Offsets should be both defined or both undefined!");
|
|
|
|
int32_t childIdx = GetChildIndexAtOffset(aOffset);
|
|
if (childIdx == -1) {
|
|
return false;
|
|
}
|
|
|
|
Accessible* child = Acc()->ChildAt(childIdx);
|
|
child->AppendTextTo(aChar, aOffset - GetChildOffset(childIdx), 1);
|
|
|
|
if (aStartOffset && aEndOffset) {
|
|
*aStartOffset = aOffset;
|
|
*aEndOffset = aOffset + aChar.Length();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
LayoutDeviceIntRect HyperTextAccessibleBase::CharBounds(int32_t aOffset,
|
|
uint32_t aCoordType) {
|
|
index_t offset = ConvertMagicOffset(aOffset);
|
|
if (!offset.IsValid() || offset > CharacterCount()) {
|
|
return LayoutDeviceIntRect();
|
|
}
|
|
TextLeafPoint point = ToTextLeafPoint(static_cast<int32_t>(offset), false);
|
|
if (!point.mAcc) {
|
|
return LayoutDeviceIntRect();
|
|
}
|
|
|
|
LayoutDeviceIntRect bounds = point.CharBounds();
|
|
if (!bounds.x && !bounds.y && bounds.IsZeroArea()) {
|
|
return bounds;
|
|
}
|
|
nsAccUtils::ConvertScreenCoordsTo(&bounds.x, &bounds.y, aCoordType, Acc());
|
|
return bounds;
|
|
}
|
|
|
|
LayoutDeviceIntRect HyperTextAccessibleBase::TextBounds(int32_t aStartOffset,
|
|
int32_t aEndOffset,
|
|
uint32_t aCoordType) {
|
|
LayoutDeviceIntRect result;
|
|
if (CharacterCount() == 0) {
|
|
result = Acc()->Bounds();
|
|
nsAccUtils::ConvertScreenCoordsTo(&result.x, &result.y, aCoordType, Acc());
|
|
return result;
|
|
}
|
|
|
|
index_t startOffset = ConvertMagicOffset(aStartOffset);
|
|
index_t endOffset = ConvertMagicOffset(aEndOffset);
|
|
if (!startOffset.IsValid() || startOffset > endOffset) {
|
|
return LayoutDeviceIntRect();
|
|
}
|
|
|
|
TextLeafPoint startPoint =
|
|
ToTextLeafPoint(static_cast<int32_t>(startOffset), false);
|
|
TextLeafPoint endPoint =
|
|
ToTextLeafPoint(static_cast<int32_t>(endOffset), true);
|
|
if (!endPoint) {
|
|
// The caller provided an invalid offset.
|
|
return LayoutDeviceIntRect();
|
|
}
|
|
|
|
TextLeafRange range(startPoint, endPoint);
|
|
result = range.Bounds();
|
|
|
|
// Calls to TextLeafRange::Bounds() will construct screen coordinates.
|
|
// Perform any additional conversions here.
|
|
nsAccUtils::ConvertScreenCoordsTo(&result.x, &result.y, aCoordType, Acc());
|
|
return result;
|
|
}
|
|
|
|
int32_t HyperTextAccessibleBase::OffsetAtPoint(int32_t aX, int32_t aY,
|
|
uint32_t aCoordType) {
|
|
Accessible* thisAcc = Acc();
|
|
LayoutDeviceIntPoint coords =
|
|
nsAccUtils::ConvertToScreenCoords(aX, aY, aCoordType, thisAcc);
|
|
if (!thisAcc->Bounds().Contains(coords.x, coords.y)) {
|
|
// The requested point does not exist in this accessible.
|
|
// Check if we used fuzzy hittesting to get here and, if
|
|
// so, return 0 to indicate this text leaf is a valid match.
|
|
LayoutDeviceIntPoint p(aX, aY);
|
|
if (aCoordType != nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE) {
|
|
p = nsAccUtils::ConvertToScreenCoords(aX, aY, aCoordType, thisAcc);
|
|
}
|
|
if (Accessible* doc = nsAccUtils::DocumentFor(thisAcc)) {
|
|
Accessible* hittestMatch = doc->ChildAtPoint(
|
|
p.x, p.y, Accessible::EWhichChildAtPoint::DeepestChild);
|
|
if (hittestMatch && thisAcc == hittestMatch->Parent()) {
|
|
return 0;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
TextLeafPoint startPoint = ToTextLeafPoint(0, false);
|
|
// Walk to the very end of the text contained in this hypertext in order to
|
|
// hit test it in its entirety.
|
|
TextLeafPoint endPoint =
|
|
ToTextLeafPoint(static_cast<int32_t>(CharacterCount()), true);
|
|
TextLeafRange range{startPoint, endPoint};
|
|
TextLeafPoint point = range.TextLeafPointAtScreenPoint(coords.x, coords.y);
|
|
if (!point.ContainsPoint(coords.x, coords.y)) {
|
|
LayoutDeviceIntRect startRect = startPoint.CharBounds();
|
|
if (coords.x < startRect.x || coords.y < startRect.y) {
|
|
// Bug 1816601: The point is within the container but above or to the left
|
|
// of the rectangle at offset 0. We should really return -1, but we've
|
|
// returned 0 for many years due to a bug. Some users have unfortunately
|
|
// come to rely on this, so perpetuate this here.
|
|
return 0;
|
|
}
|
|
return -1;
|
|
}
|
|
DebugOnly<bool> ok = false;
|
|
int32_t htOffset;
|
|
std::tie(ok, htOffset) =
|
|
TransformOffset(point.mAcc, point.mOffset, /* aIsEndOffset */ false);
|
|
MOZ_ASSERT(ok, "point should be a descendant of this");
|
|
return htOffset;
|
|
}
|
|
|
|
TextLeafPoint HyperTextAccessibleBase::ToTextLeafPoint(int32_t aOffset,
|
|
bool aDescendToEnd) {
|
|
Accessible* thisAcc = Acc();
|
|
if (!thisAcc->HasChildren()) {
|
|
return TextLeafPoint(thisAcc, 0);
|
|
}
|
|
Accessible* child = GetChildAtOffset(aOffset);
|
|
if (!child) {
|
|
return TextLeafPoint();
|
|
}
|
|
int32_t offset = aOffset - GetChildOffset(child);
|
|
if (HyperTextAccessibleBase* childHt = child->AsHyperTextBase()) {
|
|
// This child is an embedded object, so the offset can only be 0 or 1.
|
|
MOZ_ASSERT(offset == 0 || offset == 1);
|
|
// Offset 1 refers to the end of this container, so descend to its end.
|
|
const bool end = aDescendToEnd || offset == 1;
|
|
return childHt->ToTextLeafPoint(
|
|
end ? static_cast<int32_t>(childHt->CharacterCount()) : 0, end);
|
|
}
|
|
return TextLeafPoint(child, offset);
|
|
}
|
|
|
|
std::pair<bool, int32_t> HyperTextAccessibleBase::TransformOffset(
|
|
Accessible* aDescendant, int32_t aOffset, bool aIsEndOffset) const {
|
|
const Accessible* thisAcc = Acc();
|
|
// From the descendant, go up and get the immediate child of this hypertext.
|
|
int32_t offset = aOffset;
|
|
Accessible* descendant = aDescendant;
|
|
while (descendant) {
|
|
Accessible* parent = descendant->Parent();
|
|
if (parent == thisAcc) {
|
|
return {true, GetChildOffset(descendant) + offset};
|
|
}
|
|
|
|
// This offset no longer applies because the passed-in text object is not
|
|
// a child of the hypertext. This happens when there are nested hypertexts,
|
|
// e.g. <div>abc<h1>def</h1>ghi</div>. Thus we need to adjust the offset
|
|
// to make it relative the hypertext.
|
|
// If the end offset is not supposed to be inclusive and the original point
|
|
// is not at 0 offset then the returned offset should be after an embedded
|
|
// character the original point belongs to.
|
|
if (aIsEndOffset) {
|
|
offset = (offset > 0 || descendant->IndexInParent() > 0) ? 1 : 0;
|
|
} else {
|
|
offset = 0;
|
|
}
|
|
|
|
descendant = parent;
|
|
}
|
|
|
|
// The given a11y point cannot be mapped to an offset relative to this
|
|
// hypertext accessible. Return the start or the end depending on whether this
|
|
// is a start ofset or an end offset, thus clipping to the relevant endpoint.
|
|
return {false, aIsEndOffset ? static_cast<int32_t>(CharacterCount()) : 0};
|
|
}
|
|
|
|
void HyperTextAccessibleBase::AdjustOriginIfEndBoundary(
|
|
TextLeafPoint& aOrigin, AccessibleTextBoundary aBoundaryType,
|
|
bool aAtOffset) const {
|
|
if (aBoundaryType != nsIAccessibleText::BOUNDARY_LINE_END &&
|
|
aBoundaryType != nsIAccessibleText::BOUNDARY_WORD_END) {
|
|
return;
|
|
}
|
|
TextLeafPoint actualOrig = aOrigin;
|
|
// We explicitly care about the character at this offset. We don't want
|
|
// FindBoundary to behave differently even if this is the insertion point at
|
|
// the end of a line.
|
|
actualOrig.mIsEndOfLineInsertionPoint = false;
|
|
if (aBoundaryType == nsIAccessibleText::BOUNDARY_LINE_END) {
|
|
if (!actualOrig.IsLineFeedChar()) {
|
|
return;
|
|
}
|
|
aOrigin =
|
|
actualOrig.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious);
|
|
} else { // BOUNDARY_WORD_END
|
|
if (aAtOffset) {
|
|
// For TextAtOffset with BOUNDARY_WORD_END, we follow WebKitGtk here and
|
|
// return the word which ends after the origin if the origin is a word end
|
|
// boundary. Also, if the caret is at the end of a line, our tests expect
|
|
// the word after the caret, not the word before. The reason for that
|
|
// is a mystery lost to history. We can do that by explicitly using the
|
|
// caret without adjusting for end of line.
|
|
aOrigin = actualOrig;
|
|
return;
|
|
}
|
|
if (!actualOrig.IsSpace()) {
|
|
return;
|
|
}
|
|
TextLeafPoint prevChar =
|
|
actualOrig.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious);
|
|
if (prevChar != actualOrig && !prevChar.IsSpace()) {
|
|
// aOrigin is a word end boundary.
|
|
aOrigin = prevChar;
|
|
}
|
|
}
|
|
}
|
|
|
|
void HyperTextAccessibleBase::TextBeforeOffset(
|
|
int32_t aOffset, AccessibleTextBoundary aBoundaryType,
|
|
int32_t* aStartOffset, int32_t* aEndOffset, nsAString& aText) {
|
|
*aStartOffset = *aEndOffset = 0;
|
|
aText.Truncate();
|
|
|
|
if (aBoundaryType == nsIAccessibleText::BOUNDARY_SENTENCE_START ||
|
|
aBoundaryType == nsIAccessibleText::BOUNDARY_SENTENCE_END) {
|
|
return; // Not implemented.
|
|
}
|
|
|
|
uint32_t adjustedOffset = ConvertMagicOffset(aOffset);
|
|
if (adjustedOffset == std::numeric_limits<uint32_t>::max()) {
|
|
NS_ERROR("Wrong given offset!");
|
|
return;
|
|
}
|
|
|
|
if (aBoundaryType == nsIAccessibleText::BOUNDARY_CHAR) {
|
|
if (adjustedOffset > 0) {
|
|
CharAt(static_cast<int32_t>(adjustedOffset) - 1, aText, aStartOffset,
|
|
aEndOffset);
|
|
}
|
|
return;
|
|
}
|
|
|
|
TextLeafPoint orig;
|
|
if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) {
|
|
orig = TextLeafPoint::GetCaret(Acc());
|
|
} else {
|
|
orig = ToTextLeafPoint(static_cast<int32_t>(adjustedOffset));
|
|
}
|
|
if (!orig) {
|
|
// This can happen if aOffset is invalid.
|
|
return;
|
|
}
|
|
AdjustOriginIfEndBoundary(orig, aBoundaryType);
|
|
TextLeafPoint end =
|
|
orig.FindBoundary(aBoundaryType, eDirPrevious,
|
|
TextLeafPoint::BoundaryFlags::eIncludeOrigin);
|
|
bool ok;
|
|
std::tie(ok, *aEndOffset) = TransformOffset(end.mAcc, end.mOffset,
|
|
/* aIsEndOffset */ true);
|
|
if (!ok) {
|
|
// There is no previous boundary inside this HyperText.
|
|
*aStartOffset = *aEndOffset = 0;
|
|
return;
|
|
}
|
|
TextLeafPoint start = end.FindBoundary(aBoundaryType, eDirPrevious);
|
|
// If TransformOffset fails because start is outside this HyperText,
|
|
// *aStartOffset will be 0, which is what we want.
|
|
std::tie(ok, *aStartOffset) = TransformOffset(start.mAcc, start.mOffset,
|
|
/* aIsEndOffset */ false);
|
|
TextSubstring(*aStartOffset, *aEndOffset, aText);
|
|
}
|
|
|
|
void HyperTextAccessibleBase::TextAtOffset(int32_t aOffset,
|
|
AccessibleTextBoundary aBoundaryType,
|
|
int32_t* aStartOffset,
|
|
int32_t* aEndOffset,
|
|
nsAString& aText) {
|
|
*aStartOffset = *aEndOffset = 0;
|
|
aText.Truncate();
|
|
|
|
if (aBoundaryType == nsIAccessibleText::BOUNDARY_SENTENCE_START ||
|
|
aBoundaryType == nsIAccessibleText::BOUNDARY_SENTENCE_END) {
|
|
return; // Not implemented.
|
|
}
|
|
|
|
uint32_t adjustedOffset = ConvertMagicOffset(aOffset);
|
|
if (adjustedOffset == std::numeric_limits<uint32_t>::max()) {
|
|
NS_ERROR("Wrong given offset!");
|
|
return;
|
|
}
|
|
|
|
if (aBoundaryType == nsIAccessibleText::BOUNDARY_CHAR) {
|
|
if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) {
|
|
TextLeafPoint caret = TextLeafPoint::GetCaret(Acc());
|
|
if (caret.mIsEndOfLineInsertionPoint) {
|
|
// The caret is at the end of the line. Return no character.
|
|
*aStartOffset = *aEndOffset = static_cast<int32_t>(adjustedOffset);
|
|
return;
|
|
}
|
|
}
|
|
CharAt(adjustedOffset, aText, aStartOffset, aEndOffset);
|
|
return;
|
|
}
|
|
|
|
TextLeafPoint start, end;
|
|
if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) {
|
|
start = TextLeafPoint::GetCaret(Acc());
|
|
AdjustOriginIfEndBoundary(start, aBoundaryType, /* aAtOffset */ true);
|
|
end = start;
|
|
} else {
|
|
start = ToTextLeafPoint(static_cast<int32_t>(adjustedOffset));
|
|
Accessible* childAcc = GetChildAtOffset(adjustedOffset);
|
|
if (childAcc && childAcc->IsHyperText()) {
|
|
// We're searching for boundaries enclosing an embedded object.
|
|
// An embedded object might contain several boundaries itself.
|
|
// Thus, we must ensure we search for the end boundary from the last
|
|
// text in the subtree, not just the first.
|
|
// For example, if the embedded object is a link and it contains two
|
|
// words, but the second word expands beyond the link, we want to
|
|
// include the part of the second word which is outside of the link.
|
|
end = ToTextLeafPoint(static_cast<int32_t>(adjustedOffset),
|
|
/* aDescendToEnd */ true);
|
|
} else {
|
|
AdjustOriginIfEndBoundary(start, aBoundaryType,
|
|
/* aAtOffset */ true);
|
|
end = start;
|
|
}
|
|
}
|
|
if (!start) {
|
|
// This can happen if aOffset is invalid.
|
|
return;
|
|
}
|
|
start = start.FindBoundary(aBoundaryType, eDirPrevious,
|
|
TextLeafPoint::BoundaryFlags::eIncludeOrigin);
|
|
bool ok;
|
|
std::tie(ok, *aStartOffset) = TransformOffset(start.mAcc, start.mOffset,
|
|
/* aIsEndOffset */ false);
|
|
end = end.FindBoundary(aBoundaryType, eDirNext);
|
|
std::tie(ok, *aEndOffset) = TransformOffset(end.mAcc, end.mOffset,
|
|
/* aIsEndOffset */ true);
|
|
TextSubstring(*aStartOffset, *aEndOffset, aText);
|
|
}
|
|
|
|
void HyperTextAccessibleBase::TextAfterOffset(
|
|
int32_t aOffset, AccessibleTextBoundary aBoundaryType,
|
|
int32_t* aStartOffset, int32_t* aEndOffset, nsAString& aText) {
|
|
*aStartOffset = *aEndOffset = 0;
|
|
aText.Truncate();
|
|
|
|
if (aBoundaryType == nsIAccessibleText::BOUNDARY_SENTENCE_START ||
|
|
aBoundaryType == nsIAccessibleText::BOUNDARY_SENTENCE_END) {
|
|
return; // Not implemented.
|
|
}
|
|
|
|
uint32_t adjustedOffset = ConvertMagicOffset(aOffset);
|
|
if (adjustedOffset == std::numeric_limits<uint32_t>::max()) {
|
|
NS_ERROR("Wrong given offset!");
|
|
return;
|
|
}
|
|
|
|
if (aBoundaryType == nsIAccessibleText::BOUNDARY_CHAR) {
|
|
if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET && adjustedOffset > 0 &&
|
|
TextLeafPoint::GetCaret(Acc()).mIsEndOfLineInsertionPoint) {
|
|
--adjustedOffset;
|
|
}
|
|
uint32_t count = CharacterCount();
|
|
if (adjustedOffset >= count) {
|
|
*aStartOffset = *aEndOffset = static_cast<int32_t>(count);
|
|
} else {
|
|
CharAt(static_cast<int32_t>(adjustedOffset) + 1, aText, aStartOffset,
|
|
aEndOffset);
|
|
}
|
|
return;
|
|
}
|
|
|
|
TextLeafPoint orig;
|
|
if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) {
|
|
orig = TextLeafPoint::GetCaret(Acc());
|
|
} else {
|
|
orig = ToTextLeafPoint(static_cast<int32_t>(adjustedOffset),
|
|
/* aDescendToEnd */ true);
|
|
}
|
|
if (!orig) {
|
|
// This can happen if aOffset is invalid.
|
|
return;
|
|
}
|
|
AdjustOriginIfEndBoundary(orig, aBoundaryType);
|
|
TextLeafPoint start = orig.FindBoundary(aBoundaryType, eDirNext);
|
|
bool ok;
|
|
std::tie(ok, *aStartOffset) = TransformOffset(start.mAcc, start.mOffset,
|
|
/* aIsEndOffset */ false);
|
|
if (!ok) {
|
|
// There is no next boundary inside this HyperText.
|
|
*aStartOffset = *aEndOffset = static_cast<int32_t>(CharacterCount());
|
|
return;
|
|
}
|
|
TextLeafPoint end = start.FindBoundary(aBoundaryType, eDirNext);
|
|
// If TransformOffset fails because end is outside this HyperText,
|
|
// *aEndOffset will be CharacterCount(), which is what we want.
|
|
std::tie(ok, *aEndOffset) = TransformOffset(end.mAcc, end.mOffset,
|
|
/* aIsEndOffset */ true);
|
|
TextSubstring(*aStartOffset, *aEndOffset, aText);
|
|
}
|
|
|
|
int32_t HyperTextAccessibleBase::CaretOffset() const {
|
|
TextLeafPoint point = TextLeafPoint::GetCaret(const_cast<Accessible*>(Acc()));
|
|
if (point.mOffset == 0 && point.mAcc == Acc()) {
|
|
// If a text box is empty, there will be no children, so point.mAcc will be
|
|
// this HyperText.
|
|
return 0;
|
|
}
|
|
auto [ok, htOffset] =
|
|
TransformOffset(point.mAcc, point.mOffset, /* aIsEndOffset */ false);
|
|
if (!ok) {
|
|
// The caret is not within this HyperText.
|
|
return -1;
|
|
}
|
|
return htOffset;
|
|
}
|
|
|
|
void HyperTextAccessibleBase::SetCaretOffset(int32_t aOffset) {
|
|
TextLeafPoint point = ToTextLeafPoint(aOffset);
|
|
TextLeafRange range(point, point);
|
|
if (!range) {
|
|
NS_ERROR("Wrong in offset");
|
|
return;
|
|
}
|
|
range.SetSelection(TextLeafRange::kRemoveAllExistingSelectedRanges);
|
|
}
|
|
|
|
int32_t HyperTextAccessibleBase::CaretLineNumber() {
|
|
TextLeafPoint point = TextLeafPoint::GetCaret(const_cast<Accessible*>(Acc()));
|
|
if (point.mOffset == 0 && point.mAcc == Acc()) {
|
|
MOZ_ASSERT(CharacterCount() == 0);
|
|
// If a text box is empty, there will be no children, so point.mAcc will be
|
|
// this HyperText.
|
|
return 1;
|
|
}
|
|
|
|
if (!point.mAcc ||
|
|
(point.mAcc != Acc() && !Acc()->IsAncestorOf(point.mAcc))) {
|
|
// The caret is not within this HyperText.
|
|
return -1;
|
|
}
|
|
|
|
// Walk forward by line from the start of the container.
|
|
TextLeafPoint line = TextLeafPoint(Acc(), 0);
|
|
int32_t lineNumber = 0;
|
|
for (; line && line < point;
|
|
line = line.FindBoundary(nsIAccessibleText::BOUNDARY_LINE_START,
|
|
eDirNext)) {
|
|
++lineNumber;
|
|
}
|
|
// The caret might be right at the start of a line, in which case we should
|
|
// increment the line number. We shouldn't do that if the caret is at the end
|
|
// of a line or container, though.
|
|
if (line == point && !point.mIsEndOfLineInsertionPoint &&
|
|
point.mOffset <
|
|
static_cast<int32_t>(nsAccUtils::TextLength(point.mAcc))) {
|
|
++lineNumber;
|
|
}
|
|
|
|
return lineNumber;
|
|
}
|
|
|
|
bool HyperTextAccessibleBase::IsValidOffset(int32_t aOffset) {
|
|
index_t offset = ConvertMagicOffset(aOffset);
|
|
return offset.IsValid() && offset <= CharacterCount();
|
|
}
|
|
|
|
bool HyperTextAccessibleBase::IsValidRange(int32_t aStartOffset,
|
|
int32_t aEndOffset) {
|
|
index_t startOffset = ConvertMagicOffset(aStartOffset);
|
|
index_t endOffset = ConvertMagicOffset(aEndOffset);
|
|
return startOffset.IsValid() && endOffset.IsValid() &&
|
|
startOffset <= endOffset && endOffset <= CharacterCount();
|
|
}
|
|
|
|
uint32_t HyperTextAccessibleBase::LinkCount() {
|
|
return Acc()->EmbeddedChildCount();
|
|
}
|
|
|
|
Accessible* HyperTextAccessibleBase::LinkAt(uint32_t aIndex) {
|
|
return Acc()->EmbeddedChildAt(aIndex);
|
|
}
|
|
|
|
int32_t HyperTextAccessibleBase::LinkIndexOf(Accessible* aLink) {
|
|
return Acc()->IndexOfEmbeddedChild(aLink);
|
|
}
|
|
|
|
already_AddRefed<AccAttributes> HyperTextAccessibleBase::TextAttributes(
|
|
bool aIncludeDefAttrs, int32_t aOffset, int32_t* aStartOffset,
|
|
int32_t* aEndOffset) {
|
|
*aStartOffset = *aEndOffset = 0;
|
|
index_t offset = ConvertMagicOffset(aOffset);
|
|
if (!offset.IsValid() || offset > CharacterCount()) {
|
|
NS_ERROR("Wrong in offset!");
|
|
return RefPtr{new AccAttributes()}.forget();
|
|
}
|
|
|
|
Accessible* originAcc = GetChildAtOffset(offset);
|
|
if (!originAcc) {
|
|
// Offset 0 is correct offset when accessible has empty text. Include
|
|
// default attributes if they were requested, otherwise return empty set.
|
|
if (offset == 0) {
|
|
if (aIncludeDefAttrs) {
|
|
return DefaultTextAttributes();
|
|
}
|
|
}
|
|
return RefPtr{new AccAttributes()}.forget();
|
|
}
|
|
|
|
if (!originAcc->IsText()) {
|
|
// This is an embedded object. One or more consecutive embedded objects
|
|
// form a single attrs run with no attributes.
|
|
*aStartOffset = aOffset;
|
|
*aEndOffset = aOffset + 1;
|
|
Accessible* parent = originAcc->Parent();
|
|
if (!parent) {
|
|
return RefPtr{new AccAttributes()}.forget();
|
|
}
|
|
int32_t originIdx = originAcc->IndexInParent();
|
|
if (originIdx > 0) {
|
|
// Check for embedded objects before the origin.
|
|
for (uint32_t idx = originIdx - 1;; --idx) {
|
|
Accessible* sibling = parent->ChildAt(idx);
|
|
if (sibling->IsText()) {
|
|
break;
|
|
}
|
|
--*aStartOffset;
|
|
if (idx == 0) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// Check for embedded objects after the origin.
|
|
for (uint32_t idx = originIdx + 1;; ++idx) {
|
|
Accessible* sibling = parent->ChildAt(idx);
|
|
if (!sibling || sibling->IsText()) {
|
|
break;
|
|
}
|
|
++*aEndOffset;
|
|
}
|
|
return RefPtr{new AccAttributes()}.forget();
|
|
}
|
|
|
|
TextLeafPoint origin = ToTextLeafPoint(static_cast<int32_t>(offset));
|
|
TextLeafPoint start =
|
|
origin.FindTextAttrsStart(eDirPrevious, /* aIncludeOrigin */ true);
|
|
bool ok;
|
|
std::tie(ok, *aStartOffset) = TransformOffset(start.mAcc, start.mOffset,
|
|
/* aIsEndOffset */ false);
|
|
TextLeafPoint end =
|
|
origin.FindTextAttrsStart(eDirNext, /* aIncludeOrigin */ false);
|
|
std::tie(ok, *aEndOffset) = TransformOffset(end.mAcc, end.mOffset,
|
|
/* aIsEndOffset */ true);
|
|
return origin.GetTextAttributes(aIncludeDefAttrs);
|
|
}
|
|
|
|
void HyperTextAccessibleBase::CroppedSelectionRanges(
|
|
nsTArray<TextRange>& aRanges) const {
|
|
SelectionRanges(&aRanges);
|
|
Accessible* acc = const_cast<Accessible*>(Acc());
|
|
|
|
size_t startIndex = 0;
|
|
size_t endIndex = aRanges.Length();
|
|
// If this is the document, it contains all ranges, so there's no need to
|
|
// search for overlapping ranges.
|
|
if (!acc->IsDoc()) {
|
|
// Find overlapping ranges. We use binary searches here, which is far more
|
|
// efficient than cropping every range in the list.
|
|
// Find the first range that ends after acc starts.
|
|
TextPoint thisPoint = TextPoint(acc, 0);
|
|
startIndex =
|
|
UpperBound(aRanges, 0, aRanges.Length(), [&](const TextRange& range) {
|
|
return thisPoint.Compare(range.EndPoint());
|
|
});
|
|
// Find the first range that starts after acc ends. This will be the first
|
|
// non-overlapping range after startIndex; i.e. the exclusive end of our
|
|
// desired span.
|
|
thisPoint = TextPoint(acc, CharacterCount());
|
|
endIndex = UpperBound(aRanges, startIndex, aRanges.Length(),
|
|
[&](const TextRange& range) {
|
|
return thisPoint.Compare(range.StartPoint());
|
|
});
|
|
}
|
|
|
|
// Exclude ranges that don't overlap acc.
|
|
size_t nextIndex = 0;
|
|
aRanges.RemoveElementsBy([&](TextRange& range) {
|
|
// Set index to the index of range. Increment nextIndex ready for the next
|
|
// iteration.
|
|
size_t index = nextIndex++;
|
|
if (range.StartPoint() == range.EndPoint()) {
|
|
return true; // Collapsed, so remove this range.
|
|
}
|
|
// Remove (return true for) ranges that aren't between startIndex
|
|
// (inclusive) and endIndex (exclusive).
|
|
if (index < startIndex || index >= endIndex) {
|
|
return true;
|
|
}
|
|
// The first and last ranges might extend beyond acc, so crop them.
|
|
if (index == startIndex || index == endIndex - 1) {
|
|
DebugOnly<bool> cropped = range.Crop(const_cast<Accessible*>(acc));
|
|
MOZ_ASSERT(cropped, "range should overlap and thus crop successfully");
|
|
}
|
|
return false;
|
|
});
|
|
}
|
|
|
|
int32_t HyperTextAccessibleBase::SelectionCount() {
|
|
nsTArray<TextRange> ranges;
|
|
CroppedSelectionRanges(ranges);
|
|
return static_cast<int32_t>(ranges.Length());
|
|
}
|
|
|
|
bool HyperTextAccessibleBase::SelectionBoundsAt(int32_t aSelectionNum,
|
|
int32_t* aStartOffset,
|
|
int32_t* aEndOffset) {
|
|
nsTArray<TextRange> ranges;
|
|
CroppedSelectionRanges(ranges);
|
|
if (aSelectionNum >= static_cast<int32_t>(ranges.Length())) {
|
|
return false;
|
|
}
|
|
TextRange& range = ranges[aSelectionNum];
|
|
Accessible* thisAcc = Acc();
|
|
if (range.StartContainer() == thisAcc) {
|
|
*aStartOffset = range.StartOffset();
|
|
} else {
|
|
bool ok;
|
|
// range.StartContainer() isn't a text leaf, so don't use its offset.
|
|
std::tie(ok, *aStartOffset) =
|
|
TransformOffset(range.StartContainer(), 0, /* aDescendToEnd */ false);
|
|
}
|
|
if (range.EndContainer() == thisAcc) {
|
|
*aEndOffset = range.EndOffset();
|
|
} else {
|
|
bool ok;
|
|
// range.EndContainer() isn't a text leaf, so don't use its offset. If
|
|
// range.EndOffset() is > 0, we want to include this container, so pas
|
|
// offset 1.
|
|
std::tie(ok, *aEndOffset) =
|
|
TransformOffset(range.EndContainer(), range.EndOffset() == 0 ? 0 : 1,
|
|
/* aDescendToEnd */ true);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool HyperTextAccessibleBase::SetSelectionBoundsAt(int32_t aSelectionNum,
|
|
int32_t aStartOffset,
|
|
int32_t aEndOffset) {
|
|
TextLeafRange range(ToTextLeafPoint(aStartOffset),
|
|
ToTextLeafPoint(aEndOffset, true));
|
|
if (!range) {
|
|
NS_ERROR("Wrong in offset");
|
|
return false;
|
|
}
|
|
|
|
return range.SetSelection(aSelectionNum);
|
|
}
|
|
|
|
void HyperTextAccessibleBase::ScrollSubstringTo(int32_t aStartOffset,
|
|
int32_t aEndOffset,
|
|
uint32_t aScrollType) {
|
|
TextLeafRange range(ToTextLeafPoint(aStartOffset),
|
|
ToTextLeafPoint(aEndOffset, true));
|
|
range.ScrollIntoView(aScrollType);
|
|
}
|
|
|
|
} // namespace mozilla::a11y
|