Commit bff96be5 authored by Kurt Catti-Schmidt's avatar Kurt Catti-Schmidt Committed by Commit Bot

Implement ITextProvider::GetSelection

This change implements ITextProvider::GetSelection based on existing
selection data from AXTreeData.

Unit tests were added.

Bug: 928948
Change-Id: Ifa5695077e67b312b9efd23051841a29f8fa8e62
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1534142
Commit-Queue: Kurt Catti-Schmidt <kschmi@microsoft.com>
Reviewed-by: default avatarDominic Mazzoni <dmazzoni@chromium.org>
Cr-Commit-Position: refs/heads/master@{#644841}
parent ae5eec1f
......@@ -102,6 +102,9 @@ class AX_EXPORT AXPlatformNode {
// Return this object's delegate.
virtual AXPlatformNodeDelegate* GetDelegate() const = 0;
// Return true if this object is equal to or a descendant of |ancestor|.
virtual bool IsDescendantOf(AXPlatformNode* ancestor) const = 0;
// Return the unique ID
int32_t GetUniqueId() const;
......
......@@ -130,6 +130,20 @@ AXPlatformNodeDelegate* AXPlatformNodeBase::GetDelegate() const {
return delegate_;
}
bool AXPlatformNodeBase::IsDescendantOf(AXPlatformNode* ancestor) const {
if (!ancestor)
return false;
if (this == ancestor)
return true;
AXPlatformNodeBase* parent = FromNativeViewAccessible(GetParent());
if (!parent)
return false;
return parent->IsDescendantOf(ancestor);
}
// Helpers.
AXPlatformNodeBase* AXPlatformNodeBase::GetPreviousSibling() {
......@@ -595,20 +609,6 @@ bool AXPlatformNodeBase::HasCaret() {
return focus_object->IsDescendantOf(this);
}
bool AXPlatformNodeBase::IsDescendantOf(AXPlatformNodeBase* ancestor) {
if (!ancestor)
return false;
if (this == ancestor)
return true;
AXPlatformNodeBase* parent = FromNativeViewAccessible(GetParent());
if (!parent)
return false;
return parent->IsDescendantOf(ancestor);
}
bool AXPlatformNodeBase::IsLeaf() {
if (GetChildCount() == 0)
return true;
......
......@@ -71,6 +71,7 @@ class AX_EXPORT AXPlatformNodeBase : public AXPlatformNode {
#endif
AXPlatformNodeDelegate* GetDelegate() const override;
bool IsDescendantOf(AXPlatformNode* ancestor) const override;
// Helpers.
AXPlatformNodeBase* GetPreviousSibling();
......@@ -170,9 +171,6 @@ class AX_EXPORT AXPlatformNodeBase : public AXPlatformNode {
// if this node is a simple text element and has text selection attributes.
bool HasCaret();
// Return true if this object is equal to or a descendant of |ancestor|.
bool IsDescendantOf(AXPlatformNodeBase* ancestor);
// Returns true if an ancestor of this node (not including itself) is a
// leaf node, meaning that this node is not actually exposed to the
// platform.
......
......@@ -6,6 +6,7 @@
#include <utility>
#include "base/win/scoped_safearray.h"
#include "ui/accessibility/ax_node_position.h"
#include "ui/accessibility/platform/ax_platform_node_delegate.h"
#include "ui/accessibility/platform/ax_platform_node_textrangeprovider_win.h"
......@@ -49,7 +50,78 @@ HRESULT AXPlatformNodeTextProviderWin::Create(ui::AXPlatformNodeWin* owner,
STDMETHODIMP AXPlatformNodeTextProviderWin::GetSelection(
SAFEARRAY** selection) {
WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXT_GETSELECTION);
return E_NOTIMPL;
UIA_VALIDATE_TEXTPROVIDER_CALL();
*selection = nullptr;
AXPlatformNodeDelegate* delegate = owner()->GetDelegate();
AXPlatformNode* anchor_object =
delegate->GetFromNodeID(delegate->GetTreeData().sel_anchor_object_id);
AXPlatformNode* focus_object =
delegate->GetFromNodeID(delegate->GetTreeData().sel_focus_object_id);
// If there's no selected object (or the selected object is not in the
// subtree), return success and don't fill the SAFEARRAY
//
// Note that if a selection spans multiple elements, this will report
// that no selection took place. This is expected for this API, rather
// than returning the subset of the selection within this node, because
// subsequently expanding the ITextRange wouldn't expand to the full
// selection.
if (!anchor_object || !focus_object || (anchor_object != focus_object) ||
(!anchor_object->IsDescendantOf(owner())))
return S_OK;
// sel_anchor_offset corresponds to the selection start index
// and sel_focus_offset is where the selection ends.
// If they are equal, that indicates a caret on editable text,
// which should return a degenerate (empty) text range.
auto start_offset = delegate->GetTreeData().sel_anchor_offset;
auto end_offset = delegate->GetTreeData().sel_focus_offset;
// Reverse start and end if the selection goes backwards
if (start_offset > end_offset)
std::swap(start_offset, end_offset);
AXNodePosition::AXPositionInstance start =
anchor_object->GetDelegate()->CreateTextPositionAt(start_offset);
AXNodePosition::AXPositionInstance end =
anchor_object->GetDelegate()->CreateTextPositionAt(end_offset);
DCHECK(!start->IsNullPosition());
DCHECK(!end->IsNullPosition());
CComPtr<ITextRangeProvider> text_range_provider;
HRESULT hr = AXPlatformNodeTextRangeProviderWin::CreateTextRangeProvider(
owner_, std::move(start), std::move(end), &text_range_provider);
DCHECK(SUCCEEDED(hr));
if (FAILED(hr))
return E_FAIL;
// 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;
hr = SafeArrayPutElement(selections_to_return.Get(), &index,
text_range_provider);
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;
}
STDMETHODIMP AXPlatformNodeTextProviderWin::GetVisibleRanges(
......
......@@ -8,13 +8,15 @@
#include "ui/accessibility/platform/ax_platform_node_win.h"
namespace ui {
class AXPlatformNodeTextProviderWin
class __declspec(uuid("3e1c192b-4348-45ac-8eb6-4b58eeb3dcca"))
AXPlatformNodeTextProviderWin
: public CComObjectRootEx<CComMultiThreadModel>,
public ITextEditProvider {
public:
BEGIN_COM_MAP(AXPlatformNodeTextProviderWin)
COM_INTERFACE_ENTRY(ITextProvider)
COM_INTERFACE_ENTRY(ITextEditProvider)
COM_INTERFACE_ENTRY(AXPlatformNodeTextProviderWin)
END_COM_MAP()
AXPlatformNodeTextProviderWin();
......@@ -52,6 +54,7 @@ class AXPlatformNodeTextProviderWin
STDMETHOD(GetConversionTarget)(ITextRangeProvider** range) override;
private:
friend class AXPlatformNodeTextProviderTest;
ui::AXPlatformNodeWin* owner() const;
CComPtr<ui::AXPlatformNodeWin> owner_;
......
......@@ -8,7 +8,9 @@
#include <UIAutomationCoreApi.h>
#include "base/win/scoped_bstr.h"
#include "base/win/scoped_safearray.h"
#include "ui/accessibility/platform/ax_fragment_root_win.h"
#include "ui/accessibility/platform/ax_platform_node_textprovider_win.h"
#include "ui/base/win/accessibility_misc_utils.h"
using Microsoft::WRL::ComPtr;
......@@ -21,7 +23,13 @@ namespace ui {
#define EXPECT_INVALIDARG(expr) \
EXPECT_EQ(static_cast<HRESULT>(E_INVALIDARG), (expr))
class AXPlatformNodeTextProviderTest : public ui::AXPlatformNodeWinTest {};
class AXPlatformNodeTextProviderTest : public ui::AXPlatformNodeWinTest {
public:
ui::AXPlatformNodeWin* GetOwner(
const AXPlatformNodeTextProviderWin* text_provider) {
return text_provider->owner_;
}
};
class MockIRawElementProviderSimple
: public CComObjectRootEx<CComMultiThreadModel>,
......@@ -241,4 +249,151 @@ TEST_F(AXPlatformNodeTextProviderTest, TestITextProviderSupportedSelection) {
EXPECT_EQ(text_selection_mode, SupportedTextSelection_Single);
}
TEST_F(AXPlatformNodeTextProviderTest, TestITextProviderGetSelection) {
ui::AXNodeData text_data;
text_data.id = 2;
text_data.role = ax::mojom::Role::kStaticText;
text_data.SetName("some text");
ui::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);
ui::AXNodeData root_data;
root_data.id = 1;
root_data.SetName("Document");
root_data.role = ax::mojom::Role::kRootWebArea;
root_data.child_ids.push_back(2);
root_data.child_ids.push_back(3);
ui::AXTreeUpdate update;
ui::AXTreeData tree_data;
tree_data.tree_id = ui::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(textbox_data);
Init(update);
AXNodePosition::SetTreeForTesting(tree_.get());
ComPtr<IRawElementProviderSimple> root_node =
GetRootIRawElementProviderSimple();
ComPtr<ITextProvider> 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<AXPlatformNodeTextProviderWin> 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<AXTreeData&>(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;
CComPtr<ITextRangeProvider> text_range_provider;
EXPECT_HRESULT_SUCCEEDED(
SafeArrayGetElement(selections.Get(), &index, &text_range_provider));
base::win::ScopedBstr text_content;
EXPECT_HRESULT_SUCCEEDED(
text_range_provider->GetText(-1, text_content.Receive()));
EXPECT_EQ(0, wcscmp(text_content, L"some"));
text_content.Reset();
selections.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, &text_range_provider));
EXPECT_HRESULT_SUCCEEDED(
text_range_provider->GetText(-1, text_content.Receive()));
EXPECT_EQ(0, wcscmp(text_content, L"some"));
text_content.Reset();
selections.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 = GetRootNode()->children()[1];
ComPtr<IRawElementProviderSimple> text_edit_com =
QueryInterfaceFromNode<IRawElementProviderSimple>(text_edit_node);
ComPtr<ITextProvider> 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);
CComPtr<ITextRangeProvider> text_edit_range_provider;
EXPECT_HRESULT_SUCCEEDED(
SafeArrayGetElement(selections.Get(), &index, &text_edit_range_provider));
EXPECT_HRESULT_SUCCEEDED(
text_edit_range_provider->GetText(-1, text_content.Receive()));
EXPECT_EQ(0U, SysStringLen(text_content));
text_content.Reset();
selections.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
tree_.reset();
EXPECT_EQ(static_cast<HRESULT>(UIA_E_ELEMENTNOTAVAILABLE),
text_edit_provider->GetSelection(selections.Receive()));
AXNodePosition::SetTreeForTesting(nullptr);
}
} // namespace ui
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment