From ede1495f62ad56826f1d8af34222d5f5c3723044 Mon Sep 17 00:00:00 2001 From: James Teh Date: Tue, 9 Jul 2024 02:52:16 +0000 Subject: [PATCH] Bug 1901459 part 3: Implement ITextRangeProvider::GetChildren. r=nlapre Differential Revision: https://phabricator.services.mozilla.com/D215759 --- .../windows/uia/browser_textPatterns.js | 49 ++++++++++ accessible/windows/uia/UiaTextRange.cpp | 92 +++++++++++++++++++ accessible/windows/uia/uiaRawElmProvider.cpp | 10 +- 3 files changed, 145 insertions(+), 6 deletions(-) diff --git a/accessible/tests/browser/windows/uia/browser_textPatterns.js b/accessible/tests/browser/windows/uia/browser_textPatterns.js index 04ce9f3904e9..40196cfcdc15 100644 --- a/accessible/tests/browser/windows/uia/browser_textPatterns.js +++ b/accessible/tests/browser/windows/uia/browser_textPatterns.js @@ -970,3 +970,52 @@ addUiaTask( } } ); + +/** + * Test the TextRange pattern's GetChildren method. + */ +addUiaTask( + `
ab cd ef g
`, + async function testTextRangeGetChildren() { + info("Getting editable DocumentRange"); + await runPython(` + doc = getDocUia() + editable = findUiaByDomId(doc, "editable") + text = getUiaPattern(editable, "Text") + global r + r = text.DocumentRange + `); + await isUiaElementArray( + `r.GetChildren()`, + ["cdef", "g"], + "Children are correct" + ); + info("Expanding to word"); + await runPython(`r.ExpandToEnclosingUnit(TextUnit_Word)`); + // Range is now "ab ". + await isUiaElementArray(`r.GetChildren()`, [], "Children are correct"); + info("Moving 1 word"); + await runPython(`r.Move(TextUnit_Word, 1)`); + // Range is now "cd ". + await isUiaElementArray(`r.GetChildren()`, [], "Children are correct"); + info("Moving 1 word"); + await runPython(`r.Move(TextUnit_Word, 1)`); + // Range is now "ef ". The range includes the link but is not completely + // enclosed by the link. + await isUiaElementArray(`r.GetChildren()`, ["ef"], "Children are correct"); + info("Moving end -1 character"); + await runPython( + `r.MoveEndpointByUnit(TextPatternRangeEndpoint_End, TextUnit_Character, -1)` + ); + // Range is now "ef". The range encloses the link, so there are no children. + await isUiaElementArray(`r.GetChildren()`, [], "Children are correct"); + info("Moving 1 word"); + await runPython(`r.Move(TextUnit_Word, 1)`); + // Range is now the embedded object character for the img (g). The range is + // completely enclosed by the image. + // The IA2 -> UIA proxy gets this wrong. + if (gIsUiaEnabled) { + await isUiaElementArray(`r.GetChildren()`, [], "Children are correct"); + } + } +); diff --git a/accessible/windows/uia/UiaTextRange.cpp b/accessible/windows/uia/UiaTextRange.cpp index 9eb2fa1b4323..f5c081fad7c6 100644 --- a/accessible/windows/uia/UiaTextRange.cpp +++ b/accessible/windows/uia/UiaTextRange.cpp @@ -58,6 +58,36 @@ static void RemoveExcludedAccessiblesFromRange(TextLeafRange& aRange) { } } +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; +} + // UiaTextRange UiaTextRange::UiaTextRange(TextLeafRange& aRange) { @@ -454,6 +484,68 @@ UiaTextRange::GetChildren(__RPC__deref_out_opt SAFEARRAY** 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(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 children; + for (int32_t i = startIndex; i <= endIndex; ++i) { + Accessible* child = common->ChildAt(static_cast(i)); + if (IsUiaEmbeddedObject(child)) { + children.AppendElement(child); + } + } + *aRetVal = AccessibleArrayToUiaArray(children); return S_OK; } diff --git a/accessible/windows/uia/uiaRawElmProvider.cpp b/accessible/windows/uia/uiaRawElmProvider.cpp index aae8d84eff62..a3ee83dc6886 100644 --- a/accessible/windows/uia/uiaRawElmProvider.cpp +++ b/accessible/windows/uia/uiaRawElmProvider.cpp @@ -1458,12 +1458,10 @@ long uiaRawElmProvider::GetLiveSetting() const { } SAFEARRAY* a11y::AccessibleArrayToUiaArray(const nsTArray& aAccs) { - if (aAccs.IsEmpty()) { - // The UIA documentation is unclear about this, but the UIA client - // framework seems to treat a null value the same as an empty array. This - // is also what Chromium does. - return nullptr; - } + // The UIA client framework seems to treat a null value the same as an empty + // array most of the time, but not always. In particular, Narrator breaks if + // ITextRangeProvider::GetChildren returns null instead of an empty array. + // Therefore, don't return null for an empty array. SAFEARRAY* uias = SafeArrayCreateVector(VT_UNKNOWN, 0, aAccs.Length()); LONG indices[1] = {0}; for (Accessible* acc : aAccs) {