Bug 1901467: Implement ITextRangeProvider::FindAttribute, r=Jamie

This revision implements UIA function ITextRangeProvider::FindAttribute,
which allows clients to search within a UiaTextRange for a range where a
given attribute matches a given value. This revision adds the
functionality and tests for that functionality. On the way, this
revision adds GetAttribute functionality for TextLeafPoint (it already
existed for TextLeafRange). This is useful for checking single points
without wasting work constructing or searching a range.

Differential Revision: https://phabricator.services.mozilla.com/D235395
This commit is contained in:
Nathan LaPre
2025-01-29 18:38:34 +00:00
parent a49bb76e1e
commit fe1dc63dbf
3 changed files with 251 additions and 35 deletions

View File

@@ -2106,3 +2106,106 @@ addUiaTask(
// The IA2 -> UIA proxy doesn't support RemoveFromSelection correctly.
{ uiaEnabled: true, uiaDisabled: false }
);
/**
* Test the TextRange pattern's FindAttribute method.
*/
addUiaTask(
`
<div id="font-weight-container">a <span tabindex="0"><b>bcd</b></span><b> ef</b> ghi</div>
`,
async function testTextRangeFindAttribute(_browser, _docAcc) {
info("Constructing range on bold text run");
await runPython(`
global doc, docText, range, fontWeightContainerAcc
doc = getDocUia()
docText = getUiaPattern(doc, "Text")
fontWeightContainerAcc = findUiaByDomId(doc, "font-weight-container")
range = docText.RangeFromChild(fontWeightContainerAcc)
`);
is(
await runPython(`range.GetText(-1)`),
"a bcd ef ghi",
"range text correct"
);
info("Finding first font-weight 400 text range");
await runPython(`
global subrange
subrange = range.FindAttribute(UIA_FontWeightAttributeId, 400, False)
`);
is(await runPython(`subrange.GetText(-1)`), "a ", "range text correct");
info("Finding first font-weight 700 text range");
await runPython(`
global subrange
subrange = range.FindAttribute(UIA_FontWeightAttributeId, 700, False)
`);
is(await runPython(`subrange.GetText(-1)`), "bcd ef", "range text correct");
info("Finding last font-weight 700 text range");
await runPython(`
global subrange
subrange = range.FindAttribute(UIA_FontWeightAttributeId, 700, True)
`);
is(await runPython(`subrange.GetText(-1)`), "bcd ef", "range text correct");
info("Finding last font-weight 400 text range");
await runPython(`
global subrange
subrange = range.FindAttribute(UIA_FontWeightAttributeId, 400, True)
`);
is(await runPython(`subrange.GetText(-1)`), " ghi", "range text correct");
// The IA2 -> UIA proxy gets things below this wrong.
if (!gIsUiaEnabled) {
return;
}
info("Moving range to the middle of a text attribute run");
is(
await runPython(
`range.MoveEndpointByUnit(TextPatternRangeEndpoint_Start, TextUnit_Character, 4)`
),
4,
"MoveEndpointByUnit return correct"
);
is(await runPython(`range.GetText(-1)`), "cd ef ghi", "range text correct");
info(
"Finding first font-weight 700 text range (range starts in middle of text attribute run)"
);
await runPython(`
global subrange
subrange = range.FindAttribute(UIA_FontWeightAttributeId, 700, False)
`);
is(await runPython(`subrange.GetText(-1)`), "cd ef", "range text correct");
await runPython(`
global range
range = docText.RangeFromChild(fontWeightContainerAcc)
`);
is(
await runPython(`range.GetText(-1)`),
"a bcd ef ghi",
"range text correct"
);
is(
await runPython(
`range.MoveEndpointByUnit(TextPatternRangeEndpoint_End, TextUnit_Character, -5)`
),
-5,
"MoveEndpointByUnit return correct"
);
is(await runPython(`range.GetText(-1)`), "a bcd e", "range text correct");
info(
"Finding last font-weight 700 text range (range ends in middle of text attribute run)"
);
await runPython(`
global subrange
subrange = range.FindAttribute(UIA_FontWeightAttributeId, 700, True)
`);
is(await runPython(`subrange.GetText(-1)`), "bcd e", "range text correct");
},
{ uiaEnabled: true, uiaDisabled: true }
);

