From adb78300dc587b71091937e74fd69966f65aae4b Mon Sep 17 00:00:00 2001 From: schectman Date: Wed, 28 Dec 2022 12:27:54 -0500 Subject: [PATCH 01/17] Import files from Chromium as-is --- .../ax_platform_node_textprovider_win.cc | 380 + .../ax_platform_node_textprovider_win.h | 79 + ...platform_node_textprovider_win_unittest.cc | 964 +++ .../ax_platform_node_textrangeprovider_win.cc | 1825 ++++ .../ax_platform_node_textrangeprovider_win.h | 292 + ...orm_node_textrangeprovider_win_unittest.cc | 7621 +++++++++++++++++ .../ax/platform/ax_platform_tree_manager.h | 39 + 7 files changed, 11200 insertions(+) create mode 100644 third_party/accessibility/ax/platform/ax_platform_node_textprovider_win.cc create mode 100644 third_party/accessibility/ax/platform/ax_platform_node_textprovider_win.h create mode 100644 third_party/accessibility/ax/platform/ax_platform_node_textprovider_win_unittest.cc create mode 100644 third_party/accessibility/ax/platform/ax_platform_node_textrangeprovider_win.cc create mode 100644 third_party/accessibility/ax/platform/ax_platform_node_textrangeprovider_win.h create mode 100644 third_party/accessibility/ax/platform/ax_platform_node_textrangeprovider_win_unittest.cc create mode 100644 third_party/accessibility/ax/platform/ax_platform_tree_manager.h diff --git a/third_party/accessibility/ax/platform/ax_platform_node_textprovider_win.cc b/third_party/accessibility/ax/platform/ax_platform_node_textprovider_win.cc new file mode 100644 index 0000000000000..cc3f98bfa4afc --- /dev/null +++ b/third_party/accessibility/ax/platform/ax_platform_node_textprovider_win.cc @@ -0,0 +1,380 @@ +// 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. + +#include "ui/accessibility/platform/ax_platform_node_textprovider_win.h" + +#include + +#include "base/win/scoped_safearray.h" +#include "ui/accessibility/ax_node_position.h" +#include "ui/accessibility/ax_selection.h" +#include "ui/accessibility/platform/ax_platform_node_base.h" +#include "ui/accessibility/platform/ax_platform_node_delegate.h" +#include "ui/accessibility/platform/ax_platform_node_textrangeprovider_win.h" + +#define UIA_VALIDATE_TEXTPROVIDER_CALL() \ + if (!owner()->GetDelegate()) \ + return UIA_E_ELEMENTNOTAVAILABLE; +#define UIA_VALIDATE_TEXTPROVIDER_CALL_1_ARG(arg) \ + if (!owner()->GetDelegate()) \ + return UIA_E_ELEMENTNOTAVAILABLE; \ + if (!arg) \ + return E_INVALIDARG; + +namespace ui { + +AXPlatformNodeTextProviderWin::AXPlatformNodeTextProviderWin() { + DVLOG(1) << __func__; +} + +AXPlatformNodeTextProviderWin::~AXPlatformNodeTextProviderWin() {} + +// static +AXPlatformNodeTextProviderWin* AXPlatformNodeTextProviderWin::Create( + AXPlatformNodeWin* owner) { + CComObject* text_provider = nullptr; + if (SUCCEEDED(CComObject::CreateInstance( + &text_provider))) { + DCHECK(text_provider); + text_provider->owner_ = owner; + text_provider->AddRef(); + return text_provider; + } + + return nullptr; +} + +// static +void AXPlatformNodeTextProviderWin::CreateIUnknown(AXPlatformNodeWin* owner, + IUnknown** unknown) { + Microsoft::WRL::ComPtr text_provider( + Create(owner)); + if (text_provider) + *unknown = text_provider.Detach(); +} + +// +// ITextProvider methods. +// + +HRESULT AXPlatformNodeTextProviderWin::GetSelection(SAFEARRAY** selection) { + WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXT_GETSELECTION); + UIA_VALIDATE_TEXTPROVIDER_CALL(); + + *selection = nullptr; + + AXPlatformNodeDelegate* delegate = owner()->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); + + // anchor_offset corresponds to the selection start index + // and focus_offset is where the selection ends. + auto start_offset = unignored_selection.anchor_offset; + auto end_offset = unignored_selection.focus_offset; + + // If there's no selected object, return success and don't fill the SAFEARRAY. + if (!anchor_object || !focus_object) + return S_OK; + + AXNodePosition::AXPositionInstance start = + anchor_object->GetDelegate()->CreatePositionAt(start_offset); + AXNodePosition::AXPositionInstance end = + focus_object->GetDelegate()->CreatePositionAt(end_offset); + + DCHECK(!start->IsNullPosition()); + DCHECK(!end->IsNullPosition()); + + // Reverse start and end if the selection goes backwards + if (*start > *end) + std::swap(start, end); + + Microsoft::WRL::ComPtr text_range_provider = + AXPlatformNodeTextRangeProviderWin::CreateTextRangeProvider( + std::move(start), std::move(end)); + if (&text_range_provider == nullptr) + return E_OUTOFMEMORY; + + // Since we don't support disjoint text ranges, the SAFEARRAY returned + // will always have one element + base::win::ScopedSafearray selections_to_return( + SafeArrayCreateVector(VT_UNKNOWN /* element type */, 0 /* lower bound */, + 1 /* number of elements */)); + + if (!selections_to_return.Get()) + return E_OUTOFMEMORY; + + LONG index = 0; + HRESULT hr = SafeArrayPutElement(selections_to_return.Get(), &index, + text_range_provider.Get()); + DCHECK(SUCCEEDED(hr)); + + // Since DCHECK only happens in debug builds, return immediately to ensure + // that we're not leaking the SAFEARRAY on release builds + if (FAILED(hr)) + return E_FAIL; + + *selection = selections_to_return.Release(); + + return S_OK; +} + +HRESULT AXPlatformNodeTextProviderWin::GetVisibleRanges( + SAFEARRAY** visible_ranges) { + WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXT_GETVISIBLERANGES); + UIA_VALIDATE_TEXTPROVIDER_CALL(); + + const AXPlatformNodeDelegate* delegate = owner()->GetDelegate(); + + // Get the Clipped Frame Bounds of the current node, not from the root, + // so if this node is wrapped with overflow styles it will have the + // correct bounds + const gfx::Rect frame_rect = delegate->GetBoundsRect( + AXCoordinateSystem::kFrame, AXClippingBehavior::kClipped); + + const auto start = delegate->CreateTextPositionAt(0); + const auto end = start->CreatePositionAtEndOfAnchor(); + DCHECK(start->GetAnchor() == end->GetAnchor()); + + // SAFEARRAYs are not dynamic, so fill the visible ranges in a vector + // and then transfer to an appropriately-sized SAFEARRAY + std::vector> ranges; + + auto current_line_start = start->Clone(); + while (!current_line_start->IsNullPosition() && *current_line_start < *end) { + auto current_line_end = current_line_start->CreateNextLineEndPosition( + {AXBoundaryBehavior::kCrossBoundary, + AXBoundaryDetection::kDontCheckInitialPosition}); + if (current_line_end->IsNullPosition() || *current_line_end > *end) + current_line_end = end->Clone(); + + gfx::Rect current_rect = delegate->GetInnerTextRangeBoundsRect( + current_line_start->text_offset(), current_line_end->text_offset(), + AXCoordinateSystem::kFrame, AXClippingBehavior::kUnclipped); + + if (frame_rect.Contains(current_rect)) { + Microsoft::WRL::ComPtr text_range_provider = + AXPlatformNodeTextRangeProviderWin::CreateTextRangeProvider( + current_line_start->Clone(), current_line_end->Clone()); + + ranges.emplace_back(text_range_provider); + } + + current_line_start = current_line_start->CreateNextLineStartPosition( + {AXBoundaryBehavior::kCrossBoundary, + AXBoundaryDetection::kDontCheckInitialPosition}); + } + + base::win::ScopedSafearray scoped_visible_ranges( + SafeArrayCreateVector(VT_UNKNOWN /* element type */, 0 /* lower bound */, + ranges.size() /* number of elements */)); + + if (!scoped_visible_ranges.Get()) + return E_OUTOFMEMORY; + + LONG index = 0; + for (Microsoft::WRL::ComPtr& current_provider : ranges) { + HRESULT hr = SafeArrayPutElement(scoped_visible_ranges.Get(), &index, + current_provider.Get()); + DCHECK(SUCCEEDED(hr)); + + // Since DCHECK only happens in debug builds, return immediately to ensure + // that we're not leaking the SAFEARRAY on release builds + if (FAILED(hr)) + return E_FAIL; + + ++index; + } + + *visible_ranges = scoped_visible_ranges.Release(); + + return S_OK; +} + +HRESULT AXPlatformNodeTextProviderWin::RangeFromChild( + IRawElementProviderSimple* child, + ITextRangeProvider** range) { + WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXT_RANGEFROMCHILD); + UIA_VALIDATE_TEXTPROVIDER_CALL_1_ARG(child); + + *range = nullptr; + + Microsoft::WRL::ComPtr child_platform_node; + if (!SUCCEEDED(child->QueryInterface(IID_PPV_ARGS(&child_platform_node)))) + return UIA_E_INVALIDOPERATION; + + if (!owner()->IsDescendant(child_platform_node.Get())) + return E_INVALIDARG; + + *range = GetRangeFromChild(owner(), child_platform_node.Get()); + + return S_OK; +} + +HRESULT AXPlatformNodeTextProviderWin::RangeFromPoint( + UiaPoint uia_point, + ITextRangeProvider** range) { + WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXT_RANGEFROMPOINT); + WIN_ACCESSIBILITY_API_PERF_HISTOGRAM(UMA_API_TEXT_RANGEFROMPOINT); + UIA_VALIDATE_TEXTPROVIDER_CALL(); + *range = nullptr; + + gfx::Point point(uia_point.x, uia_point.y); + // Retrieve the closest accessibility node. No coordinate unit conversion is + // needed, hit testing input is also in screen coordinates. + + AXPlatformNodeWin* nearest_node = + static_cast(owner()->NearestLeafToPoint(point)); + DCHECK(nearest_node); + DCHECK(nearest_node->IsLeaf()); + + AXNodePosition::AXPositionInstance start, end; + start = nearest_node->GetDelegate()->CreateTextPositionAt( + nearest_node->NearestTextIndexToPoint(point)); + DCHECK(!start->IsNullPosition()); + end = start->Clone(); + + *range = AXPlatformNodeTextRangeProviderWin::CreateTextRangeProvider( + std::move(start), std::move(end)); + return S_OK; +} + +HRESULT AXPlatformNodeTextProviderWin::get_DocumentRange( + ITextRangeProvider** range) { + WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXT_GET_DOCUMENTRANGE); + UIA_VALIDATE_TEXTPROVIDER_CALL(); + + // Get range from child, where child is the current node. In other words, + // getting the text range of the current owner AxPlatformNodeWin node. + *range = GetRangeFromChild(owner(), owner()); + + return S_OK; +} + +HRESULT AXPlatformNodeTextProviderWin::get_SupportedTextSelection( + enum SupportedTextSelection* text_selection) { + WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXT_GET_SUPPORTEDTEXTSELECTION); + UIA_VALIDATE_TEXTPROVIDER_CALL(); + + *text_selection = SupportedTextSelection_Single; + return S_OK; +} + +// +// ITextEditProvider methods. +// + +HRESULT AXPlatformNodeTextProviderWin::GetActiveComposition( + ITextRangeProvider** range) { + WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXTEDIT_GETACTIVECOMPOSITION); + UIA_VALIDATE_TEXTPROVIDER_CALL(); + + *range = nullptr; + return GetTextRangeProviderFromActiveComposition(range); +} + +HRESULT AXPlatformNodeTextProviderWin::GetConversionTarget( + ITextRangeProvider** range) { + WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXTEDIT_GETCONVERSIONTARGET); + UIA_VALIDATE_TEXTPROVIDER_CALL(); + + *range = nullptr; + return GetTextRangeProviderFromActiveComposition(range); +} + +ITextRangeProvider* AXPlatformNodeTextProviderWin::GetRangeFromChild( + ui::AXPlatformNodeWin* ancestor, + ui::AXPlatformNodeWin* descendant) { + + DCHECK(ancestor); + DCHECK(descendant); + DCHECK(descendant->GetDelegate()); + DCHECK(ancestor->IsDescendant(descendant)); + + // Start and end should be leaf text positions that span the beginning and end + // of text content within a node. The start position should be the directly + // first child and the end position should be the deepest last child node. + AXNodePosition::AXPositionInstance start = + descendant->GetDelegate()->CreateTextPositionAt(0)->AsLeafTextPosition(); + + AXNodePosition::AXPositionInstance end; + if (descendant->IsPlatformDocument()) { + // Fast path for getting the range of the web or PDF root. + // If the last position is ignored, we need to get an unignored position + // otherwise future comparisons can end up with null positions (which in + // turn might collapse the range). Note that we move backwards, since there + // is no position after the end-of-content position (i.e. moving forward + // results in a null position). + end = start->CreatePositionAtEndOfContent()->AsUnignoredPosition( + AXPositionAdjustmentBehavior::kMoveBackward); + } else if (descendant->GetChildCount() == 0) { + end = descendant->GetDelegate() + ->CreateTextPositionAt(0) + ->CreatePositionAtEndOfAnchor() + ->AsLeafTextPosition(); + } else { + AXPlatformNodeBase* deepest_last_child = descendant->GetLastChild(); + while (deepest_last_child && deepest_last_child->GetChildCount() > 0) + deepest_last_child = deepest_last_child->GetLastChild(); + + end = deepest_last_child->GetDelegate() + ->CreateTextPositionAt(0) + ->CreatePositionAtEndOfAnchor() + ->AsLeafTextPosition(); + } + + return AXPlatformNodeTextRangeProviderWin::CreateTextRangeProvider( + std::move(start), std::move(end)); +} + +ITextRangeProvider* AXPlatformNodeTextProviderWin::CreateDegenerateRangeAtStart( + ui::AXPlatformNodeWin* node) { + DCHECK(node); + DCHECK(node->GetDelegate()); + + // Create a degenerate range positioned at the node's start. + AXNodePosition::AXPositionInstance start, end; + start = node->GetDelegate()->CreateTextPositionAt(0)->AsLeafTextPosition(); + end = start->Clone(); + return AXPlatformNodeTextRangeProviderWin::CreateTextRangeProvider( + std::move(start), std::move(end)); +} + +ui::AXPlatformNodeWin* AXPlatformNodeTextProviderWin::owner() const { + return owner_.Get(); +} + +HRESULT +AXPlatformNodeTextProviderWin::GetTextRangeProviderFromActiveComposition( + ITextRangeProvider** range) { + *range = nullptr; + // We fetch the start and end offset of an active composition only if + // this object has focus and TSF is in composition mode. + // The offsets here refer to the character positions in a plain text + // view of the DOM tree. Ex: if the active composition in an element + // has "abc" then the range will be (0,3) in both TSF and accessibility + if ((AXPlatformNode::FromNativeViewAccessible( + owner()->GetDelegate()->GetFocus()) == + static_cast(owner())) && + owner()->HasActiveComposition()) { + gfx::Range active_composition_offset = + owner()->GetActiveCompositionOffsets(); + AXNodePosition::AXPositionInstance start = + owner()->GetDelegate()->CreateTextPositionAt( + /*offset*/ active_composition_offset.start()); + AXNodePosition::AXPositionInstance end = + owner()->GetDelegate()->CreateTextPositionAt( + /*offset*/ active_composition_offset.end()); + + *range = AXPlatformNodeTextRangeProviderWin::CreateTextRangeProvider( + std::move(start), std::move(end)); + } + + return S_OK; +} + +} // namespace ui diff --git a/third_party/accessibility/ax/platform/ax_platform_node_textprovider_win.h b/third_party/accessibility/ax/platform/ax_platform_node_textprovider_win.h new file mode 100644 index 0000000000000..1ac4cc63dfa50 --- /dev/null +++ b/third_party/accessibility/ax/platform/ax_platform_node_textprovider_win.h @@ -0,0 +1,79 @@ +// 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. + +#ifndef UI_ACCESSIBILITY_PLATFORM_AX_PLATFORM_NODE_TEXTPROVIDER_WIN_H_ +#define UI_ACCESSIBILITY_PLATFORM_AX_PLATFORM_NODE_TEXTPROVIDER_WIN_H_ + +#include + +#include "base/component_export.h" +#include "ui/accessibility/platform/ax_platform_node_win.h" + +namespace ui { + +class COMPONENT_EXPORT(AX_PLATFORM) __declspec( + uuid("3e1c192b-4348-45ac-8eb6-4b58eeb3dcca")) AXPlatformNodeTextProviderWin + : public CComObjectRootEx, + public ITextEditProvider { + public: + BEGIN_COM_MAP(AXPlatformNodeTextProviderWin) + COM_INTERFACE_ENTRY(ITextProvider) + COM_INTERFACE_ENTRY(ITextEditProvider) + COM_INTERFACE_ENTRY(AXPlatformNodeTextProviderWin) + END_COM_MAP() + + AXPlatformNodeTextProviderWin(); + ~AXPlatformNodeTextProviderWin(); + + static AXPlatformNodeTextProviderWin* Create(AXPlatformNodeWin* owner); + static void CreateIUnknown(AXPlatformNodeWin* owner, IUnknown** unknown); + + // + // ITextProvider methods. + // + + IFACEMETHODIMP GetSelection(SAFEARRAY** selection) override; + + IFACEMETHODIMP GetVisibleRanges(SAFEARRAY** visible_ranges) override; + + IFACEMETHODIMP RangeFromChild(IRawElementProviderSimple* child, + ITextRangeProvider** range) override; + + IFACEMETHODIMP RangeFromPoint(UiaPoint point, + ITextRangeProvider** range) override; + + IFACEMETHODIMP get_DocumentRange(ITextRangeProvider** range) override; + + IFACEMETHODIMP get_SupportedTextSelection( + enum SupportedTextSelection* text_selection) override; + + // + // ITextEditProvider methods. + // + + IFACEMETHODIMP GetActiveComposition(ITextRangeProvider** range) override; + + IFACEMETHODIMP GetConversionTarget(ITextRangeProvider** range) override; + + // ITextProvider supporting methods. + + static ITextRangeProvider* GetRangeFromChild( + ui::AXPlatformNodeWin* ancestor, + ui::AXPlatformNodeWin* descendant); + + // Create a dengerate text range at the start of the specified node. + static ITextRangeProvider* CreateDegenerateRangeAtStart( + ui::AXPlatformNodeWin* node); + + private: + friend class AXPlatformNodeTextProviderTest; + ui::AXPlatformNodeWin* owner() const; + HRESULT GetTextRangeProviderFromActiveComposition(ITextRangeProvider** range); + + Microsoft::WRL::ComPtr owner_; +}; + +} // namespace ui + +#endif // UI_ACCESSIBILITY_PLATFORM_AX_PLATFORM_NODE_TEXTPROVIDER_WIN_H_ diff --git a/third_party/accessibility/ax/platform/ax_platform_node_textprovider_win_unittest.cc b/third_party/accessibility/ax/platform/ax_platform_node_textprovider_win_unittest.cc new file mode 100644 index 0000000000000..ed5619b6ffbaf --- /dev/null +++ b/third_party/accessibility/ax/platform/ax_platform_node_textprovider_win_unittest.cc @@ -0,0 +1,964 @@ +// 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. + +#include "base/memory/raw_ptr.h" +#include "ui/accessibility/platform/ax_platform_node_win_unittest.h" + +#include +#include + +#include + +#include "base/win/scoped_bstr.h" +#include "base/win/scoped_safearray.h" +#include "ui/accessibility/ax_action_data.h" +#include "ui/accessibility/platform/ax_fragment_root_win.h" +#include "ui/accessibility/platform/ax_platform_node_textprovider_win.h" +#include "ui/accessibility/platform/ax_platform_node_textrangeprovider_win.h" +#include "ui/accessibility/platform/test_ax_node_wrapper.h" + +using Microsoft::WRL::ComPtr; + +namespace ui { + +// Helper macros for UIAutomation HRESULT expectations +#define EXPECT_UIA_INVALIDOPERATION(expr) \ + EXPECT_EQ(static_cast(UIA_E_INVALIDOPERATION), (expr)) +#define EXPECT_INVALIDARG(expr) \ + EXPECT_EQ(static_cast(E_INVALIDARG), (expr)) + +class AXPlatformNodeTextProviderTest : public AXPlatformNodeWinTest { + public: + AXPlatformNodeTextProviderTest() = default; + ~AXPlatformNodeTextProviderTest() override = default; + AXPlatformNodeTextProviderTest(const AXPlatformNodeTextProviderTest&) = + delete; + AXPlatformNodeTextProviderTest& operator=( + const AXPlatformNodeTextProviderTest&) = delete; + + protected: + void SetOwner(AXPlatformNodeWin* owner, + ITextRangeProvider* destination_range) { + ComPtr destination_provider = destination_range; + ComPtr destination_provider_interal; + + destination_provider->QueryInterface( + IID_PPV_ARGS(&destination_provider_interal)); + destination_provider_interal->SetOwnerForTesting(owner); + } + AXPlatformNodeWin* GetOwner( + const AXPlatformNodeTextProviderWin* text_provider) { + return text_provider->owner_.Get(); + } + const AXNodePosition::AXPositionInstance& GetStart( + const AXPlatformNodeTextRangeProviderWin* text_range) { + return text_range->start(); + } + const AXNodePosition::AXPositionInstance& GetEnd( + const AXPlatformNodeTextRangeProviderWin* text_range) { + return text_range->end(); + } +}; + +TEST_F(AXPlatformNodeTextProviderTest, CreateDegenerateRangeFromStart) { + AXNodeData text1_data; + text1_data.id = 3; + text1_data.role = ax::mojom::Role::kStaticText; + text1_data.SetName("some text"); + + AXNodeData text2_data; + text2_data.id = 4; + text2_data.role = ax::mojom::Role::kStaticText; + text2_data.SetName("more text"); + + AXNodeData link_data; + link_data.id = 2; + link_data.role = ax::mojom::Role::kLink; + link_data.child_ids = {3, 4}; + + AXNodeData root_data; + root_data.id = 1; + root_data.role = ax::mojom::Role::kRootWebArea; + root_data.SetName("Document"); + root_data.child_ids = {2}; + + AXTreeUpdate update; + AXTreeData tree_data; + tree_data.tree_id = AXTreeID::CreateNewAXTreeID(); + update.tree_data = tree_data; + update.has_tree_data = true; + update.root_id = root_data.id; + update.nodes = {root_data, link_data, text1_data, text2_data}; + + Init(update); + AXNode* root_node = GetRoot(); + AXNode* link_node = root_node->children()[0]; + AXNode* text2_node = link_node->children()[1]; + AXPlatformNodeWin* owner = + static_cast(AXPlatformNodeFromNode(root_node)); + DCHECK(owner); + + ComPtr root_node_raw = + QueryInterfaceFromNode(root_node); + ComPtr link_node_raw = + QueryInterfaceFromNode(link_node); + ComPtr text2_node_raw = + QueryInterfaceFromNode(text2_node); + + ComPtr root_platform_node; + EXPECT_HRESULT_SUCCEEDED( + root_node_raw->QueryInterface(IID_PPV_ARGS(&root_platform_node))); + ComPtr link_platform_node; + EXPECT_HRESULT_SUCCEEDED( + link_node_raw->QueryInterface(IID_PPV_ARGS(&link_platform_node))); + ComPtr text2_platform_node; + EXPECT_HRESULT_SUCCEEDED( + text2_node_raw->QueryInterface(IID_PPV_ARGS(&text2_platform_node))); + + // Degenerate range created on root node should be: + // <>some textmore text + ComPtr text_range_provider = + AXPlatformNodeTextProviderWin::CreateDegenerateRangeAtStart( + root_platform_node.Get()); + SetOwner(owner, text_range_provider.Get()); + base::win::ScopedBstr text_content; + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->GetText(-1, text_content.Receive())); + EXPECT_EQ(0, wcscmp(text_content.Get(), L"")); + + ComPtr actual_range; + text_range_provider->QueryInterface(IID_PPV_ARGS(&actual_range)); + AXNodePosition::AXPositionInstance expected_start, expected_end; + expected_start = root_platform_node->GetDelegate()->CreateTextPositionAt(0); + expected_end = expected_start->Clone(); + EXPECT_EQ(*GetStart(actual_range.Get()), *expected_start); + EXPECT_EQ(*GetEnd(actual_range.Get()), *expected_end); + text_content.Release(); + + // Degenerate range created on link node should be: + // <>some textmore text + text_range_provider = + AXPlatformNodeTextProviderWin::CreateDegenerateRangeAtStart( + link_platform_node.Get()); + SetOwner(owner, text_range_provider.Get()); + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->GetText(-1, text_content.Receive())); + EXPECT_EQ(0, wcscmp(text_content.Get(), L"")); + text_range_provider->QueryInterface(IID_PPV_ARGS(&actual_range)); + EXPECT_EQ(*GetStart(actual_range.Get()), *expected_start); + EXPECT_EQ(*GetEnd(actual_range.Get()), *expected_end); + text_content.Release(); + + // Degenerate range created on more text node should be: + // some text<>more text + text_range_provider = + AXPlatformNodeTextProviderWin::CreateDegenerateRangeAtStart( + text2_platform_node.Get()); + SetOwner(owner, text_range_provider.Get()); + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->GetText(-1, text_content.Receive())); + EXPECT_EQ(0, wcscmp(text_content.Get(), L"")); + text_range_provider->QueryInterface(IID_PPV_ARGS(&actual_range)); + expected_start = text2_platform_node->GetDelegate()->CreateTextPositionAt(0); + expected_end = expected_start->Clone(); + EXPECT_EQ(*GetStart(actual_range.Get()), *expected_start); + EXPECT_EQ(*GetEnd(actual_range.Get()), *expected_end); + text_content.Release(); +} + +TEST_F(AXPlatformNodeTextProviderTest, ITextProviderRangeFromChild) { + AXNodeData text_data; + text_data.id = 2; + text_data.role = ax::mojom::Role::kStaticText; + text_data.SetName("some text"); + + AXNodeData empty_text_data; + empty_text_data.id = 3; + empty_text_data.role = ax::mojom::Role::kStaticText; + + AXNodeData root_data; + root_data.id = 1; + root_data.role = ax::mojom::Role::kRootWebArea; + root_data.SetName("Document"); + root_data.child_ids.push_back(2); + root_data.child_ids.push_back(3); + + AXTreeUpdate update; + AXTreeData tree_data; + tree_data.tree_id = AXTreeID::CreateNewAXTreeID(); + update.tree_data = tree_data; + update.has_tree_data = true; + update.root_id = root_data.id; + update.nodes.push_back(root_data); + update.nodes.push_back(text_data); + update.nodes.push_back(empty_text_data); + + Init(update); + + AXNode* root_node = GetRoot(); + AXNode* text_node = root_node->children()[0]; + AXNode* empty_text_node = root_node->children()[1]; + AXPlatformNodeWin* owner = + static_cast(AXPlatformNodeFromNode(root_node)); + DCHECK(owner); + + ComPtr root_node_raw = + QueryInterfaceFromNode(root_node); + ComPtr text_node_raw = + QueryInterfaceFromNode(text_node); + ComPtr empty_text_node_raw = + QueryInterfaceFromNode(empty_text_node); + + // Call RangeFromChild on the root with the text child passed in. + ComPtr text_provider; + EXPECT_HRESULT_SUCCEEDED( + root_node_raw->GetPatternProvider(UIA_TextPatternId, &text_provider)); + + ComPtr text_range_provider; + EXPECT_HRESULT_SUCCEEDED( + text_provider->RangeFromChild(text_node_raw.Get(), &text_range_provider)); + SetOwner(owner, text_range_provider.Get()); + + base::win::ScopedBstr text_content; + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->GetText(-1, text_content.Receive())); + EXPECT_EQ(0, wcscmp(text_content.Get(), L"some text")); + + // Now test that the reverse relation doesn't return a valid + // ITextRangeProvider, and instead returns E_INVALIDARG. + EXPECT_HRESULT_SUCCEEDED( + text_node_raw->GetPatternProvider(UIA_TextPatternId, &text_provider)); + + EXPECT_INVALIDARG( + text_provider->RangeFromChild(root_node_raw.Get(), &text_range_provider)); + + // Now test that a child with no text returns a degenerate range. + EXPECT_HRESULT_SUCCEEDED( + root_node_raw->GetPatternProvider(UIA_TextPatternId, &text_provider)); + + EXPECT_HRESULT_SUCCEEDED(text_provider->RangeFromChild( + empty_text_node_raw.Get(), &text_range_provider)); + SetOwner(owner, text_range_provider.Get()); + + base::win::ScopedBstr empty_text_content; + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->GetText(-1, empty_text_content.Receive())); + EXPECT_EQ(0, wcscmp(empty_text_content.Get(), L"")); + + // Test that passing in an object from a different instance of + // IRawElementProviderSimple than that of the valid text provider + // returns UIA_E_INVALIDOPERATION. + ComPtr other_root_node_raw; + MockIRawElementProviderSimple::CreateMockIRawElementProviderSimple( + &other_root_node_raw); + + EXPECT_HRESULT_SUCCEEDED( + root_node_raw->GetPatternProvider(UIA_TextPatternId, &text_provider)); + + EXPECT_UIA_INVALIDOPERATION(text_provider->RangeFromChild( + other_root_node_raw.Get(), &text_range_provider)); +} + +TEST_F(AXPlatformNodeTextProviderTest, + ITextProviderRangeFromChildMultipleChildren) { + const int ROOT_ID = 1; + const int DIALOG_ID = 2; + const int DIALOG_LABEL_ID = 3; + const int DIALOG_DESCRIPTION_ID = 4; + const int BUTTON_ID = 5; + const int BUTTON_IMG_ID = 6; + const int BUTTON_TEXT_ID = 7; + const int DIALOG_DETAIL_ID = 8; + + AXNodeData root; + root.id = ROOT_ID; + root.role = ax::mojom::Role::kRootWebArea; + root.SetName("Document"); + root.child_ids = {DIALOG_ID}; + + AXNodeData dialog; + dialog.id = DIALOG_ID; + dialog.role = ax::mojom::Role::kDialog; + dialog.child_ids = {DIALOG_LABEL_ID, DIALOG_DESCRIPTION_ID, BUTTON_ID, + DIALOG_DETAIL_ID}; + + AXNodeData dialog_label; + dialog_label.id = DIALOG_LABEL_ID; + dialog_label.role = ax::mojom::Role::kStaticText; + dialog_label.SetName("Dialog label."); + + AXNodeData dialog_description; + dialog_description.id = DIALOG_DESCRIPTION_ID; + dialog_description.role = ax::mojom::Role::kStaticText; + dialog_description.SetName("Dialog description."); + + AXNodeData button; + button.id = BUTTON_ID; + button.role = ax::mojom::Role::kButton; + button.child_ids = {BUTTON_IMG_ID, BUTTON_TEXT_ID}; + + AXNodeData button_img; + button_img.id = BUTTON_IMG_ID; + button_img.role = ax::mojom::Role::kImage; + + AXNodeData button_text; + button_text.id = BUTTON_TEXT_ID; + button_text.role = ax::mojom::Role::kStaticText; + button_text.SetName("ok."); + + AXNodeData dialog_detail; + dialog_detail.id = DIALOG_DETAIL_ID; + dialog_detail.role = ax::mojom::Role::kStaticText; + dialog_detail.SetName("Some more detail about dialog."); + + AXTreeUpdate update; + AXTreeData tree_data; + tree_data.tree_id = AXTreeID::CreateNewAXTreeID(); + update.tree_data = tree_data; + update.has_tree_data = true; + update.root_id = ROOT_ID; + update.nodes = {root, dialog, dialog_label, dialog_description, + button, button_img, button_text, dialog_detail}; + + Init(update); + + AXNode* root_node = GetRoot(); + AXNode* dialog_node = root_node->children()[0]; + AXPlatformNodeWin* owner = + static_cast(AXPlatformNodeFromNode(root_node)); + DCHECK(owner); + + ComPtr root_node_raw = + QueryInterfaceFromNode(root_node); + ComPtr dialog_node_raw = + QueryInterfaceFromNode(dialog_node); + + // Call RangeFromChild on the root with the dialog child passed in. + ComPtr text_provider; + EXPECT_HRESULT_SUCCEEDED( + root_node_raw->GetPatternProvider(UIA_TextPatternId, &text_provider)); + + ComPtr text_range_provider; + EXPECT_HRESULT_SUCCEEDED(text_provider->RangeFromChild(dialog_node_raw.Get(), + &text_range_provider)); + SetOwner(owner, text_range_provider.Get()); + + base::win::ScopedBstr text_content; + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->GetText(-1, text_content.Receive())); + EXPECT_EQ(base::WideToUTF16(text_content.Get()), + u"Dialog label.Dialog description.\n" + kEmbeddedCharacterAsString + + u"\nok.Some more detail " + u"about dialog."); + + // Check the reverse relationship that GetEnclosingElement on the text range + // gives back the dialog. + ComPtr enclosing_element; + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->GetEnclosingElement(&enclosing_element)); + EXPECT_EQ(enclosing_element.Get(), dialog_node_raw.Get()); +} + +TEST_F(AXPlatformNodeTextProviderTest, NearestTextIndexToPoint) { + AXNodeData text_data; + text_data.id = 2; + text_data.role = ax::mojom::Role::kInlineTextBox; + text_data.SetName("text"); + // spacing: "t-e-x---t-" + text_data.AddIntListAttribute(ax::mojom::IntListAttribute::kCharacterOffsets, + {2, 4, 8, 10}); + + AXNodeData root_data; + root_data.id = 1; + root_data.role = ax::mojom::Role::kRootWebArea; + root_data.relative_bounds.bounds = gfx::RectF(1, 1, 2, 2); + root_data.child_ids.push_back(2); + + Init(root_data, text_data); + + AXNode* root_node = GetRoot(); + AXNode* text_node = root_node->children()[0]; + + struct NearestTextIndexTestData { + raw_ptr node; + struct point_offset_expected_index_pair { + int point_offset_x; + int expected_index; + }; + std::vector test_data; + }; + NearestTextIndexTestData nodes[] = { + {text_node, + {{0, 0}, {2, 0}, {3, 1}, {4, 1}, {5, 2}, {8, 2}, {9, 3}, {10, 3}}}, + {root_node, + {{0, 0}, {2, 0}, {3, 0}, {4, 0}, {5, 0}, {8, 0}, {9, 0}, {10, 0}}}}; + for (auto data : nodes) { + ComPtr element_provider = + QueryInterfaceFromNode(data.node); + ComPtr text_provider; + EXPECT_HRESULT_SUCCEEDED(element_provider->GetPatternProvider( + UIA_TextPatternId, &text_provider)); + // get internal implementation to access helper for testing + ComPtr platform_text_provider; + EXPECT_HRESULT_SUCCEEDED( + text_provider->QueryInterface(IID_PPV_ARGS(&platform_text_provider))); + + ComPtr platform_node; + EXPECT_HRESULT_SUCCEEDED( + element_provider->QueryInterface(IID_PPV_ARGS(&platform_node))); + + for (auto pair : data.test_data) { + EXPECT_EQ(pair.expected_index, platform_node->NearestTextIndexToPoint( + gfx::Point(pair.point_offset_x, 0))); + } + } +} + +TEST_F(AXPlatformNodeTextProviderTest, ITextProviderDocumentRange) { + AXNodeData text_data; + text_data.id = 2; + text_data.role = ax::mojom::Role::kStaticText; + text_data.SetName("some text"); + + AXNodeData root_data; + root_data.id = 1; + root_data.role = ax::mojom::Role::kRootWebArea; + root_data.SetName("Document"); + root_data.child_ids.push_back(2); + + Init(root_data, text_data); + + ComPtr root_node = + GetRootIRawElementProviderSimple(); + + ComPtr text_provider; + EXPECT_HRESULT_SUCCEEDED( + root_node->GetPatternProvider(UIA_TextPatternId, &text_provider)); + + ComPtr text_range_provider; + EXPECT_HRESULT_SUCCEEDED( + text_provider->get_DocumentRange(&text_range_provider)); +} + +TEST_F(AXPlatformNodeTextProviderTest, + ITextProviderDocumentRangeTrailingIgnored) { + // ++1 root + // ++++2 kGenericContainer + // ++++++3 kStaticText "Hello" + // ++++4 kGenericContainer + // ++++++5 kGenericContainer + // ++++++++6 kStaticText "3.14" + // ++++7 kGenericContainer (ignored) + // ++++++8 kGenericContainer (ignored) + // ++++++++9 kStaticText "ignored" + AXNodeData root_1; + AXNodeData gc_2; + AXNodeData static_text_3; + AXNodeData gc_4; + AXNodeData gc_5; + AXNodeData static_text_6; + AXNodeData gc_7_ignored; + AXNodeData gc_8_ignored; + AXNodeData static_text_9_ignored; + + root_1.id = 1; + gc_2.id = 2; + static_text_3.id = 3; + gc_4.id = 4; + gc_5.id = 5; + static_text_6.id = 6; + gc_7_ignored.id = 7; + gc_8_ignored.id = 8; + static_text_9_ignored.id = 9; + + root_1.role = ax::mojom::Role::kRootWebArea; + root_1.child_ids = {gc_2.id, gc_4.id, gc_7_ignored.id}; + root_1.SetName("Document"); + + gc_2.role = ax::mojom::Role::kGenericContainer; + gc_2.AddIntListAttribute(ax::mojom::IntListAttribute::kLabelledbyIds, + {static_text_3.id}); + gc_2.child_ids = {static_text_3.id}; + + static_text_3.role = ax::mojom::Role::kStaticText; + static_text_3.SetName("Hello"); + + gc_4.role = ax::mojom::Role::kGenericContainer; + gc_4.AddIntListAttribute(ax::mojom::IntListAttribute::kLabelledbyIds, + {gc_5.id}); + gc_4.child_ids = {gc_5.id}; + + gc_5.role = ax::mojom::Role::kGenericContainer; + gc_5.child_ids = {static_text_6.id}; + + static_text_6.role = ax::mojom::Role::kStaticText; + static_text_6.SetName("3.14"); + + gc_7_ignored.role = ax::mojom::Role::kGenericContainer; + gc_7_ignored.child_ids = {gc_8_ignored.id}; + gc_7_ignored.AddState(ax::mojom::State::kIgnored); + + gc_8_ignored.role = ax::mojom::Role::kGenericContainer; + gc_8_ignored.child_ids = {static_text_9_ignored.id}; + gc_8_ignored.AddState(ax::mojom::State::kIgnored); + + static_text_9_ignored.role = ax::mojom::Role::kStaticText; + static_text_9_ignored.SetName("ignored"); + static_text_9_ignored.AddState(ax::mojom::State::kIgnored); + + AXTreeUpdate update; + AXTreeData tree_data; + tree_data.tree_id = AXTreeID::CreateNewAXTreeID(); + update.tree_data = tree_data; + update.has_tree_data = true; + update.root_id = root_1.id; + update.nodes = {root_1, gc_2, static_text_3, + gc_4, gc_5, static_text_6, + gc_7_ignored, gc_8_ignored, static_text_9_ignored}; + + Init(update); + + ComPtr root_node = + GetRootIRawElementProviderSimple(); + + ComPtr text_provider; + EXPECT_HRESULT_SUCCEEDED( + root_node->GetPatternProvider(UIA_TextPatternId, &text_provider)); + + ComPtr text_range_provider; + EXPECT_HRESULT_SUCCEEDED( + text_provider->get_DocumentRange(&text_range_provider)); + + ComPtr text_range; + text_range_provider->QueryInterface(IID_PPV_ARGS(&text_range)); + + ComPtr root_text_provider; + EXPECT_HRESULT_SUCCEEDED( + root_node->GetPatternProvider(UIA_TextPatternId, &root_text_provider)); + ComPtr root_platform_node; + root_text_provider->QueryInterface(IID_PPV_ARGS(&root_platform_node)); + AXPlatformNodeWin* owner = GetOwner(root_platform_node.Get()); + + AXNodePosition::AXPositionInstance expected_start = + owner->GetDelegate()->CreateTextPositionAt(0)->AsLeafTextPosition(); + AXNodePosition::AXPositionInstance expected_end = + owner->GetDelegate() + ->CreateTextPositionAt(0) + ->CreatePositionAtEndOfAnchor() + ->AsLeafTextPosition(); + EXPECT_EQ(*GetStart(text_range.Get()), *expected_start); + EXPECT_EQ(*GetEnd(text_range.Get()), *expected_end); +} + +TEST_F(AXPlatformNodeTextProviderTest, ITextProviderDocumentRangeNested) { + AXNodeData text_data; + text_data.id = 3; + text_data.role = ax::mojom::Role::kStaticText; + text_data.SetName("some text"); + + AXNodeData paragraph_data; + paragraph_data.id = 2; + paragraph_data.role = ax::mojom::Role::kParagraph; + paragraph_data.child_ids.push_back(3); + + AXNodeData root_data; + root_data.id = 1; + root_data.role = ax::mojom::Role::kRootWebArea; + root_data.SetName("Document"); + root_data.child_ids.push_back(2); + + Init(root_data, paragraph_data, text_data); + + ComPtr root_node = + GetRootIRawElementProviderSimple(); + + ComPtr text_provider; + EXPECT_HRESULT_SUCCEEDED( + root_node->GetPatternProvider(UIA_TextPatternId, &text_provider)); + + ComPtr text_range_provider; + EXPECT_HRESULT_SUCCEEDED( + text_provider->get_DocumentRange(&text_range_provider)); +} + +TEST_F(AXPlatformNodeTextProviderTest, ITextProviderSupportedSelection) { + AXNodeData text_data; + text_data.id = 2; + text_data.role = ax::mojom::Role::kStaticText; + text_data.SetName("some text"); + + AXNodeData root_data; + root_data.id = 1; + root_data.role = ax::mojom::Role::kRootWebArea; + root_data.SetName("Document"); + root_data.child_ids.push_back(2); + + Init(root_data, text_data); + + ComPtr root_node = + GetRootIRawElementProviderSimple(); + + ComPtr text_provider; + EXPECT_HRESULT_SUCCEEDED( + root_node->GetPatternProvider(UIA_TextPatternId, &text_provider)); + + SupportedTextSelection text_selection_mode; + EXPECT_HRESULT_SUCCEEDED( + text_provider->get_SupportedTextSelection(&text_selection_mode)); + EXPECT_EQ(text_selection_mode, SupportedTextSelection_Single); +} + +TEST_F(AXPlatformNodeTextProviderTest, ITextProviderGetSelection) { + AXNodeData text_data; + text_data.id = 2; + text_data.role = ax::mojom::Role::kStaticText; + text_data.SetName("some text"); + + AXNodeData textbox_data; + textbox_data.id = 3; + textbox_data.role = ax::mojom::Role::kInlineTextBox; + textbox_data.SetName("textbox text"); + textbox_data.AddState(ax::mojom::State::kEditable); + + AXNodeData nonatomic_textfield_data; + nonatomic_textfield_data.id = 4; + nonatomic_textfield_data.role = ax::mojom::Role::kTextField; + nonatomic_textfield_data.AddBoolAttribute( + ax::mojom::BoolAttribute::kNonAtomicTextFieldRoot, true); + nonatomic_textfield_data.child_ids = {5}; + + AXNodeData text_child_data; + text_child_data.id = 5; + text_child_data.role = ax::mojom::Role::kStaticText; + text_child_data.SetName("text"); + + AXNodeData root_data; + root_data.id = 1; + root_data.role = ax::mojom::Role::kRootWebArea; + root_data.SetName("Document"); + root_data.child_ids = {2, 3, 4}; + + AXTreeUpdate update; + AXTreeData tree_data; + tree_data.tree_id = AXTreeID::CreateNewAXTreeID(); + update.tree_data = tree_data; + update.has_tree_data = true; + update.root_id = root_data.id; + update.nodes = {root_data, text_data, textbox_data, nonatomic_textfield_data, + text_child_data}; + Init(update); + + ComPtr root_node = + GetRootIRawElementProviderSimple(); + + ComPtr root_text_provider; + EXPECT_HRESULT_SUCCEEDED( + root_node->GetPatternProvider(UIA_TextPatternId, &root_text_provider)); + + base::win::ScopedSafearray selections; + root_text_provider->GetSelection(selections.Receive()); + ASSERT_EQ(nullptr, selections.Get()); + + ComPtr root_platform_node; + root_text_provider->QueryInterface(IID_PPV_ARGS(&root_platform_node)); + + AXPlatformNodeWin* owner = GetOwner(root_platform_node.Get()); + AXTreeData& selected_tree_data = + const_cast(owner->GetDelegate()->GetTreeData()); + selected_tree_data.sel_focus_object_id = 2; + selected_tree_data.sel_anchor_object_id = 2; + selected_tree_data.sel_anchor_offset = 0; + selected_tree_data.sel_focus_offset = 4; + + root_text_provider->GetSelection(selections.Receive()); + ASSERT_NE(nullptr, selections.Get()); + + LONG ubound; + EXPECT_HRESULT_SUCCEEDED(SafeArrayGetUBound(selections.Get(), 1, &ubound)); + EXPECT_EQ(0, ubound); + LONG lbound; + EXPECT_HRESULT_SUCCEEDED(SafeArrayGetLBound(selections.Get(), 1, &lbound)); + EXPECT_EQ(0, lbound); + + LONG index = 0; + ComPtr text_range_provider; + EXPECT_HRESULT_SUCCEEDED(SafeArrayGetElement( + selections.Get(), &index, static_cast(&text_range_provider))); + SetOwner(owner, text_range_provider.Get()); + + base::win::ScopedBstr text_content; + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->GetText(-1, text_content.Receive())); + EXPECT_EQ(0, wcscmp(text_content.Get(), L"some")); + text_content.Reset(); + selections.Reset(); + text_range_provider.Reset(); + + // Verify that start and end are appropriately swapped when sel_anchor_offset + // is greater than sel_focus_offset + selected_tree_data.sel_focus_object_id = 2; + selected_tree_data.sel_anchor_object_id = 2; + selected_tree_data.sel_anchor_offset = 4; + selected_tree_data.sel_focus_offset = 0; + + root_text_provider->GetSelection(selections.Receive()); + ASSERT_NE(nullptr, selections.Get()); + + EXPECT_HRESULT_SUCCEEDED(SafeArrayGetUBound(selections.Get(), 1, &ubound)); + EXPECT_EQ(0, ubound); + EXPECT_HRESULT_SUCCEEDED(SafeArrayGetLBound(selections.Get(), 1, &lbound)); + EXPECT_EQ(0, lbound); + + EXPECT_HRESULT_SUCCEEDED(SafeArrayGetElement( + selections.Get(), &index, static_cast(&text_range_provider))); + SetOwner(owner, text_range_provider.Get()); + + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->GetText(-1, text_content.Receive())); + EXPECT_EQ(0, wcscmp(text_content.Get(), L"some")); + text_content.Reset(); + selections.Reset(); + text_range_provider.Reset(); + + // Verify that text ranges at an insertion point returns a degenerate (empty) + // text range via textbox with sel_anchor_offset equal to sel_focus_offset + selected_tree_data.sel_focus_object_id = 3; + selected_tree_data.sel_anchor_object_id = 3; + selected_tree_data.sel_anchor_offset = 1; + selected_tree_data.sel_focus_offset = 1; + + AXNode* text_edit_node = GetRoot()->children()[1]; + + ComPtr text_edit_com = + QueryInterfaceFromNode(text_edit_node); + + ComPtr text_edit_provider; + EXPECT_HRESULT_SUCCEEDED(text_edit_com->GetPatternProvider( + UIA_TextPatternId, &text_edit_provider)); + + selections.Reset(); + EXPECT_HRESULT_SUCCEEDED( + text_edit_provider->GetSelection(selections.Receive())); + EXPECT_NE(nullptr, selections.Get()); + + EXPECT_HRESULT_SUCCEEDED(SafeArrayGetUBound(selections.Get(), 1, &ubound)); + EXPECT_EQ(0, ubound); + EXPECT_HRESULT_SUCCEEDED(SafeArrayGetLBound(selections.Get(), 1, &lbound)); + EXPECT_EQ(0, lbound); + + ComPtr text_edit_range_provider; + EXPECT_HRESULT_SUCCEEDED( + SafeArrayGetElement(selections.Get(), &index, + static_cast(&text_edit_range_provider))); + SetOwner(owner, text_edit_range_provider.Get()); + EXPECT_HRESULT_SUCCEEDED( + text_edit_range_provider->GetText(-1, text_content.Receive())); + EXPECT_EQ(0U, text_content.Length()); + text_content.Reset(); + selections.Reset(); + text_edit_range_provider.Reset(); + + // Verify selections that span multiple nodes + selected_tree_data.sel_focus_object_id = 2; + selected_tree_data.sel_focus_offset = 0; + selected_tree_data.sel_anchor_object_id = 3; + selected_tree_data.sel_anchor_offset = 12; + + root_text_provider->GetSelection(selections.Receive()); + ASSERT_NE(nullptr, selections.Get()); + + EXPECT_HRESULT_SUCCEEDED(SafeArrayGetUBound(selections.Get(), 1, &ubound)); + EXPECT_EQ(0, ubound); + EXPECT_HRESULT_SUCCEEDED(SafeArrayGetLBound(selections.Get(), 1, &lbound)); + EXPECT_EQ(0, lbound); + + EXPECT_HRESULT_SUCCEEDED(SafeArrayGetElement( + selections.Get(), &index, static_cast(&text_range_provider))); + + SetOwner(owner, text_range_provider.Get()); + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->GetText(-1, text_content.Receive())); + EXPECT_EQ(0, wcscmp(text_content.Get(), L"some texttextbox text")); + text_content.Reset(); + selections.Reset(); + text_range_provider.Reset(); + + // Verify SAFEARRAY value for degenerate selection. + selected_tree_data.sel_focus_object_id = 2; + selected_tree_data.sel_anchor_object_id = 2; + selected_tree_data.sel_anchor_offset = 1; + selected_tree_data.sel_focus_offset = 1; + + root_text_provider->GetSelection(selections.Receive()); + ASSERT_NE(nullptr, selections.Get()); + + EXPECT_HRESULT_SUCCEEDED(SafeArrayGetUBound(selections.Get(), 1, &ubound)); + EXPECT_EQ(0, ubound); + EXPECT_HRESULT_SUCCEEDED(SafeArrayGetLBound(selections.Get(), 1, &lbound)); + EXPECT_EQ(0, lbound); + + EXPECT_HRESULT_SUCCEEDED(SafeArrayGetElement( + selections.Get(), &index, static_cast(&text_range_provider))); + + SetOwner(owner, text_range_provider.Get()); + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->GetText(-1, text_content.Receive())); + EXPECT_EQ(0, wcscmp(text_content.Get(), L"")); + text_content.Reset(); + selections.Reset(); + text_range_provider.Reset(); + + // Verify that the selection set on a non-atomic text field returns the + // correct selection. Because the anchor/focus is a non-leaf element, the + // offset passed here is a child offset and not a text offset. This means that + // the accessible selection received should include the entire leaf text child + // and not only the first character of that non-atomic text field. + selected_tree_data.sel_anchor_object_id = 4; + selected_tree_data.sel_anchor_offset = 0; + selected_tree_data.sel_focus_object_id = 4; + selected_tree_data.sel_focus_offset = 1; + + root_text_provider->GetSelection(selections.Receive()); + ASSERT_NE(nullptr, selections.Get()); + + EXPECT_HRESULT_SUCCEEDED(SafeArrayGetElement( + selections.Get(), &index, static_cast(&text_range_provider))); + + SetOwner(owner, text_range_provider.Get()); + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->GetText(-1, text_content.Receive())); + EXPECT_EQ(0, wcscmp(text_content.Get(), L"text")); + text_content.Reset(); + selections.Reset(); + text_range_provider.Reset(); + + // Now delete the tree (which will delete the associated elements) and verify + // that UIA_E_ELEMENTNOTAVAILABLE is returned when calling GetSelection on + // a dead element + DestroyTree(); + + EXPECT_EQ(static_cast(UIA_E_ELEMENTNOTAVAILABLE), + text_edit_provider->GetSelection(selections.Receive())); +} + +TEST_F(AXPlatformNodeTextProviderTest, ITextProviderGetActiveComposition) { + AXNodeData text_data; + text_data.id = 2; + text_data.role = ax::mojom::Role::kStaticText; + text_data.SetName("some text"); + + AXNodeData root_data; + root_data.id = 1; + root_data.role = ax::mojom::Role::kRootWebArea; + root_data.SetName("Document"); + root_data.child_ids.push_back(2); + + AXTreeUpdate update; + AXTreeData tree_data; + tree_data.tree_id = AXTreeID::CreateNewAXTreeID(); + update.tree_data = tree_data; + update.has_tree_data = true; + update.root_id = root_data.id; + update.nodes.push_back(root_data); + update.nodes.push_back(text_data); + Init(update); + + ComPtr root_node = + GetRootIRawElementProviderSimple(); + + ComPtr root_text_provider; + EXPECT_HRESULT_SUCCEEDED( + root_node->GetPatternProvider(UIA_TextPatternId, &root_text_provider)); + + ComPtr root_text_edit_provider; + EXPECT_HRESULT_SUCCEEDED(root_node->GetPatternProvider( + UIA_TextEditPatternId, &root_text_edit_provider)); + + ComPtr text_range_provider; + root_text_edit_provider->GetActiveComposition(&text_range_provider); + ASSERT_EQ(nullptr, text_range_provider); + + ComPtr root_platform_node; + root_text_provider->QueryInterface(IID_PPV_ARGS(&root_platform_node)); + + AXActionData action_data; + action_data.action = ax::mojom::Action::kFocus; + action_data.target_node_id = 1; + AXPlatformNodeWin* owner = GetOwner(root_platform_node.Get()); + owner->GetDelegate()->AccessibilityPerformAction(action_data); + const std::u16string active_composition_text = u"a"; + owner->OnActiveComposition(gfx::Range(0, 1), active_composition_text, false); + + root_text_edit_provider->GetActiveComposition(&text_range_provider); + ASSERT_NE(nullptr, text_range_provider); + ComPtr actual_range; + AXNodePosition::AXPositionInstance expected_start = + owner->GetDelegate()->CreateTextPositionAt(0); + AXNodePosition::AXPositionInstance expected_end = + owner->GetDelegate()->CreateTextPositionAt(1); + text_range_provider->QueryInterface(IID_PPV_ARGS(&actual_range)); + EXPECT_EQ(*GetStart(actual_range.Get()), *expected_start); + EXPECT_EQ(*GetEnd(actual_range.Get()), *expected_end); +} + +TEST_F(AXPlatformNodeTextProviderTest, ITextProviderGetConversionTarget) { + AXNodeData text_data; + text_data.id = 2; + text_data.role = ax::mojom::Role::kStaticText; + text_data.SetName("some text"); + + AXNodeData root_data; + root_data.id = 1; + root_data.role = ax::mojom::Role::kRootWebArea; + root_data.SetName("Document"); + root_data.child_ids.push_back(2); + + AXTreeUpdate update; + AXTreeData tree_data; + tree_data.tree_id = AXTreeID::CreateNewAXTreeID(); + update.tree_data = tree_data; + update.has_tree_data = true; + update.root_id = root_data.id; + update.nodes.push_back(root_data); + update.nodes.push_back(text_data); + Init(update); + + ComPtr root_node = + GetRootIRawElementProviderSimple(); + + ComPtr root_text_provider; + EXPECT_HRESULT_SUCCEEDED( + root_node->GetPatternProvider(UIA_TextPatternId, &root_text_provider)); + + ComPtr root_text_edit_provider; + EXPECT_HRESULT_SUCCEEDED(root_node->GetPatternProvider( + UIA_TextEditPatternId, &root_text_edit_provider)); + + ComPtr text_range_provider; + root_text_edit_provider->GetConversionTarget(&text_range_provider); + ASSERT_EQ(nullptr, text_range_provider); + + ComPtr root_platform_node; + root_text_provider->QueryInterface(IID_PPV_ARGS(&root_platform_node)); + + AXActionData action_data; + action_data.action = ax::mojom::Action::kFocus; + action_data.target_node_id = 1; + AXPlatformNodeWin* owner = GetOwner(root_platform_node.Get()); + owner->GetDelegate()->AccessibilityPerformAction(action_data); + const std::u16string active_composition_text = u"a"; + owner->OnActiveComposition(gfx::Range(0, 1), active_composition_text, false); + + root_text_edit_provider->GetConversionTarget(&text_range_provider); + ASSERT_NE(nullptr, text_range_provider); + ComPtr actual_range; + AXNodePosition::AXPositionInstance expected_start = + owner->GetDelegate()->CreateTextPositionAt(0); + AXNodePosition::AXPositionInstance expected_end = + owner->GetDelegate()->CreateTextPositionAt(1); + text_range_provider->QueryInterface(IID_PPV_ARGS(&actual_range)); + EXPECT_EQ(*GetStart(actual_range.Get()), *expected_start); + EXPECT_EQ(*GetEnd(actual_range.Get()), *expected_end); +} + +} // namespace ui diff --git a/third_party/accessibility/ax/platform/ax_platform_node_textrangeprovider_win.cc b/third_party/accessibility/ax/platform/ax_platform_node_textrangeprovider_win.cc new file mode 100644 index 0000000000000..1e87e1cd950de --- /dev/null +++ b/third_party/accessibility/ax/platform/ax_platform_node_textrangeprovider_win.cc @@ -0,0 +1,1825 @@ +// 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. + +#include "ui/accessibility/platform/ax_platform_node_textrangeprovider_win.h" + +#include +#include + +#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, + ui::AXClippingBehavior clipping_behavior, + AXOffscreenResult* offscreen_result) override { + AXPlatformNodeDelegate* delegate = host_->GetDelegate(tree_id, node_id); + DCHECK(delegate); + return delegate->GetInnerTextRangeBoundsRect( + start_offset, end_offset, ui::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( + ui::AXCoordinateSystem::kScreenPhysicalPixels, + ui::AXClippingBehavior::kClipped, offscreen_result); + } + + private: + raw_ptr host_; +}; + +AXPlatformNodeTextRangeProviderWin::AXPlatformNodeTextRangeProviderWin() { + DVLOG(1) << __func__; +} + +AXPlatformNodeTextRangeProviderWin::~AXPlatformNodeTextRangeProviderWin() {} + +ITextRangeProvider* AXPlatformNodeTextRangeProviderWin::CreateTextRangeProvider( + AXPositionInstance start, + AXPositionInstance end) { + CComObject* text_range_provider = nullptr; + if (SUCCEEDED(CComObject::CreateInstance( + &text_range_provider))) { + DCHECK(text_range_provider); + text_range_provider->SetStart(std::move(start)); + text_range_provider->SetEnd(std::move(end)); + text_range_provider->AddRef(); + return text_range_provider; + } + + return nullptr; +} + +ITextRangeProvider* +AXPlatformNodeTextRangeProviderWin::CreateTextRangeProviderForTesting( + AXPlatformNodeWin* owner, + AXPositionInstance start, + AXPositionInstance end) { + Microsoft::WRL::ComPtr text_range_provider = + CreateTextRangeProvider(start->Clone(), end->Clone()); + Microsoft::WRL::ComPtr + 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 + return text_range_provider_win.Get(); + } + + return nullptr; +} + +// +// ITextRangeProvider methods. +// +HRESULT AXPlatformNodeTextRangeProviderWin::Clone(ITextRangeProvider** clone) { + WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXTRANGE_CLONE); + UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_OUT(clone); + + *clone = CreateTextRangeProvider(start()->Clone(), end()->Clone()); + return S_OK; +} + +HRESULT AXPlatformNodeTextRangeProviderWin::Compare(ITextRangeProvider* other, + BOOL* result) { + 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 other_provider; + if (other->QueryInterface(IID_PPV_ARGS(&other_provider)) != S_OK) + return UIA_E_INVALIDOPERATION; + + 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 other_provider; + if (other->QueryInterface(IID_PPV_ARGS(&other_provider)) != S_OK) + return UIA_E_INVALIDOPERATION; + + 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(); + + absl::optional 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)); + } + + // 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 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) + *result = CreateTextRangeProvider(std::move(matched_range_start), + std::move(matched_range_end)); + return S_OK; +} + +template +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( + 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) { + WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXTRANGE_FINDTEXT); + WIN_ACCESSIBILITY_API_PERF_HISTOGRAM(UMA_API_TEXTRANGE_FINDTEXT); + UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_IN_1_OUT(string, 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 kExposeCharacter... 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( + AXEmbeddedObjectBehavior::kSuppressCharacter); + + std::u16string search_string = base::WideToUTF16(string); + if (search_string.length() <= 0) + return E_INVALIDARG; + + size_t appended_newlines_count = 0; + std::u16string text_range = GetString(-1, &appended_newlines_count); + size_t find_start; + size_t find_length; + if (base::i18n::StringSearch(search_string, text_range, &find_start, + &find_length, !ignore_case, !backwards) && + find_length > appended_newlines_count) { + // TODO(https://crbug.com/1023599): 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 - appended_newlines_count; + const int max_end_offset = end_ancestor_position->text_offset(); + DCHECK(start_offset <= end_offset && end_offset <= max_end_offset); + + AXPositionInstance start = + ui::AXNodePosition::CreateTextPosition( + *anchor, start_offset, ax::mojom::TextAffinity::kDownstream) + ->AsLeafTextPosition(); + AXPositionInstance end = + ui::AXNodePosition::CreateTextPosition( + *anchor, end_offset, ax::mojom::TextAffinity::kDownstream) + ->AsLeafTextPosition(); + + *result = CreateTextRangeProvider(start->Clone(), end->Clone()); + } + 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, absl::nullopt, absl::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( + 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( + 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 absl::optional start_offset = + it->IsTextPosition() ? absl::make_optional(it->text_offset()) + : absl::nullopt; + const absl::optional end_offset = + at_end_leaf_text_anchor + ? absl::make_optional(end_leaf_text_position->text_offset()) + : absl::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 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(&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) { + 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(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) { + 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. + absl::optional 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 other_provider; + if (other->QueryInterface(IID_PPV_ARGS(&other_provider)) != S_OK) + return UIA_E_INVALIDOPERATION; + + 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(); + } + + DCHECK(!selection_start->IsNullPosition()); + DCHECK(!selection_end->IsNullPosition()); + DCHECK_EQ(selection_start->tree_id(), selection_end->tree_id()); + + // TODO(crbug.com/1124051): 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(); + + 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 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() || position->AtStartOfInlineBlock()); +} + +// static +bool AXPlatformNodeTextRangeProviderWin::AtEndOfLinePredicate( + const AXPositionInstance& position) { + return !position->IsIgnored() && position->AtEndOfAnchor() && + (position->AtEndOfLine() || position->AtStartOfInlineBlock()); +} + +// 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, + size_t* appended_newlines_count) { + AXNodeRange range(start()->Clone(), end()->Clone()); + return range.GetText(AXTextConcatenationBehavior::kWithParagraphBreaks, + AXEmbeddedObjectBehavior::kExposeCharacter, max_count, + false, 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(); + DCHECK(ax_tree_manager); + + const AXPlatformTreeManager* platform_tree_manager = + static_cast(ax_tree_manager); + DCHECK(platform_tree_manager); + + return static_cast( + platform_tree_manager->GetPlatformNodeFromTree(*anchor)); +} + +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 ui::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::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. Since |ax::mojom::Action::kBlur| is not implemented, +// we perform a |ax::mojom::Action::focus| action on the root node. The result +// is the same. +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; + + 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); + + AXActionData focus_action; + focus_action.action = ax::mojom::Action::kFocus; + root_delegate->AccessibilityPerformAction(focus_action); + } +} + +AXPlatformNodeWin* +AXPlatformNodeTextRangeProviderWin::GetPlatformNodeFromAXNode( + const AXNode* node) const { + if (!node) + return nullptr; + + // TODO(kschmi): Update to use AXTreeManager. + AXPlatformNodeWin* platform_node = + static_cast(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 ,