1374 lines
47 KiB
C++
1374 lines
47 KiB
C++
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
/* vim: set ts=2 et sw=2 tw=80: */
|
|
/* 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 "UiaTextRange.h"
|
|
|
|
#include "mozilla/a11y/HyperTextAccessibleBase.h"
|
|
#include "nsAccUtils.h"
|
|
#include "nsIAccessibleTypes.h"
|
|
#include "TextLeafRange.h"
|
|
#include <comdef.h>
|
|
#include <propvarutil.h>
|
|
#include <unordered_set>
|
|
|
|
namespace mozilla::a11y {
|
|
|
|
template <typename T>
|
|
HRESULT GetAttribute(TEXTATTRIBUTEID aAttributeId, T const& aRangeOrPoint,
|
|
VARIANT& aRetVal);
|
|
|
|
static int CompareVariants(const VARIANT& aFirst, const VARIANT& aSecond) {
|
|
// MinGW lacks support for VariantCompare, but does support converting to
|
|
// PROPVARIANT and PropVariantCompareEx. Use this as a workaround for MinGW
|
|
// builds, but avoid the extra work otherwise. See Bug 1944732.
|
|
#if defined(__MINGW32__) || defined(__MINGW64__) || defined(__MINGW__)
|
|
PROPVARIANT firstPropVar;
|
|
PROPVARIANT secondPropVar;
|
|
VariantToPropVariant(&aFirst, &firstPropVar);
|
|
VariantToPropVariant(&aSecond, &secondPropVar);
|
|
return PropVariantCompareEx(firstPropVar, secondPropVar, PVCU_DEFAULT,
|
|
PVCHF_DEFAULT);
|
|
#else
|
|
return VariantCompare(aFirst, aSecond);
|
|
#endif
|
|
}
|
|
|
|
// Used internally to safely get a UiaTextRange from a COM pointer provided
|
|
// to us by a client.
|
|
// {74B8E664-4578-4B52-9CBC-30A7A8271AE8}
|
|
static const GUID IID_UiaTextRange = {
|
|
0x74b8e664,
|
|
0x4578,
|
|
0x4b52,
|
|
{0x9c, 0xbc, 0x30, 0xa7, 0xa8, 0x27, 0x1a, 0xe8}};
|
|
|
|
// Helpers
|
|
|
|
static TextLeafPoint GetEndpoint(TextLeafRange& aRange,
|
|
enum TextPatternRangeEndpoint aEndpoint) {
|
|
if (aEndpoint == TextPatternRangeEndpoint_Start) {
|
|
return aRange.Start();
|
|
}
|
|
return aRange.End();
|
|
}
|
|
|
|
static void RemoveExcludedAccessiblesFromRange(TextLeafRange& aRange) {
|
|
MOZ_ASSERT(aRange);
|
|
TextLeafPoint start = aRange.Start();
|
|
TextLeafPoint end = aRange.End();
|
|
if (start == end) {
|
|
// The range is collapsed. It doesn't include anything.
|
|
return;
|
|
}
|
|
if (end.mOffset != 0) {
|
|
// It is theoretically possible for start to be at the exclusive end of a
|
|
// previous Accessible (i.e. mOffset is its length), so the range doesn't
|
|
// really encompass that Accessible's text and we should thus exclude that
|
|
// Accessible. However, that hasn't been seen in practice yet. If it does
|
|
// occur and cause problems, we should adjust the start point here.
|
|
return;
|
|
}
|
|
// end is at the start of its Accessible. This can happen because we always
|
|
// search for the start of a character, word, etc. Since the end of a range
|
|
// is exclusive, the range doesn't include anything in this Accessible.
|
|
// Move the end back so that it doesn't touch this Accessible at all. This
|
|
// is important when determining what Accessibles lie within this range
|
|
// because otherwise, we'd incorrectly consider an Accessible which the range
|
|
// doesn't actually cover.
|
|
// Move to the previous character.
|
|
end = end.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious);
|
|
// We want the position immediately after this character in the same
|
|
// Accessible.
|
|
++end.mOffset;
|
|
if (start <= end) {
|
|
aRange.SetEnd(end);
|
|
}
|
|
}
|
|
|
|
static bool IsUiaEmbeddedObject(const Accessible* aAcc) {
|
|
// "For UI Automation, an embedded object is any element that has non-textual
|
|
// boundaries such as an image, hyperlink, table, or document type"
|
|
// https://learn.microsoft.com/en-us/windows/win32/winauto/uiauto-textpattern-and-embedded-objects-overview
|
|
if (aAcc->IsText()) {
|
|
return false;
|
|
}
|
|
switch (aAcc->Role()) {
|
|
case roles::CONTENT_DELETION:
|
|
case roles::CONTENT_INSERTION:
|
|
case roles::EMPHASIS:
|
|
case roles::LANDMARK:
|
|
case roles::MARK:
|
|
case roles::NAVIGATION:
|
|
case roles::NOTE:
|
|
case roles::PARAGRAPH:
|
|
case roles::REGION:
|
|
case roles::SECTION:
|
|
case roles::STRONG:
|
|
case roles::SUBSCRIPT:
|
|
case roles::SUPERSCRIPT:
|
|
case roles::TEXT:
|
|
case roles::TEXT_CONTAINER:
|
|
return false;
|
|
default:
|
|
break;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static NotNull<Accessible*> GetSelectionContainer(TextLeafRange& aRange) {
|
|
Accessible* acc = aRange.Start().mAcc;
|
|
MOZ_ASSERT(acc);
|
|
if (acc->IsTextLeaf()) {
|
|
if (Accessible* parent = acc->Parent()) {
|
|
acc = parent;
|
|
}
|
|
}
|
|
if (acc->IsTextField()) {
|
|
// Gecko uses an independent selection for <input> and <textarea>.
|
|
return WrapNotNull(acc);
|
|
}
|
|
// For everything else (including contentEditable), Gecko uses the document
|
|
// selection.
|
|
return WrapNotNull(nsAccUtils::DocumentFor(acc));
|
|
}
|
|
|
|
static TextLeafPoint NormalizePoint(Accessible* aAcc, int32_t aOffset) {
|
|
if (!aAcc) {
|
|
return TextLeafPoint(aAcc, aOffset);
|
|
}
|
|
int32_t length = static_cast<int32_t>(nsAccUtils::TextLength(aAcc));
|
|
if (aOffset > length) {
|
|
// This range was created when this leaf contained more characters, but some
|
|
// characters were since removed. Restrict to the new length.
|
|
aOffset = length;
|
|
}
|
|
return TextLeafPoint(aAcc, aOffset);
|
|
}
|
|
|
|
// UiaTextRange
|
|
|
|
UiaTextRange::UiaTextRange(const TextLeafRange& aRange) {
|
|
MOZ_ASSERT(aRange);
|
|
SetRange(aRange);
|
|
}
|
|
|
|
void UiaTextRange::SetRange(const TextLeafRange& aRange) {
|
|
TextLeafPoint start = aRange.Start();
|
|
mStartAcc = MsaaAccessible::GetFrom(start.mAcc);
|
|
MOZ_ASSERT(mStartAcc);
|
|
mStartOffset = start.mOffset;
|
|
TextLeafPoint end = aRange.End();
|
|
mEndAcc = MsaaAccessible::GetFrom(end.mAcc);
|
|
MOZ_ASSERT(mEndAcc);
|
|
mEndOffset = end.mOffset;
|
|
// Special handling of the insertion point at the end of a line only makes
|
|
// sense when dealing with the caret, which is a collapsed range.
|
|
mIsEndOfLineInsertionPoint = start == end && start.mIsEndOfLineInsertionPoint;
|
|
}
|
|
|
|
TextLeafRange UiaTextRange::GetRange() const {
|
|
// Either Accessible might have been shut down because it was removed from the
|
|
// tree. In that case, Acc() will return null, resulting in an invalid
|
|
// TextLeafPoint and thus an invalid TextLeafRange. Any caller is expected to
|
|
// handle this case.
|
|
if (mIsEndOfLineInsertionPoint) {
|
|
MOZ_ASSERT(mStartAcc == mEndAcc && mStartOffset == mEndOffset);
|
|
TextLeafPoint point = NormalizePoint(mStartAcc->Acc(), mStartOffset);
|
|
point.mIsEndOfLineInsertionPoint = true;
|
|
return TextLeafRange(point, point);
|
|
}
|
|
return TextLeafRange(NormalizePoint(mStartAcc->Acc(), mStartOffset),
|
|
NormalizePoint(mEndAcc->Acc(), mEndOffset));
|
|
}
|
|
|
|
/* static */
|
|
TextLeafRange UiaTextRange::GetRangeFrom(ITextRangeProvider* aProvider) {
|
|
if (aProvider) {
|
|
RefPtr<UiaTextRange> uiaRange;
|
|
aProvider->QueryInterface(IID_UiaTextRange, getter_AddRefs(uiaRange));
|
|
if (uiaRange) {
|
|
return uiaRange->GetRange();
|
|
}
|
|
}
|
|
return TextLeafRange();
|
|
}
|
|
|
|
/* static */
|
|
TextLeafPoint UiaTextRange::FindBoundary(const TextLeafPoint& aOrigin,
|
|
enum TextUnit aUnit,
|
|
nsDirection aDirection,
|
|
bool aIncludeOrigin) {
|
|
if (aUnit == TextUnit_Page || aUnit == TextUnit_Document) {
|
|
// The UIA documentation is a little inconsistent regarding the Document
|
|
// unit:
|
|
// https://learn.microsoft.com/en-us/windows/win32/winauto/uiauto-textpattern-and-embedded-objects-overview
|
|
// First, it says:
|
|
// "Objects backed by the same text store as their container are referred to
|
|
// as "compatible" embedded objects. These objects can be TextPattern
|
|
// objects themselves and, in this case, their text ranges are comparable to
|
|
// text ranges obtained from their container. This enables the providers to
|
|
// expose client information about the individual TextPattern objects as if
|
|
// they were one, large text provider."
|
|
// But later, it says:
|
|
// "For embedded TextPattern objects, the Document unit only recognizes the
|
|
// content contained within that element."
|
|
// If ranges are equivalent regardless of what object they were created
|
|
// from, this doesn't make sense because this would mean that the Document
|
|
// unit would change depending on where the range was positioned at the
|
|
// time. Instead, Gecko restricts the range to an editable text control for
|
|
// ITextProvider::get_DocumentRange, but returns the full document for
|
|
// TextUnit_Document. This is consistent with Microsoft Word and Chromium.
|
|
Accessible* doc = nsAccUtils::DocumentFor(aOrigin.mAcc);
|
|
if (aDirection == eDirPrevious) {
|
|
return TextLeafPoint(doc, 0);
|
|
}
|
|
return TextLeafPoint(doc, nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT);
|
|
}
|
|
if (aUnit == TextUnit_Format) {
|
|
// The UIA documentation says that TextUnit_Format aims to define ranges
|
|
// that "include all text that shares all the same attributes."
|
|
// FindTextAttrsStart considers container boundaries to be format boundaries
|
|
// even if UIA may not. UIA's documentation may consider the next container
|
|
// to be part of the same format run, since it may have the same attributes.
|
|
// UIA considers embedded objects to be format boundaries, which is a more
|
|
// restrictive understanding of boundaries than what Gecko implements here.
|
|
return aOrigin.FindTextAttrsStart(aDirection, aIncludeOrigin);
|
|
}
|
|
AccessibleTextBoundary boundary;
|
|
switch (aUnit) {
|
|
case TextUnit_Character:
|
|
boundary = nsIAccessibleText::BOUNDARY_CLUSTER;
|
|
break;
|
|
case TextUnit_Word:
|
|
boundary = nsIAccessibleText::BOUNDARY_WORD_START;
|
|
break;
|
|
case TextUnit_Line:
|
|
boundary = nsIAccessibleText::BOUNDARY_LINE_START;
|
|
break;
|
|
case TextUnit_Paragraph:
|
|
boundary = nsIAccessibleText::BOUNDARY_PARAGRAPH;
|
|
break;
|
|
default:
|
|
return TextLeafPoint();
|
|
}
|
|
return aOrigin.FindBoundary(
|
|
boundary, aDirection,
|
|
aIncludeOrigin ? TextLeafPoint::BoundaryFlags::eIncludeOrigin
|
|
: TextLeafPoint::BoundaryFlags::eDefaultBoundaryFlags);
|
|
}
|
|
|
|
bool UiaTextRange::MovePoint(TextLeafPoint& aPoint, enum TextUnit aUnit,
|
|
const int aRequestedCount, int& aActualCount) {
|
|
aActualCount = 0;
|
|
const nsDirection direction = aRequestedCount < 0 ? eDirPrevious : eDirNext;
|
|
while (aActualCount != aRequestedCount) {
|
|
TextLeafPoint oldPoint = aPoint;
|
|
aPoint = FindBoundary(aPoint, aUnit, direction);
|
|
if (!aPoint) {
|
|
return false; // Unit not supported.
|
|
}
|
|
if (aPoint == oldPoint) {
|
|
break; // Can't go any further.
|
|
}
|
|
direction == eDirPrevious ? --aActualCount : ++aActualCount;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void UiaTextRange::SetEndpoint(enum TextPatternRangeEndpoint aEndpoint,
|
|
const TextLeafPoint& aDest) {
|
|
// Per the UIA documentation:
|
|
// https://learn.microsoft.com/en-us/windows/win32/api/uiautomationcore/nf-uiautomationcore-itextrangeprovider-moveendpointbyrange#remarks
|
|
// https://learn.microsoft.com/en-us/windows/win32/api/uiautomationcore/nf-uiautomationcore-itextrangeprovider-moveendpointbyunit#remarks
|
|
// "If the endpoint being moved crosses the other endpoint of the same text
|
|
// range, that other endpoint is moved also, resulting in a degenerate (empty)
|
|
// range and ensuring the correct ordering of the endpoints (that is, the
|
|
// start is always less than or equal to the end)."
|
|
TextLeafRange origRange = GetRange();
|
|
MOZ_ASSERT(origRange);
|
|
if (aEndpoint == TextPatternRangeEndpoint_Start) {
|
|
TextLeafPoint end = origRange.End();
|
|
if (end < aDest) {
|
|
end = aDest;
|
|
}
|
|
SetRange({aDest, end});
|
|
} else {
|
|
TextLeafPoint start = origRange.Start();
|
|
if (aDest < start) {
|
|
start = aDest;
|
|
}
|
|
SetRange({start, aDest});
|
|
}
|
|
}
|
|
|
|
// IUnknown
|
|
IMPL_IUNKNOWN2(UiaTextRange, ITextRangeProvider, UiaTextRange)
|
|
|
|
// ITextRangeProvider methods
|
|
|
|
STDMETHODIMP
|
|
UiaTextRange::Clone(__RPC__deref_out_opt ITextRangeProvider** aRetVal) {
|
|
if (!aRetVal) {
|
|
return E_INVALIDARG;
|
|
}
|
|
TextLeafRange range = GetRange();
|
|
if (!range) {
|
|
return CO_E_OBJNOTCONNECTED;
|
|
}
|
|
RefPtr uiaRange = new UiaTextRange(range);
|
|
uiaRange.forget(aRetVal);
|
|
return S_OK;
|
|
}
|
|
|
|
STDMETHODIMP
|
|
UiaTextRange::Compare(__RPC__in_opt ITextRangeProvider* aRange,
|
|
__RPC__out BOOL* aRetVal) {
|
|
if (!aRange || !aRetVal) {
|
|
return E_INVALIDARG;
|
|
}
|
|
*aRetVal = GetRange() == GetRangeFrom(aRange);
|
|
return S_OK;
|
|
}
|
|
|
|
STDMETHODIMP
|
|
UiaTextRange::CompareEndpoints(enum TextPatternRangeEndpoint aEndpoint,
|
|
__RPC__in_opt ITextRangeProvider* aTargetRange,
|
|
enum TextPatternRangeEndpoint aTargetEndpoint,
|
|
__RPC__out int* aRetVal) {
|
|
if (!aTargetRange || !aRetVal) {
|
|
return E_INVALIDARG;
|
|
}
|
|
TextLeafRange origRange = GetRange();
|
|
if (!origRange) {
|
|
return CO_E_OBJNOTCONNECTED;
|
|
}
|
|
TextLeafPoint origPoint = GetEndpoint(origRange, aEndpoint);
|
|
TextLeafRange targetRange = GetRangeFrom(aTargetRange);
|
|
if (!targetRange) {
|
|
return E_INVALIDARG;
|
|
}
|
|
TextLeafPoint targetPoint = GetEndpoint(targetRange, aTargetEndpoint);
|
|
if (origPoint == targetPoint) {
|
|
*aRetVal = 0;
|
|
} else if (origPoint < targetPoint) {
|
|
*aRetVal = -1;
|
|
} else {
|
|
*aRetVal = 1;
|
|
}
|
|
return S_OK;
|
|
}
|
|
|
|
STDMETHODIMP
|
|
UiaTextRange::ExpandToEnclosingUnit(enum TextUnit aUnit) {
|
|
TextLeafRange range = GetRange();
|
|
if (!range) {
|
|
return CO_E_OBJNOTCONNECTED;
|
|
}
|
|
TextLeafPoint origin = range.Start();
|
|
TextLeafPoint start = FindBoundary(origin, aUnit, eDirPrevious,
|
|
/* aIncludeOrigin */ true);
|
|
if (!start) {
|
|
return E_FAIL; // Unit not supported.
|
|
}
|
|
TextLeafPoint end = FindBoundary(origin, aUnit, eDirNext);
|
|
SetRange({start, end});
|
|
return S_OK;
|
|
}
|
|
|
|
// Search within the text range for the first subrange that has the given
|
|
// attribute value. The resulting range might span multiple text attribute runs.
|
|
// If aBackward, start the search from the end of the range.
|
|
STDMETHODIMP
|
|
UiaTextRange::FindAttribute(TEXTATTRIBUTEID aAttributeId, VARIANT aVal,
|
|
BOOL aBackward,
|
|
__RPC__deref_out_opt ITextRangeProvider** aRetVal) {
|
|
if (!aRetVal) {
|
|
return E_INVALIDARG;
|
|
}
|
|
*aRetVal = nullptr;
|
|
TextLeafRange range = GetRange();
|
|
if (!range) {
|
|
return CO_E_OBJNOTCONNECTED;
|
|
}
|
|
MOZ_ASSERT(range.Start() <= range.End(), "Range must be valid to proceed.");
|
|
|
|
VARIANT value{};
|
|
|
|
if (!aBackward) {
|
|
Maybe<TextLeafPoint> matchingRangeStart{};
|
|
// Begin with a range starting at the start of our original range and ending
|
|
// at the next attribute run start point.
|
|
TextLeafPoint startPoint = range.Start();
|
|
TextLeafPoint endPoint = startPoint;
|
|
endPoint = endPoint.FindTextAttrsStart(eDirNext);
|
|
do {
|
|
// Get the attribute value at the start point. Since we're moving through
|
|
// text attribute runs, we don't need to check the entire range; this
|
|
// point's attributes are those of the entire range.
|
|
GetAttribute(aAttributeId, startPoint, value);
|
|
// CompareVariants is not valid if types are different. Verify the type
|
|
// first so the result is well-defined.
|
|
if (aVal.vt == value.vt && CompareVariants(aVal, value) == 0) {
|
|
if (!matchingRangeStart) {
|
|
matchingRangeStart = Some(startPoint);
|
|
}
|
|
} else if (matchingRangeStart) {
|
|
// We fell out of a matching range. We're moving forward, so the
|
|
// matching range is [matchingRangeStart, startPoint).
|
|
RefPtr uiaRange = new UiaTextRange(
|
|
TextLeafRange{matchingRangeStart.value(), startPoint});
|
|
uiaRange.forget(aRetVal);
|
|
return S_OK;
|
|
}
|
|
startPoint = endPoint;
|
|
// Advance only if startPoint != endPoint to avoid infinite loops if
|
|
// FindTextAttrsStart returns the TextLeafPoint unchanged. This covers
|
|
// cases like hitting the end of the document.
|
|
} while ((endPoint = endPoint.FindTextAttrsStart(eDirNext)) &&
|
|
endPoint <= range.End() && startPoint != endPoint);
|
|
if (matchingRangeStart) {
|
|
// We found a start point and reached the end of the range. The result is
|
|
// [matchingRangeStart, stopPoint].
|
|
RefPtr uiaRange = new UiaTextRange(
|
|
TextLeafRange{matchingRangeStart.value(), range.End()});
|
|
uiaRange.forget(aRetVal);
|
|
return S_OK;
|
|
}
|
|
} else {
|
|
Maybe<TextLeafPoint> matchingRangeEnd{};
|
|
TextLeafPoint endPoint = range.End();
|
|
TextLeafPoint startPoint = endPoint;
|
|
startPoint = startPoint.FindTextAttrsStart(eDirPrevious);
|
|
do {
|
|
GetAttribute(aAttributeId, startPoint, value);
|
|
if (aVal.vt == value.vt && CompareVariants(aVal, value) == 0) {
|
|
if (!matchingRangeEnd) {
|
|
matchingRangeEnd = Some(endPoint);
|
|
}
|
|
} else if (matchingRangeEnd) {
|
|
// We fell out of a matching range. We're moving backward, so the
|
|
// matching range is [endPoint, matchingRangeEnd).
|
|
RefPtr uiaRange =
|
|
new UiaTextRange(TextLeafRange{endPoint, matchingRangeEnd.value()});
|
|
uiaRange.forget(aRetVal);
|
|
return S_OK;
|
|
}
|
|
endPoint = startPoint;
|
|
// Advance only if startPoint != endPoint to avoid infinite loops if
|
|
// FindTextAttrsStart returns the TextLeafPoint unchanged. This covers
|
|
// cases like hitting the start of the document.
|
|
} while ((startPoint = startPoint.FindTextAttrsStart(eDirPrevious)) &&
|
|
range.Start() <= startPoint && startPoint != endPoint);
|
|
if (matchingRangeEnd) {
|
|
// We found an end point and reached the start of the range. The result is
|
|
// [range.Start(), matchingRangeEnd).
|
|
RefPtr uiaRange = new UiaTextRange(
|
|
TextLeafRange{range.Start(), matchingRangeEnd.value()});
|
|
uiaRange.forget(aRetVal);
|
|
return S_OK;
|
|
}
|
|
}
|
|
return S_OK;
|
|
}
|
|
|
|
STDMETHODIMP
|
|
UiaTextRange::FindText(__RPC__in BSTR aText, BOOL aBackward, BOOL aIgnoreCase,
|
|
__RPC__deref_out_opt ITextRangeProvider** aRetVal) {
|
|
if (!aRetVal) {
|
|
return E_INVALIDARG;
|
|
}
|
|
*aRetVal = nullptr;
|
|
TextLeafRange range = GetRange();
|
|
if (!range) {
|
|
return CO_E_OBJNOTCONNECTED;
|
|
}
|
|
MOZ_ASSERT(range.Start() <= range.End(), "Range must be valid to proceed.");
|
|
|
|
// We can't find anything in an empty range.
|
|
if (range.Start() == range.End()) {
|
|
return S_OK;
|
|
}
|
|
|
|
// Iterate over the range's leaf segments and append each leaf's text. Keep
|
|
// track of the indices in the built string, associating them with the
|
|
// Accessible pointer whose text begins at that index.
|
|
nsTArray<std::pair<int32_t, Accessible*>> indexToAcc;
|
|
nsAutoString rangeText;
|
|
for (const TextLeafRange leafSegment : range) {
|
|
Accessible* startAcc = leafSegment.Start().mAcc;
|
|
MOZ_ASSERT(startAcc, "Start acc of leaf segment was unexpectedly null.");
|
|
indexToAcc.EmplaceBack(rangeText.Length(), startAcc);
|
|
startAcc->AppendTextTo(rangeText);
|
|
}
|
|
|
|
// Find the search string's start position in the text of the range, ignoring
|
|
// case if requested.
|
|
const nsDependentString searchStr{aText};
|
|
const int32_t startIndex = [&]() {
|
|
if (aIgnoreCase) {
|
|
ToLowerCase(rangeText);
|
|
nsAutoString searchStrLower;
|
|
ToLowerCase(searchStr, searchStrLower);
|
|
return aBackward ? rangeText.RFind(searchStrLower)
|
|
: rangeText.Find(searchStrLower);
|
|
} else {
|
|
return aBackward ? rangeText.RFind(searchStr) : rangeText.Find(searchStr);
|
|
}
|
|
}();
|
|
if (startIndex == kNotFound) {
|
|
return S_OK;
|
|
}
|
|
const int32_t endIndex = startIndex + searchStr.Length();
|
|
|
|
// Binary search for the (index, Accessible*) pair where the index is as large
|
|
// as possible without exceeding the size of the search index. The associated
|
|
// Accessible* is the Accessible for the resulting TextLeafPoint.
|
|
auto GetNearestAccLessThanIndex = [&indexToAcc](int32_t aIndex) {
|
|
MOZ_ASSERT(aIndex >= 0, "Search index is less than 0.");
|
|
auto itr =
|
|
std::lower_bound(indexToAcc.begin(), indexToAcc.end(), aIndex,
|
|
[](const std::pair<int32_t, Accessible*>& aPair,
|
|
int32_t aIndex) { return aPair.first <= aIndex; });
|
|
MOZ_ASSERT(itr != indexToAcc.begin(),
|
|
"Iterator is unexpectedly at the beginning.");
|
|
--itr;
|
|
return itr;
|
|
};
|
|
|
|
// Calculate the TextLeafPoint for the start and end of the found text.
|
|
auto itr = GetNearestAccLessThanIndex(startIndex);
|
|
Accessible* foundTextStart = itr->second;
|
|
const int32_t offsetFromStart = startIndex - itr->first;
|
|
const TextLeafPoint rangeStart{foundTextStart, offsetFromStart};
|
|
|
|
itr = GetNearestAccLessThanIndex(endIndex);
|
|
Accessible* foundTextEnd = itr->second;
|
|
const int32_t offsetFromEndAccStart = endIndex - itr->first;
|
|
const TextLeafPoint rangeEnd{foundTextEnd, offsetFromEndAccStart};
|
|
|
|
TextLeafRange resultRange{rangeStart, rangeEnd};
|
|
RefPtr uiaRange = new UiaTextRange(resultRange);
|
|
uiaRange.forget(aRetVal);
|
|
return S_OK;
|
|
}
|
|
|
|
template <TEXTATTRIBUTEID Attr>
|
|
struct AttributeTraits {
|
|
/*
|
|
* To define a trait of this type, define the following members on a
|
|
* specialization of this template struct:
|
|
* // Define the (Gecko) representation of the attribute type.
|
|
* using AttrType = <the type associated with the TEXTATTRIBUTEID>;
|
|
*
|
|
* // Returns the attribute value at the TextLeafPoint, or Nothing{} if none
|
|
* // can be calculated.
|
|
* static Maybe<AttrType> GetValue(TextLeafPoint aPoint);
|
|
*
|
|
* // Return the default value specified by the UIA documentation.
|
|
* static AttrType DefaultValue();
|
|
*
|
|
* // Write the given value to the VARIANT output parameter. This may
|
|
* // require a non-trivial transformation from Gecko's idea of the value
|
|
* // into VARIANT form.
|
|
* static HRESULT WriteToVariant(VARIANT& aVariant, const AttrType& aValue);
|
|
*/
|
|
};
|
|
|
|
template <TEXTATTRIBUTEID Attr>
|
|
HRESULT GetAttribute(const TextLeafRange& aRange, VARIANT& aVariant) {
|
|
// Select the traits of the given TEXTATTRIBUTEID. This helps us choose the
|
|
// correct functions to call to handle each attribute.
|
|
using Traits = AttributeTraits<Attr>;
|
|
using AttrType = typename Traits::AttrType;
|
|
|
|
// Get the value at the start point. All other runs in the range must match
|
|
// this value, otherwise the result is "mixed."
|
|
const TextLeafPoint end = aRange.End();
|
|
TextLeafPoint current = aRange.Start();
|
|
Maybe<AttrType> val = Traits::GetValue(current);
|
|
if (!val) {
|
|
// Fall back to the UIA-specified default when we don't have an answer.
|
|
val = Some(Traits::DefaultValue());
|
|
}
|
|
|
|
// Walk through the range one text attribute run start at a time, poking the
|
|
// start points to check for the requested attribute. Stop before we hit the
|
|
// end since the end point is either:
|
|
// 1. At the start of the one-past-last text attribute run and hence
|
|
// excluded from the range
|
|
// 2. After the start of the last text attribute run in the range and hence
|
|
// tested by that last run's start point
|
|
while ((current = current.FindTextAttrsStart(eDirNext)) && current < end) {
|
|
Maybe<AttrType> currentVal = Traits::GetValue(current);
|
|
if (!currentVal) {
|
|
// Fall back to the UIA-specified default when we don't have an answer.
|
|
currentVal = Some(Traits::DefaultValue());
|
|
}
|
|
if (*currentVal != *val) {
|
|
// If the attribute ever changes, then we need to return "[t]he address
|
|
// of the value retrieved by the UiaGetReservedMixedAttributeValue
|
|
// function."
|
|
aVariant.vt = VT_UNKNOWN;
|
|
return UiaGetReservedMixedAttributeValue(&aVariant.punkVal);
|
|
}
|
|
}
|
|
|
|
// Write the value to the VARIANT output parameter.
|
|
return Traits::WriteToVariant(aVariant, *val);
|
|
}
|
|
|
|
template <TEXTATTRIBUTEID Attr>
|
|
HRESULT GetAttribute(TextLeafPoint const& aPoint, VARIANT& aVariant) {
|
|
// Select the traits of the given TEXTATTRIBUTEID. This helps us choose the
|
|
// correct functions to call to handle each attribute.
|
|
using Traits = AttributeTraits<Attr>;
|
|
using AttrType = typename Traits::AttrType;
|
|
|
|
// Get the value at the given point.
|
|
Maybe<AttrType> val = Traits::GetValue(aPoint);
|
|
if (!val) {
|
|
// Fall back to the UIA-specified default when we don't have an answer.
|
|
val = Some(Traits::DefaultValue());
|
|
}
|
|
// Write the value to the VARIANT output parameter.
|
|
return Traits::WriteToVariant(aVariant, *val);
|
|
}
|
|
|
|
// Dispatch to the proper GetAttribute template specialization for the given
|
|
// TEXTATTRIBUTEID. T may be a TextLeafPoint or TextLeafRange; this function
|
|
// will call the appropriate specialization and overload.
|
|
template <typename T>
|
|
HRESULT GetAttribute(TEXTATTRIBUTEID aAttributeId, T const& aRangeOrPoint,
|
|
VARIANT& aRetVal) {
|
|
switch (aAttributeId) {
|
|
case UIA_AnnotationTypesAttributeId:
|
|
return GetAttribute<UIA_AnnotationTypesAttributeId>(aRangeOrPoint,
|
|
aRetVal);
|
|
case UIA_FontNameAttributeId:
|
|
return GetAttribute<UIA_FontNameAttributeId>(aRangeOrPoint, aRetVal);
|
|
case UIA_FontSizeAttributeId:
|
|
return GetAttribute<UIA_FontSizeAttributeId>(aRangeOrPoint, aRetVal);
|
|
case UIA_FontWeightAttributeId:
|
|
return GetAttribute<UIA_FontWeightAttributeId>(aRangeOrPoint, aRetVal);
|
|
case UIA_IsHiddenAttributeId:
|
|
return GetAttribute<UIA_IsHiddenAttributeId>(aRangeOrPoint, aRetVal);
|
|
case UIA_IsItalicAttributeId:
|
|
return GetAttribute<UIA_IsItalicAttributeId>(aRangeOrPoint, aRetVal);
|
|
case UIA_IsReadOnlyAttributeId:
|
|
return GetAttribute<UIA_IsReadOnlyAttributeId>(aRangeOrPoint, aRetVal);
|
|
case UIA_StyleIdAttributeId:
|
|
return GetAttribute<UIA_StyleIdAttributeId>(aRangeOrPoint, aRetVal);
|
|
case UIA_IsSubscriptAttributeId:
|
|
return GetAttribute<UIA_IsSubscriptAttributeId>(aRangeOrPoint, aRetVal);
|
|
case UIA_IsSuperscriptAttributeId:
|
|
return GetAttribute<UIA_IsSuperscriptAttributeId>(aRangeOrPoint, aRetVal);
|
|
default:
|
|
// If the attribute isn't supported, return "[t]he address of the value
|
|
// retrieved by the UiaGetReservedNotSupportedValue function."
|
|
aRetVal.vt = VT_UNKNOWN;
|
|
return UiaGetReservedNotSupportedValue(&aRetVal.punkVal);
|
|
break;
|
|
}
|
|
MOZ_ASSERT_UNREACHABLE("Unhandled UIA Attribute ID");
|
|
return S_OK;
|
|
}
|
|
|
|
STDMETHODIMP
|
|
UiaTextRange::GetAttributeValue(TEXTATTRIBUTEID aAttributeId,
|
|
__RPC__out VARIANT* aRetVal) {
|
|
if (!aRetVal) {
|
|
return E_INVALIDARG;
|
|
}
|
|
VariantInit(aRetVal);
|
|
TextLeafRange range = GetRange();
|
|
if (!range) {
|
|
return CO_E_OBJNOTCONNECTED;
|
|
}
|
|
MOZ_ASSERT(range.Start() <= range.End(), "Range must be valid to proceed.");
|
|
return GetAttribute(aAttributeId, range, *aRetVal);
|
|
}
|
|
|
|
STDMETHODIMP
|
|
UiaTextRange::GetBoundingRectangles(__RPC__deref_out_opt SAFEARRAY** aRetVal) {
|
|
if (!aRetVal) {
|
|
return E_INVALIDARG;
|
|
}
|
|
*aRetVal = nullptr;
|
|
TextLeafRange range = GetRange();
|
|
if (!range) {
|
|
return CO_E_OBJNOTCONNECTED;
|
|
}
|
|
|
|
// Get the rectangles for each line.
|
|
nsTArray<LayoutDeviceIntRect> lineRects = range.LineRects();
|
|
if (lineRects.IsEmpty() && !mIsEndOfLineInsertionPoint &&
|
|
range.Start() == range.End()) {
|
|
// The documentation for GetBoundingRectangles says that we should return
|
|
// "An empty array for a degenerate range.":
|
|
// https://learn.microsoft.com/en-us/windows/win32/api/uiautomationcore/nf-uiautomationcore-itextrangeprovider-getboundingrectangles#return-value
|
|
// This is exactly what range.LineRects() just did. However, contrary to
|
|
// this, some clients (including Microsoft Text Cursor Indicator) call
|
|
// GetBoundingRectangles on a degenerate range when querying the caret and
|
|
// expect rectangles to be returned. Therefore, use the character bounds.
|
|
// Bug 1966812: Ideally, we would also return a rectangle when
|
|
// mIsEndOfLineInsertionPoint is true. However, we don't currently have code
|
|
// to calculate a rectangle in that case.
|
|
lineRects.AppendElement(range.Start().CharBounds());
|
|
}
|
|
|
|
// For UIA's purposes, the rectangles of this array are four doubles arranged
|
|
// in order {left, top, width, height}.
|
|
SAFEARRAY* rectsVec = SafeArrayCreateVector(VT_R8, 0, lineRects.Length() * 4);
|
|
if (!rectsVec) {
|
|
return E_OUTOFMEMORY;
|
|
}
|
|
|
|
// Empty range, return an empty array.
|
|
if (lineRects.IsEmpty()) {
|
|
*aRetVal = rectsVec;
|
|
return S_OK;
|
|
}
|
|
|
|
// Get the double array out of the SAFEARRAY so we can write to it directly.
|
|
double* safeArrayData = nullptr;
|
|
HRESULT hr =
|
|
SafeArrayAccessData(rectsVec, reinterpret_cast<void**>(&safeArrayData));
|
|
if (FAILED(hr) || !safeArrayData) {
|
|
SafeArrayDestroy(rectsVec);
|
|
return E_FAIL;
|
|
}
|
|
|
|
// Convert the int array to a double array.
|
|
for (size_t index = 0; index < lineRects.Length(); ++index) {
|
|
const LayoutDeviceIntRect& lineRect = lineRects[index];
|
|
safeArrayData[index * 4 + 0] = static_cast<double>(lineRect.x);
|
|
safeArrayData[index * 4 + 1] = static_cast<double>(lineRect.y);
|
|
safeArrayData[index * 4 + 2] = static_cast<double>(lineRect.width);
|
|
safeArrayData[index * 4 + 3] = static_cast<double>(lineRect.height);
|
|
}
|
|
|
|
// Release the lock on the data. If that fails, bail out.
|
|
hr = SafeArrayUnaccessData(rectsVec);
|
|
if (FAILED(hr)) {
|
|
SafeArrayDestroy(rectsVec);
|
|
return E_FAIL;
|
|
}
|
|
|
|
*aRetVal = rectsVec;
|
|
return S_OK;
|
|
}
|
|
|
|
STDMETHODIMP
|
|
UiaTextRange::GetEnclosingElement(
|
|
__RPC__deref_out_opt IRawElementProviderSimple** aRetVal) {
|
|
if (!aRetVal) {
|
|
return E_INVALIDARG;
|
|
}
|
|
*aRetVal = nullptr;
|
|
TextLeafRange range = GetRange();
|
|
if (!range) {
|
|
return CO_E_OBJNOTCONNECTED;
|
|
}
|
|
RemoveExcludedAccessiblesFromRange(range);
|
|
Accessible* enclosing =
|
|
range.Start().mAcc->GetClosestCommonInclusiveAncestor(range.End().mAcc);
|
|
if (!enclosing) {
|
|
return S_OK;
|
|
}
|
|
for (Accessible* acc = enclosing; acc && !acc->IsDoc(); acc = acc->Parent()) {
|
|
if (nsAccUtils::MustPrune(acc) ||
|
|
// Bug 1950535: Narrator won't report a link correctly when navigating
|
|
// by character or word if we return a child text leaf. However, if
|
|
// there is more than a single text leaf, we need to return the child
|
|
// because it might have semantic significance; e.g. an embedded image.
|
|
(acc->Role() == roles::LINK && acc->ChildCount() == 1 &&
|
|
acc->FirstChild()->IsText())) {
|
|
enclosing = acc;
|
|
break;
|
|
}
|
|
}
|
|
RefPtr<IRawElementProviderSimple> uia = MsaaAccessible::GetFrom(enclosing);
|
|
uia.forget(aRetVal);
|
|
return S_OK;
|
|
}
|
|
|
|
STDMETHODIMP
|
|
UiaTextRange::GetText(int aMaxLength, __RPC__deref_out_opt BSTR* aRetVal) {
|
|
if (!aRetVal || aMaxLength < -1) {
|
|
return E_INVALIDARG;
|
|
}
|
|
TextLeafRange range = GetRange();
|
|
if (!range) {
|
|
return CO_E_OBJNOTCONNECTED;
|
|
}
|
|
nsAutoString text;
|
|
for (TextLeafRange segment : range) {
|
|
TextLeafPoint start = segment.Start();
|
|
int segmentLength = segment.End().mOffset - start.mOffset;
|
|
// aMaxLength can be -1 to indicate no maximum.
|
|
if (aMaxLength >= 0) {
|
|
const int remaining = aMaxLength - text.Length();
|
|
if (segmentLength > remaining) {
|
|
segmentLength = remaining;
|
|
}
|
|
}
|
|
start.mAcc->AppendTextTo(text, start.mOffset, segmentLength);
|
|
if (aMaxLength >= 0 && static_cast<int32_t>(text.Length()) >= aMaxLength) {
|
|
break;
|
|
}
|
|
}
|
|
*aRetVal = ::SysAllocString(text.get());
|
|
return S_OK;
|
|
}
|
|
|
|
STDMETHODIMP
|
|
UiaTextRange::Move(enum TextUnit aUnit, int aCount, __RPC__out int* aRetVal) {
|
|
if (!aRetVal) {
|
|
return E_INVALIDARG;
|
|
}
|
|
TextLeafRange range = GetRange();
|
|
if (!range) {
|
|
return CO_E_OBJNOTCONNECTED;
|
|
}
|
|
TextLeafPoint start = range.Start();
|
|
const bool wasCollapsed = start == range.End();
|
|
if (!wasCollapsed) {
|
|
// Per the UIA documentation:
|
|
// https://learn.microsoft.com/en-us/windows/win32/api/uiautomationcore/nf-uiautomationcore-itextrangeprovider-move#remarks
|
|
// "For a non-degenerate (non-empty) text range, ITextRangeProvider::Move
|
|
// should normalize and move the text range by performing the following
|
|
// steps. ...
|
|
// 2. If necessary, move the resulting text range backward in the document
|
|
// to the beginning of the requested unit boundary."
|
|
start = FindBoundary(start, aUnit, eDirPrevious, /* aIncludeOrigin */ true);
|
|
}
|
|
if (!MovePoint(start, aUnit, aCount, *aRetVal)) {
|
|
return E_FAIL;
|
|
}
|
|
if (wasCollapsed) {
|
|
// "For a degenerate text range, ITextRangeProvider::Move should simply move
|
|
// the text insertion point by the specified number of text units."
|
|
SetRange({start, start});
|
|
} else {
|
|
// "4. Expand the text range from the degenerate state by moving the ending
|
|
// endpoint forward by one requested text unit boundary."
|
|
TextLeafPoint end = FindBoundary(start, aUnit, eDirNext);
|
|
if (end == start) {
|
|
// start was already at the last boundary. Move start back to the previous
|
|
// boundary.
|
|
start = FindBoundary(start, aUnit, eDirPrevious);
|
|
// In doing that, we ended up moving 1 less unit.
|
|
--*aRetVal;
|
|
}
|
|
SetRange({start, end});
|
|
}
|
|
return S_OK;
|
|
}
|
|
|
|
STDMETHODIMP
|
|
UiaTextRange::MoveEndpointByUnit(enum TextPatternRangeEndpoint aEndpoint,
|
|
enum TextUnit aUnit, int aCount,
|
|
__RPC__out int* aRetVal) {
|
|
if (!aRetVal) {
|
|
return E_INVALIDARG;
|
|
}
|
|
TextLeafRange range = GetRange();
|
|
if (!range) {
|
|
return CO_E_OBJNOTCONNECTED;
|
|
}
|
|
TextLeafPoint point = GetEndpoint(range, aEndpoint);
|
|
if (!MovePoint(point, aUnit, aCount, *aRetVal)) {
|
|
return E_FAIL;
|
|
}
|
|
SetEndpoint(aEndpoint, point);
|
|
return S_OK;
|
|
}
|
|
|
|
STDMETHODIMP
|
|
UiaTextRange::MoveEndpointByRange(
|
|
enum TextPatternRangeEndpoint aEndpoint,
|
|
__RPC__in_opt ITextRangeProvider* aTargetRange,
|
|
enum TextPatternRangeEndpoint aTargetEndpoint) {
|
|
if (!aTargetRange) {
|
|
return E_INVALIDARG;
|
|
}
|
|
TextLeafRange origRange = GetRange();
|
|
if (!origRange) {
|
|
return CO_E_OBJNOTCONNECTED;
|
|
}
|
|
TextLeafRange targetRange = GetRangeFrom(aTargetRange);
|
|
if (!targetRange) {
|
|
return E_INVALIDARG;
|
|
}
|
|
TextLeafPoint dest = GetEndpoint(targetRange, aTargetEndpoint);
|
|
SetEndpoint(aEndpoint, dest);
|
|
return S_OK;
|
|
}
|
|
|
|
// XXX Use MOZ_CAN_RUN_SCRIPT_BOUNDARY for now due to bug 1543294.
|
|
MOZ_CAN_RUN_SCRIPT_BOUNDARY STDMETHODIMP UiaTextRange::Select() {
|
|
TextLeafRange range = GetRange();
|
|
if (!range) {
|
|
return CO_E_OBJNOTCONNECTED;
|
|
}
|
|
if (!range.SetSelection(TextLeafRange::kRemoveAllExistingSelectedRanges)) {
|
|
return UIA_E_INVALIDOPERATION;
|
|
}
|
|
return S_OK;
|
|
}
|
|
|
|
// XXX Use MOZ_CAN_RUN_SCRIPT_BOUNDARY for now due to bug 1543294.
|
|
MOZ_CAN_RUN_SCRIPT_BOUNDARY STDMETHODIMP UiaTextRange::AddToSelection() {
|
|
TextLeafRange range = GetRange();
|
|
if (!range) {
|
|
return CO_E_OBJNOTCONNECTED;
|
|
}
|
|
if (!range.SetSelection(-1)) {
|
|
return UIA_E_INVALIDOPERATION;
|
|
}
|
|
return S_OK;
|
|
}
|
|
|
|
// XXX Use MOZ_CAN_RUN_SCRIPT_BOUNDARY for now due to bug 1543294.
|
|
MOZ_CAN_RUN_SCRIPT_BOUNDARY STDMETHODIMP UiaTextRange::RemoveFromSelection() {
|
|
TextLeafRange range = GetRange();
|
|
if (!range) {
|
|
return CO_E_OBJNOTCONNECTED;
|
|
}
|
|
NotNull<Accessible*> container = GetSelectionContainer(range);
|
|
nsTArray<TextLeafRange> ranges;
|
|
TextLeafRange::GetSelection(container, ranges);
|
|
auto index = ranges.IndexOf(range);
|
|
if (index != ranges.NoIndex) {
|
|
HyperTextAccessibleBase* conHyp = container->AsHyperTextBase();
|
|
MOZ_ASSERT(conHyp);
|
|
conHyp->RemoveFromSelection(index);
|
|
return S_OK;
|
|
}
|
|
// This range isn't in the collection of selected ranges.
|
|
return UIA_E_INVALIDOPERATION;
|
|
}
|
|
|
|
// XXX Use MOZ_CAN_RUN_SCRIPT_BOUNDARY for now due to bug 1543294.
|
|
MOZ_CAN_RUN_SCRIPT_BOUNDARY STDMETHODIMP
|
|
UiaTextRange::ScrollIntoView(BOOL aAlignToTop) {
|
|
TextLeafRange range = GetRange();
|
|
if (!range) {
|
|
return CO_E_OBJNOTCONNECTED;
|
|
}
|
|
range.ScrollIntoView(aAlignToTop
|
|
? nsIAccessibleScrollType::SCROLL_TYPE_TOP_LEFT
|
|
: nsIAccessibleScrollType::SCROLL_TYPE_BOTTOM_RIGHT);
|
|
return S_OK;
|
|
}
|
|
|
|
STDMETHODIMP
|
|
UiaTextRange::GetChildren(__RPC__deref_out_opt SAFEARRAY** aRetVal) {
|
|
if (!aRetVal) {
|
|
return E_INVALIDARG;
|
|
}
|
|
*aRetVal = nullptr;
|
|
TextLeafRange range = GetRange();
|
|
if (!range) {
|
|
return CO_E_OBJNOTCONNECTED;
|
|
}
|
|
RemoveExcludedAccessiblesFromRange(range);
|
|
Accessible* startAcc = range.Start().mAcc;
|
|
Accessible* endAcc = range.End().mAcc;
|
|
Accessible* common = startAcc->GetClosestCommonInclusiveAncestor(endAcc);
|
|
if (!common) {
|
|
return S_OK;
|
|
}
|
|
// Get all the direct children of `common` from `startAcc` through `endAcc`.
|
|
// Find the index of the direct child containing startAcc.
|
|
int32_t startIndex = -1;
|
|
if (startAcc == common) {
|
|
startIndex = 0;
|
|
} else {
|
|
Accessible* child = startAcc;
|
|
for (;;) {
|
|
Accessible* parent = child->Parent();
|
|
if (parent == common) {
|
|
startIndex = child->IndexInParent();
|
|
break;
|
|
}
|
|
child = parent;
|
|
}
|
|
MOZ_ASSERT(startIndex >= 0);
|
|
}
|
|
// Find the index of the direct child containing endAcc.
|
|
int32_t endIndex = -1;
|
|
if (endAcc == common) {
|
|
endIndex = static_cast<int32_t>(common->ChildCount()) - 1;
|
|
} else {
|
|
Accessible* child = endAcc;
|
|
for (;;) {
|
|
Accessible* parent = child->Parent();
|
|
if (parent == common) {
|
|
endIndex = child->IndexInParent();
|
|
break;
|
|
}
|
|
child = parent;
|
|
}
|
|
MOZ_ASSERT(endIndex >= 0);
|
|
}
|
|
// Now get the children between startIndex and endIndex.
|
|
// We guess 30 children because:
|
|
// 1. It's unlikely that a client would call GetChildren on a very large range
|
|
// because GetChildren is normally only called when reporting content and
|
|
// reporting the entire content of a massive range in one hit isn't ideal for
|
|
// performance.
|
|
// 2. A client is more likely to query the content of a line, paragraph, etc.
|
|
// 3. It seems unlikely that there would be more than 30 children in a line or
|
|
// paragraph, especially because we're only including children that are
|
|
// considered embedded objects by UIA.
|
|
AutoTArray<Accessible*, 30> children;
|
|
for (int32_t i = startIndex; i <= endIndex; ++i) {
|
|
Accessible* child = common->ChildAt(static_cast<uint32_t>(i));
|
|
if (IsUiaEmbeddedObject(child)) {
|
|
children.AppendElement(child);
|
|
}
|
|
}
|
|
*aRetVal = AccessibleArrayToUiaArray(children);
|
|
return S_OK;
|
|
}
|
|
|
|
/*
|
|
* AttributeTraits template specializations
|
|
*/
|
|
|
|
template <>
|
|
struct AttributeTraits<UIA_AnnotationTypesAttributeId> {
|
|
// Avoiding nsTHashSet here because it has no operator==.
|
|
using AttrType = std::unordered_set<int32_t>;
|
|
static Maybe<AttrType> GetValue(TextLeafPoint aPoint) {
|
|
// Check all of the given annotations. Build a set of the annotations that
|
|
// are present at the given TextLeafPoint.
|
|
RefPtr<AccAttributes> attrs = aPoint.GetTextAttributes();
|
|
if (!attrs) {
|
|
return {};
|
|
}
|
|
AttrType annotationsAtPoint{};
|
|
|
|
// The "invalid" atom as a key in text attributes could have value
|
|
// "spelling", "grammar", or "true". Spelling and grammar map directly to
|
|
// UIA. A non-specific "invalid" indicates a generic data validation error,
|
|
// and is mapped as such.
|
|
if (auto invalid =
|
|
attrs->GetAttribute<RefPtr<nsAtom>>(nsGkAtoms::invalid)) {
|
|
const nsAtom* invalidAtom = invalid->get();
|
|
if (invalidAtom == nsGkAtoms::spelling) {
|
|
annotationsAtPoint.insert(AnnotationType_SpellingError);
|
|
} else if (invalidAtom == nsGkAtoms::grammar) {
|
|
annotationsAtPoint.insert(AnnotationType_GrammarError);
|
|
} else if (invalidAtom == nsGkAtoms::_true) {
|
|
annotationsAtPoint.insert(AnnotationType_DataValidationError);
|
|
}
|
|
}
|
|
|
|
// The presence of the "mark" atom as a key in text attributes indicates a
|
|
// highlight at this point.
|
|
if (attrs->GetAttribute<bool>(nsGkAtoms::mark)) {
|
|
annotationsAtPoint.insert(AnnotationType_Highlighted);
|
|
}
|
|
|
|
return Some(annotationsAtPoint);
|
|
}
|
|
|
|
static AttrType DefaultValue() {
|
|
// Per UIA documentation, the default is an empty collection.
|
|
return {};
|
|
}
|
|
|
|
static HRESULT WriteToVariant(VARIANT& aVariant, const AttrType& aValue) {
|
|
SAFEARRAY* outputArr =
|
|
SafeArrayCreateVector(VT_I4, 0, static_cast<ULONG>(aValue.size()));
|
|
if (!outputArr) {
|
|
return E_OUTOFMEMORY;
|
|
}
|
|
|
|
// Copy the elements from the unordered_set to the SAFEARRAY.
|
|
LONG index = 0;
|
|
for (auto value : aValue) {
|
|
const HRESULT hr = SafeArrayPutElement(outputArr, &index, &value);
|
|
if (FAILED(hr)) {
|
|
SafeArrayDestroy(outputArr);
|
|
return hr;
|
|
}
|
|
++index;
|
|
}
|
|
|
|
aVariant.vt = VT_ARRAY | VT_I4;
|
|
aVariant.parray = outputArr;
|
|
return S_OK;
|
|
}
|
|
};
|
|
|
|
template <>
|
|
struct AttributeTraits<UIA_FontWeightAttributeId> {
|
|
using AttrType = int32_t; // LONG, but AccAttributes only accepts int32_t
|
|
static Maybe<AttrType> GetValue(TextLeafPoint aPoint) {
|
|
RefPtr<AccAttributes> attrs = aPoint.GetTextAttributes();
|
|
if (!attrs) {
|
|
return {};
|
|
}
|
|
return attrs->GetAttribute<AttrType>(nsGkAtoms::fontWeight);
|
|
}
|
|
|
|
static AttrType DefaultValue() {
|
|
// See GDI LOGFONT structure and related standards.
|
|
return FW_DONTCARE;
|
|
}
|
|
|
|
static HRESULT WriteToVariant(VARIANT& aVariant, const AttrType& aValue) {
|
|
aVariant.vt = VT_I4;
|
|
aVariant.lVal = aValue;
|
|
return S_OK;
|
|
}
|
|
};
|
|
|
|
template <>
|
|
struct AttributeTraits<UIA_FontSizeAttributeId> {
|
|
using AttrType = FontSize;
|
|
static Maybe<AttrType> GetValue(TextLeafPoint aPoint) {
|
|
RefPtr<AccAttributes> attrs = aPoint.GetTextAttributes();
|
|
if (!attrs) {
|
|
return {};
|
|
}
|
|
return attrs->GetAttribute<AttrType>(nsGkAtoms::font_size);
|
|
}
|
|
|
|
static AttrType DefaultValue() { return FontSize{0}; }
|
|
|
|
static HRESULT WriteToVariant(VARIANT& aVariant, const AttrType& aValue) {
|
|
aVariant.vt = VT_I4;
|
|
aVariant.lVal = aValue.mValue;
|
|
return S_OK;
|
|
}
|
|
};
|
|
|
|
template <>
|
|
struct AttributeTraits<UIA_FontNameAttributeId> {
|
|
using AttrType = RefPtr<nsAtom>;
|
|
static Maybe<AttrType> GetValue(TextLeafPoint aPoint) {
|
|
RefPtr<AccAttributes> attrs = aPoint.GetTextAttributes();
|
|
if (!attrs) {
|
|
return {};
|
|
}
|
|
return attrs->GetAttribute<AttrType>(nsGkAtoms::font_family);
|
|
}
|
|
|
|
static AttrType DefaultValue() {
|
|
// Default to the empty string (not null).
|
|
return RefPtr<nsAtom>(nsGkAtoms::_empty);
|
|
}
|
|
|
|
static HRESULT WriteToVariant(VARIANT& aVariant, const AttrType& aValue) {
|
|
if (!aValue) {
|
|
return E_INVALIDARG;
|
|
}
|
|
BSTR valueBStr = ::SysAllocString(aValue->GetUTF16String());
|
|
if (!valueBStr) {
|
|
return E_OUTOFMEMORY;
|
|
}
|
|
aVariant.vt = VT_BSTR;
|
|
aVariant.bstrVal = valueBStr;
|
|
return S_OK;
|
|
}
|
|
};
|
|
|
|
template <>
|
|
struct AttributeTraits<UIA_IsItalicAttributeId> {
|
|
using AttrType = bool;
|
|
static Maybe<AttrType> GetValue(TextLeafPoint aPoint) {
|
|
RefPtr<AccAttributes> attrs = aPoint.GetTextAttributes();
|
|
if (!attrs) {
|
|
return {};
|
|
}
|
|
|
|
// If the value in the attributes is a RefPtr<nsAtom>, it may be "italic" or
|
|
// "normal"; check whether it is "italic".
|
|
auto atomResult =
|
|
attrs->GetAttribute<RefPtr<nsAtom>>(nsGkAtoms::font_style);
|
|
if (atomResult) {
|
|
MOZ_ASSERT(*atomResult, "Atom must be non-null");
|
|
return Some((*atomResult)->Equals(u"italic"_ns));
|
|
}
|
|
// If the FontSlantStyle is not italic, the value is not stored as an nsAtom
|
|
// in AccAttributes, so there's no need to check further.
|
|
return {};
|
|
}
|
|
|
|
static AttrType DefaultValue() { return false; }
|
|
|
|
static HRESULT WriteToVariant(VARIANT& aVariant, const AttrType& aValue) {
|
|
aVariant = _variant_t(aValue);
|
|
return S_OK;
|
|
}
|
|
};
|
|
|
|
template <>
|
|
struct AttributeTraits<UIA_StyleIdAttributeId> {
|
|
using AttrType = int32_t;
|
|
static Maybe<AttrType> GetValue(TextLeafPoint aPoint) {
|
|
Accessible* acc = aPoint.mAcc;
|
|
if (!acc || !acc->Parent()) {
|
|
return {};
|
|
}
|
|
acc = acc->Parent();
|
|
const role role = acc->Role();
|
|
if (role == roles::HEADING) {
|
|
switch (acc->GetLevel(true)) {
|
|
case 1:
|
|
return Some(StyleId_Heading1);
|
|
case 2:
|
|
return Some(StyleId_Heading2);
|
|
case 3:
|
|
return Some(StyleId_Heading3);
|
|
case 4:
|
|
return Some(StyleId_Heading4);
|
|
case 5:
|
|
return Some(StyleId_Heading5);
|
|
case 6:
|
|
return Some(StyleId_Heading6);
|
|
default:
|
|
return {};
|
|
}
|
|
}
|
|
if (role == roles::BLOCKQUOTE) {
|
|
return Some(StyleId_Quote);
|
|
}
|
|
if (role == roles::EMPHASIS) {
|
|
return Some(StyleId_Emphasis);
|
|
}
|
|
return {};
|
|
}
|
|
|
|
static AttrType DefaultValue() { return 0; }
|
|
|
|
static HRESULT WriteToVariant(VARIANT& aVariant, const AttrType& aValue) {
|
|
aVariant.vt = VT_I4;
|
|
aVariant.lVal = aValue;
|
|
return S_OK;
|
|
}
|
|
};
|
|
|
|
template <>
|
|
struct AttributeTraits<UIA_IsSubscriptAttributeId> {
|
|
using AttrType = bool;
|
|
static Maybe<AttrType> GetValue(TextLeafPoint aPoint) {
|
|
RefPtr<AccAttributes> attrs = aPoint.GetTextAttributes();
|
|
if (!attrs) {
|
|
return {};
|
|
}
|
|
auto atomResult =
|
|
attrs->GetAttribute<RefPtr<nsAtom>>(nsGkAtoms::textPosition);
|
|
if (atomResult) {
|
|
MOZ_ASSERT(*atomResult, "Atom must be non-null");
|
|
return Some(*atomResult == nsGkAtoms::sub);
|
|
}
|
|
return {};
|
|
}
|
|
|
|
static AttrType DefaultValue() { return false; }
|
|
|
|
static HRESULT WriteToVariant(VARIANT& aVariant, const AttrType& aValue) {
|
|
aVariant = _variant_t(aValue);
|
|
return S_OK;
|
|
}
|
|
};
|
|
|
|
template <>
|
|
struct AttributeTraits<UIA_IsSuperscriptAttributeId> {
|
|
using AttrType = bool;
|
|
static Maybe<AttrType> GetValue(TextLeafPoint aPoint) {
|
|
RefPtr<AccAttributes> attrs = aPoint.GetTextAttributes();
|
|
if (!attrs) {
|
|
return {};
|
|
}
|
|
auto atomResult =
|
|
attrs->GetAttribute<RefPtr<nsAtom>>(nsGkAtoms::textPosition);
|
|
if (atomResult) {
|
|
MOZ_ASSERT(*atomResult, "Atom must be non-null");
|
|
return Some((*atomResult)->Equals(NS_ConvertASCIItoUTF16("super")));
|
|
}
|
|
return {};
|
|
}
|
|
|
|
static AttrType DefaultValue() { return false; }
|
|
|
|
static HRESULT WriteToVariant(VARIANT& aVariant, const AttrType& aValue) {
|
|
aVariant = _variant_t(aValue);
|
|
return S_OK;
|
|
}
|
|
};
|
|
|
|
template <>
|
|
struct AttributeTraits<UIA_IsHiddenAttributeId> {
|
|
using AttrType = bool;
|
|
static Maybe<AttrType> GetValue(TextLeafPoint aPoint) {
|
|
if (!aPoint.mAcc) {
|
|
return {};
|
|
}
|
|
const uint64_t state = aPoint.mAcc->State();
|
|
return Some(!!(state & states::INVISIBLE));
|
|
}
|
|
|
|
static AttrType DefaultValue() { return false; }
|
|
|
|
static HRESULT WriteToVariant(VARIANT& aVariant, const AttrType& aValue) {
|
|
aVariant = _variant_t(aValue);
|
|
return S_OK;
|
|
}
|
|
};
|
|
|
|
template <>
|
|
struct AttributeTraits<UIA_IsReadOnlyAttributeId> {
|
|
using AttrType = bool;
|
|
static Maybe<AttrType> GetValue(TextLeafPoint aPoint) {
|
|
if (!aPoint.mAcc) {
|
|
return {};
|
|
}
|
|
Accessible* acc = aPoint.mAcc;
|
|
// If the TextLeafPoint we're dealing with is itself a hypertext, don't
|
|
// bother checking its parent since this is the Accessible we care about.
|
|
if (!acc->IsHyperText()) {
|
|
// Check the parent of the leaf, since the leaf itself will never be
|
|
// editable, but the parent may. Check for both text fields and
|
|
// hypertexts, since we might have something like <input> or a
|
|
// contenteditable <span>.
|
|
Accessible* parent = acc->Parent();
|
|
if (parent && parent->IsHyperText()) {
|
|
acc = parent;
|
|
} else {
|
|
return Some(true);
|
|
}
|
|
}
|
|
const uint64_t state = acc->State();
|
|
if (state & states::READONLY) {
|
|
return Some(true);
|
|
}
|
|
if (state & states::EDITABLE) {
|
|
return Some(false);
|
|
}
|
|
// Fall back to true if not editable or explicitly marked READONLY.
|
|
return Some(true);
|
|
}
|
|
|
|
static AttrType DefaultValue() {
|
|
// UIA says the default is false, but we fall back to true in GetValue since
|
|
// most things on the web are read-only.
|
|
return false;
|
|
}
|
|
|
|
static HRESULT WriteToVariant(VARIANT& aVariant, const AttrType& aValue) {
|
|
aVariant = _variant_t(aValue);
|
|
return S_OK;
|
|
}
|
|
};
|
|
|
|
} // namespace mozilla::a11y
|