View File

@@ -11,6 +11,7 @@
#include "nsIAccessibleTypes.h"
#include "TextLeafRange.h"
#include <comdef.h>
#include <propvarutil.h>
#include <unordered_set>
// Handle MinGW builds - see bug 1929755 for more info
@@ -20,6 +21,10 @@
namespace mozilla::a11y {
template <typename T>
HRESULT GetAttribute(TEXTATTRIBUTEID aAttributeId, T const& aRangeOrPoint,
VARIANT& aRetVal);
// Used internally to safely get a UiaTextRange from a COM pointer provided
// to us by a client.
// {74B8E664-4578-4B52-9CBC-30A7A8271AE8}
@@ -121,7 +126,7 @@ static NotNull<Accessible*> GetSelectionContainer(TextLeafRange& aRange) {
// UiaTextRange
UiaTextRange::UiaTextRange(TextLeafRange& aRange) {
UiaTextRange::UiaTextRange(const TextLeafRange& aRange) {
MOZ_ASSERT(aRange);
SetRange(aRange);
}
@@ -349,11 +354,94 @@ UiaTextRange::ExpandToEnclosingUnit(enum TextUnit aUnit) {
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) {
return E_NOTIMPL;
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);
// VariantCompare is not valid if types are different. Verify the type
// first so the result is well-defined.
if (aVal.vt == value.vt && VariantCompare(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;
} 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 && VariantCompare(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;
} while ((startPoint = startPoint.FindTextAttrsStart(eDirPrevious)) &&
range.Start() <= startPoint);
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
@@ -427,6 +515,62 @@ HRESULT GetAttribute(const TextLeafRange& aRange, VARIANT& aVariant) {
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) {
@@ -438,39 +582,8 @@ UiaTextRange::GetAttributeValue(TEXTATTRIBUTEID aAttributeId,
if (!range) {
return CO_E_OBJNOTCONNECTED;
}
MOZ_ASSERT(range.Start() <= range.End(), "Range must be valid to proceed.");
switch (aAttributeId) {
case UIA_AnnotationTypesAttributeId:
return GetAttribute<UIA_AnnotationTypesAttributeId>(range, *aRetVal);
case UIA_FontNameAttributeId:
return GetAttribute<UIA_FontNameAttributeId>(range, *aRetVal);
case UIA_FontSizeAttributeId:
return GetAttribute<UIA_FontSizeAttributeId>(range, *aRetVal);
case UIA_FontWeightAttributeId:
return GetAttribute<UIA_FontWeightAttributeId>(range, *aRetVal);
case UIA_IsHiddenAttributeId:
return GetAttribute<UIA_IsHiddenAttributeId>(range, *aRetVal);
case UIA_IsItalicAttributeId:
return GetAttribute<UIA_IsItalicAttributeId>(range, *aRetVal);
case UIA_IsReadOnlyAttributeId:
return GetAttribute<UIA_IsReadOnlyAttributeId>(range, *aRetVal);
case UIA_StyleIdAttributeId:
return GetAttribute<UIA_StyleIdAttributeId>(range, *aRetVal);
case UIA_IsSubscriptAttributeId:
return GetAttribute<UIA_IsSubscriptAttributeId>(range, *aRetVal);
case UIA_IsSuperscriptAttributeId:
return GetAttribute<UIA_IsSuperscriptAttributeId>(range, *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);
}
MOZ_ASSERT_UNREACHABLE("Unhandled UIA Attribute ID");
return S_OK;
return GetAttribute(aAttributeId, range, *aRetVal);
}
STDMETHODIMP

View File

@@ -24,7 +24,7 @@ class TextLeafPoint;
*/
class UiaTextRange : public ITextRangeProvider {
public:
explicit UiaTextRange(TextLeafRange& aRange);
explicit UiaTextRange(const TextLeafRange& aRange);
virtual ~UiaTextRange() = default;
// IUnknown