// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif
#include "ui/accessibility/platform/ax_platform_node_textrangeprovider_win.h"
#include <utility>
#include <vector>
#include "base/debug/crash_logging.h"
#include "base/debug/dump_without_crashing.h"
#include "base/i18n/string_search.h"
#include "base/memory/raw_ptr.h"
#include "base/win/scoped_safearray.h"
#include "base/win/scoped_variant.h"
#include "base/win/variant_vector.h"
#include "ui/accessibility/ax_action_data.h"
#include "ui/accessibility/ax_selection.h"
#include "ui/accessibility/platform/ax_platform_node_delegate.h"
#include "ui/accessibility/platform/ax_platform_tree_manager.h"
#define UIA_VALIDATE_TEXTRANGEPROVIDER_CALL() \
if (!GetOwner() || !GetOwner()->GetDelegate() || !start() || \
!start()->GetAnchor() || !end() || !end()->GetAnchor()) \
return UIA_E_ELEMENTNOTAVAILABLE; \
SetStart(start()->AsValidPosition()); \
SetEnd(end()->AsValidPosition());
#define UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_IN(in) \
if (!GetOwner() || !GetOwner()->GetDelegate() || !start() || \
!start()->GetAnchor() || !end() || !end()->GetAnchor()) \
return UIA_E_ELEMENTNOTAVAILABLE; \
if (!in) \
return E_POINTER; \
SetStart(start()->AsValidPosition()); \
SetEnd(end()->AsValidPosition());
#define UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_OUT(out) \
if (!GetOwner() || !GetOwner()->GetDelegate() || !start() || \
!start()->GetAnchor() || !end() || !end()->GetAnchor()) \
return UIA_E_ELEMENTNOTAVAILABLE; \
if (!out) \
return E_POINTER; \
*out = {}; \
SetStart(start()->AsValidPosition()); \
SetEnd(end()->AsValidPosition());
#define UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_IN_1_OUT(in, out) \
if (!GetOwner() || !GetOwner()->GetDelegate() || !start() || \
!start()->GetAnchor() || !end() || !end()->GetAnchor()) \
return UIA_E_ELEMENTNOTAVAILABLE; \
if (!in || !out) \
return E_POINTER; \
*out = {}; \
SetStart(start()->AsValidPosition()); \
SetEnd(end()->AsValidPosition());
// Validate bounds calculated by AXPlatformNodeDelegate. Degenerate bounds
// indicate the interface is not yet supported on the platform.
#define UIA_VALIDATE_BOUNDS(bounds) \
if (bounds.OffsetFromOrigin().IsZero() && bounds.IsEmpty()) \
return UIA_E_NOTSUPPORTED;
namespace ui {
class AXRangePhysicalPixelRectDelegate : public AXRangeRectDelegate {
public:
explicit AXRangePhysicalPixelRectDelegate(
AXPlatformNodeTextRangeProviderWin* host)
: host_(host) {}
gfx::Rect GetInnerTextRangeBoundsRect(
AXTreeID tree_id,
AXNodeID node_id,
int start_offset,
int end_offset,
AXClippingBehavior clipping_behavior,
AXOffscreenResult* offscreen_result) override {
AXPlatformNodeDelegate* delegate = host_->GetDelegate(tree_id, node_id);
DCHECK(delegate);
return delegate->GetInnerTextRangeBoundsRect(
start_offset, end_offset, AXCoordinateSystem::kScreenPhysicalPixels,
clipping_behavior, offscreen_result);
}
gfx::Rect GetBoundsRect(AXTreeID tree_id,
AXNodeID node_id,
AXOffscreenResult* offscreen_result) override {
AXPlatformNodeDelegate* delegate = host_->GetDelegate(tree_id, node_id);
DCHECK(delegate);
return delegate->GetBoundsRect(AXCoordinateSystem::kScreenPhysicalPixels,
AXClippingBehavior::kClipped,
offscreen_result);
}
private:
raw_ptr<AXPlatformNodeTextRangeProviderWin> host_;
};
AXPlatformNodeTextRangeProviderWin::AXPlatformNodeTextRangeProviderWin() {
DVLOG(1) << __func__;
}
AXPlatformNodeTextRangeProviderWin::~AXPlatformNodeTextRangeProviderWin() {}
void AXPlatformNodeTextRangeProviderWin::CreateTextRangeProvider(
AXPositionInstance start,
AXPositionInstance end,
ITextRangeProvider** text_range_provider) {
DCHECK(text_range_provider);
DCHECK_EQ(*text_range_provider, nullptr);
*text_range_provider = nullptr;
CComObject<AXPlatformNodeTextRangeProviderWin>* text_range_provider_win =
nullptr;
if (SUCCEEDED(CComObject<AXPlatformNodeTextRangeProviderWin>::CreateInstance(
&text_range_provider_win))) {
DCHECK(text_range_provider_win);
text_range_provider_win->SetStart(std::move(start));
text_range_provider_win->SetEnd(std::move(end));
text_range_provider_win->AddRef();
*text_range_provider = text_range_provider_win;
}
}
void AXPlatformNodeTextRangeProviderWin::CreateTextRangeProviderForTesting(
AXPlatformNodeWin* owner,
AXPositionInstance start,
AXPositionInstance end,
ITextRangeProvider** text_range_provider) {
CreateTextRangeProvider(start->Clone(), end->Clone(), text_range_provider);
Microsoft::WRL::ComPtr<AXPlatformNodeTextRangeProviderWin>
text_range_provider_win;
if (SUCCEEDED((*text_range_provider)
->QueryInterface(IID_PPV_ARGS(&text_range_provider_win)))) {
text_range_provider_win->SetOwnerForTesting(owner); // IN-TEST
}
}
//
// ITextRangeProvider methods.
//
HRESULT AXPlatformNodeTextRangeProviderWin::Clone(ITextRangeProvider** clone) {
WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXTRANGE_CLONE);
UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_OUT(clone);
CreateTextRangeProvider(start()->Clone(), end()->Clone(), clone);
return S_OK;
}
HRESULT AXPlatformNodeTextRangeProviderWin::Compare(ITextRangeProvider* other,
BOOL* result) {
ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior(
AXEmbeddedObjectBehavior::kUIAExposeCharacterForTextContent);
WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXTRANGE_COMPARE);
WIN_ACCESSIBILITY_API_PERF_HISTOGRAM(UMA_API_TEXTRANGE_COMPARE);
UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_IN_1_OUT(other, result);
Microsoft::WRL::ComPtr<AXPlatformNodeTextRangeProviderWin> other_provider;
if (other->QueryInterface(IID_PPV_ARGS(&other_provider)) != S_OK)
return UIA_E_INVALIDOPERATION;
other_provider->SnapStartAndEndToMaxTextOffsetIfBeyond();
if (*start() == *(other_provider->start()) &&
*end() == *(other_provider->end())) {
*result = TRUE;
}
return S_OK;
}
HRESULT AXPlatformNodeTextRangeProviderWin::CompareEndpoints(
TextPatternRangeEndpoint this_endpoint,
ITextRangeProvider* other,
TextPatternRangeEndpoint other_endpoint,
int* result) {
WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXTRANGE_COMPAREENDPOINTS);
WIN_ACCESSIBILITY_API_PERF_HISTOGRAM(UMA_API_TEXTRANGE_COMPAREENDPOINTS);
UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_IN_1_OUT(other, result);
Microsoft::WRL::ComPtr<AXPlatformNodeTextRangeProviderWin> other_provider;
if (other->QueryInterface(IID_PPV_ARGS(&other_provider)) != S_OK)
return UIA_E_INVALIDOPERATION;
other_provider->SnapStartAndEndToMaxTextOffsetIfBeyond();
const AXPositionInstance& this_provider_endpoint =
(this_endpoint == TextPatternRangeEndpoint_Start) ? start() : end();
const AXPositionInstance& other_provider_endpoint =
(other_endpoint == TextPatternRangeEndpoint_Start)
? other_provider->start()
: other_provider->end();
std::optional<int> comparison =
this_provider_endpoint->CompareTo(*other_provider_endpoint);
if (!comparison)
return UIA_E_INVALIDOPERATION;
if (comparison.value() < 0)
*result = -1;
else if (comparison.value() > 0)
*result = 1;
else
*result = 0;
return S_OK;
}
HRESULT AXPlatformNodeTextRangeProviderWin::ExpandToEnclosingUnit(
TextUnit unit) {
WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXTRANGE_EXPANDTOENCLOSINGUNIT);
WIN_ACCESSIBILITY_API_PERF_HISTOGRAM(UMA_API_TEXTRANGE_EXPANDTOENCLOSINGUNIT);
return ExpandToEnclosingUnitImpl(unit);
}
HRESULT AXPlatformNodeTextRangeProviderWin::ExpandToEnclosingUnitImpl(
TextUnit unit) {
UIA_VALIDATE_TEXTRANGEPROVIDER_CALL();
{
AXPositionInstance normalized_start = start()->Clone();
AXPositionInstance normalized_end = end()->Clone();
NormalizeTextRange(normalized_start, normalized_end);
SetStart(std::move(normalized_start));
SetEnd(std::move(normalized_end));
}
SnapStartAndEndToMaxTextOffsetIfBeyond();
// Determine if start is on a boundary of the specified TextUnit, if it is
// not, move backwards until it is. Move the end forwards from start until it
// is on the next TextUnit boundary, if one exists.
switch (unit) {
case TextUnit_Character: {
// For characters, the start endpoint will always be on a TextUnit
// boundary, thus we only need to move the end position.
AXPositionInstance end_backup = end()->Clone();
SetEnd(start()->CreateNextCharacterPosition(
{AXBoundaryBehavior::kCrossBoundary,
AXBoundaryDetection::kDontCheckInitialPosition}));
if (end()->IsNullPosition()) {
// The previous could fail if the start is at the end of the last anchor
// of the tree, try expanding to the previous character instead.
AXPositionInstance start_backup = start()->Clone();
SetStart(start()->CreatePreviousCharacterPosition(
{AXBoundaryBehavior::kCrossBoundary,
AXBoundaryDetection::kDontCheckInitialPosition}));
if (start()->IsNullPosition()) {
// Text representation is empty, undo everything and exit.
SetStart(std::move(start_backup));
SetEnd(std::move(end_backup));
return S_OK;
}
SetEnd(start()->CreateNextCharacterPosition(
{AXBoundaryBehavior::kCrossBoundary,
AXBoundaryDetection::kDontCheckInitialPosition}));
DCHECK(!end()->IsNullPosition());
}
AXPositionInstance normalized_start = start()->Clone();
AXPositionInstance normalized_end = end()->Clone();
NormalizeTextRange(normalized_start, normalized_end);
SetStart(std::move(normalized_start));
SetEnd(std::move(normalized_end));
break;
}
case TextUnit_Format:
SetStart(start()->CreatePreviousFormatStartPosition(
{AXBoundaryBehavior::kStopAtAnchorBoundary,
AXBoundaryDetection::kCheckInitialPosition}));
SetEnd(start()->CreateNextFormatEndPosition(
{AXBoundaryBehavior::kStopAtLastAnchorBoundary,
AXBoundaryDetection::kDontCheckInitialPosition}));
break;
case TextUnit_Word: {
AXPositionInstance start_backup = start()->Clone();
SetStart(start()->CreatePreviousWordStartPosition(
{AXBoundaryBehavior::kStopAtAnchorBoundary,
AXBoundaryDetection::kCheckInitialPosition}));
// Since start_ is already located at a word boundary, we need to cross it
// in order to move to the next one. Because Windows ATs behave
// undesirably when the start and end endpoints are not in the same anchor
// (for character and word navigation), stop at anchor boundary.
SetEnd(start()->CreateNextWordStartPosition(
{AXBoundaryBehavior::kStopAtAnchorBoundary,
AXBoundaryDetection::kDontCheckInitialPosition}));
break;
}
case TextUnit_Line:
// Walk backwards to the previous line start (but don't walk backwards
// if we're already at the start of a line). The previous line start can
// occur in a different node than where `start` is currently pointing, so
// use kStopAtLastAnchorBoundary, which will stop at the tree boundary if
// no previous line start is found.
SetStart(start()->CreateBoundaryStartPosition(
{AXBoundaryBehavior::kStopAtLastAnchorBoundary,
AXBoundaryDetection::kCheckInitialPosition},
ax::mojom::MoveDirection::kBackward,
base::BindRepeating(&AtStartOfLinePredicate),
base::BindRepeating(&AtEndOfLinePredicate)));
// From the start we just walked backwards to, walk forwards to the line
// end position.
SetEnd(start()->CreateBoundaryEndPosition(
{AXBoundaryBehavior::kStopAtLastAnchorBoundary,
AXBoundaryDetection::kDontCheckInitialPosition},
ax::mojom::MoveDirection::kForward,
base::BindRepeating(&AtStartOfLinePredicate),
base::BindRepeating(&AtEndOfLinePredicate)));
break;
case TextUnit_Paragraph:
SetStart(
start()->CreatePreviousParagraphStartPositionSkippingEmptyParagraphs(
{AXBoundaryBehavior::kStopAtLastAnchorBoundary,
AXBoundaryDetection::kCheckInitialPosition}));
SetEnd(start()->CreateNextParagraphStartPositionSkippingEmptyParagraphs(
{AXBoundaryBehavior::kStopAtLastAnchorBoundary,
AXBoundaryDetection::kDontCheckInitialPosition}));
break;
case TextUnit_Page: {
// Per UIA spec, if the document containing the current range doesn't
// support pagination, default to document navigation.
const AXNode* common_anchor = start()->LowestCommonAnchor(*end());
if (common_anchor->tree()->HasPaginationSupport()) {
SetStart(start()->CreatePreviousPageStartPosition(
{AXBoundaryBehavior::kStopAtLastAnchorBoundary,
AXBoundaryDetection::kCheckInitialPosition}));
SetEnd(start()->CreateNextPageEndPosition(
{AXBoundaryBehavior::kStopAtAnchorBoundary,
AXBoundaryDetection::kCheckInitialPosition}));
break;
}
}
[[fallthrough]];
case TextUnit_Document:
SetStart(start()->CreatePositionAtStartOfContent()->AsLeafTextPosition());
SetEnd(start()->CreatePositionAtEndOfContent());
break;
default:
return UIA_E_NOTSUPPORTED;
}
DCHECK(!start()->IsNullPosition());
DCHECK(!end()->IsNullPosition());
return S_OK;
}
HRESULT AXPlatformNodeTextRangeProviderWin::FindAttribute(
TEXTATTRIBUTEID text_attribute_id,
VARIANT attribute_val,
BOOL is_backward,
ITextRangeProvider** result) {
// Algorithm description:
// Performs linear search. Expand forward or backward to fetch the first
// instance of a sub text range that matches the attribute and its value.
// |is_backward| determines the direction of our search.
// |is_backward=true|, we search from the end of this text range to its
// beginning.
// |is_backward=false|, we search from the beginning of this text range to its
// end.
//
// 1. Iterate through the vector of AXRanges in this text range in the
// direction denoted by |is_backward|.
// 2. The |matched_range| is initially denoted as null since no range
// currently matches. We initialize |matched_range| to non-null value when
// we encounter the first AXRange instance that matches in attribute and
// value. We then set the |matched_range_start| to be the start (anchor) of
// the current AXRange, and |matched_range_end| to be the end (focus) of
// the current AXRange.
// 3. If the current AXRange we are iterating on continues to match attribute
// and value, we extend |matched_range| in one of the two following ways:
// - If |is_backward=true|, we extend the |matched_range| by moving
// |matched_range_start| backward. We do so by setting
// |matched_range_start| to the start (anchor) of the current AXRange.
// - If |is_backward=false|, we extend the |matched_range| by moving
// |matched_range_end| forward. We do so by setting |matched_range_end|
// to the end (focus) of the current AXRange.
// 4. We found a match when the current AXRange we are iterating on does not
// match the attribute and value and there is a previously matched range.
// The previously matched range is the final match we found.
WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXTRANGE_FINDATTRIBUTE);
WIN_ACCESSIBILITY_API_PERF_HISTOGRAM(UMA_API_TEXTRANGE_FINDATTRIBUTE);
UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_OUT(result);
// Use a cloned range so that FindAttribute does not introduce side-effects
// while normalizing the original range.
AXPositionInstance normalized_start = start()->Clone();
AXPositionInstance normalized_end = end()->Clone();
NormalizeTextRange(normalized_start, normalized_end);
*result = nullptr;
AXPositionInstance matched_range_start = nullptr;
AXPositionInstance matched_range_end = nullptr;
std::vector<AXNodeRange> anchors;
AXNodeRange range(normalized_start->Clone(), normalized_end->Clone());
for (AXNodeRange leaf_text_range : range)
anchors.emplace_back(std::move(leaf_text_range));
auto expand_match = [&matched_range_start, &matched_range_end, is_backward](
auto& current_start, auto& current_end) {
// The current AXRange has the attribute and its value that we are looking
// for, we expand the matched text range if a previously matched exists,
// otherwise initialize a newly matched text range.
if (matched_range_start != nullptr && matched_range_end != nullptr) {
// Continue expanding the matched text range forward/backward based on
// the search direction.
if (is_backward)
matched_range_start = current_start->Clone();
else
matched_range_end = current_end->Clone();
} else {
// Initialize the matched text range. The first AXRange instance that
// matches the attribute and its value encountered.
matched_range_start = current_start->Clone();
matched_range_end = current_end->Clone();
}
};
HRESULT hr_result =
is_backward
? FindAttributeRange(text_attribute_id, attribute_val,
anchors.crbegin(), anchors.crend(), expand_match)
: FindAttributeRange(text_attribute_id, attribute_val,
anchors.cbegin(), anchors.cend(), expand_match);
if (FAILED(hr_result))
return E_FAIL;
if (matched_range_start != nullptr && matched_range_end != nullptr) {
CreateTextRangeProvider(std::move(matched_range_start),
std::move(matched_range_end), result);
}
return S_OK;
}
template <typename AnchorIterator, typename ExpandMatchLambda>
HRESULT AXPlatformNodeTextRangeProviderWin::FindAttributeRange(
const TEXTATTRIBUTEID text_attribute_id,
VARIANT attribute_val,
const AnchorIterator first,
const AnchorIterator last,
ExpandMatchLambda expand_match) {
AXPlatformNodeWin* current_platform_node;
bool is_match_found = false;
for (auto it = first; it != last; ++it) {
const auto& current_start = it->anchor();
const auto& current_end = it->focus();
DCHECK(current_start->GetAnchor() == current_end->GetAnchor());
AXPlatformNodeDelegate* delegate = GetDelegate(current_start);
DCHECK(delegate);
current_platform_node = static_cast<AXPlatformNodeWin*>(
delegate->GetFromNodeID(current_start->GetAnchor()->id()));
base::win::VariantVector current_attribute_value;
if (FAILED(current_platform_node->GetTextAttributeValue(
text_attribute_id, current_start->text_offset(),
current_end->text_offset(), ¤t_attribute_value))) {
return E_FAIL;
}
if (!current_attribute_value.Compare(attribute_val)) {
// When we encounter an AXRange instance that matches the attribute
// and its value which we are looking for and no previously matched text
// range exists, we expand or initialize the matched range.
is_match_found = true;
expand_match(current_start, current_end);
} else if (is_match_found) {
// When we encounter an AXRange instance that does not match the attribute
// and its value which we are looking for and a previously matched text
// range exists, the previously matched text range is the result we found.
break;
}
}
return S_OK;
}
HRESULT AXPlatformNodeTextRangeProviderWin::FindText(
BSTR string,
BOOL backwards,
BOOL ignore_case,
ITextRangeProvider** result) {
// On Windows, there's a dichotomy in the definition of a text offset in a
// text position between different APIs:
// - on UIA, a text offset translates to the offset in the text itself
// - on IA2, it translates to the offset in the hypertext
//
// All unignored non-text nodes are represented with an "embedded object
// character" in their parent's text representation on IA2, but aren't on UIA.
// This leads to different expected MaxTextOffset values for a same text
// position. If `string` is found in the text represented by the start/end
// endpoints, we'll create text positions in the least common ancestor, use
// the flat text representation's offsets of found string, then convert the
// positions to leaf. If 'embedded object characters' are considered, instead
// of the flat text representation, this falls apart.
//
// Whether we expose embedded object characters for nodes is managed by the
// |g_ax_embedded_object_behavior| global variable set in ax_node_position.cc.
// When on Windows, this variable is always set to
// kExposeCharacterForHypertext... which is incorrect if we run UIA-specific
// code. To avoid problems caused by that, we use the following
// ScopedAXEmbeddedObjectBehaviorSetter to modify the value of the global
// variable to what is really expected on UIA.
ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior_find_text(
AXEmbeddedObjectBehavior::kSuppressCharacter);
WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXTRANGE_FINDTEXT);
WIN_ACCESSIBILITY_API_PERF_HISTOGRAM(UMA_API_TEXTRANGE_FINDTEXT);
// The following has to be called after setting the
// ax_embedded_object_behavior. This is because it can modify `this`'s `start`
// and `end`, and it will do so assuming
// `AXEmbeddedObjectBehavior::kExposeCharacterForHypertext` if we do not set
// it to `kSuppressCharacter' above. This would lead to incorrect behavior
// where the `text_range` length = 1, since that is the length of the embedded
// object character.
UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_IN_1_OUT(string, result);
std::u16string search_string = base::WideToUTF16(string);
if (search_string.length() <= 0)
return E_INVALIDARG;
std::vector<size_t> appended_newlines_indices;
std::u16string text_range = GetString(-1, &appended_newlines_indices);
size_t find_start;
size_t find_length;
if (base::i18n::StringSearch(search_string, text_range, &find_start,
&find_length, !ignore_case, !backwards)) {
// TODO(crbug.com/40658243): There is a known issue here related to
// text searches of a |string| starting and ending with a "\n", e.g.
// "\nsometext" or "sometext\n" if the newline is computed from a line
// breaking object. FindText() is rarely called, and when it is, it's not to
// look for a string starting or ending with a newline. This may change
// someday, and if so, we'll have to address this issue.
const AXNode* common_anchor = start()->LowestCommonAnchor(*end());
AXPositionInstance start_ancestor_position =
start()->CreateAncestorPosition(common_anchor,
ax::mojom::MoveDirection::kForward);
DCHECK(!start_ancestor_position->IsNullPosition());
AXPositionInstance end_ancestor_position = end()->CreateAncestorPosition(
common_anchor, ax::mojom::MoveDirection::kForward);
DCHECK(!end_ancestor_position->IsNullPosition());
const AXNode* anchor = start_ancestor_position->GetAnchor();
DCHECK(anchor);
const int start_offset =
start_ancestor_position->text_offset() + find_start;
const int end_offset =
start_offset + find_length -
GetAppendedNewLinesCountInRange(find_start, find_length,
appended_newlines_indices);
const int max_end_offset = end_ancestor_position->text_offset();
DCHECK(start_offset <= end_offset && end_offset <= max_end_offset);
AXPositionInstance start =
AXNodePosition::CreateTextPosition(*anchor, start_offset,
ax::mojom::TextAffinity::kDownstream)
->AsLeafTextPosition();
AXPositionInstance end =
AXNodePosition::CreateTextPosition(*anchor, end_offset,
ax::mojom::TextAffinity::kDownstream)
->AsLeafTextPosition();
CreateTextRangeProvider(start->Clone(), end->Clone(), result);
}
return S_OK;
}
HRESULT AXPlatformNodeTextRangeProviderWin::GetAttributeValue(
TEXTATTRIBUTEID attribute_id,
VARIANT* value) {
WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXTRANGE_GETATTRIBUTEVALUE);
WIN_ACCESSIBILITY_API_PERF_HISTOGRAM(UMA_API_TEXTRANGE_GETATTRIBUTEVALUE);
UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_OUT(value);
base::win::VariantVector attribute_value;
// When the range spans only a generated newline (a generated newline is not
// part of a node, but rather introduced by AXRange::GetText when at a
// paragraph boundary), it doesn't make sense to return the readonly value of
// the start or end anchor since the newline character is not part of any of
// those nodes. Thus, this attribute value is independent from these nodes.
//
// Instead, we should return the readonly attribute value of the common anchor
// for these two endpoints since the newline character has more in common with
// its ancestor than its siblings. Important: This might not be true for all
// attributes, but it appears to be reasonable enough for the readonly one.
//
// To determine if the range encompasses *only* a generated newline, we need
// to validate that both the start and end endpoints are around the same
// paragraph boundary.
if (attribute_id == UIA_IsReadOnlyAttributeId &&
start()->anchor_id() != end()->anchor_id() &&
start()->AtEndOfParagraph() && end()->AtStartOfParagraph() &&
*start()->CreateNextCharacterPosition(
{AXBoundaryBehavior::kCrossBoundary,
AXBoundaryDetection::kDontCheckInitialPosition}) == *end()) {
AXPlatformNodeWin* common_anchor = GetLowestAccessibleCommonPlatformNode();
DCHECK(common_anchor);
HRESULT hr = common_anchor->GetTextAttributeValue(
attribute_id, std::nullopt, std::nullopt, &attribute_value);
if (FAILED(hr))
return E_FAIL;
*value = attribute_value.ReleaseAsScalarVariant();
return S_OK;
}
// Use a cloned range so that GetAttributeValue does not introduce
// side-effects while normalizing the original range.
AXPositionInstance normalized_start = start()->Clone();
AXPositionInstance normalized_end = end()->Clone();
NormalizeTextRange(normalized_start, normalized_end);
// The range is inclusive, so advance our endpoint to the next position
const auto end_leaf_text_position = normalized_end->AsLeafTextPosition();
auto end = end_leaf_text_position->CreateNextAnchorPosition();
// Iterate over anchor positions
for (auto it = normalized_start->AsLeafTextPosition();
it->anchor_id() != end->anchor_id() || it->tree_id() != end->tree_id();
it = it->CreateNextAnchorPosition()) {
// If the iterator creates a null position, then it has likely overrun the
// range, return failure. This is unexpected but may happen if the range
// became inverted.
DCHECK(!it->IsNullPosition());
if (it->IsNullPosition())
return E_FAIL;
AXPlatformNodeDelegate* delegate = GetDelegate(it.get());
DCHECK(it && delegate);
AXPlatformNodeWin* platform_node = static_cast<AXPlatformNodeWin*>(
delegate->GetFromNodeID(it->anchor_id()));
DCHECK(platform_node);
// Only get attributes for nodes in the tree. Exclude descendants of leaves
// and ignored objects.
platform_node = static_cast<AXPlatformNodeWin*>(
AXPlatformNode::FromNativeViewAccessible(
platform_node->GetDelegate()->GetLowestPlatformAncestor()));
DCHECK(platform_node);
base::win::VariantVector current_value;
const bool at_end_leaf_text_anchor =
it->anchor_id() == end_leaf_text_position->anchor_id() &&
it->tree_id() == end_leaf_text_position->tree_id();
const std::optional<int> start_offset =
it->IsTextPosition() ? std::make_optional(it->text_offset())
: std::nullopt;
const std::optional<int> end_offset =
at_end_leaf_text_anchor
? std::make_optional(end_leaf_text_position->text_offset())
: std::nullopt;
HRESULT hr = platform_node->GetTextAttributeValue(
attribute_id, start_offset, end_offset, ¤t_value);
if (FAILED(hr))
return E_FAIL;
if (attribute_value.Type() == VT_EMPTY) {
attribute_value = std::move(current_value);
} else if (attribute_value != current_value) {
V_VT(value) = VT_UNKNOWN;
return ::UiaGetReservedMixedAttributeValue(&V_UNKNOWN(value));
}
}
if (ShouldReleaseTextAttributeAsSafearray(attribute_id, attribute_value))
*value = attribute_value.ReleaseAsSafearrayVariant();
else
*value = attribute_value.ReleaseAsScalarVariant();
return S_OK;
}
HRESULT AXPlatformNodeTextRangeProviderWin::GetBoundingRectangles(
SAFEARRAY** screen_physical_pixel_rectangles) {
WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXTRANGE_GETBOUNDINGRECTANGLES);
WIN_ACCESSIBILITY_API_PERF_HISTOGRAM(UMA_API_TEXTRANGE_GETBOUNDINGRECTANGLES);
UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_OUT(screen_physical_pixel_rectangles);
*screen_physical_pixel_rectangles = nullptr;
AXNodeRange range(start()->Clone(), end()->Clone());
AXRangePhysicalPixelRectDelegate rect_delegate(this);
std::vector<gfx::Rect> rects = range.GetRects(&rect_delegate);
// 4 array items per rect: left, top, width, height
SAFEARRAY* safe_array = SafeArrayCreateVector(
VT_R8 /* element type */, 0 /* lower bound */, rects.size() * 4);
if (!safe_array)
return E_OUTOFMEMORY;
if (rects.size() > 0) {
double* double_array = nullptr;
HRESULT hr = SafeArrayAccessData(safe_array,
reinterpret_cast<void**>(&double_array));
if (SUCCEEDED(hr)) {
for (size_t rect_index = 0; rect_index < rects.size(); rect_index++) {
const gfx::Rect& rect = rects[rect_index];
double_array[rect_index * 4] = rect.x();
double_array[rect_index * 4 + 1] = rect.y();
double_array[rect_index * 4 + 2] = rect.width();
double_array[rect_index * 4 + 3] = rect.height();
}
hr = SafeArrayUnaccessData(safe_array);
}
if (FAILED(hr)) {
DCHECK(safe_array);
SafeArrayDestroy(safe_array);
return E_FAIL;
}
}
*screen_physical_pixel_rectangles = safe_array;
return S_OK;
}
HRESULT AXPlatformNodeTextRangeProviderWin::GetEnclosingElement(
IRawElementProviderSimple** element) {
WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXTRANGE_GETENCLOSINGELEMENT);
WIN_ACCESSIBILITY_API_PERF_HISTOGRAM(UMA_API_TEXTRANGE_GETENCLOSINGELEMENT);
UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_OUT(element);
AXPlatformNodeWin* enclosing_node = GetLowestAccessibleCommonPlatformNode();
if (!enclosing_node)
return UIA_E_ELEMENTNOTAVAILABLE;
enclosing_node->GetNativeViewAccessible()->QueryInterface(
IID_PPV_ARGS(element));
DCHECK(*element);
return S_OK;
}
HRESULT AXPlatformNodeTextRangeProviderWin::GetText(int max_count, BSTR* text) {
ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior(
AXEmbeddedObjectBehavior::kUIAExposeCharacterForTextContent);
WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXTRANGE_GETTEXT);
WIN_ACCESSIBILITY_API_PERF_HISTOGRAM(UMA_API_TEXTRANGE_GETTEXT);
UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_OUT(text);
// -1 is a valid value that signifies that the caller wants complete text.
// Any other negative value is an invalid argument.
if (max_count < -1)
return E_INVALIDARG;
std::wstring full_text = base::UTF16ToWide(GetString(max_count));
if (!full_text.empty()) {
size_t length = full_text.length();
if (max_count != -1 && max_count < static_cast<int>(length))
*text = SysAllocStringLen(full_text.c_str(), max_count);
else
*text = SysAllocStringLen(full_text.c_str(), length);
} else {
*text = SysAllocString(L"");
}
return S_OK;
}
HRESULT AXPlatformNodeTextRangeProviderWin::Move(TextUnit unit,
int count,
int* units_moved) {
WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXTRANGE_MOVE);
WIN_ACCESSIBILITY_API_PERF_HISTOGRAM(UMA_API_TEXTRANGE_MOVE);
UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_OUT(units_moved);
// Per MSDN, move with zero count has no effect.
if (count == 0)
return S_OK;
// Save a clone of start and end, in case one of the moves fails.
auto start_backup = start()->Clone();
auto end_backup = end()->Clone();
bool is_degenerate_range = (*start() == *end());
// Move the start of the text range forward or backward in the document by the
// requested number of text unit boundaries.
int start_units_moved = 0;
HRESULT hr = MoveEndpointByUnitImpl(TextPatternRangeEndpoint_Start, unit,
count, &start_units_moved);
bool succeeded_move = SUCCEEDED(hr) && start_units_moved != 0;
if (succeeded_move) {
SetEnd(start()->Clone());
if (!is_degenerate_range) {
bool forwards = count > 0;
if (forwards && start()->AtEndOfContent()) {
// The start is at the end of the document, so move the start backward
// by one text unit to expand the text range from the degenerate range
// state.
int current_start_units_moved = 0;
hr = MoveEndpointByUnitImpl(TextPatternRangeEndpoint_Start, unit, -1,
¤t_start_units_moved);
start_units_moved -= 1;
succeeded_move = SUCCEEDED(hr) && current_start_units_moved == -1 &&
start_units_moved > 0;
} else {
// The start is not at the end of the document, so move the endpoint
// forward by one text unit to expand the text range from the degenerate
// state.
int end_units_moved = 0;
hr = MoveEndpointByUnitImpl(TextPatternRangeEndpoint_End, unit, 1,
&end_units_moved);
succeeded_move = SUCCEEDED(hr) && end_units_moved == 1;
}
// Because Windows ATs behave undesirably when the start and end endpoints
// are not in the same anchor (for character and word navigation), make
// sure to bring back the end endpoint to the end of the start's anchor.
if (start()->anchor_id() != end()->anchor_id() &&
(unit == TextUnit_Character || unit == TextUnit_Word)) {
ExpandToEnclosingUnitImpl(unit);
}
}
}
if (!succeeded_move) {
SetStart(std::move(start_backup));
SetEnd(std::move(end_backup));
start_units_moved = 0;
if (!SUCCEEDED(hr))
return hr;
}
*units_moved = start_units_moved;
return S_OK;
}
HRESULT AXPlatformNodeTextRangeProviderWin::MoveEndpointByUnit(
TextPatternRangeEndpoint endpoint,
TextUnit unit,
int count,
int* units_moved) {
WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXTRANGE_MOVEENDPOINTBYUNIT);
WIN_ACCESSIBILITY_API_PERF_HISTOGRAM(UMA_API_TEXTRANGE_MOVEENDPOINTBYUNIT);
return MoveEndpointByUnitImpl(endpoint, unit, count, units_moved);
}
HRESULT AXPlatformNodeTextRangeProviderWin::MoveEndpointByUnitImpl(
TextPatternRangeEndpoint endpoint,
TextUnit unit,
int count,
int* units_moved) {
ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior(
AXEmbeddedObjectBehavior::kUIAExposeCharacterForTextContent);
UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_OUT(units_moved);
// Per MSDN, MoveEndpointByUnit with zero count has no effect.
if (count == 0) {
*units_moved = 0;
return S_OK;
}
bool is_start_endpoint = endpoint == TextPatternRangeEndpoint_Start;
AXPositionInstance position_to_move =
is_start_endpoint ? start()->Clone() : end()->Clone();
AXPositionInstance new_position;
switch (unit) {
case TextUnit_Character:
new_position =
MoveEndpointByCharacter(position_to_move, count, units_moved);
break;
case TextUnit_Format:
new_position = MoveEndpointByFormat(position_to_move, is_start_endpoint,
count, units_moved);
break;
case TextUnit_Word:
new_position = MoveEndpointByWord(position_to_move, count, units_moved);
break;
case TextUnit_Line:
new_position = MoveEndpointByLine(position_to_move, is_start_endpoint,
count, units_moved);
break;
case TextUnit_Paragraph:
new_position = MoveEndpointByParagraph(
position_to_move, is_start_endpoint, count, units_moved);
break;
case TextUnit_Page:
new_position = MoveEndpointByPage(position_to_move, is_start_endpoint,
count, units_moved);
break;
case TextUnit_Document:
new_position =
MoveEndpointByDocument(position_to_move, count, units_moved);
break;
default:
return UIA_E_NOTSUPPORTED;
}
if (is_start_endpoint)
SetStart(std::move(new_position));
else
SetEnd(std::move(new_position));
// If the start was moved past the end, create a degenerate range with the end
// equal to the start; do the equivalent if the end moved past the start.
std::optional<int> endpoint_comparison =
AXNodeRange::CompareEndpoints(start().get(), end().get());
DCHECK(endpoint_comparison.has_value());
if (endpoint_comparison.value_or(0) > 0) {
if (is_start_endpoint)
SetEnd(start()->Clone());
else
SetStart(end()->Clone());
}
return S_OK;
}
HRESULT AXPlatformNodeTextRangeProviderWin::MoveEndpointByRange(
TextPatternRangeEndpoint this_endpoint,
ITextRangeProvider* other,
TextPatternRangeEndpoint other_endpoint) {
WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXTRANGE_MOVEENPOINTBYRANGE);
WIN_ACCESSIBILITY_API_PERF_HISTOGRAM(UMA_API_TEXTRANGE_MOVEENPOINTBYRANGE);
UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_IN(other);
Microsoft::WRL::ComPtr<AXPlatformNodeTextRangeProviderWin> other_provider;
if (other->QueryInterface(IID_PPV_ARGS(&other_provider)) != S_OK)
return UIA_E_INVALIDOPERATION;
SnapStartAndEndToMaxTextOffsetIfBeyond();
other_provider->SnapStartAndEndToMaxTextOffsetIfBeyond();
const AXPositionInstance& other_provider_endpoint =
(other_endpoint == TextPatternRangeEndpoint_Start)
? other_provider->start()
: other_provider->end();
if (this_endpoint == TextPatternRangeEndpoint_Start) {
SetStart(other_provider_endpoint->Clone());
if (*start() > *end()) {
SetEnd(start()->Clone());
}
} else {
SetEnd(other_provider_endpoint->Clone());
if (*start() > *end()) {
SetStart(end()->Clone());
}
}
return S_OK;
}
HRESULT AXPlatformNodeTextRangeProviderWin::Select() {
WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXTRANGE_SELECT);
UIA_VALIDATE_TEXTRANGEPROVIDER_CALL();
AXPositionInstance selection_start = start()->Clone();
AXPositionInstance selection_end = end()->Clone();
// Blink only supports selections within a single tree. So if start_ and end_
// are in different trees, we can't directly pass them to the render process
// for selection.
if (selection_start->tree_id() != selection_end->tree_id()) {
// Prioritize the end position's tree, as a selection's focus object is the
// end of a selection.
selection_start = selection_end->CreatePositionAtStartOfAXTree();
}
// In the renderer side accessibility, we have checks that prevent selections
// being made that cross shadow DOM boundaries. Thus, these checks make sure
// that if we are attempting to make such a selection, we move the positions
// to the text field ancestor such that this does not happen. The new
// positions are equivalent to the old ones.
AXNode* start_anchor = selection_start->GetAnchor();
AXNode* end_anchor = selection_end->GetAnchor();
AXNode* atomic_text_field = nullptr;
if (start_anchor->data().IsAtomicTextField()) {
atomic_text_field = start_anchor;
} else if (end_anchor->data().IsAtomicTextField()) {
atomic_text_field = end_anchor;
}
if (atomic_text_field && start_anchor != end_anchor) {
AXNode* non_atomic_text_field = end_anchor;
if (end_anchor == atomic_text_field) {
non_atomic_text_field = start_anchor;
}
if (non_atomic_text_field->GetTextFieldAncestor() == atomic_text_field) {
if (start_anchor == atomic_text_field) {
selection_end = selection_end->CreateAncestorPosition(
start_anchor, ax::mojom::MoveDirection::kForward);
} else {
selection_start = selection_start->CreateAncestorPosition(
end_anchor, ax::mojom::MoveDirection::kForward);
}
}
}
DCHECK(!selection_start->IsNullPosition());
DCHECK(!selection_end->IsNullPosition());
DCHECK_EQ(selection_start->tree_id(), selection_end->tree_id());
// TODO(crbug.com/40717049): Blink does not support selection on the list
// markers. So if |selection_start| or |selection_end| are in list markers, we
// don't perform selection and return success. Remove this check once this bug
// is fixed.
if (selection_start->GetAnchor()->IsInListMarker() ||
selection_end->GetAnchor()->IsInListMarker()) {
return S_OK;
}
AXPlatformNodeDelegate* delegate =
GetDelegate(selection_start->tree_id(), selection_start->anchor_id());
DCHECK(delegate);
AXNodeRange new_selection_range(std::move(selection_start),
std::move(selection_end));
RemoveFocusFromPreviousSelectionIfNeeded(new_selection_range);
AXActionData action_data;
action_data.anchor_node_id = new_selection_range.anchor()->anchor_id();
action_data.anchor_offset = new_selection_range.anchor()->text_offset();
action_data.focus_node_id = new_selection_range.focus()->anchor_id();
action_data.focus_offset = new_selection_range.focus()->text_offset();
action_data.action = ax::mojom::Action::kSetSelection;
delegate->AccessibilityPerformAction(action_data);
return S_OK;
}
HRESULT AXPlatformNodeTextRangeProviderWin::AddToSelection() {
WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXTRANGE_ADDTOSELECTION);
// Blink does not support disjoint text selections.
return UIA_E_INVALIDOPERATION;
}
HRESULT
AXPlatformNodeTextRangeProviderWin::RemoveFromSelection() {
WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXTRANGE_REMOVEFROMSELECTION);
// Blink does not support disjoint text selections.
return UIA_E_INVALIDOPERATION;
}
HRESULT AXPlatformNodeTextRangeProviderWin::ScrollIntoView(BOOL align_to_top) {
WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXTRANGE_SCROLLINTOVIEW);
UIA_VALIDATE_TEXTRANGEPROVIDER_CALL();
// Return early when we're trying to scroll in a View.
// TODO(accessibility): Investigate if Views support scrolling and how to
// implement it.
if (!GetOwner()->GetDelegate()->IsWebContent()) {
return S_OK;
}
AXPlatformNode* start_platform_node =
GetOwner()->GetDelegate()->GetFromTreeIDAndNodeID(
start()->tree_id(), start()->GetAnchor()->id());
AXPlatformNode* end_platform_node =
GetOwner()->GetDelegate()->GetFromTreeIDAndNodeID(
end()->tree_id(), end()->GetAnchor()->id());
// If both anchors are onscreen, don't scroll.
if (!start_platform_node->GetDelegate()->IsOffscreen() &&
!end_platform_node->GetDelegate()->IsOffscreen()) {
return S_OK;
}
const AXPositionInstance start_common_ancestor =
start()->LowestCommonAncestorPosition(
*end(), ax::mojom::MoveDirection::kBackward);
const AXPositionInstance end_common_ancestor =
end()->LowestCommonAncestorPosition(*start(),
ax::mojom::MoveDirection::kForward);
if (start_common_ancestor->IsNullPosition() ||
end_common_ancestor->IsNullPosition()) {
return E_INVALIDARG;
}
const AXNode* common_ancestor_anchor = start_common_ancestor->GetAnchor();
DCHECK(common_ancestor_anchor == end_common_ancestor->GetAnchor());
const AXTreeID common_ancestor_tree_id = start_common_ancestor->tree_id();
const AXPlatformNodeDelegate* root_delegate =
GetRootDelegate(common_ancestor_tree_id);
DCHECK(root_delegate);
const gfx::Rect root_frame_bounds = root_delegate->GetBoundsRect(
AXCoordinateSystem::kFrame, AXClippingBehavior::kUnclipped);
UIA_VALIDATE_BOUNDS(root_frame_bounds);
const AXPlatformNode* common_ancestor_platform_node =
GetOwner()->GetDelegate()->GetFromTreeIDAndNodeID(
common_ancestor_tree_id, common_ancestor_anchor->id());
DCHECK(common_ancestor_platform_node);
AXPlatformNodeDelegate* common_ancestor_delegate =
common_ancestor_platform_node->GetDelegate();
DCHECK(common_ancestor_delegate);
const gfx::Rect text_range_container_frame_bounds =
common_ancestor_delegate->GetBoundsRect(AXCoordinateSystem::kFrame,
AXClippingBehavior::kUnclipped);
UIA_VALIDATE_BOUNDS(text_range_container_frame_bounds);
gfx::Point target_point;
if (align_to_top) {
target_point = gfx::Point(root_frame_bounds.x(), root_frame_bounds.y());
} else {
target_point =
gfx::Point(root_frame_bounds.x(),
root_frame_bounds.y() + root_frame_bounds.height());
}
if ((align_to_top && start()->GetAnchor()->IsText()) ||
(!align_to_top && end()->GetAnchor()->IsText())) {
const gfx::Rect text_range_frame_bounds =
common_ancestor_delegate->GetInnerTextRangeBoundsRect(
start_common_ancestor->text_offset(),
end_common_ancestor->text_offset(), AXCoordinateSystem::kFrame,
AXClippingBehavior::kUnclipped);
UIA_VALIDATE_BOUNDS(text_range_frame_bounds);
if (align_to_top) {
target_point.Offset(0, -(text_range_container_frame_bounds.height() -
text_range_frame_bounds.height()));
} else {
target_point.Offset(0, -text_range_frame_bounds.height());
}
} else {
if (!align_to_top)
target_point.Offset(0, -text_range_container_frame_bounds.height());
}
const gfx::Rect root_screen_bounds = root_delegate->GetBoundsRect(
AXCoordinateSystem::kScreenDIPs, AXClippingBehavior::kUnclipped);
UIA_VALIDATE_BOUNDS(root_screen_bounds);
target_point += root_screen_bounds.OffsetFromOrigin();
AXActionData action_data;
action_data.action = ax::mojom::Action::kScrollToPoint;
action_data.target_node_id = common_ancestor_anchor->id();
action_data.target_point = target_point;
if (!common_ancestor_delegate->AccessibilityPerformAction(action_data))
return E_FAIL;
return S_OK;
}
// This function is expected to return a subset of the *direct* children of the
// common ancestor node. The subset should only include the direct children
// included - fully or partially - in the range.
HRESULT AXPlatformNodeTextRangeProviderWin::GetChildren(SAFEARRAY** children) {
WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXTRANGE_GETCHILDREN);
WIN_ACCESSIBILITY_API_PERF_HISTOGRAM(UMA_API_TEXTRANGE_GETCHILDREN);
UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_OUT(children);
std::vector<gfx::NativeViewAccessible> descendants;
AXPlatformNodeWin* start_anchor =
GetPlatformNodeFromAXNode(start()->GetAnchor());
AXPlatformNodeWin* end_anchor = GetPlatformNodeFromAXNode(end()->GetAnchor());
AXPlatformNodeWin* common_anchor = GetLowestAccessibleCommonPlatformNode();
if (!common_anchor || !start_anchor || !end_anchor)
return UIA_E_ELEMENTNOTAVAILABLE;
AXPlatformNodeDelegate* start_delegate = start_anchor->GetDelegate();
AXPlatformNodeDelegate* end_delegate = end_anchor->GetDelegate();
AXPlatformNodeDelegate* common_delegate = common_anchor->GetDelegate();
descendants = common_delegate->GetUIADirectChildrenInRange(start_delegate,
end_delegate);
SAFEARRAY* safe_array =
SafeArrayCreateVector(VT_UNKNOWN, 0, descendants.size());
if (!safe_array)
return E_OUTOFMEMORY;
if (safe_array->rgsabound->cElements != descendants.size()) {
DCHECK(safe_array);
SafeArrayDestroy(safe_array);
return E_OUTOFMEMORY;
}
LONG i = 0;
for (const gfx::NativeViewAccessible& descendant : descendants) {
IRawElementProviderSimple* raw_provider;
descendant->QueryInterface(IID_PPV_ARGS(&raw_provider));
SafeArrayPutElement(safe_array, &i, raw_provider);
++i;
}
*children = safe_array;
return S_OK;
}
// static
bool AXPlatformNodeTextRangeProviderWin::AtStartOfLinePredicate(
const AXPositionInstance& position) {
return !position->IsIgnored() && position->AtStartOfAnchor() &&
position->AtStartOfLine();
}
// static
bool AXPlatformNodeTextRangeProviderWin::AtEndOfLinePredicate(
const AXPositionInstance& position) {
return !position->IsIgnored() && position->AtEndOfAnchor() &&
position->AtEndOfLine();
}
// static
AXPlatformNodeTextRangeProviderWin::AXPositionInstance
AXPlatformNodeTextRangeProviderWin::GetNextTextBoundaryPosition(
const AXPositionInstance& position,
ax::mojom::TextBoundary boundary_type,
AXMovementOptions options,
ax::mojom::MoveDirection boundary_direction) {
// Override At[Start|End]OfLinePredicate for behavior specific to UIA.
DCHECK_NE(boundary_type, ax::mojom::TextBoundary::kNone);
switch (boundary_type) {
case ax::mojom::TextBoundary::kLineStart:
return position->CreateBoundaryStartPosition(
options, boundary_direction,
base::BindRepeating(&AtStartOfLinePredicate),
base::BindRepeating(&AtEndOfLinePredicate));
case ax::mojom::TextBoundary::kLineEnd:
return position->CreateBoundaryEndPosition(
options, boundary_direction,
base::BindRepeating(&AtStartOfLinePredicate),
base::BindRepeating(&AtEndOfLinePredicate));
default:
return position->CreatePositionAtTextBoundary(
boundary_type, boundary_direction, options);
}
}
std::u16string AXPlatformNodeTextRangeProviderWin::GetString(
int max_count,
std::vector<size_t>* appended_newlines_indices) {
AXNodeRange range(start()->Clone(), end()->Clone());
return range.GetText(
AXTextConcatenationBehavior::kWithParagraphBreaks,
AXEmbeddedObjectBehavior::kUIAExposeCharacterForTextContent, max_count,
false, appended_newlines_indices);
}
size_t AXPlatformNodeTextRangeProviderWin::GetAppendedNewLinesCountInRange(
size_t find_start,
size_t find_length,
const std::vector<size_t>& appended_newlines_indices) {
size_t relevant_appended_newlines_count = 0;
for (size_t i = 0; i < appended_newlines_indices.size(); ++i) {
// Since the vector is ordered, we can break out of the loop once we've
// passed the end of the range.
if (appended_newlines_indices[i] > find_start + find_length) {
break;
}
if (appended_newlines_indices[i] >= find_start &&
appended_newlines_indices[i] < find_start + find_length) {
++relevant_appended_newlines_count;
}
}
return relevant_appended_newlines_count;
}
AXPlatformNodeWin* AXPlatformNodeTextRangeProviderWin::GetOwner() const {
// Unit tests can't call `GetPlatformNodeFromTree`, so they must provide an
// owner node.
if (owner_for_test_.Get())
return owner_for_test_.Get();
const AXPositionInstance& position =
!start()->IsNullPosition() ? start() : end();
// If start and end are both null, there's no owner.
if (position->IsNullPosition())
return nullptr;
const AXNode* anchor = position->GetAnchor();
DCHECK(anchor);
const AXTreeManager* ax_tree_manager = position->GetManager();
if (ax_tree_manager && ax_tree_manager->IsPlatformTreeManager()) {
const AXPlatformTreeManager* platform_tree_manager =
static_cast<const AXPlatformTreeManager*>(ax_tree_manager);
DCHECK(platform_tree_manager);
return static_cast<AXPlatformNodeWin*>(
platform_tree_manager->GetPlatformNodeFromTree(*anchor));
}
return nullptr;
}
AXPlatformNodeDelegate* AXPlatformNodeTextRangeProviderWin::GetDelegate(
const AXPositionInstanceType* position) const {
return GetDelegate(position->tree_id(), position->anchor_id());
}
AXPlatformNodeDelegate* AXPlatformNodeTextRangeProviderWin::GetDelegate(
const AXTreeID tree_id,
const AXNodeID node_id) const {
AXPlatformNode* platform_node =
GetOwner()->GetDelegate()->GetFromTreeIDAndNodeID(tree_id, node_id);
if (!platform_node)
return nullptr;
return platform_node->GetDelegate();
}
AXPlatformNodeTextRangeProviderWin::AXPositionInstance
AXPlatformNodeTextRangeProviderWin::MoveEndpointByCharacter(
const AXPositionInstance& endpoint,
const int count,
int* units_moved) {
return MoveEndpointByUnitHelper(std::move(endpoint),
ax::mojom::TextBoundary::kCharacter, count,
units_moved);
}
AXPlatformNodeTextRangeProviderWin::AXPositionInstance
AXPlatformNodeTextRangeProviderWin::MoveEndpointByWord(
const AXPositionInstance& endpoint,
const int count,
int* units_moved) {
return MoveEndpointByUnitHelper(std::move(endpoint),
ax::mojom::TextBoundary::kWordStart, count,
units_moved);
}
AXPlatformNodeTextRangeProviderWin::AXPositionInstance
AXPlatformNodeTextRangeProviderWin::MoveEndpointByLine(
const AXPositionInstance& endpoint,
bool is_start_endpoint,
const int count,
int* units_moved) {
return MoveEndpointByUnitHelper(std::move(endpoint),
is_start_endpoint
? ax::mojom::TextBoundary::kLineStart
: ax::mojom::TextBoundary::kLineEnd,
count, units_moved);
}
AXPlatformNodeTextRangeProviderWin::AXPositionInstance
AXPlatformNodeTextRangeProviderWin::MoveEndpointByFormat(
const AXPositionInstance& endpoint,
const bool is_start_endpoint,
const int count,
int* units_moved) {
return MoveEndpointByUnitHelper(std::move(endpoint),
is_start_endpoint
? ax::mojom::TextBoundary::kFormatStart
: ax::mojom::TextBoundary::kFormatEnd,
count, units_moved);
}
AXPlatformNodeTextRangeProviderWin::AXPositionInstance
AXPlatformNodeTextRangeProviderWin::MoveEndpointByParagraph(
const AXPositionInstance& endpoint,
const bool is_start_endpoint,
const int count,
int* units_moved) {
return MoveEndpointByUnitHelper(
std::move(endpoint),
ax::mojom::TextBoundary::kParagraphStartSkippingEmptyParagraphs, count,
units_moved);
}
AXPlatformNodeTextRangeProviderWin::AXPositionInstance
AXPlatformNodeTextRangeProviderWin::MoveEndpointByPage(
const AXPositionInstance& endpoint,
const bool is_start_endpoint,
const int count,
int* units_moved) {
// Per UIA spec, if the document containing the current endpoint doesn't
// support pagination, default to document navigation.
//
// Note that the "ax::mojom::MoveDirection" should not matter when calculating
// the ancestor position for use when navigating by page or document, so we
// use a backward direction as the default.
AXPositionInstance common_ancestor = start()->LowestCommonAncestorPosition(
*end(), ax::mojom::MoveDirection::kBackward);
if (!common_ancestor->GetAnchor()->tree()->HasPaginationSupport())
return MoveEndpointByDocument(std::move(endpoint), count, units_moved);
return MoveEndpointByUnitHelper(std::move(endpoint),
is_start_endpoint
? ax::mojom::TextBoundary::kPageStart
: ax::mojom::TextBoundary::kPageEnd,
count, units_moved);
}
AXPlatformNodeTextRangeProviderWin::AXPositionInstance
AXPlatformNodeTextRangeProviderWin::MoveEndpointByDocument(
const AXPositionInstance& endpoint,
const int count,
int* units_moved) {
DCHECK_NE(count, 0);
if (count < 0) {
*units_moved = !endpoint->AtStartOfContent() ? -1 : 0;
return endpoint->CreatePositionAtStartOfContent();
}
*units_moved = !endpoint->AtEndOfContent() ? 1 : 0;
return endpoint->CreatePositionAtEndOfContent();
}
AXPlatformNodeTextRangeProviderWin::AXPositionInstance
AXPlatformNodeTextRangeProviderWin::MoveEndpointByUnitHelper(
const AXPositionInstance& endpoint,
const ax::mojom::TextBoundary boundary_type,
const int count,
int* units_moved) {
DCHECK_NE(count, 0);
const ax::mojom::MoveDirection boundary_direction =
(count > 0) ? ax::mojom::MoveDirection::kForward
: ax::mojom::MoveDirection::kBackward;
const AXNode* initial_endpoint = endpoint->GetAnchor();
// Most of the methods used to create the next/previous position go back and
// forth creating a leaf text position and rooting the result to the original
// position's anchor; avoid this by normalizing to a leaf text position.
AXPositionInstance current_endpoint = endpoint->AsLeafTextPosition();
AXPositionInstance next_endpoint = GetNextTextBoundaryPosition(
current_endpoint, boundary_type,
{AXBoundaryBehavior::kStopAtLastAnchorBoundary,
AXBoundaryDetection::kDontCheckInitialPosition},
boundary_direction);
DCHECK(next_endpoint->IsLeafTextPosition());
bool is_ignored_for_text_navigation = false;
int iteration = 0;
// Since AXBoundaryBehavior::kStopAtLastAnchorBoundary forces the next
// text boundary position to be different than the input position, the
// only case where these are equal is when they're already located at the
// last anchor boundary. In such case, there is no next position to move
// to.
while (iteration < std::abs(count) &&
!(next_endpoint->GetAnchor() == current_endpoint->GetAnchor() &&
*next_endpoint == *current_endpoint)) {
is_ignored_for_text_navigation = false;
current_endpoint = std::move(next_endpoint);
next_endpoint = GetNextTextBoundaryPosition(
current_endpoint, boundary_type,
{AXBoundaryBehavior::kStopAtLastAnchorBoundary,
AXBoundaryDetection::kDontCheckInitialPosition},
boundary_direction);
DCHECK(next_endpoint->IsLeafTextPosition());
// Loop until we're not on a position that is ignored for text navigation.
// There is one exception for character navigation - since the ignored
// anchor is represented by an embedded object character, we allow
// navigation by character for consistency (i.e. you should be able to
// move by character the same number of characters that are represented by
// the ranges flat string buffer).
is_ignored_for_text_navigation =
boundary_type != ax::mojom::TextBoundary::kCharacter &&
current_endpoint->GetAnchor()->IsIgnoredForTextNavigation();
if (!is_ignored_for_text_navigation)
iteration++;
}
*units_moved = (count > 0) ? iteration : -iteration;
if (is_ignored_for_text_navigation &&
initial_endpoint != current_endpoint->GetAnchor()) {
// If the last node in the tree is ignored for text navigation, we
// should still be able to return an endpoint located on that node. We
// also need to ensure that the value of |units_moved| is accurate.
*units_moved += (count > 0) ? 1 : -1;
}
return current_endpoint;
}
void AXPlatformNodeTextRangeProviderWin::NormalizeTextRange(
AXPositionInstance& start,
AXPositionInstance& end) {
if (!start->IsValid() || !end->IsValid())
return;
// If either endpoint is anchored to an ignored node,
// first snap them both to be unignored positions.
NormalizeAsUnignoredTextRange(start, end);
// When a text range or one end of AXSelection is inside the atomic text
// field, the precise state of the TextPattern must be preserved so that the
// UIA client can handle scenarios such as determining which characters were
// deleted. So normalization must be bypassed.
if (HasTextRangeOrSelectionInAtomicTextField(start, end))
return;
AXPositionInstance normalized_start =
start->AsLeafTextPositionBeforeCharacter();
// For a degenerate range, the |end_| will always be the same as the
// normalized start, so there's no need to compute the normalized end.
// However, a degenerate range might go undetected if there's an ignored node
// (or many) between the two endpoints. For this reason, we need to
// compare the |end_| with both the |start_| and the |normalized_start|.
bool is_degenerate = *start == *end || *normalized_start == *end;
AXPositionInstance normalized_end =
is_degenerate ? normalized_start->Clone()
: end->AsLeafTextPositionAfterCharacter();
if (!normalized_start->IsNullPosition() &&
!normalized_end->IsNullPosition()) {
start = std::move(normalized_start);
end = std::move(normalized_end);
}
DCHECK_LE(*start, *end);
}
// static
void AXPlatformNodeTextRangeProviderWin::NormalizeAsUnignoredPosition(
AXPositionInstance& position) {
if (position->IsNullPosition() || !position->IsValid())
return;
if (position->IsIgnored()) {
AXPositionInstance normalized_position = position->AsUnignoredPosition(
AXPositionAdjustmentBehavior::kMoveForward);
if (normalized_position->IsNullPosition()) {
normalized_position = position->AsUnignoredPosition(
AXPositionAdjustmentBehavior::kMoveBackward);
}
if (!normalized_position->IsNullPosition())
position = std::move(normalized_position);
}
DCHECK(!position->IsNullPosition());
}
// static
void AXPlatformNodeTextRangeProviderWin::NormalizeAsUnignoredTextRange(
AXPositionInstance& start,
AXPositionInstance& end) {
if (!start->IsValid() || !end->IsValid())
return;
if (!start->IsIgnored() && !end->IsIgnored())
return;
NormalizeAsUnignoredPosition(start);
NormalizeAsUnignoredPosition(end);
DCHECK_LE(*start, *end);
}
AXPlatformNodeDelegate* AXPlatformNodeTextRangeProviderWin::GetRootDelegate(
const AXTreeID tree_id) {
const AXTreeManager* ax_tree_manager = AXTreeManager::FromID(tree_id);
DCHECK(ax_tree_manager);
AXNode* root_node = ax_tree_manager->GetRoot();
const AXPlatformNode* root_platform_node =
GetOwner()->GetDelegate()->GetFromTreeIDAndNodeID(tree_id,
root_node->id());
DCHECK(root_platform_node);
return root_platform_node->GetDelegate();
}
void AXPlatformNodeTextRangeProviderWin::SetStart(
AXPositionInstance new_start) {
endpoints_.SetStart(std::move(new_start));
}
void AXPlatformNodeTextRangeProviderWin::SetEnd(AXPositionInstance new_end) {
endpoints_.SetEnd(std::move(new_end));
}
void AXPlatformNodeTextRangeProviderWin::
SnapStartAndEndToMaxTextOffsetIfBeyond() {
if (start()) {
start()->SnapToMaxTextOffsetIfBeyond();
}
if (end()) {
end()->SnapToMaxTextOffsetIfBeyond();
}
}
void AXPlatformNodeTextRangeProviderWin::SetOwnerForTesting(
AXPlatformNodeWin* owner) {
owner_for_test_ = owner;
}
AXNode* AXPlatformNodeTextRangeProviderWin::GetSelectionCommonAnchor() {
AXPlatformNodeDelegate* delegate = GetOwner()->GetDelegate();
AXSelection unignored_selection = delegate->GetUnignoredSelection();
AXPlatformNode* anchor_object =
delegate->GetFromNodeID(unignored_selection.anchor_object_id);
AXPlatformNode* focus_object =
delegate->GetFromNodeID(unignored_selection.focus_object_id);
if (!anchor_object || !focus_object)
return nullptr;
AXNodePosition::AXPositionInstance start =
anchor_object->GetDelegate()->CreateTextPositionAt(
unignored_selection.anchor_offset);
AXNodePosition::AXPositionInstance end =
focus_object->GetDelegate()->CreateTextPositionAt(
unignored_selection.focus_offset);
return start->LowestCommonAnchor(*end);
}
// When the current selection is inside a focusable element, the DOM focused
// element will correspond to this element. When we update the selection to be
// on a different element that is not focusable, the new selection won't be
// applied unless we remove the DOM focused element. For example, with Narrator,
// if we move by word from a text field (focusable) to a static text (not
// focusable), the selection will stay on the text field because the DOM focused
// element will still be the text field. To avoid that, we need to remove the
// focus from this element.
void AXPlatformNodeTextRangeProviderWin::
RemoveFocusFromPreviousSelectionIfNeeded(const AXNodeRange& new_selection) {
const AXNode* old_selection_node = GetSelectionCommonAnchor();
const AXNode* new_selection_node =
new_selection.anchor()->LowestCommonAnchor(*new_selection.focus());
if (!old_selection_node)
return;
// We should not remove the focus when the selection remains in the same text
// field. It's possible for the new selection to be located on a descendant
// inline text box in the text field, so make sure we compare the nodes at the
// root of the text field.
AXNode* old_text_field_ancestor = old_selection_node->GetTextFieldAncestor();
AXNode* new_text_field_ancestor = new_selection_node->GetTextFieldAncestor();
if (old_text_field_ancestor && new_text_field_ancestor &&
old_text_field_ancestor == new_text_field_ancestor) {
return;
}
if (!new_selection_node ||
(old_selection_node->HasState(ax::mojom::State::kFocusable) &&
!new_selection_node->HasState(ax::mojom::State::kFocusable))) {
AXPlatformNodeDelegate* root_delegate =
GetRootDelegate(old_selection_node->tree()->GetAXTreeID());
DCHECK(root_delegate);
AXPlatformNodeWin* old_selection_platform_node =
GetPlatformNodeFromAXNode(old_selection_node);
if (!old_selection_platform_node) {
return;
}
AXActionData blur_action;
blur_action.action = ax::mojom::Action::kBlur;
old_selection_platform_node->GetDelegate()->AccessibilityPerformAction(
blur_action);
}
}
AXPlatformNodeWin*
AXPlatformNodeTextRangeProviderWin::GetPlatformNodeFromAXNode(
const AXNode* node) const {
if (!node)
return nullptr;
// TODO(kschmi): Update to use AXTreeManager.
AXPlatformNodeWin* platform_node =
static_cast<AXPlatformNodeWin*>(AXPlatformNode::FromNativeViewAccessible(
GetDelegate(node->tree()->GetAXTreeID(), node->id())
->GetNativeViewAccessible()));
DCHECK(platform_node);
return platform_node;
}
AXPlatformNodeWin*
AXPlatformNodeTextRangeProviderWin::GetLowestAccessibleCommonPlatformNode()
const {
AXNode* common_anchor = start()->LowestCommonAnchor(*end());
if (!common_anchor)
return nullptr;
return GetPlatformNodeFromAXNode(common_anchor)
->GetLowestAccessibleElementForUIA();
}
bool AXPlatformNodeTextRangeProviderWin::
HasTextRangeOrSelectionInAtomicTextField(
const AXPositionInstance& start_position,
const AXPositionInstance& end_position) const {
// This condition fixes issues when the caret is inside an atomic text field,
// but causes more issues when used inside of a non-atomic text field. An
// atomic text field does not expose its internal implementation to assistive
// software, appearing as a single leaf node in the accessibility tree. It
// includes <input>, <textarea> and Views-based text fields.
//
// For this reason, if we have a caret or a selection inside of an editable
// node, restrict this to an atomic text field as we gain nothing from using
// it in a non-atomic text field.
//
// Note that "AXPlatformNodeDelegate::IsDescendantOfAtomicTextField()" also
// returns true when this node is at the root of an atomic text field, i.e.
// the node could either be a descendant or it could be equivalent to the
// field's root node.
bool is_start_in_text_field =
start_position->GetAnchor()->IsDescendantOfAtomicTextField();
bool is_end_in_text_field =
end_position->GetAnchor()->IsDescendantOfAtomicTextField();
AXPlatformNodeDelegate* start_delegate = GetDelegate(start_position.get());
AXPlatformNodeDelegate* end_delegate = GetDelegate(start_position.get());
// Return true when both ends of a text range are inside the atomic
// text field (e.g. a caret perceived by the AT), or when either endpoint of
// the AXSelection is inside the atomic text field.
return (is_start_in_text_field && is_end_in_text_field) ||
(is_start_in_text_field && start_delegate &&
start_delegate->HasVisibleCaretOrSelection()) ||
(is_end_in_text_field && end_delegate &&
end_delegate->HasVisibleCaretOrSelection());
}
// static
bool AXPlatformNodeTextRangeProviderWin::TextAttributeIsArrayType(
TEXTATTRIBUTEID attribute_id) {
// https://docs.microsoft.com/en-us/windows/win32/winauto/uiauto-textattribute-ids
return attribute_id == UIA_AnnotationObjectsAttributeId ||
attribute_id == UIA_AnnotationTypesAttributeId ||
attribute_id == UIA_TabsAttributeId;
}
// static
bool AXPlatformNodeTextRangeProviderWin::TextAttributeIsUiaReservedValue(
const base::win::VariantVector& vector) {
// Reserved values are always IUnknown.
if (vector.Type() != VT_UNKNOWN)
return false;
base::win::ScopedVariant mixed_attribute_value_variant;
{
Microsoft::WRL::ComPtr<IUnknown> mixed_attribute_value;
HRESULT hr = ::UiaGetReservedMixedAttributeValue(&mixed_attribute_value);
DCHECK(SUCCEEDED(hr));
mixed_attribute_value_variant.Set(mixed_attribute_value.Get());
}
base::win::ScopedVariant not_supported_value_variant;
{
Microsoft::WRL::ComPtr<IUnknown> not_supported_value;
HRESULT hr = ::UiaGetReservedNotSupportedValue(¬_supported_value);
DCHECK(SUCCEEDED(hr));
not_supported_value_variant.Set(not_supported_value.Get());
}
return !vector.Compare(mixed_attribute_value_variant) ||
!vector.Compare(not_supported_value_variant);
}
// static
bool AXPlatformNodeTextRangeProviderWin::ShouldReleaseTextAttributeAsSafearray(
TEXTATTRIBUTEID attribute_id,
const base::win::VariantVector& attribute_value) {
// |vector| may be pre-populated with a UIA reserved value. In such a case, we
// must release as a scalar variant.
return TextAttributeIsArrayType(attribute_id) &&
!TextAttributeIsUiaReservedValue(attribute_value);
}
AXPlatformNodeTextRangeProviderWin::TextRangeEndpoints::TextRangeEndpoints() {
start_ = AXNodePosition::CreateNullPosition();
end_ = AXNodePosition::CreateNullPosition();
}
AXPlatformNodeTextRangeProviderWin::TextRangeEndpoints::~TextRangeEndpoints() {
SetStart(AXNodePosition::CreateNullPosition());
SetEnd(AXNodePosition::CreateNullPosition());
}
const AXPlatformNodeTextRangeProviderWin::AXPositionInstance&
AXPlatformNodeTextRangeProviderWin::TextRangeEndpoints::GetStart() {
ValidateEndpointsAfterNodeDeletionIfNeeded();
return start_;
}
const AXPlatformNodeTextRangeProviderWin::AXPositionInstance&
AXPlatformNodeTextRangeProviderWin::TextRangeEndpoints::GetEnd() {
ValidateEndpointsAfterNodeDeletionIfNeeded();
return end_;
}
void AXPlatformNodeTextRangeProviderWin::TextRangeEndpoints::SetStart(
AXPositionInstance new_start) {
bool did_tree_change = start_->tree_id() != new_start->tree_id();
// TODO(bebeaudr): We can't use IsNullPosition() here because of
// https://crbug.com/1152939. Once this is fixed, we can go back to
// IsNullPosition().
if (did_tree_change && start_->kind() != AXPositionKind::NULL_POSITION &&
start_->tree_id() != end_->tree_id()) {
RemoveObserver(start_->tree_id());
}
start_ = std::move(new_start);
if (did_tree_change && !start_->IsNullPosition() &&
start_->tree_id() != end_->tree_id()) {
AddObserver(start_->tree_id());
}
}
void AXPlatformNodeTextRangeProviderWin::TextRangeEndpoints::SetEnd(
AXPositionInstance new_end) {
bool did_tree_change = end_->tree_id() != new_end->tree_id();
// TODO(bebeaudr): We can't use IsNullPosition() here because of
// https://crbug.com/1152939. Once this is fixed, we can go back to
// IsNullPosition().
if (did_tree_change && end_->kind() != AXPositionKind::NULL_POSITION &&
end_->tree_id() != start_->tree_id()) {
RemoveObserver(end_->tree_id());
}
end_ = std::move(new_end);
if (did_tree_change && !end_->IsNullPosition() &&
start_->tree_id() != end_->tree_id()) {
AddObserver(end_->tree_id());
}
}
void AXPlatformNodeTextRangeProviderWin::TextRangeEndpoints::AddObserver(
const AXTreeID tree_id) {
AXTreeManager* ax_tree_manager = AXTreeManager::FromID(tree_id);
DCHECK(ax_tree_manager);
ax_tree_manager->ax_tree()->AddObserver(this);
}
void AXPlatformNodeTextRangeProviderWin::TextRangeEndpoints::RemoveObserver(
const AXTreeID tree_id) {
AXTreeManager* ax_tree_manager = AXTreeManager::FromID(tree_id);
if (ax_tree_manager)
ax_tree_manager->ax_tree()->RemoveObserver(this);
}
void AXPlatformNodeTextRangeProviderWin::TextRangeEndpoints::
OnStringAttributeChanged(AXTree* tree,
AXNode* node,
ax::mojom::StringAttribute attr,
const std::string& old_value,
const std::string& new_value) {
if (attr != ax::mojom::StringAttribute::kName ||
new_value.length() >= old_value.length()) {
return;
}
if (!start_->IsNullPosition() &&
start_->tree_id() == node->tree()->GetAXTreeID() &&
start_->anchor_id() == node->id()) {
start_->SnapToMaxTextOffsetIfBeyond();
}
if (!end_->IsNullPosition() &&
end_->tree_id() == node->tree()->GetAXTreeID() &&
end_->anchor_id() == node->id()) {
end_->SnapToMaxTextOffsetIfBeyond();
}
}
// Ensures that our endpoints are located on non-deleted nodes (step 1, case A
// and B). See comment in header file for more details.
void AXPlatformNodeTextRangeProviderWin::TextRangeEndpoints::
OnSubtreeWillBeDeleted(AXTree* tree, AXNode* node) {
// If an endpoint is on a node that is included in a subtree that is about to
// be deleted, move endpoint up to the parent of the deleted subtree's root
// since we want to ensure that the endpoints of a text range provider are
// always valid positions. Otherwise, the range will be stuck on nodes that
// don't exist anymore.
DCHECK(tree);
DCHECK(node);
DCHECK_EQ(tree->GetAXTreeID(), node->tree()->GetAXTreeID());
// Validate now if we haven't done so yet.
ValidateEndpointsAfterNodeDeletionIfNeeded();
AdjustEndpointForSubtreeDeletion(tree, node, true /* is_start_endpoint */);
AdjustEndpointForSubtreeDeletion(tree, node, false /* is_start_endpoint */);
}
void AXPlatformNodeTextRangeProviderWin::TextRangeEndpoints::
AdjustEndpointForSubtreeDeletion(AXTree* tree,
const AXNode* const node,
bool is_start_endpoint) {
AXPositionInstance endpoint =
is_start_endpoint ? start_->Clone() : end_->Clone();
if (tree->GetAXTreeID() != endpoint->tree_id())
return;
// When the subtree of the root node will be deleted, we can be certain that
// our endpoint should be invalidated. We know it's the root node when the
// node doesn't have a parent.
AXNode* endpoint_anchor = endpoint->GetAnchor();
if (!node->GetParent() || !endpoint_anchor) {
is_start_endpoint ? SetStart(AXNodePosition::CreateNullPosition())
: SetEnd(AXNodePosition::CreateNullPosition());
return;
}
DeletionOfInterest deletion_of_interest = {tree->GetAXTreeID(), node->id()};
// If the root of subtree being deleted is a child of the anchor of the
// endpoint, ensure `AXPosition::AsValidPosition` is called after the node is
// deleted so that the index doesn't go out of bounds of the child array.
if (endpoint->kind() == AXPositionKind::TREE_POSITION &&
endpoint_anchor == node->GetParent()) {
if (is_start_endpoint)
validation_necessary_for_start_ = deletion_of_interest;
else
validation_necessary_for_end_ = deletion_of_interest;
return;
}
// Fast check for the common case - there are many tree updates and the
// endpoints probably are not in the deleted subtree. Note that
// CreateAncestorPosition/GetParentPosition can be expensive for text
// positions.
if (!endpoint_anchor->IsDescendantOfCrossingTreeBoundary(node))
return;
AXPositionInstance new_endpoint = endpoint->CreateAncestorPosition(
node, ax::mojom::MoveDirection::kForward);
// Obviously, we want the position to be on the parent of |node| and not on
// |node| itself since it's about to be deleted.
new_endpoint = new_endpoint->CreateParentPosition();
AXPositionInstance other_endpoint =
is_start_endpoint ? end_->Clone() : start_->Clone();
// Convert |new_endpoint| and |other_endpoint| to unignored positions to avoid
// AXPosition::SlowCompareTo in the < operator below.
NormalizeAsUnignoredPosition(new_endpoint);
NormalizeAsUnignoredPosition(other_endpoint);
DCHECK(!new_endpoint->IsIgnored());
DCHECK(!other_endpoint->IsIgnored());
// If after all the above operations we're still left with a new endpoint that
// is a descendant of the subtree root being deleted, just point at a null
// position and don't crash later on. This can happen when the entire parent
// chain of the subtree is ignored.
endpoint_anchor = new_endpoint->GetAnchor();
if (!endpoint_anchor ||
endpoint_anchor->IsDescendantOfCrossingTreeBoundary(node))
new_endpoint = AXNodePosition::CreateNullPosition();
// Create a degenerate range at the new position if we have an inverted range
// - which occurs when the |end_| comes before the |start_|. This could have
// happened due to the new endpoint walking forwards or backwards when
// normalizing above. If we don't set the opposite endpoint to something that
// we know will be safe (i.e. not in a deleted subtree) we'll crash later on
// when trying to create a valid position.
other_endpoint->SnapToMaxTextOffsetIfBeyond();
new_endpoint->SnapToMaxTextOffsetIfBeyond();
if (is_start_endpoint) {
if (*other_endpoint < *new_endpoint)
SetEnd(new_endpoint->Clone());
SetStart(std::move(new_endpoint));
validation_necessary_for_start_ = deletion_of_interest;
} else {
if (*new_endpoint < *other_endpoint)
SetStart(new_endpoint->Clone());
SetEnd(std::move(new_endpoint));
validation_necessary_for_end_ = deletion_of_interest;
}
}
// Ensures that our endpoints are always valid (step 2, all scenarios). See
// comment in header file for more details.
void AXPlatformNodeTextRangeProviderWin::TextRangeEndpoints::OnNodeDeleted(
AXTree* tree,
AXNodeID node_id) {
DCHECK(tree);
// We only need validation in the case where a deleted node matches the
// previously stored |validation_necessary_for_*|. If this is the case,
// mark this any needed so that we force validation before using the endpoint.
if (validation_necessary_for_start_.has_value() &&
validation_necessary_for_start_->tree_id == tree->GetAXTreeID() &&
validation_necessary_for_start_->node_id == node_id) {
validation_necessary_for_start_->validation_needed = true;
}
if (validation_necessary_for_end_.has_value() &&
validation_necessary_for_end_->tree_id == tree->GetAXTreeID() &&
validation_necessary_for_end_->node_id == node_id) {
validation_necessary_for_end_->validation_needed = true;
}
}
void AXPlatformNodeTextRangeProviderWin::TextRangeEndpoints::
OnTreeManagerWillBeRemoved(AXTreeID previous_tree_id) {
if (start_->tree_id() == previous_tree_id ||
end_->tree_id() == previous_tree_id) {
RemoveObserver(previous_tree_id);
}
}
void AXPlatformNodeTextRangeProviderWin::TextRangeEndpoints::
OnTextDeletionOrInsertion(const AXNode& node, const AXNodeData& new_data) {
DCHECK(new_data.IsTextField());
const std::vector<int>& start_offsets = new_data.GetIntListAttribute(
ax::mojom::IntListAttribute::kTextOperationStartOffsets);
const std::vector<int>& end_offsets = new_data.GetIntListAttribute(
ax::mojom::IntListAttribute::kTextOperationEndOffsets);
const std::vector<int>& start_ids = new_data.GetIntListAttribute(
ax::mojom::IntListAttribute::kTextOperationStartAnchorIds);
const std::vector<int>& end_ids = new_data.GetIntListAttribute(
ax::mojom::IntListAttribute::kTextOperationEndAnchorIds);
const std::vector<int>& op_vector = new_data.GetIntListAttribute(
ax::mojom::IntListAttribute::kTextOperations);
// Each position in the vectors corresponds to a single operation, and these
// positions across the vectors correspond to each other. As such the vectors
// must always be the same size.
DCHECK(start_offsets.size() == end_offsets.size());
DCHECK(start_offsets.size() == start_ids.size());
DCHECK(start_offsets.size() == end_ids.size());
DCHECK(start_offsets.size() == op_vector.size());
AXTreeManager* manager = node.GetManager();
DCHECK(manager);
for (size_t i = 0; i < start_offsets.size(); i++) {
int edit_start = start_offsets[i];
int edit_end = end_offsets[i];
int edit_start_id = start_ids[i];
int edit_end_id = end_ids[i];
ax::mojom::Command op = static_cast<ax::mojom::Command>(op_vector[i]);
DCHECK(op == ax::mojom::Command::kDelete ||
op == ax::mojom::Command::kInsert);
AXNode* edit_start_node = manager->GetNode(edit_start_id);
AXNode* edit_end_node = manager->GetNode(edit_end_id);
AdjustEndpointForTextFieldEdit(node, GetStart(), edit_start_node,
edit_end_node, edit_start, edit_end,
/* is_start */ true, op);
AdjustEndpointForTextFieldEdit(node, GetEnd(), edit_start_node,
edit_end_node, edit_start, edit_end,
/* is_start */ false, op);
}
}
void AXPlatformNodeTextRangeProviderWin::TextRangeEndpoints::
AdjustEndpointForTextFieldEdit(const AXNode& text_field_node,
const AXPositionInstance& current_position,
AXNode* edit_start_anchor,
AXNode* edit_end_anchor,
int edit_start,
int edit_end,
bool is_start,
ax::mojom::Command op) {
if (!edit_start_anchor || !edit_end_anchor ||
!current_position->GetAnchor()) {
return;
}
DCHECK(op == ax::mojom::Command::kDelete ||
op == ax::mojom::Command::kInsert);
// At this point there are three possibilities for the relevant deletion or
// insertion anchors: Either both the start and the end are relevant, only
// one is, or neither. There are two conditions as to why one might not be
// relevant:
// The deletion/insertion anchor is different from the endpoint's anchor
// AND one is not a descendant of the other.
// If these conditions are met, the deletion/insertion would be
// considered irrelevant for this endpoint since it would not affect the text
// offset. First we check if the insertion or deletion start is relevant:
bool start_is_relevant = true;
bool end_is_relevant = true;
if (current_position->GetAnchor()->id() != edit_start_anchor->id() &&
!current_position->GetAnchor()->IsDescendantOf(edit_start_anchor) &&
!edit_start_anchor->IsDescendantOf(current_position->GetAnchor())) {
start_is_relevant = false;
}
// Then we check if the end is relevant.
if (current_position->GetAnchor()->id() != edit_end_anchor->id() &&
!current_position->GetAnchor()->IsDescendantOf(edit_end_anchor) &&
!edit_end_anchor->IsDescendantOf(current_position->GetAnchor())) {
end_is_relevant = false;
}
// If neither is relevant, then the offset will be unaffected.
if (!start_is_relevant && !end_is_relevant) {
return;
}
// Now we calculate the amount by which we'll have to modify the offset,
// depending on whether both the start are end are relevant, or only one of
// them. For example we could have this structure inside a contenteditable;
// <div contenteditable><span> hello world </span> sport team</div>
// and our text range could be "sport team" but the deletion range could be
// "world sport". As far as our text range is concerned, only the deletion of
// "sport" is relevant.
int difference_or_increase = edit_end - edit_start;
if (!start_is_relevant && end_is_relevant) {
difference_or_increase = edit_end;
} else if (start_is_relevant && !end_is_relevant) {
difference_or_increase = edit_start;
}
bool deletion_encompasses_endpoint = false;
if (op == ax::mojom::Command::kInsert) {
// The + 1 is needed since if one character is inserted,
// the offsets for the insertion will both be just the position of the new
// character.
difference_or_increase = edit_end - edit_start + 1;
} else {
difference_or_increase *= -1;
// We now create ancestor positions on the text field node for the deletion
// start, end, and the current position. This is so that we can determine if
// the deletion encompasses the current position, this is a scenario we need
// to account for.
AXPositionInstance deletion_start_position =
AXNodePosition::CreateTextPosition(*edit_start_anchor, edit_start,
current_position->affinity())
->CreateAncestorPosition(&text_field_node,
ax::mojom::MoveDirection::kForward);
AXPositionInstance deletion_end_position =
AXNodePosition::CreateTextPosition(*edit_end_anchor, edit_end,
current_position->affinity())
->CreateAncestorPosition(&text_field_node,
ax::mojom::MoveDirection::kForward);
AXPositionInstance current_text_field_position =
current_position->CreateAncestorPosition(
&text_field_node, ax::mojom::MoveDirection::kForward);
deletion_encompasses_endpoint =
deletion_start_position->text_offset() <=
current_text_field_position->text_offset() &&
deletion_end_position->text_offset() >=
current_text_field_position->text_offset();
}
if (edit_start < current_position->text_offset()) {
if (deletion_encompasses_endpoint) {
if (is_start) {
SetStart(AXNodePosition::CreateNullPosition());
return;
}
SetEnd(AXNodePosition::CreateNullPosition());
return;
}
if (is_start) {
SetStart(AXNodePosition::CreateTextPosition(
*current_position->GetAnchor(),
current_position->text_offset() + difference_or_increase,
current_position->affinity()));
return;
}
SetEnd(AXNodePosition::CreateTextPosition(
*current_position->GetAnchor(),
current_position->text_offset() + difference_or_increase,
current_position->affinity()));
return;
}
if (is_start) {
SetStart(current_position->Clone());
} else {
SetEnd(current_position->Clone());
}
}
// Ensures that our endpoints are always valid (step 2, all scenarios). See
// comment in header file for more details.
void AXPlatformNodeTextRangeProviderWin::TextRangeEndpoints::
ValidateEndpointsAfterNodeDeletionIfNeeded() {
if (validation_necessary_for_start_.has_value() &&
validation_necessary_for_start_->validation_needed) {
if (!start_->IsNullPosition() && start_->GetAnchor()->IsDataValid()) {
SetStart(start_->AsValidPosition());
} else {
SetStart(AXNodePosition::CreateNullPosition());
}
validation_necessary_for_start_ = std::nullopt;
}
if (validation_necessary_for_end_.has_value() &&
validation_necessary_for_end_->validation_needed) {
if (!end_->IsNullPosition() && end_->GetAnchor()->IsDataValid()) {
SetEnd(end_->AsValidPosition());
} else {
SetEnd(AXNodePosition::CreateNullPosition());
}
validation_necessary_for_end_ = std::nullopt;
}
}
} // namespace ui