diff options
author | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-01-23 17:21:03 +0100 |
---|---|---|
committer | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-01-23 16:25:15 +0000 |
commit | c551f43206405019121bd2b2c93714319a0a3300 (patch) | |
tree | 1f48c30631c421fd4bbb3c36da20183c8a2ed7d7 /chromium/ui/accessibility/platform | |
parent | 7961cea6d1041e3e454dae6a1da660b453efd238 (diff) | |
download | qtwebengine-chromium-c551f43206405019121bd2b2c93714319a0a3300.tar.gz |
BASELINE: Update Chromium to 79.0.3945.139
Change-Id: I336b7182fab9bca80b709682489c07db112eaca5
Reviewed-by: Allan Sandfeld Jensen <allan.jensen@qt.io>
Diffstat (limited to 'chromium/ui/accessibility/platform')
26 files changed, 1991 insertions, 1074 deletions
diff --git a/chromium/ui/accessibility/platform/atk_util_auralinux.cc b/chromium/ui/accessibility/platform/atk_util_auralinux.cc index 60d24858e33..5eec1b51f99 100644 --- a/chromium/ui/accessibility/platform/atk_util_auralinux.cc +++ b/chromium/ui/accessibility/platform/atk_util_auralinux.cc @@ -22,51 +22,23 @@ const char* kAccessibilityEnabledVariables[] = { "QT_ACCESSIBILITY", }; -} // namespace - -G_BEGIN_DECLS - // -// atk_util_auralinux AtkObject definition and implementation. +// AtkUtilAuraLinux definition and implementation. // - -#define ATK_UTIL_AURALINUX_TYPE (atk_util_auralinux_get_type()) -#define ATK_UTIL_AURALINUX(obj) \ - (G_TYPE_CHECK_INSTANCE_CAST((obj), \ - ATK_UTIL_AURALINUX_TYPE, \ - AtkUtilAuraLinux)) -#define ATK_UTIL_AURALINUX_CLASS(klass) \ - (G_TYPE_CHECK_CLASS_CAST((klass), \ - ATK_UTIL_AURALINUX_TYPE, \ - AtkUtilAuraLinuxClass)) -#define IS_ATK_UTIL_AURALINUX(obj) \ - (G_TYPE_CHECK_INSTANCE_TYPE((obj), ATK_UTIL_AURALINUX_TYPE)) -#define IS_ATK_UTIL_AURALINUX_CLASS(klass) \ - (G_TYPE_CHECK_CLASS_TYPE((klass), ATK_UTIL_AURALINUX_TYPE)) -#define ATK_UTIL_AURALINUX_GET_CLASS(obj) \ - (G_TYPE_INSTANCE_GET_CLASS((obj), \ - ATK_UTIL_AURALINUX_TYPE, \ - AtkUtilAuraLinuxClass)) - -typedef struct _AtkUtilAuraLinux AtkUtilAuraLinux; -typedef struct _AtkUtilAuraLinuxClass AtkUtilAuraLinuxClass; - -struct _AtkUtilAuraLinux { +struct AtkUtilAuraLinux { AtkUtil parent; }; -struct _AtkUtilAuraLinuxClass { +struct AtkUtilAuraLinuxClass { AtkUtilClass parent_class; }; -GType atk_util_auralinux_get_type(); - G_DEFINE_TYPE(AtkUtilAuraLinux, atk_util_auralinux, ATK_TYPE_UTIL) static void atk_util_auralinux_init(AtkUtilAuraLinux *ax_util) { } -static AtkObject* atk_util_auralinux_get_root() { +static AtkObject* AtkUtilAuraLinuxGetRoot() { ui::AXPlatformNode* application = ui::AXPlatformNodeAuraLinux::application(); if (application) { return application->GetNativeViewAccessible(); @@ -74,22 +46,15 @@ static AtkObject* atk_util_auralinux_get_root() { return nullptr; } -static G_CONST_RETURN gchar* atk_util_auralinux_get_toolkit_name(void) { - return "Chromium"; -} - -static G_CONST_RETURN gchar* atk_util_auralinux_get_toolkit_version(void) { - return "1.0"; -} - using KeySnoopFuncMap = std::map<guint, std::pair<AtkKeySnoopFunc, gpointer>>; static KeySnoopFuncMap& GetActiveKeySnoopFunctions() { static base::NoDestructor<KeySnoopFuncMap> active_key_snoop_functions; return *active_key_snoop_functions; } -static guint atk_util_add_key_event_listener(AtkKeySnoopFunc key_snoop_function, - gpointer data) { +static guint AtkUtilAuraLinuxAddKeyEventListener( + AtkKeySnoopFunc key_snoop_function, + gpointer data) { static guint current_key_event_listener_id = 0; current_key_event_listener_id++; @@ -98,29 +63,21 @@ static guint atk_util_add_key_event_listener(AtkKeySnoopFunc key_snoop_function, return current_key_event_listener_id; } -static void atk_util_remove_key_event_listener(guint listener_id) { +static void AtkUtilAuraLinuxRemoveKeyEventListener(guint listener_id) { GetActiveKeySnoopFunctions().erase(listener_id); } static void atk_util_auralinux_class_init(AtkUtilAuraLinuxClass *klass) { - AtkUtilClass* atk_class; - gpointer data; + AtkUtilClass* atk_class = ATK_UTIL_CLASS(g_type_class_peek(ATK_TYPE_UTIL)); - data = g_type_class_peek(ATK_TYPE_UTIL); - atk_class = ATK_UTIL_CLASS(data); - - atk_class->get_root = atk_util_auralinux_get_root; - atk_class->get_toolkit_name = atk_util_auralinux_get_toolkit_name; - atk_class->get_toolkit_version = atk_util_auralinux_get_toolkit_version; - atk_class->add_key_event_listener = atk_util_add_key_event_listener; - atk_class->remove_key_event_listener = atk_util_remove_key_event_listener; + atk_class->get_root = AtkUtilAuraLinuxGetRoot; + atk_class->get_toolkit_name = []() { return "Chromium"; }; + atk_class->get_toolkit_version = []() { return "1.0"; }; + atk_class->add_key_event_listener = AtkUtilAuraLinuxAddKeyEventListener; + atk_class->remove_key_event_listener = AtkUtilAuraLinuxRemoveKeyEventListener; } -G_END_DECLS - -// -// AtkUtilAuraLinuxClass implementation. -// +} // namespace namespace ui { @@ -150,7 +107,7 @@ void AtkUtilAuraLinux::InitializeAsync() { initialized = true; // Register our util class. - g_type_class_unref(g_type_class_ref(ATK_UTIL_AURALINUX_TYPE)); + g_type_class_unref(g_type_class_ref(atk_util_auralinux_get_type())); PlatformInitializeAsync(); } diff --git a/chromium/ui/accessibility/platform/ax_fragment_root_win.cc b/chromium/ui/accessibility/platform/ax_fragment_root_win.cc index cd12e01f0ca..a4380c4b846 100644 --- a/chromium/ui/accessibility/platform/ax_fragment_root_win.cc +++ b/chromium/ui/accessibility/platform/ax_fragment_root_win.cc @@ -90,14 +90,6 @@ class AXFragmentRootPlatformNodeWin : public AXPlatformNodeWin, return S_OK; } - STDMETHOD(GetRuntimeId)(SAFEARRAY** runtime_id) { - UIA_VALIDATE_CALL_1_ARG(runtime_id); - - // UIA obtains a runtime ID for a fragment root from the associated HWND. - *runtime_id = nullptr; - return S_OK; - } - // // IRawElementProviderFragmentRoot methods. // @@ -272,6 +264,16 @@ AXFragmentRootWin::GetTargetForNativeAccessibilityEvent() { return widget_; } +AXPlatformNode* AXFragmentRootWin::GetFromTreeIDAndNodeID( + const ui::AXTreeID& ax_tree_id, + int32_t node_id) { + AXPlatformNodeDelegate* child_delegate = GetChildNodeDelegate(); + if (child_delegate) + return child_delegate->GetFromTreeIDAndNodeID(ax_tree_id, node_id); + + return nullptr; +} + AXPlatformNodeDelegate* AXFragmentRootWin::GetChildNodeDelegate() { gfx::NativeViewAccessible child = delegate_->GetChildOfAXFragmentRoot(); if (child) diff --git a/chromium/ui/accessibility/platform/ax_fragment_root_win.h b/chromium/ui/accessibility/platform/ax_fragment_root_win.h index c71af511aba..a765aebe558 100644 --- a/chromium/ui/accessibility/platform/ax_fragment_root_win.h +++ b/chromium/ui/accessibility/platform/ax_fragment_root_win.h @@ -39,7 +39,7 @@ class AX_EXPORT AXFragmentRootWin : public ui::AXPlatformNodeDelegateBase { gfx::AcceleratedWidget widget); // Returns the NativeViewAccessible for this fragment root. - gfx::NativeViewAccessible GetNativeViewAccessible(); + gfx::NativeViewAccessible GetNativeViewAccessible() override; // Assistive technologies will typically use UI Automation's control or // content view rather than the raw view. @@ -56,6 +56,8 @@ class AX_EXPORT AXFragmentRootWin : public ui::AXPlatformNodeDelegateBase { gfx::NativeViewAccessible GetFocus() override; const ui::AXUniqueId& GetUniqueId() const override; gfx::AcceleratedWidget GetTargetForNativeAccessibilityEvent() override; + AXPlatformNode* GetFromTreeIDAndNodeID(const ui::AXTreeID& ax_tree_id, + int32_t id) override; // If a child node is available, return its delegate. AXPlatformNodeDelegate* GetChildNodeDelegate(); diff --git a/chromium/ui/accessibility/platform/ax_fragment_root_win_unittest.cc b/chromium/ui/accessibility/platform/ax_fragment_root_win_unittest.cc index 1df70f4c769..87f2774a6c5 100644 --- a/chromium/ui/accessibility/platform/ax_fragment_root_win_unittest.cc +++ b/chromium/ui/accessibility/platform/ax_fragment_root_win_unittest.cc @@ -5,10 +5,12 @@ #include "ui/accessibility/platform/ax_fragment_root_win.h" #include "ui/accessibility/platform/ax_platform_node_win.h" #include "ui/accessibility/platform/ax_platform_node_win_unittest.h" +#include "ui/accessibility/platform/test_ax_node_wrapper.h" #include <UIAutomationClient.h> #include <UIAutomationCoreApi.h> +#include "base/auto_reset.h" #include "base/win/scoped_safearray.h" #include "base/win/scoped_variant.h" #include "testing/gtest/include/gtest/gtest.h" @@ -35,22 +37,6 @@ TEST_F(AXFragmentRootTest, TestUIAGetFragmentRoot) { EXPECT_EQ(expected_fragment_root.Get(), actual_fragment_root.Get()); } -TEST_F(AXFragmentRootTest, TestUIAGetRuntimeId) { - AXNodeData root; - Init(root); - InitFragmentRoot(); - - ComPtr<IRawElementProviderFragmentRoot> fragment_root_provider = - GetFragmentRoot(); - ComPtr<IRawElementProviderFragment> fragment_provider; - fragment_root_provider.As(&fragment_provider); - - base::win::ScopedSafearray runtime_id; - EXPECT_HRESULT_SUCCEEDED( - fragment_provider->GetRuntimeId(runtime_id.Receive())); - EXPECT_EQ(runtime_id.Get(), nullptr); -} - TEST_F(AXFragmentRootTest, TestUIAElementProviderFromPoint) { AXNodeData root_data; root_data.id = 1; @@ -93,6 +79,13 @@ TEST_F(AXFragmentRootTest, TestUIAElementProviderFromPoint) { EXPECT_HRESULT_SUCCEEDED(fragment_root_prov->ElementProviderFromPoint( 47, 67, &provider_from_point)); EXPECT_EQ(root_provider.Get(), provider_from_point.Get()); + + // This is on node 1 with scale factor of 1.5. + std::unique_ptr<base::AutoReset<float>> scale_factor_reset = + TestAXNodeWrapper::SetScaleFactor(1.5); + EXPECT_HRESULT_SUCCEEDED(fragment_root_prov->ElementProviderFromPoint( + 60, 60, &provider_from_point)); + EXPECT_EQ(element1_provider.Get(), provider_from_point.Get()); } TEST_F(AXFragmentRootTest, TestUIAGetFocus) { diff --git a/chromium/ui/accessibility/platform/ax_platform_atk_hyperlink.cc b/chromium/ui/accessibility/platform/ax_platform_atk_hyperlink.cc index c3d8f32a69d..e59b7c3a2d7 100644 --- a/chromium/ui/accessibility/platform/ax_platform_atk_hyperlink.cc +++ b/chromium/ui/accessibility/platform/ax_platform_atk_hyperlink.cc @@ -12,8 +12,6 @@ namespace ui { struct _AXPlatformAtkHyperlinkPrivate { AXPlatformNodeAuraLinux* platform_node = nullptr; - base::Optional<int> end_index; - base::Optional<int> start_index; }; static gpointer kAXPlatformAtkHyperlinkParentClass = nullptr; @@ -84,13 +82,17 @@ static gboolean AXPlatformAtkHyperlinkIsSelectedLink( static int AXPlatformAtkHyperlinkGetStartIndex(AtkHyperlink* atk_hyperlink) { g_return_val_if_fail(IS_AX_PLATFORM_ATK_HYPERLINK(atk_hyperlink), 0); AXPlatformAtkHyperlink* link = AX_PLATFORM_ATK_HYPERLINK(atk_hyperlink); - return link->priv->start_index ? *link->priv->start_index : 0; + base::Optional<std::pair<int, int>> indices = + link->priv->platform_node->GetEmbeddedObjectIndices(); + return indices.has_value() ? indices->first : 0; } static int AXPlatformAtkHyperlinkGetEndIndex(AtkHyperlink* atk_hyperlink) { g_return_val_if_fail(IS_AX_PLATFORM_ATK_HYPERLINK(atk_hyperlink), 0); AXPlatformAtkHyperlink* link = AX_PLATFORM_ATK_HYPERLINK(atk_hyperlink); - return link->priv->end_index ? *link->priv->end_index : 0; + base::Optional<std::pair<int, int>> indices = + link->priv->platform_node->GetEmbeddedObjectIndices(); + return indices.has_value() ? indices->second : 0; } static void AXPlatformAtkHyperlinkClassInit(AtkHyperlinkClass* klass) { @@ -231,14 +233,6 @@ void ax_platform_atk_hyperlink_set_object( atk_hyperlink->priv->platform_node = platform_node; } -void ax_platform_atk_hyperlink_set_indices( - AXPlatformAtkHyperlink* atk_hyperlink, - int start_index, - int end_index) { - atk_hyperlink->priv->start_index = start_index; - atk_hyperlink->priv->end_index = end_index; -} - static void AXPlatformAtkHyperlinkInit(AXPlatformAtkHyperlink* self, gpointer) { AXPlatformAtkHyperlinkPrivate* priv = G_TYPE_INSTANCE_GET_PRIVATE(self, ax_platform_atk_hyperlink_get_type(), diff --git a/chromium/ui/accessibility/platform/ax_platform_atk_hyperlink.h b/chromium/ui/accessibility/platform/ax_platform_atk_hyperlink.h index 733d418ec20..30e9112038b 100644 --- a/chromium/ui/accessibility/platform/ax_platform_atk_hyperlink.h +++ b/chromium/ui/accessibility/platform/ax_platform_atk_hyperlink.h @@ -46,10 +46,6 @@ struct _AXPlatformAtkHyperlinkClass { GType ax_platform_atk_hyperlink_get_type(void) G_GNUC_CONST; void ax_platform_atk_hyperlink_set_object(AXPlatformAtkHyperlink* hyperlink, AXPlatformNodeAuraLinux* obj); -void ax_platform_atk_hyperlink_set_indices( - AXPlatformAtkHyperlink* atk_hyperlink, - int start_index, - int end_index); G_END_DECLS diff --git a/chromium/ui/accessibility/platform/ax_platform_node_auralinux.cc b/chromium/ui/accessibility/platform/ax_platform_node_auralinux.cc index 1fd5a3e15ab..53b0513c7a0 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_auralinux.cc +++ b/chromium/ui/accessibility/platform/ax_platform_node_auralinux.cc @@ -18,6 +18,7 @@ #include "base/debug/leak_annotations.h" #include "base/memory/protected_memory_cfi.h" #include "base/no_destructor.h" +#include "base/numerics/ranges.h" #include "base/optional.h" #include "base/strings/string_number_conversions.h" #include "base/strings/string_util.h" @@ -26,7 +27,6 @@ #include "base/strings/utf_string_conversion_utils.h" #include "base/strings/utf_string_conversions.h" #include "build/build_config.h" -#include "third_party/skia/include/core/SkColor.h" #include "ui/accessibility/ax_action_data.h" #include "ui/accessibility/ax_enums.mojom.h" #include "ui/accessibility/ax_mode_observer.h" @@ -61,6 +61,10 @@ #define ATK_230 #endif +#if defined(ATK_CHECK_VERSION) && ATK_CHECK_VERSION(2, 34, 0) +#define ATK_234 +#endif + namespace ui { namespace { @@ -145,6 +149,14 @@ constexpr AtkRole kAtkFootnoteRole = ATK_ROLE_FOOTNOTE; constexpr AtkRole kAtkFootnoteRole = ATK_ROLE_LIST_ITEM; #endif +#if defined(ATK_234) +constexpr AtkRole kAtkRoleContentDeletion = ATK_ROLE_CONTENT_DELETION; +constexpr AtkRole kAtkRoleContentInsertion = ATK_ROLE_CONTENT_INSERTION; +#else +constexpr AtkRole kAtkRoleContentDeletion = ATK_ROLE_SECTION; +constexpr AtkRole kAtkRoleContentInsertion = ATK_ROLE_SECTION; +#endif + AXPlatformNodeAuraLinux* AtkObjectToAXPlatformNodeAuraLinux( AtkObject* atk_object) { if (!atk_object) @@ -191,21 +203,6 @@ AtkObject* FindAtkObjectToplevelParentDocument(AtkObject* atk_object) { return toplevel_document; } -bool EmitsAtkTextEvents(AtkObject* atk_object) { - // If this node is not a static text node, it supports the full AtkText - // interface. - AtkRole role = atk_object_get_role(atk_object); - if (role != ATK_ROLE_TEXT) - return true; - - // If this node is not a static text leaf node, it supports the full AtkText - // interface. - if (atk_object_get_n_accessible_children(atk_object)) - return true; - - return false; -} - bool IsFrameAncestorOfAtkObject(AtkObject* frame, AtkObject* atk_object) { AtkObject* current_frame = FindAtkObjectParentFrame(atk_object); while (current_frame) { @@ -353,14 +350,12 @@ AtkObject* GetActiveDescendantOfCurrentFocused() { return nullptr; } -bool SelectionOffsetsIndicateSelection(const std::pair<int, int>& offsets) { - return offsets.first >= 0 && offsets.second >= 0 && - offsets.first != offsets.second; -} -void AddTextAttributeToSet(const AtkTextAttribute attribute, - const std::string& value, - AtkAttributeSet** attributes) { +void PrependTextAttributeToSet(const AtkTextAttribute attribute, + const std::string& value, + AtkAttributeSet** attributes) { + DCHECK(attributes); + AtkAttribute* new_attribute = static_cast<AtkAttribute*>(g_malloc(sizeof(AtkAttribute))); new_attribute->name = g_strdup(atk_text_attribute_get_name(attribute)); @@ -368,59 +363,56 @@ void AddTextAttributeToSet(const AtkTextAttribute attribute, *attributes = g_slist_prepend(*attributes, new_attribute); } -bool HasInvalidAttributeInSet(AtkAttributeSet* attributes) { - const char* underline_attribute = - atk_text_attribute_get_name(ATK_TEXT_ATTR_UNDERLINE); - DCHECK(underline_attribute); - - AtkAttributeSet* current = attributes; - while (current) { - const AtkAttribute* attribute = static_cast<AtkAttribute*>(current->data); - if (!g_strcmp0(attribute->name, underline_attribute) && - !g_strcmp0(attribute->value, "error")) { - return true; - } - current = g_slist_next(current); - } - return false; +std::string ToAtkTextAttributeColor(const std::string color) { + // The platform-independent color string is in the form "rgb(r, g, b)", + // but ATK expects a string like "r, g, b". We convert the string here + // by stripping away the unnecessary characters. + DCHECK(base::StartsWith(color, "rgb(", base::CompareCase::INSENSITIVE_ASCII)); + DCHECK(base::EndsWith(color, ")", base::CompareCase::INSENSITIVE_ASCII)); + return color.substr(4, color.length() - 5); } -bool AttributeSetsEqual(AtkAttributeSet* one, AtkAttributeSet* two) { - AtkAttributeSet* current_one = one; - AtkAttributeSet* current_two = two; - - while (current_one && current_two) { - const AtkAttribute* attribute_one = - static_cast<AtkAttribute*>(current_one->data); - const AtkAttribute* attribute_two = - static_cast<AtkAttribute*>(current_two->data); - - if (g_strcmp0(attribute_one->name, attribute_two->name) || - g_strcmp0(attribute_one->value, attribute_two->value)) { - return false; - } - - current_one = g_slist_next(current_one); - current_two = g_slist_next(current_two); - } - - // The sets are only equal if they have the same length, which means that we - // need to have iterated to the end of both sets. - return !current_one && !current_two; -} - -AtkAttributeSet* CopyAttributeSet(AtkAttributeSet* attributes) { +AtkAttributeSet* ToAtkAttributeSet(const TextAttributeList& attributes) { AtkAttributeSet* copied_attributes = nullptr; - while (attributes) { - const AtkAttribute* attribute = - static_cast<const AtkAttribute*>(attributes->data); - AtkAttribute* new_attribute = - static_cast<AtkAttribute*>(g_malloc(sizeof(AtkAttribute))); - new_attribute->name = g_strdup(attribute->name); - new_attribute->value = g_strdup(attribute->value); - copied_attributes = g_slist_prepend(copied_attributes, new_attribute); - - attributes = g_slist_next(attributes); + for (const auto& attribute : attributes) { + if (attribute.first == "background-color") { + PrependTextAttributeToSet(ATK_TEXT_ATTR_BG_COLOR, + ToAtkTextAttributeColor(attribute.second), + &copied_attributes); + } else if (attribute.first == "color") { + PrependTextAttributeToSet(ATK_TEXT_ATTR_FG_COLOR, + ToAtkTextAttributeColor(attribute.second), + &copied_attributes); + } else if (attribute.first == "font-family") { + PrependTextAttributeToSet(ATK_TEXT_ATTR_FAMILY_NAME, attribute.second, + &copied_attributes); + } else if (attribute.first == "font-size") { + PrependTextAttributeToSet(ATK_TEXT_ATTR_SIZE, attribute.second, + &copied_attributes); + } else if (attribute.first == "font-weight" && attribute.second == "bold") { + PrependTextAttributeToSet(ATK_TEXT_ATTR_WEIGHT, "700", + &copied_attributes); + } else if (attribute.first == "font-style") { + PrependTextAttributeToSet(ATK_TEXT_ATTR_STYLE, "italic", + &copied_attributes); + } else if (attribute.first == "text-line-through-style") { + PrependTextAttributeToSet(ATK_TEXT_ATTR_STRIKETHROUGH, "true", + &copied_attributes); + } else if (attribute.first == "text-underline-style") { + PrependTextAttributeToSet(ATK_TEXT_ATTR_UNDERLINE, "single", + &copied_attributes); + } else if (attribute.first == "invalid") { + PrependTextAttributeToSet(ATK_TEXT_ATTR_INVALID, attribute.second, + &copied_attributes); + PrependTextAttributeToSet(ATK_TEXT_ATTR_UNDERLINE, "error", + &copied_attributes); + } else if (attribute.first == "language") { + PrependTextAttributeToSet(ATK_TEXT_ATTR_LANGUAGE, attribute.second, + &copied_attributes); + } else if (attribute.first == "writing-mode") { + PrependTextAttributeToSet(ATK_TEXT_ATTR_DIRECTION, attribute.second, + &copied_attributes); + } } return g_slist_reverse(copied_attributes); @@ -869,8 +861,7 @@ gchar* GetText(AtkText* atk_text, gint start_offset, gint end_offset) { end_offset = text.size(); } else { end_offset = obj->UnicodeToUTF16OffsetInText(end_offset); - end_offset = std::max(start_offset, - std::min(end_offset, static_cast<int>(text.size()))); + end_offset = base::ClampToRange(int{text.size()}, start_offset, end_offset); } DCHECK_GE(start_offset, 0); @@ -900,7 +891,7 @@ gunichar GetCharacterAtOffset(AtkText* atk_text, int offset) { int32_t text_length = text.length(); offset = obj->UnicodeToUTF16OffsetInText(offset); - int32_t limited_offset = std::max(0, std::min(text_length, offset)); + int32_t limited_offset = base::ClampToRange(offset, 0, text_length); uint32_t code_point; base::ReadUnicodeCharacter(text.c_str(), text_length + 1, &limited_offset, @@ -1251,7 +1242,7 @@ AtkAttributeSet* GetRunAttributes(AtkText* atk_text, if (!obj) return nullptr; - return CopyAttributeSet( + return ToAtkAttributeSet( obj->GetTextAttributes(offset, start_offset, end_offset)); } @@ -1260,7 +1251,7 @@ AtkAttributeSet* GetDefaultAttributes(AtkText* atk_text) { AXPlatformNodeAuraLinux* obj = AtkObjectToAXPlatformNodeAuraLinux(atk_object); if (!obj) return nullptr; - return CopyAttributeSet(obj->GetDefaultTextAttributes()); + return ToAtkAttributeSet(obj->GetDefaultTextAttributes()); } void Init(AtkTextIface* iface) { @@ -1832,7 +1823,23 @@ gint GetIndexInParent(AtkObject* atk_object) { if (!parent) return -1; + AXPlatformNodeAuraLinux* obj = AtkObjectToAXPlatformNodeAuraLinux(atk_object); + if (!obj) + return -1; + int n_children = atk_object_get_n_accessible_children(parent); + + // Ask the delegate for the index in parent, and return it if it's plausible. + // + // Delegates are allowed to not implement this (AXPlatformNodeDelegateBase + // returns -1). Also, delegates may not know the correct answer if this + // node is the root of a tree that's embedded in another tree, in which + // case the delegate should return -1 and we'll compute it. + int index_in_parent = obj->GetDelegate()->GetIndexInParent(); + if (index_in_parent >= 0 && index_in_parent < n_children) + return index_in_parent; + + // Otherwise, search the parent's children. for (int i = 0; i < n_children; i++) { AtkObject* child = atk_object_ref_accessible_child(parent, i); g_object_unref(child); @@ -2288,7 +2295,7 @@ void AXPlatformNodeAuraLinux::StaticInitialize() { AtkUtilAuraLinux::GetInstance()->InitializeAsync(); } -AtkRole AXPlatformNodeAuraLinux::GetAtkRole() { +AtkRole AXPlatformNodeAuraLinux::GetAtkRole() const { switch (GetData().role) { case ax::mojom::Role::kAlert: return ATK_ROLE_ALERT; @@ -2296,10 +2303,12 @@ AtkRole AXPlatformNodeAuraLinux::GetAtkRole() { return ATK_ROLE_DIALOG; case ax::mojom::Role::kAnchor: return ATK_ROLE_LINK; - case ax::mojom::Role::kAnnotation: - // TODO(accessibility) Panels are generally for containers of widgets. - // This should probably be a section (if a container) or static if text. - return ATK_ROLE_PANEL; + case ax::mojom::Role::kAnnotationAttribution: + case ax::mojom::Role::kAnnotationCommentary: + case ax::mojom::Role::kAnnotationPresence: + case ax::mojom::Role::kAnnotationRevision: + case ax::mojom::Role::kAnnotationSuggestion: + return ATK_ROLE_SECTION; case ax::mojom::Role::kApplication: // Only use ATK_ROLE_APPLICATION for elements with no parent, since it // is only for top level app windows and not ARIA applications. @@ -2313,6 +2322,7 @@ AtkRole AXPlatformNodeAuraLinux::GetAtkRole() { case ax::mojom::Role::kAudio: return ATK_ROLE_AUDIO; case ax::mojom::Role::kBanner: + case ax::mojom::Role::kHeader: return ATK_ROLE_LANDMARK; case ax::mojom::Role::kBlockquote: return ATK_ROLE_BLOCK_QUOTE; @@ -2343,9 +2353,9 @@ AtkRole AXPlatformNodeAuraLinux::GetAtkRole() { case ax::mojom::Role::kComplementary: return ATK_ROLE_LANDMARK; case ax::mojom::Role::kContentDeletion: + return kAtkRoleContentDeletion; case ax::mojom::Role::kContentInsertion: - // TODO(accessibility) https://github.com/w3c/html-aam/issues/141 - return ATK_ROLE_SECTION; + return kAtkRoleContentInsertion; case ax::mojom::Role::kContentInfo: case ax::mojom::Role::kFooter: return ATK_ROLE_LANDMARK; @@ -2429,6 +2439,8 @@ AtkRole AXPlatformNodeAuraLinux::GetAtkRole() { case ax::mojom::Role::kFeed: return ATK_ROLE_PANEL; case ax::mojom::Role::kGenericContainer: + case ax::mojom::Role::kFooterAsNonLandmark: + case ax::mojom::Role::kHeaderAsNonLandmark: return ATK_ROLE_SECTION; case ax::mojom::Role::kGraphicsDocument: return ATK_ROLE_DOCUMENT_FRAME; @@ -2544,11 +2556,22 @@ AtkRole AXPlatformNodeAuraLinux::GetAtkRole() { return ATK_ROLE_RADIO_BUTTON; case ax::mojom::Role::kRadioGroup: return ATK_ROLE_PANEL; - case ax::mojom::Role::kRegion: { - std::string html_tag = - GetData().GetStringAttribute(ax::mojom::StringAttribute::kHtmlTag); - if (html_tag == "section" && - GetData() + case ax::mojom::Role::kRegion: + return ATK_ROLE_LANDMARK; + case ax::mojom::Role::kRootWebArea: + return ATK_ROLE_DOCUMENT_WEB; + case ax::mojom::Role::kRow: + return ATK_ROLE_TABLE_ROW; + case ax::mojom::Role::kRowHeader: + return ATK_ROLE_ROW_HEADER; + case ax::mojom::Role::kRuby: + return kStaticRole; + case ax::mojom::Role::kRubyAnnotation: + // TODO(accessibility) Panels are generally for containers of widgets. + // This should probably be a section (if a container) or static if text. + return ATK_ROLE_PANEL; + case ax::mojom::Role::kSection: { + if (GetData() .GetString16Attribute(ax::mojom::StringAttribute::kName) .empty()) { // Do not use ARIA mapping for nameless <section>. @@ -2558,14 +2581,6 @@ AtkRole AXPlatformNodeAuraLinux::GetAtkRole() { return ATK_ROLE_LANDMARK; } } - case ax::mojom::Role::kRootWebArea: - return ATK_ROLE_DOCUMENT_WEB; - case ax::mojom::Role::kRow: - return ATK_ROLE_TABLE_ROW; - case ax::mojom::Role::kRowHeader: - return ATK_ROLE_ROW_HEADER; - case ax::mojom::Role::kRuby: - return kStaticRole; case ax::mojom::Role::kScrollBar: return ATK_ROLE_SCROLL_BAR; case ax::mojom::Role::kSearch: @@ -3218,35 +3233,139 @@ bool AXPlatformNodeAuraLinux::SelectionAndFocusAreTheSame() { return false; } -void AXPlatformNodeAuraLinux::OnTextSelectionChanged() { - AtkObject* atk_object = GetOrCreateAtkObject(); - if (!EmitsAtkTextEvents(atk_object)) { +bool AXPlatformNodeAuraLinux::EmitsAtkTextEvents() const { + // If this node is not a static text node, it supports the full AtkText + // interface. + if (GetAtkRole() != ATK_ROLE_TEXT) + return true; + + // If this node has children it is not a static text leaf node and supports + // the full AtkText interface. + if (GetChildCount()) + return true; + + return false; +} + +void AXPlatformNodeAuraLinux::GetFullSelection(int32_t* anchor_node_id, + int* anchor_offset, + int32_t* focus_node_id, + int* focus_offset) { + DCHECK(anchor_node_id); + DCHECK(anchor_offset); + DCHECK(focus_node_id); + DCHECK(focus_offset); + + if (IsPlainTextField() && + GetIntAttribute(ax::mojom::IntAttribute::kTextSelStart, anchor_offset) && + GetIntAttribute(ax::mojom::IntAttribute::kTextSelEnd, focus_offset)) { + int32_t node_id = GetData().id != -1 ? GetData().id : GetUniqueId(); + *anchor_node_id = *focus_node_id = node_id; + return; + } + + ui::AXTree::Selection selection = GetDelegate()->GetUnignoredSelection(); + *anchor_node_id = selection.anchor_object_id; + *anchor_offset = selection.anchor_offset; + *focus_node_id = selection.focus_object_id; + *focus_offset = selection.focus_offset; +} + +AXPlatformNodeAuraLinux& AXPlatformNodeAuraLinux::FindEditableRootOrDocument() { + if (GetAtkRole() == ATK_ROLE_DOCUMENT_WEB) + return *this; + if (GetData().GetBoolAttribute(ax::mojom::BoolAttribute::kEditableRoot)) + return *this; + if (auto* parent = AtkObjectToAXPlatformNodeAuraLinux(GetParent())) + return parent->FindEditableRootOrDocument(); + return *this; +} + +AXPlatformNodeAuraLinux* AXPlatformNodeAuraLinux::FindCommonAncestor( + AXPlatformNodeAuraLinux* other) { + if (this == other || other->IsDescendantOf(this)) + return this; + if (auto* parent = AtkObjectToAXPlatformNodeAuraLinux(GetParent())) + return parent->FindCommonAncestor(other); + return nullptr; +} + +void AXPlatformNodeAuraLinux::UpdateSelectionInformation(int32_t anchor_node_id, + int anchor_offset, + int32_t focus_node_id, + int focus_offset) { + had_nonzero_width_selection = + focus_node_id != anchor_node_id || focus_offset != anchor_offset; + current_caret_ = std::make_pair(focus_node_id, focus_offset); +} + +void AXPlatformNodeAuraLinux::EmitSelectionChangedSignal(bool had_selection) { + if (!EmitsAtkTextEvents()) { if (auto* parent = AtkObjectToAXPlatformNodeAuraLinux(GetParent())) - parent->OnTextSelectionChanged(); + parent->EmitSelectionChangedSignal(had_selection); return; } + AtkObject* atk_object = GetOrCreateAtkObject(); DCHECK(ATK_IS_TEXT(atk_object)); - - std::pair<int, int> new_selection; - GetSelectionOffsets(&new_selection.first, &new_selection.second); - std::pair<int, int> old_selection = text_selection_; - text_selection_ = new_selection; + std::pair<int, int> selection; + GetSelectionOffsets(&selection.first, &selection.second); // ATK does not consider a collapsed selection a selection, so // when the collapsed selection changes (caret movement), we should // avoid sending text-selection-changed events. - bool has_selection = SelectionOffsetsIndicateSelection(new_selection); - bool had_selection = SelectionOffsetsIndicateSelection(old_selection); - if (has_selection != had_selection || - (has_selection && new_selection != old_selection)) { + if (HasSelection() || had_selection) g_signal_emit_by_name(atk_object, "text-selection-changed"); +} + +void AXPlatformNodeAuraLinux::EmitCaretChangedSignal() { + if (!EmitsAtkTextEvents()) { + if (auto* parent = AtkObjectToAXPlatformNodeAuraLinux(GetParent())) + parent->EmitCaretChangedSignal(); + return; } - if (HasCaret() && new_selection.second != old_selection.second) { - g_signal_emit_by_name(atk_object, "text-caret-moved", - UTF16ToUnicodeOffsetInText(new_selection.second)); + DCHECK(HasCaret()); + std::pair<int, int> selection; + GetSelectionOffsets(&selection.first, &selection.second); + + AtkObject* atk_object = GetOrCreateAtkObject(); + DCHECK(ATK_IS_TEXT(atk_object)); + g_signal_emit_by_name(atk_object, "text-caret-moved", + UTF16ToUnicodeOffsetInText(selection.second)); +} + +void AXPlatformNodeAuraLinux::OnTextSelectionChanged() { + int32_t anchor_node_id, focus_node_id; + int anchor_offset, focus_offset; + GetFullSelection(&anchor_node_id, &anchor_offset, &focus_node_id, + &focus_offset); + + auto* anchor_node = static_cast<AXPlatformNodeAuraLinux*>( + GetDelegate()->GetFromNodeID(anchor_node_id)); + auto* focus_node = static_cast<AXPlatformNodeAuraLinux*>( + GetDelegate()->GetFromNodeID(focus_node_id)); + if (!anchor_node || !focus_node) + return; + + AXPlatformNodeAuraLinux& editable_root = FindEditableRootOrDocument(); + AXPlatformNodeAuraLinux* common_ancestor = + focus_node->FindCommonAncestor(anchor_node); + if (common_ancestor) { + common_ancestor->EmitSelectionChangedSignal( + editable_root.HadNonZeroWidthSelection()); } + + // It's possible for the selection to change and for the caret to stay in + // place. This might happen if the selection is totally reset with a + // different anchor node, but the same focus node. We should avoid sending a + // caret changed signal in that case. + std::pair<int32_t, int> prev_caret = editable_root.GetCurrentCaret(); + if (prev_caret.first != focus_node_id || prev_caret.second != focus_offset) + focus_node->EmitCaretChangedSignal(); + + editable_root.UpdateSelectionInformation(anchor_node_id, anchor_offset, + focus_node_id, focus_offset); } bool AXPlatformNodeAuraLinux::SupportsSelectionWithAtkSelection() { @@ -3456,6 +3575,14 @@ AXPlatformNodeAuraLinux::GetEmbeddedObjectIndicesForId(int id) { UTF16ToUnicodeOffsetInText(offset->first + 1)); } +base::Optional<std::pair<int, int>> +AXPlatformNodeAuraLinux::GetEmbeddedObjectIndices() { + auto* parent = AtkObjectToAXPlatformNodeAuraLinux(GetParent()); + if (!parent) + return base::nullopt; + return parent->GetEmbeddedObjectIndicesForId(GetUniqueId()); +} + void AXPlatformNodeAuraLinux::UpdateHypertext() { EnsureAtkObjectIsValid(); AXHypertext old_hypertext = hypertext_; @@ -3474,10 +3601,12 @@ void AXPlatformNodeAuraLinux::UpdateHypertext() { ComputeHypertextRemovedAndInserted(old_hypertext, &shared_prefix, &old_len, &new_len); + offset_to_text_attributes_.clear(); + AtkObject* atk_object = GetOrCreateAtkObject(); DCHECK(ATK_IS_TEXT(atk_object)); - if (!EmitsAtkTextEvents(atk_object)) + if (!EmitsAtkTextEvents()) return; if (old_len > 0) { @@ -3827,20 +3956,6 @@ AtkHyperlink* AXPlatformNodeAuraLinux::GetAtkHyperlink() { ATK_HYPERLINK(g_object_new(AX_PLATFORM_ATK_HYPERLINK_TYPE, 0)); ax_platform_atk_hyperlink_set_object( AX_PLATFORM_ATK_HYPERLINK(atk_hyperlink_), this); - - auto* parent = AtkObjectToAXPlatformNodeAuraLinux(GetParent()); - if (!parent) - return atk_hyperlink_; - - base::Optional<std::pair<int, int>> indices = - parent->GetEmbeddedObjectIndicesForId(GetUniqueId()); - if (!indices.has_value()) - return atk_hyperlink_; - - ax_platform_atk_hyperlink_set_indices( - AX_PLATFORM_ATK_HYPERLINK(atk_hyperlink_), indices->first, - indices->second); - return atk_hyperlink_; } @@ -3908,7 +4023,6 @@ bool AXPlatformNodeAuraLinux::SetCaretOffset(int offset) { if (!SetHypertextSelection(offset, offset)) return false; - OnTextSelectionChanged(); return true; } @@ -3938,14 +4052,14 @@ bool AXPlatformNodeAuraLinux::SetTextSelectionForAtkText(int start_offset, if (!SetHypertextSelection(start_offset, end_offset)) return false; - OnTextSelectionChanged(); return true; } bool AXPlatformNodeAuraLinux::HasSelection() { std::pair<int, int> selection; GetSelectionOffsets(&selection.first, &selection.second); - return SelectionOffsetsIndicateSelection(selection); + return selection.first >= 0 && selection.second >= 0 && + selection.first != selection.second; } void AXPlatformNodeAuraLinux::GetSelectionExtents(int* start_offset, @@ -4101,242 +4215,16 @@ void AXPlatformNodeAuraLinux::ScrollNodeIntoView( #endif // defined(ATK_230) -AtkAttributes AXPlatformNodeAuraLinux::ComputeTextAttributes() const { - AtkAttributeSet* attributes = nullptr; - - int color; - if (GetIntAttribute(ax::mojom::IntAttribute::kBackgroundColor, &color)) { - unsigned int red = SkColorGetR(color); - unsigned int green = SkColorGetG(color); - unsigned int blue = SkColorGetB(color); - std::string color_value = base::NumberToString(red) + ',' + - base::NumberToString(green) + ',' + - base::NumberToString(blue); - AddTextAttributeToSet(ATK_TEXT_ATTR_BG_COLOR, color_value, &attributes); - } - - if (GetIntAttribute(ax::mojom::IntAttribute::kColor, &color)) { - unsigned int red = SkColorGetR(color); - unsigned int green = SkColorGetG(color); - unsigned int blue = SkColorGetB(color); - std::string color_value = base::NumberToString(red) + ',' + - base::NumberToString(green) + ',' + - base::NumberToString(blue); - AddTextAttributeToSet(ATK_TEXT_ATTR_FG_COLOR, color_value, &attributes); - } - - std::string font_family( - GetInheritedStringAttribute(ax::mojom::StringAttribute::kFontFamily)); - // Attribute has no default value. - if (!font_family.empty()) { - AddTextAttributeToSet(ATK_TEXT_ATTR_FAMILY_NAME, font_family, &attributes); - } - - float font_size; - // Attribute has no default value. - if (GetFloatAttribute(ax::mojom::FloatAttribute::kFontSize, &font_size)) { - // The ATK Spec requires the value to be in pt, not in pixels. - // There are 72 points per inch. - // We assume that there are 96 pixels per inch on a standard display. - // TODO(nektar): Figure out the current value of pixels per inch. - float points = font_size * 72.0 / 96.0; - AddTextAttributeToSet(ATK_TEXT_ATTR_SIZE, - base::NumberToString(points) + "pt", &attributes); - } - - // TODO(nektar): Add Blink support for the following attributes: - // text-line-through-mode, text-line-through-width, text-outline:false, - // text-position:baseline, text-shadow:none, text-underline-mode:continuous. - - int32_t text_style = GetIntAttribute(ax::mojom::IntAttribute::kTextStyle); - if (text_style) { - if (GetData().HasTextStyle(ax::mojom::TextStyle::kBold)) { - // TODO(mrobinson): This is based on the weight that CSS uses for "bold," - // but we'd like to support more font weights here. - AddTextAttributeToSet(ATK_TEXT_ATTR_WEIGHT, "700", &attributes); - } - - if (GetData().HasTextStyle(ax::mojom::TextStyle::kItalic)) { - // TODO(mrobinson): We'd also like to support "oblique" here. - AddTextAttributeToSet(ATK_TEXT_ATTR_STYLE, "italic", &attributes); - } - - if (GetData().HasTextStyle(ax::mojom::TextStyle::kLineThrough)) - AddTextAttributeToSet(ATK_TEXT_ATTR_STRIKETHROUGH, "true", &attributes); - - if (GetData().HasTextStyle(ax::mojom::TextStyle::kUnderline)) { - // TODO(mrobinson): Figure out a more specific value. - AddTextAttributeToSet(ATK_TEXT_ATTR_UNDERLINE, "single", &attributes); - } - } - - // Screen readers look at the text attributes to determine if something is - // misspelled, so we need to propagate any spelling attributes from immediate - // parents of text-only objects. AXPlatformNodeBase::GetInvalidValue takes - // care of searching upward to find the appropriate values. - std::string invalid_value = GetInvalidValue(); - if (!invalid_value.empty()) - AddTextAttributeToSet(ATK_TEXT_ATTR_UNDERLINE, "error", &attributes); - - std::string language = GetDelegate()->GetLanguage(); - if (!language.empty()) - AddTextAttributeToSet(ATK_TEXT_ATTR_LANGUAGE, language, &attributes); - - auto text_direction = static_cast<ax::mojom::TextDirection>( - GetIntAttribute(ax::mojom::IntAttribute::kTextDirection)); - switch (text_direction) { - case ax::mojom::TextDirection::kNone: - break; - case ax::mojom::TextDirection::kLtr: - AddTextAttributeToSet(ATK_TEXT_ATTR_LANGUAGE, "ltr", &attributes); - break; - case ax::mojom::TextDirection::kRtl: - AddTextAttributeToSet(ATK_TEXT_ATTR_LANGUAGE, "rtl", &attributes); - break; - case ax::mojom::TextDirection::kTtb: - // Not listed in the ATK docs. - AddTextAttributeToSet(ATK_TEXT_ATTR_LANGUAGE, "ttb", &attributes); - break; - case ax::mojom::TextDirection::kBtt: - // Not listed in the ATK docs. - AddTextAttributeToSet(ATK_TEXT_ATTR_LANGUAGE, "btt", &attributes); - break; - } - - return AtkAttributes(attributes); -} - void AXPlatformNodeAuraLinux::ComputeStylesIfNeeded() { if (!offset_to_text_attributes_.empty()) return; default_text_attributes_ = ComputeTextAttributes(); - std::map<int, AtkAttributes> attributes_map; - if (IsLeaf()) { - attributes_map[0] = - AtkAttributes(CopyAttributeSet(default_text_attributes_.get())); - MergeSpellingIntoAtkTextAttributesAtOffset(0 /* start_offset */, - &attributes_map); - offset_to_text_attributes_.swap(attributes_map); - return; - } - - int start_offset = 0; - for (auto child_iterator_ptr = GetDelegate()->ChildrenBegin(); - *child_iterator_ptr != *GetDelegate()->ChildrenEnd(); - ++(*child_iterator_ptr)) { - auto* child = AtkObjectToAXPlatformNodeAuraLinux( - child_iterator_ptr->GetNativeViewAccessible()); - if (!child) - continue; - - AtkAttributes attributes = child->ComputeTextAttributes(); - if (attributes_map.empty()) { - attributes_map[start_offset] = std::move(attributes); - } else { - // Only add the attributes for this child if we are at the start of a new - // style span. - AtkAttributeSet* previous_attributes = - attributes_map.rbegin()->second.get(); - if (!AttributeSetsEqual(attributes.get(), previous_attributes)) - attributes_map[start_offset] = std::move(attributes); - } - - if (child->IsTextOnlyObject()) { - MergeSpellingIntoAtkTextAttributesAtOffset(start_offset, &attributes_map); - start_offset += child->GetHypertext().length(); - } else { - start_offset += 1; - } - } - + TextAttributeMap attributes_map = + GetDelegate()->ComputeTextAttributeMap(default_text_attributes_); offset_to_text_attributes_.swap(attributes_map); } -void AXPlatformNodeAuraLinux::MergeSpellingIntoAtkTextAttributesAtOffset( - int offset, - std::map<int, AtkAttributes>* text_attributes) { - DCHECK(text_attributes); - - int hypertext_length = static_cast<int>(GetHypertext().length()); - std::map<int, AtkAttributeSet*> spelling_attributes; - if (IsTextOnlyObject()) { - const std::vector<int32_t>& marker_types = - GetIntListAttribute(ax::mojom::IntListAttribute::kMarkerTypes); - const std::vector<int>& marker_starts = - GetIntListAttribute(ax::mojom::IntListAttribute::kMarkerStarts); - const std::vector<int>& marker_ends = - GetIntListAttribute(ax::mojom::IntListAttribute::kMarkerEnds); - for (size_t i = 0; i < marker_types.size(); ++i) { - bool isSpellingError = - (marker_types[i] & - static_cast<int32_t>(ax::mojom::MarkerType::kSpelling)) != 0; - bool isGrammarError = - (marker_types[i] & - static_cast<int32_t>(ax::mojom::MarkerType::kGrammar)) != 0; - if (!isSpellingError && !isGrammarError) - continue; - - // Add a starting attribute for the error, if one does not already exist. - // We use a default value of an AtkAttributes (unique_ptr of - // AtkAttributeSet*) set to a nullptr. Since AtkAttributeSet is a GSList, - // a nullptr is an empty set. - int marker_start_offset = offset + marker_starts[i]; - auto default_value = std::make_pair(marker_start_offset, nullptr); - auto iterator = text_attributes->insert(default_value).first; - - if (!HasInvalidAttributeInSet(iterator->second.get())) { - // `attributes` is a unique_ptr, but here we want to modify the head of - // the list without having the unique_ptr delete it. We leak the - // pointer to `attribute_set` and then reset the new list value into - // the iterator. - AtkAttributeSet* attribute_set = iterator->second.release(); - AddTextAttributeToSet(ATK_TEXT_ATTR_UNDERLINE, "error", &attribute_set); - if (isSpellingError) { - AddTextAttributeToSet(ATK_TEXT_ATTR_INVALID, "spelling", - &attribute_set); - } - if (isGrammarError) { - AddTextAttributeToSet(ATK_TEXT_ATTR_INVALID, "grammar", - &attribute_set); - } - iterator->second.reset(attribute_set); - } - - // Make sure there is at least an empty attribute set to end the marker. - int marker_end_offset = offset + marker_ends[i]; - DCHECK_LE(marker_end_offset, hypertext_length); - text_attributes->insert(std::make_pair(marker_end_offset, nullptr)); - } - } - - if (IsPlainTextField()) { - AXPlatformNodeDelegate* delegate = GetDelegate(); - DCHECK(delegate); - - int anchor_start_offset = offset; - AXNodeRange range(delegate->CreateTextPositionAt(0), - delegate->CreateTextPositionAt(hypertext_length)); - for (const auto& leaf_text_range : range) { - DCHECK(!leaf_text_range.IsNull()); - DCHECK_EQ(leaf_text_range.anchor()->GetAnchor(), - leaf_text_range.focus()->GetAnchor()) - << "An leaf text range should only span a single object."; - auto* node = static_cast<AXPlatformNodeAuraLinux*>( - delegate->GetFromNodeID(leaf_text_range.anchor()->GetAnchor()->id())); - - // The AXNodeRange may include this node as well. We need to skip - // that one to avoid an infinite recursive loop. - if (node == this) - continue; - - node->MergeSpellingIntoAtkTextAttributesAtOffset(anchor_start_offset, - text_attributes); - anchor_start_offset += leaf_text_range.GetText().length(); - } - } -} - int AXPlatformNodeAuraLinux::FindStartOfStyle( int start_offset, AXTextBoundaryDirection direction) { @@ -4364,9 +4252,10 @@ int AXPlatformNodeAuraLinux::FindStartOfStyle( return start_offset; } -AtkAttributeSet* AXPlatformNodeAuraLinux::GetTextAttributes(int offset, - int* start_offset, - int* end_offset) { +const TextAttributeList& AXPlatformNodeAuraLinux::GetTextAttributes( + int offset, + int* start_offset, + int* end_offset) { ComputeStylesIfNeeded(); DCHECK(!offset_to_text_attributes_.empty()); @@ -4383,12 +4272,12 @@ AtkAttributeSet* AXPlatformNodeAuraLinux::GetTextAttributes(int offset, UTF16ToUnicodeOffsetInText(style_start)); SetIntPointerValueIfNotNull(end_offset, UTF16ToUnicodeOffsetInText(style_end)); - return iterator->second.get(); + return iterator->second; } -AtkAttributeSet* AXPlatformNodeAuraLinux::GetDefaultTextAttributes() { +const TextAttributeList& AXPlatformNodeAuraLinux::GetDefaultTextAttributes() { ComputeStylesIfNeeded(); - return default_text_attributes_.get(); + return default_text_attributes_; } void AXPlatformNodeAuraLinux::TerminateFindInPage() { @@ -4400,7 +4289,7 @@ void AXPlatformNodeAuraLinux::ActivateFindInPageResult(int start_offset, AtkObject* atk_object = GetOrCreateAtkObject(); DCHECK(ATK_IS_TEXT(atk_object)); - if (!EmitsAtkTextEvents(atk_object)) { + if (!EmitsAtkTextEvents()) { ActivateFindInPageInParent(start_offset, end_offset); return; } diff --git a/chromium/ui/accessibility/platform/ax_platform_node_auralinux.h b/chromium/ui/accessibility/platform/ax_platform_node_auralinux.h index 2d64e2abfcc..71a3c910d5f 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_auralinux.h +++ b/chromium/ui/accessibility/platform/ax_platform_node_auralinux.h @@ -11,6 +11,7 @@ #include <memory> #include <string> #include <utility> +#include <vector> #include "base/macros.h" #include "base/memory/protected_memory.h" @@ -105,7 +106,7 @@ class AX_EXPORT AXPlatformNodeAuraLinux : public AXPlatformNodeBase { void EnsureAtkObjectIsValid(); void Destroy() override; - AtkRole GetAtkRole(); + AtkRole GetAtkRole() const; void GetAtkState(AtkStateSet* state_set); AtkRelationSet* GetAtkRelations(); void GetExtents(gint* x, gint* y, gint* width, gint* height, @@ -196,6 +197,7 @@ class AX_EXPORT AXPlatformNodeAuraLinux : public AXPlatformNodeBase { bool SetCaretOffset(int offset); bool SetTextSelectionForAtkText(int start_offset, int end_offset); bool HasSelection(); + void GetSelectionExtents(int* start_offset, int* end_offset); gchar* GetSelectionWithText(int* start_offset, int* end_offset); @@ -203,16 +205,16 @@ class AX_EXPORT AXPlatformNodeAuraLinux : public AXPlatformNodeBase { // and end attributes will be assigned to the start_offset and end_offset // pointers if they are non-null. The return value AtkAttributeSet should // be freed with atk_attribute_set_free. - AtkAttributeSet* GetTextAttributes(int offset, - int* start_offset, - int* end_offset); + const TextAttributeList& GetTextAttributes(int offset, + int* start_offset, + int* end_offset); // Return the default text attributes for this node. The default text // attributes are the ones that apply to the entire node. Attributes found at // a given offset can be thought of as overriding the default attribute. // The return value AtkAttributeSet should be freed with // atk_attribute_set_free. - AtkAttributeSet* GetDefaultTextAttributes(); + const TextAttributeList& GetDefaultTextAttributes(); void ActivateFindInPageResult(int start_offset, int end_offset); void TerminateFindInPage(); @@ -221,6 +223,11 @@ class AX_EXPORT AXPlatformNodeAuraLinux : public AXPlatformNodeBase { // return it, otherwise return base::nullopt; base::Optional<FindInPageResultInfo> GetSelectionOffsetsFromFindInPage(); + // Get the embedded object ("hyperlink") indices for this object in the + // parent. If this object doesn't have a parent or isn't embedded, return + // nullopt. + base::Optional<std::pair<int, int>> GetEmbeddedObjectIndices(); + std::string accessible_name_; protected: @@ -268,11 +275,7 @@ class AX_EXPORT AXPlatformNodeAuraLinux : public AXPlatformNodeBase { base::Optional<std::pair<int, int>> GetEmbeddedObjectIndicesForId(int id); void ComputeStylesIfNeeded(); - void MergeSpellingIntoAtkTextAttributesAtOffset( - int offset, - std::map<int, AtkAttributes>* text_attributes); - AtkAttributes ComputeTextAttributes() const; - int FindStartOfStyle(int start_offset, AXTextBoundaryDirection direction); + int FindStartOfStyle(int start_offset, ui::AXTextBoundaryDirection direction); // Reset any find in page operations for the toplevel document of this node. void ForgetCurrentFindInPageResult(); @@ -287,6 +290,41 @@ class AX_EXPORT AXPlatformNodeAuraLinux : public AXPlatformNodeBase { // Find the first child which is a document containing web content. AtkObject* FindFirstWebContentDocument(); + // If a selection that intersects this node get the full selection + // including start and end node ids. + void GetFullSelection(int32_t* anchor_node_id, + int* anchor_offset, + int32_t* focus_node_id, + int* focus_offset); + + // Returns true if this node's AtkObject is suitable for emitting AtkText + // signals. ATs don't expect static text objects to emit AtkText signals. + bool EmitsAtkTextEvents() const; + + // Find the first ancestor which is an editable root or a document. This node + // will be one which contains a single selection. + AXPlatformNodeAuraLinux& FindEditableRootOrDocument(); + + // Find the first common ancestor between this node and a given node. + AXPlatformNodeAuraLinux* FindCommonAncestor(AXPlatformNodeAuraLinux* other); + + // Update the selection information stored in this node. This should be + // called on the editable root, the root node of the accessibility tree, or + // the document (ie the node returned by FindEditableRootOrDocument()). + void UpdateSelectionInformation(int32_t anchor_node_id, + int anchor_offset, + int32_t focus_node_id, + int focus_offset); + + // Emit a GObject signal indicating a selection change. + void EmitSelectionChangedSignal(bool had_selection); + + // Emit a GObject signal indicating that the caret has moved. + void EmitCaretChangedSignal(); + + bool HadNonZeroWidthSelection() const { return had_nonzero_width_selection; } + std::pair<int32_t, int> GetCurrentCaret() const { return current_caret_; } + // If the given argument can be found as a child of this node, return its // hypertext extents, otherwise return base::nullopt; base::Optional<std::pair<int, int>> GetHypertextExtentsOfChild( @@ -310,17 +348,24 @@ class AX_EXPORT AXPlatformNodeAuraLinux : public AXPlatformNodeBase { // minimized the last time it's visibility changed. bool was_minimized_ = false; - // The previously observed text selection for this node. We store - // this in order to avoid sending duplicate text-selection-changed - // and text-caret-moved events. - std::pair<int, int> text_selection_ = std::make_pair(-1, -1); + // Information about the selection meant to be stored on the return value of + // FindEditableRootOrDocument(). + // + // Whether or not we previously had a selection where the anchor and focus + // were not equal. This is what ATK consider a "selection." + bool had_nonzero_width_selection = false; + + // Information about the current caret location (a node id and an offset). + // This is used to track when the caret actually moves during a selection + // change. + std::pair<int32_t, int> current_caret_ = {-1, -1}; // A map which converts between an offset in the node's hypertext and the // ATK text attributes at that offset. - std::map<int, AtkAttributes> offset_to_text_attributes_; + TextAttributeMap offset_to_text_attributes_; // The default ATK text attributes for this node. - AtkAttributes default_text_attributes_; + TextAttributeList default_text_attributes_; DISALLOW_COPY_AND_ASSIGN(AXPlatformNodeAuraLinux); }; diff --git a/chromium/ui/accessibility/platform/ax_platform_node_auralinux_unittest.cc b/chromium/ui/accessibility/platform/ax_platform_node_auralinux_unittest.cc index 44973634166..75908edd3d2 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_auralinux_unittest.cc +++ b/chromium/ui/accessibility/platform/ax_platform_node_auralinux_unittest.cc @@ -1399,34 +1399,52 @@ TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkTextCaretMoved) { &caret_position_from_event); atk_text_set_caret_offset(atk_text, 4); + GetRootPlatformNode()->NotifyAccessibilityEvent( + ax::mojom::Event::kTextSelectionChanged); ASSERT_EQ(atk_text_get_caret_offset(atk_text), 4); ASSERT_EQ(caret_position_from_event, 4); // Setting the same position should not trigger another event. caret_position_from_event = -1; atk_text_set_caret_offset(atk_text, 4); + GetRootPlatformNode()->NotifyAccessibilityEvent( + ax::mojom::Event::kTextSelectionChanged); ASSERT_EQ(atk_text_get_caret_offset(atk_text), 4); ASSERT_EQ(caret_position_from_event, -1); int character_count = atk_text_get_character_count(atk_text); atk_text_set_caret_offset(atk_text, -1); + GetRootPlatformNode()->NotifyAccessibilityEvent( + ax::mojom::Event::kTextSelectionChanged); ASSERT_EQ(atk_text_get_caret_offset(atk_text), character_count); ASSERT_EQ(caret_position_from_event, character_count); + atk_text_set_caret_offset(atk_text, 0); // Reset position. + GetRootPlatformNode()->NotifyAccessibilityEvent( + ax::mojom::Event::kTextSelectionChanged); caret_position_from_event = -1; atk_text_set_caret_offset(atk_text, -1000); + GetRootPlatformNode()->NotifyAccessibilityEvent( + ax::mojom::Event::kTextSelectionChanged); ASSERT_EQ(atk_text_get_caret_offset(atk_text), character_count); ASSERT_EQ(caret_position_from_event, character_count); + atk_text_set_caret_offset(atk_text, 0); // Reset position. + GetRootPlatformNode()->NotifyAccessibilityEvent( + ax::mojom::Event::kTextSelectionChanged); caret_position_from_event = -1; atk_text_set_caret_offset(atk_text, 1000); + GetRootPlatformNode()->NotifyAccessibilityEvent( + ax::mojom::Event::kTextSelectionChanged); ASSERT_EQ(atk_text_get_caret_offset(atk_text), character_count); ASSERT_EQ(caret_position_from_event, character_count); caret_position_from_event = -1; atk_text_set_caret_offset(atk_text, character_count - 1); + GetRootPlatformNode()->NotifyAccessibilityEvent( + ax::mojom::Event::kTextSelectionChanged); ASSERT_EQ(atk_text_get_caret_offset(atk_text), character_count - 1); ASSERT_EQ(caret_position_from_event, character_count - 1); @@ -2096,6 +2114,8 @@ TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkTextTextFieldSetSelection) { int selection_start, selection_end; EXPECT_TRUE(atk_text_set_selection(atk_text, 0, 0, 1)); + GetRootPlatformNode()->NotifyAccessibilityEvent( + ax::mojom::Event::kTextSelectionChanged); EXPECT_TRUE(saw_selection_change); g_free(atk_text_get_selection(atk_text, 0, &selection_start, &selection_end)); EXPECT_EQ(selection_start, 0); @@ -2103,25 +2123,22 @@ TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkTextTextFieldSetSelection) { // Reset position. EXPECT_TRUE(atk_text_set_selection(atk_text, 0, 0, 0)); + GetRootPlatformNode()->NotifyAccessibilityEvent( + ax::mojom::Event::kTextSelectionChanged); saw_selection_change = false; EXPECT_TRUE(atk_text_set_selection(atk_text, 0, 1, 0)); + GetRootPlatformNode()->NotifyAccessibilityEvent( + ax::mojom::Event::kTextSelectionChanged); EXPECT_TRUE(saw_selection_change); g_free(atk_text_get_selection(atk_text, 0, &selection_start, &selection_end)); EXPECT_EQ(selection_start, 0); EXPECT_EQ(selection_end, 1); - // Setting the selection to the same location should not trigger - // another event. - saw_selection_change = false; - EXPECT_TRUE(atk_text_set_selection(atk_text, 0, 1, 0)); - EXPECT_FALSE(saw_selection_change); - g_free(atk_text_get_selection(atk_text, 0, &selection_start, &selection_end)); - EXPECT_EQ(selection_start, 0); - EXPECT_EQ(selection_end, 1); - saw_selection_change = false; EXPECT_TRUE(atk_text_set_selection(atk_text, 0, 2, 4)); + GetRootPlatformNode()->NotifyAccessibilityEvent( + ax::mojom::Event::kTextSelectionChanged); EXPECT_TRUE(saw_selection_change); g_free(atk_text_get_selection(atk_text, 0, &selection_start, &selection_end)); EXPECT_EQ(selection_start, 2); @@ -2136,11 +2153,12 @@ TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkTextTextFieldSetSelection) { saw_selection_change = false; EXPECT_FALSE(atk_text_set_selection(atk_text, 0, 0, 50)); - EXPECT_FALSE(saw_selection_change); saw_selection_change = false; int n_characters = atk_text_get_character_count(atk_text); EXPECT_TRUE(atk_text_set_selection(atk_text, 0, 0, -1)); + GetRootPlatformNode()->NotifyAccessibilityEvent( + ax::mojom::Event::kTextSelectionChanged); EXPECT_TRUE(saw_selection_change); g_free(atk_text_get_selection(atk_text, 0, &selection_start, &selection_end)); EXPECT_EQ(selection_start, 0); @@ -2148,6 +2166,8 @@ TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkTextTextFieldSetSelection) { saw_selection_change = false; EXPECT_TRUE(atk_text_set_selection(atk_text, 0, 0, 1)); + GetRootPlatformNode()->NotifyAccessibilityEvent( + ax::mojom::Event::kTextSelectionChanged); EXPECT_EQ(1, atk_text_get_n_selections(atk_text)); EXPECT_TRUE(atk_text_remove_selection(atk_text, 0)); EXPECT_TRUE(saw_selection_change); @@ -2155,9 +2175,13 @@ TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkTextTextFieldSetSelection) { // Reset position. EXPECT_TRUE(atk_text_set_selection(atk_text, 0, 0, 0)); + GetRootPlatformNode()->NotifyAccessibilityEvent( + ax::mojom::Event::kTextSelectionChanged); saw_selection_change = false; EXPECT_TRUE(atk_text_set_selection(atk_text, 0, 0, 1)); + GetRootPlatformNode()->NotifyAccessibilityEvent( + ax::mojom::Event::kTextSelectionChanged); EXPECT_EQ(1, atk_text_get_n_selections(atk_text)); EXPECT_FALSE(atk_text_remove_selection(atk_text, 1)); EXPECT_TRUE(saw_selection_change); diff --git a/chromium/ui/accessibility/platform/ax_platform_node_base.cc b/chromium/ui/accessibility/platform/ax_platform_node_base.cc index bb3af2c0066..5f672836406 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_base.cc +++ b/chromium/ui/accessibility/platform/ax_platform_node_base.cc @@ -13,6 +13,7 @@ #include "base/strings/string_number_conversions.h" #include "base/strings/string_util.h" #include "base/strings/utf_string_conversions.h" +#include "third_party/skia/include/core/SkColor.h" #include "ui/accessibility/ax_action_data.h" #include "ui/accessibility/ax_enums.mojom.h" #include "ui/accessibility/ax_node_data.h" @@ -458,14 +459,24 @@ bool AXPlatformNodeBase::IsSelectionItemSupported() const { return table->GetData().role == ax::mojom::Role::kGrid || table->GetData().role == ax::mojom::Role::kTreeGrid; } + // https://www.w3.org/TR/core-aam-1.1/#mapping_state-property_table + // SelectionItem.IsSelected is exposed when aria-checked is True or False, + // for 'radio' and 'menuitemradio' roles. + case ax::mojom::Role::kRadioButton: + case ax::mojom::Role::kMenuItemRadio: { + if (GetData().GetCheckedState() == ax::mojom::CheckedState::kTrue || + GetData().GetCheckedState() == ax::mojom::CheckedState::kFalse) + return true; + return false; + } + // https://www.w3.org/TR/wai-aria-1.1/#aria-selected + // SelectionItem.IsSelected is exposed when aria-select is True or False. case ax::mojom::Role::kListBoxOption: case ax::mojom::Role::kListItem: - case ax::mojom::Role::kMenuItemRadio: case ax::mojom::Role::kMenuListOption: - case ax::mojom::Role::kRadioButton: case ax::mojom::Role::kTab: case ax::mojom::Role::kTreeItem: - return true; + return HasBoolAttribute(ax::mojom::BoolAttribute::kSelected); default: return false; } @@ -1711,4 +1722,148 @@ std::string AXPlatformNodeBase::GetInvalidValue() const { return invalid_value; } +ui::TextAttributeList AXPlatformNodeBase::ComputeTextAttributes() const { + ui::TextAttributeList attributes; + + // We include list markers for now, but there might be other objects that are + // auto generated. + // TODO(nektar): Compute what objects are auto-generated in Blink. + if (GetData().role == ax::mojom::Role::kListMarker) + attributes.push_back(std::make_pair("auto-generated", "true")); + + int color; + if (GetIntAttribute(ax::mojom::IntAttribute::kBackgroundColor, &color)) { + unsigned int alpha = SkColorGetA(color); + unsigned int red = SkColorGetR(color); + unsigned int green = SkColorGetG(color); + unsigned int blue = SkColorGetB(color); + // Don't expose default value of pure white. + if (alpha && (red != 255 || green != 255 || blue != 255)) { + std::string color_value = "rgb(" + base::NumberToString(red) + ',' + + base::NumberToString(green) + ',' + + base::NumberToString(blue) + ')'; + SanitizeTextAttributeValue(color_value, &color_value); + attributes.push_back(std::make_pair("background-color", color_value)); + } + } + + if (GetIntAttribute(ax::mojom::IntAttribute::kColor, &color)) { + unsigned int red = SkColorGetR(color); + unsigned int green = SkColorGetG(color); + unsigned int blue = SkColorGetB(color); + // Don't expose default value of black. + if (red || green || blue) { + std::string color_value = "rgb(" + base::NumberToString(red) + ',' + + base::NumberToString(green) + ',' + + base::NumberToString(blue) + ')'; + SanitizeTextAttributeValue(color_value, &color_value); + attributes.push_back(std::make_pair("color", color_value)); + } + } + + // First try to get the inherited font family name from the delegate. If we + // cannot find any name, fall back to looking the hierarchy of this node's + // AXNodeData instead. + std::string font_family(GetDelegate()->GetInheritedFontFamilyName()); + if (font_family.empty()) { + font_family = + GetInheritedStringAttribute(ax::mojom::StringAttribute::kFontFamily); + } + + // Attribute has no default value. + if (!font_family.empty()) { + SanitizeTextAttributeValue(font_family, &font_family); + attributes.push_back(std::make_pair("font-family", font_family)); + } + + float font_size; + // Attribute has no default value. + if (GetFloatAttribute(ax::mojom::FloatAttribute::kFontSize, &font_size)) { + // The IA2 Spec requires the value to be in pt, not in pixels. + // There are 72 points per inch. + // We assume that there are 96 pixels per inch on a standard display. + // TODO(nektar): Figure out the current value of pixels per inch. + float points = font_size * 72.0 / 96.0; + + // Round to the nearest 0.5 points. + points = std::round(points * 2.0) / 2.0; + + attributes.push_back( + std::make_pair("font-size", base::NumberToString(points) + "pt")); + } + + // TODO(nektar): Add Blink support for the following attributes: + // text-line-through-mode, text-line-through-width, text-outline:false, + // text-position:baseline, text-shadow:none, text-underline-mode:continuous. + + int32_t text_style = GetIntAttribute(ax::mojom::IntAttribute::kTextStyle); + if (text_style) { + if (GetData().HasTextStyle(ax::mojom::TextStyle::kBold)) + attributes.push_back(std::make_pair("font-weight", "bold")); + if (GetData().HasTextStyle(ax::mojom::TextStyle::kItalic)) + attributes.push_back(std::make_pair("font-style", "italic")); + if (GetData().HasTextStyle(ax::mojom::TextStyle::kLineThrough)) { + // TODO(nektar): Figure out a more specific value. + attributes.push_back(std::make_pair("text-line-through-style", "solid")); + } + if (GetData().HasTextStyle(ax::mojom::TextStyle::kUnderline)) { + // TODO(nektar): Figure out a more specific value. + attributes.push_back(std::make_pair("text-underline-style", "solid")); + } + } + + // Screen readers look at the text attributes to determine if something is + // misspelled, so we need to propagate any spelling attributes from immediate + // parents of text-only objects. + std::string invalid_value = GetInvalidValue(); + if (!invalid_value.empty()) + attributes.push_back(std::make_pair("invalid", invalid_value)); + + std::string language = GetDelegate()->GetLanguage(); + if (!language.empty()) { + SanitizeTextAttributeValue(language, &language); + attributes.push_back(std::make_pair("language", language)); + } + + auto text_direction = static_cast<ax::mojom::TextDirection>( + GetIntAttribute(ax::mojom::IntAttribute::kTextDirection)); + switch (text_direction) { + case ax::mojom::TextDirection::kNone: + break; + case ax::mojom::TextDirection::kLtr: + attributes.push_back(std::make_pair("writing-mode", "lr")); + break; + case ax::mojom::TextDirection::kRtl: + attributes.push_back(std::make_pair("writing-mode", "rl")); + break; + case ax::mojom::TextDirection::kTtb: + attributes.push_back(std::make_pair("writing-mode", "tb")); + break; + case ax::mojom::TextDirection::kBtt: + // Not listed in the IA2 Spec. + attributes.push_back(std::make_pair("writing-mode", "bt")); + break; + } + + auto text_position = static_cast<ax::mojom::TextPosition>( + GetIntAttribute(ax::mojom::IntAttribute::kTextPosition)); + switch (text_position) { + case ax::mojom::TextPosition::kNone: + break; + case ax::mojom::TextPosition::kSubscript: + attributes.push_back(std::make_pair("text-position", "sub")); + break; + case ax::mojom::TextPosition::kSuperscript: + attributes.push_back(std::make_pair("text-position", "super")); + break; + } + + return attributes; +} + +void AXPlatformNodeBase::SanitizeTextAttributeValue(const std::string& input, + std::string* output) const { + DCHECK(output); +} + } // namespace ui diff --git a/chromium/ui/accessibility/platform/ax_platform_node_base.h b/chromium/ui/accessibility/platform/ax_platform_node_base.h index 2ae8bfd736a..e545591e9a4 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_base.h +++ b/chromium/ui/accessibility/platform/ax_platform_node_base.h @@ -13,6 +13,7 @@ #include "build/build_config.h" #include "ui/accessibility/ax_enums.mojom-forward.h" #include "ui/accessibility/platform/ax_platform_node.h" +#include "ui/accessibility/platform/ax_platform_node_delegate.h" #include "ui/accessibility/platform/ax_platform_text_boundary.h" #include "ui/base/buildflags.h" #include "ui/gfx/geometry/rect.h" @@ -25,7 +26,6 @@ namespace ui { struct AXNodeData; -class AXPlatformNodeDelegate; struct AX_EXPORT AXHypertext { AXHypertext(); @@ -273,6 +273,8 @@ class AX_EXPORT AXPlatformNodeBase : public AXPlatformNode { // input node. The node's subtree will not be searched. int NearestTextIndexToPoint(gfx::Point point); + ui::TextAttributeList ComputeTextAttributes() const; + // // Delegate. This is a weak reference which owns |this|. // @@ -349,6 +351,12 @@ class AX_EXPORT AXPlatformNodeBase : public AXPlatformNode { static void SanitizeStringAttribute(const std::string& input, std::string* output); + // Escapes characters in text attribute values as required by the platform. + // It's okay for input to be the same as output. The default implementation + // does nothing to the input value. + virtual void SanitizeTextAttributeValue(const std::string& input, + std::string* output) const; + // Compute the hypertext for this node to be exposed via IA2 and ATK This // method is responsible for properly embedding children using the special // embedded element character. diff --git a/chromium/ui/accessibility/platform/ax_platform_node_delegate.h b/chromium/ui/accessibility/platform/ax_platform_node_delegate.h index dd26414994d..ff2b83be0c3 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_delegate.h +++ b/chromium/ui/accessibility/platform/ax_platform_node_delegate.h @@ -7,6 +7,7 @@ #include <stdint.h> +#include <map> #include <memory> #include <new> #include <set> @@ -24,6 +25,7 @@ #include "ui/accessibility/ax_position.h" #include "ui/accessibility/ax_text_boundary.h" #include "ui/accessibility/ax_text_utils.h" +#include "ui/accessibility/ax_tree_id.h" #include "ui/accessibility/platform/ax_unique_id.h" #include "ui/gfx/geometry/vector2d.h" #include "ui/gfx/native_widget_types.h" @@ -41,6 +43,15 @@ struct AXTreeData; class AXTree; class AXPlatformNode; +using TextAttribute = std::pair<std::string, std::string>; +using TextAttributeList = std::vector<TextAttribute>; + +// A TextAttributeMap is a map between the text offset in UTF-16 characters in +// the node hypertext and the TextAttributeList that starts at that location. +// An empty TextAttributeList signifies a return to the default node +// TextAttributeList. +using TextAttributeMap = std::map<int, TextAttributeList>; + // An object that wants to be accessible should derive from this class. // AXPlatformNode subclasses use this interface to query all of the information // about the object in order to implement native accessibility APIs. @@ -77,6 +88,10 @@ class AX_EXPORT AXPlatformNodeDelegate { // method is only meaningful on macOS. virtual gfx::NativeViewAccessible GetNSWindow() = 0; + // Get the node for this delegate, which may be an AXPlatformNode or it may + // be a native accessible object implemented by another class. + virtual gfx::NativeViewAccessible GetNativeViewAccessible() = 0; + // Get the parent of the node, which may be an AXPlatformNode or it may // be a native accessible object implemented by another class. virtual gfx::NativeViewAccessible GetParent() = 0; @@ -129,6 +144,18 @@ class AX_EXPORT AXPlatformNodeDelegate { // implementation, this may mean the new selection will span multiple nodes. virtual bool SetHypertextSelection(int start_offset, int end_offset) = 0; + // Compute the text attributes map for the node associated with this + // delegate, given a set of default text attributes that apply to the entire + // node. A text attribute map associates a list of text attributes with a + // given hypertext offset in this node. + virtual TextAttributeMap ComputeTextAttributeMap( + const TextAttributeList& default_attributes) const = 0; + + // Get the inherited font family name for text attributes. We need this + // because inheritance works differently between the different delegate + // implementations. + virtual std::string GetInheritedFontFamilyName() const = 0; + // Returns the text of this node and all descendant nodes; including text // found in embedded objects. virtual base::string16 GetInnerText() const = 0; @@ -193,8 +220,15 @@ class AX_EXPORT AXPlatformNodeDelegate { // Get whether this node is in web content. virtual bool IsWebContent() const = 0; + // Get another node from this same tree. virtual AXPlatformNode* GetFromNodeID(int32_t id) = 0; + // Get a node from a different tree using a tree ID and node ID. + // Note that this is only guaranteed to work if the other tree is of the + // same type, i.e. it won't work between web and views or vice-versa. + virtual AXPlatformNode* GetFromTreeIDAndNodeID(const ui::AXTreeID& ax_tree_id, + int32_t id) = 0; + // Given a node ID attribute (one where IsNodeIdIntAttribute is true), return // a target nodes for which this delegate's node has that relationship // attribute or NULL if there is no such relationship. diff --git a/chromium/ui/accessibility/platform/ax_platform_node_delegate_base.cc b/chromium/ui/accessibility/platform/ax_platform_node_delegate_base.cc index a07e5c145f3..b018bd486d3 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_delegate_base.cc +++ b/chromium/ui/accessibility/platform/ax_platform_node_delegate_base.cc @@ -44,6 +44,11 @@ gfx::NativeViewAccessible AXPlatformNodeDelegateBase::GetNSWindow() { return nullptr; } +gfx::NativeViewAccessible +AXPlatformNodeDelegateBase::GetNativeViewAccessible() { + return nullptr; +} + gfx::NativeViewAccessible AXPlatformNodeDelegateBase::GetParent() { return nullptr; } @@ -220,6 +225,12 @@ AXPlatformNode* AXPlatformNodeDelegateBase::GetFromNodeID(int32_t id) { return nullptr; } +AXPlatformNode* AXPlatformNodeDelegateBase::GetFromTreeIDAndNodeID( + const ui::AXTreeID& ax_tree_id, + int32_t id) { + return nullptr; +} + int AXPlatformNodeDelegateBase::GetIndexInParent() { AXPlatformNodeDelegate* parent = GetParentDelegate(); if (!parent) @@ -405,6 +416,19 @@ AXPlatformNodeDelegateBase::GetStyleNameAttributeAsLocalizedString() const { return base::string16(); } +TextAttributeMap AXPlatformNodeDelegateBase::ComputeTextAttributeMap( + const TextAttributeList& default_attributes) const { + ui::TextAttributeMap attributes_map; + attributes_map[0] = default_attributes; + return attributes_map; +} + +std::string AXPlatformNodeDelegateBase::GetInheritedFontFamilyName() const { + // We don't have access to AXNodeData here, so we cannot return + // an inherited font family name. + return std::string(); +} + bool AXPlatformNodeDelegateBase::ShouldIgnoreHoveredStateForTesting() { return true; } diff --git a/chromium/ui/accessibility/platform/ax_platform_node_delegate_base.h b/chromium/ui/accessibility/platform/ax_platform_node_delegate_base.h index 6fdcd605765..a9623c13204 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_delegate_base.h +++ b/chromium/ui/accessibility/platform/ax_platform_node_delegate_base.h @@ -7,6 +7,7 @@ #include <stdint.h> +#include <memory> #include <set> #include <string> #include <vector> @@ -44,6 +45,10 @@ class AX_EXPORT AXPlatformNodeDelegateBase : public AXPlatformNodeDelegate { // See comments in AXPlatformNodeDelegate. gfx::NativeViewAccessible GetNSWindow() override; + // Get the node for this delegate, which may be an AXPlatformNode or it may + // be a native accessible object implemented by another class. + gfx::NativeViewAccessible GetNativeViewAccessible() override; + // Get the parent of the node, which may be an AXPlatformNode or it may // be a native accessible object implemented by another class. gfx::NativeViewAccessible GetParent() override; @@ -88,6 +93,9 @@ class AX_EXPORT AXPlatformNodeDelegateBase : public AXPlatformNodeDelegate { base::string16 GetHypertext() const override; bool SetHypertextSelection(int start_offset, int end_offset) override; + TextAttributeMap ComputeTextAttributeMap( + const TextAttributeList& default_attributes) const override; + std::string GetInheritedFontFamilyName() const override; base::string16 GetInnerText() const override; @@ -141,8 +149,15 @@ class AX_EXPORT AXPlatformNodeDelegateBase : public AXPlatformNodeDelegate { // Get whether this node is in web content. bool IsWebContent() const override; + // Get another node from this same tree. AXPlatformNode* GetFromNodeID(int32_t id) override; + // Get a node from a different tree using a tree ID and node ID. + // Note that this is only guaranteed to work if the other tree is of the + // same type, i.e. it won't work between web and views or vice-versa. + AXPlatformNode* GetFromTreeIDAndNodeID(const ui::AXTreeID& ax_tree_id, + int32_t id) override; + // Given a node ID attribute (one where IsNodeIdIntAttribute is true), return // a target nodes for which this delegate's node has that relationship // attribute or NULL if there is no such relationship. diff --git a/chromium/ui/accessibility/platform/ax_platform_node_mac.mm b/chromium/ui/accessibility/platform/ax_platform_node_mac.mm index 418729482e8..485e7505dd6 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_mac.mm +++ b/chromium/ui/accessibility/platform/ax_platform_node_mac.mm @@ -43,7 +43,11 @@ RoleMap BuildRoleMap() { {ax::mojom::Role::kAlert, NSAccessibilityGroupRole}, {ax::mojom::Role::kAlertDialog, NSAccessibilityGroupRole}, {ax::mojom::Role::kAnchor, NSAccessibilityGroupRole}, - {ax::mojom::Role::kAnnotation, NSAccessibilityUnknownRole}, + {ax::mojom::Role::kAnnotationAttribution, NSAccessibilityGroupRole}, + {ax::mojom::Role::kAnnotationCommentary, NSAccessibilityGroupRole}, + {ax::mojom::Role::kAnnotationPresence, NSAccessibilityGroupRole}, + {ax::mojom::Role::kAnnotationRevision, NSAccessibilityGroupRole}, + {ax::mojom::Role::kAnnotationSuggestion, NSAccessibilityGroupRole}, {ax::mojom::Role::kApplication, NSAccessibilityGroupRole}, {ax::mojom::Role::kArticle, NSAccessibilityGroupRole}, {ax::mojom::Role::kAudio, NSAccessibilityGroupRole}, @@ -121,6 +125,7 @@ RoleMap BuildRoleMap() { {ax::mojom::Role::kFigcaption, NSAccessibilityGroupRole}, {ax::mojom::Role::kFigure, NSAccessibilityGroupRole}, {ax::mojom::Role::kFooter, NSAccessibilityGroupRole}, + {ax::mojom::Role::kFooterAsNonLandmark, NSAccessibilityGroupRole}, {ax::mojom::Role::kForm, NSAccessibilityGroupRole}, {ax::mojom::Role::kGenericContainer, NSAccessibilityGroupRole}, {ax::mojom::Role::kGraphicsDocument, NSAccessibilityGroupRole}, @@ -130,6 +135,8 @@ RoleMap BuildRoleMap() { // a list as of 10.12.6, so following WebKit and using table role: {ax::mojom::Role::kGrid, NSAccessibilityTableRole}, // crbug.com/753925 {ax::mojom::Role::kGroup, NSAccessibilityGroupRole}, + {ax::mojom::Role::kHeader, NSAccessibilityGroupRole}, + {ax::mojom::Role::kHeaderAsNonLandmark, NSAccessibilityGroupRole}, {ax::mojom::Role::kHeading, @"AXHeading"}, {ax::mojom::Role::kIframe, NSAccessibilityGroupRole}, {ax::mojom::Role::kIframePresentational, NSAccessibilityGroupRole}, @@ -179,9 +186,14 @@ RoleMap BuildRoleMap() { {ax::mojom::Role::kRootWebArea, @"AXWebArea"}, {ax::mojom::Role::kRow, NSAccessibilityRowRole}, {ax::mojom::Role::kRowHeader, @"AXCell"}, + // TODO(accessibility) What should kRuby be? It's not listed? Any others + // missing? Maybe use switch statement so that compiler doesn't allow us + // to miss any. + {ax::mojom::Role::kRubyAnnotation, NSAccessibilityUnknownRole}, {ax::mojom::Role::kScrollBar, NSAccessibilityScrollBarRole}, {ax::mojom::Role::kSearch, NSAccessibilityGroupRole}, {ax::mojom::Role::kSearchBox, NSAccessibilityTextFieldRole}, + {ax::mojom::Role::kSection, NSAccessibilityGroupRole}, {ax::mojom::Role::kSlider, NSAccessibilitySliderRole}, {ax::mojom::Role::kSliderThumb, NSAccessibilityValueIndicatorRole}, {ax::mojom::Role::kSpinButton, NSAccessibilityIncrementorRole}, @@ -238,15 +250,17 @@ RoleMap BuildSubroleMap() { {ax::mojom::Role::kFooter, @"AXLandmarkContentInfo"}, {ax::mojom::Role::kForm, @"AXLandmarkForm"}, {ax::mojom::Role::kGraphicsDocument, @"AXDocument"}, + {ax::mojom::Role::kHeader, @"AXLandmarkBanner"}, {ax::mojom::Role::kLog, @"AXApplicationLog"}, {ax::mojom::Role::kMain, @"AXLandmarkMain"}, {ax::mojom::Role::kMarquee, @"AXApplicationMarquee"}, {ax::mojom::Role::kMath, @"AXDocumentMath"}, {ax::mojom::Role::kNavigation, @"AXLandmarkNavigation"}, {ax::mojom::Role::kNote, @"AXDocumentNote"}, - {ax::mojom::Role::kRegion, @"AXDocumentRegion"}, + {ax::mojom::Role::kRegion, @"AXLandmarkRegion"}, {ax::mojom::Role::kSearch, @"AXLandmarkSearch"}, {ax::mojom::Role::kSearchBox, @"AXSearchField"}, + {ax::mojom::Role::kSection, @"AXLandmarkRegion"}, {ax::mojom::Role::kStatus, @"AXApplicationStatus"}, {ax::mojom::Role::kSwitch, @"AXSwitch"}, {ax::mojom::Role::kTabPanel, @"AXTabPanel"}, @@ -307,11 +321,6 @@ void PostAnnouncementNotification(NSString* announcement) { } void NotifyMacEvent(AXPlatformNodeCocoa* target, ax::mojom::Event event_type) { - NSString* announcement_text = [target announcementTextForEvent:event_type]; - if (announcement_text) { - PostAnnouncementNotification(announcement_text); - return; - } NSString* notification = [AXPlatformNodeCocoa nativeNotificationFromAXEvent:event_type]; if (notification) @@ -1146,27 +1155,33 @@ gfx::NativeViewAccessible AXPlatformNodeMac::GetNativeViewAccessible() { void AXPlatformNodeMac::NotifyAccessibilityEvent(ax::mojom::Event event_type) { GetNativeViewAccessible(); - // Add mappings between ax::mojom::Event and NSAccessibility notifications - // using the EventMap above. This switch contains exceptions to those - // mappings. - switch (event_type) { - case ax::mojom::Event::kTextChanged: - // If the view is a user-editable textfield, this should change the value. - if (GetData().role == ax::mojom::Role::kTextField) { - NotifyMacEvent(native_node_, ax::mojom::Event::kValueChanged); - return; - } - break; - case ax::mojom::Event::kSelection: + // Handle special cases. + NSString* announcement_text = + [native_node_ announcementTextForEvent:event_type]; + if (announcement_text) { + PostAnnouncementNotification(announcement_text); + return; + } + if (event_type == ax::mojom::Event::kSelection) { + ax::mojom::Role role = GetData().role; + if (ui::IsMenuItem(role)) { // On Mac, map menu item selection to a focus event. - if (ui::IsMenuItem(GetData().role)) { - NotifyMacEvent(native_node_, ax::mojom::Event::kFocus); - return; + NotifyMacEvent(native_node_, ax::mojom::Event::kFocus); + return; + } else if (ui::IsListItem(role)) { + if (AXPlatformNodeBase* container = GetSelectionContainer()) { + const ui::AXNodeData& data = container->GetData(); + if (data.role == ax::mojom::Role::kListBox && + !data.HasState(ax::mojom::State::kMultiselectable) && + GetDelegate()->GetFocus() == GetNativeViewAccessible()) { + NotifyMacEvent(native_node_, ax::mojom::Event::kFocus); + return; + } } - break; - default: - break; + } } + // Otherwise, use mappings between ax::mojom::Event and NSAccessibility + // notifications from the EventMap above. NotifyMacEvent(native_node_, event_type); } diff --git a/chromium/ui/accessibility/platform/ax_platform_node_textchildprovider_win_unittest.cc b/chromium/ui/accessibility/platform/ax_platform_node_textchildprovider_win_unittest.cc index b8825da748f..ac7518e5d31 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_textchildprovider_win_unittest.cc +++ b/chromium/ui/accessibility/platform/ax_platform_node_textchildprovider_win_unittest.cc @@ -71,6 +71,8 @@ class AXPlatformNodeTextChildProviderTest : public ui::AXPlatformNodeWinTest { AXNode* root_node = GetRootNode(); AXNodePosition::SetTree(tree_.get()); + AXTreeManagerMap::GetInstance().AddTreeManager(update.tree_data.tree_id, + this); AXNode* nontext_child_of_root_node = root_node->children()[0]; AXNode* text_child_of_root_node = root_node->children()[1]; AXNode* nontext_child_of_nontext_node = diff --git a/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win.cc b/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win.cc index 2c2c2e7be83..bcff2a5d35b 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win.cc +++ b/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win.cc @@ -12,10 +12,30 @@ #include "ui/accessibility/ax_action_data.h" #include "ui/accessibility/platform/ax_platform_node_delegate.h" -#define UIA_VALIDATE_TEXTRANGEPROVIDER_CALL() \ - if (!owner() || !owner()->GetDelegate() || !start_->GetAnchor() || \ - !end_->GetAnchor()) \ +#define UIA_VALIDATE_TEXTRANGEPROVIDER_CALL() \ + if (!owner() || !owner()->GetDelegate() || !start_ || \ + !start_->GetAnchor() || !end_ || !end_->GetAnchor()) \ return UIA_E_ELEMENTNOTAVAILABLE; +#define UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_IN(in) \ + if (!owner() || !owner()->GetDelegate() || !start_ || \ + !start_->GetAnchor() || !end_ || !end_->GetAnchor()) \ + return UIA_E_ELEMENTNOTAVAILABLE; \ + if (!in) \ + return E_POINTER; +#define UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_OUT(out) \ + if (!owner() || !owner()->GetDelegate() || !start_ || \ + !start_->GetAnchor() || !end_ || !end_->GetAnchor()) \ + return UIA_E_ELEMENTNOTAVAILABLE; \ + if (!out) \ + return E_POINTER; \ + *out = {}; +#define UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_IN_1_OUT(in, out) \ + if (!owner() || !owner()->GetDelegate() || !start_ || \ + !start_->GetAnchor() || !end_ || !end_->GetAnchor()) \ + return UIA_E_ELEMENTNOTAVAILABLE; \ + if (!in || !out) \ + return E_POINTER; \ + *out = {}; // Validate bounds calculated by AXPlatformNodeDelegate. Degenerate bounds // indicate the interface is not yet supported on the platform. @@ -25,6 +45,38 @@ namespace ui { +class AXRangeScreenRectDelegateImpl : public AXRangeScreenRectDelegate { + public: + AXRangeScreenRectDelegateImpl(AXPlatformNodeTextRangeProviderWin* host) + : host_(host) {} + + gfx::Rect GetInnerTextRangeBoundsRect( + AXTreeID tree_id, + AXNode::AXID node_id, + int start_offset, + int end_offset, + AXOffscreenResult* offscreen_result) override { + AXPlatformNodeDelegate* delegate = host_->GetDelegate(tree_id, node_id); + DCHECK(delegate); + return delegate->GetInnerTextRangeBoundsRect( + start_offset, end_offset, ui::AXCoordinateSystem::kScreen, + ui::AXClippingBehavior::kClipped, offscreen_result); + } + + gfx::Rect GetBoundsRect(AXTreeID tree_id, + AXNode::AXID node_id, + AXOffscreenResult* offscreen_result) override { + AXPlatformNodeDelegate* delegate = host_->GetDelegate(tree_id, node_id); + DCHECK(delegate); + return delegate->GetBoundsRect(ui::AXCoordinateSystem::kScreen, + ui::AXClippingBehavior::kClipped, + offscreen_result); + } + + private: + AXPlatformNodeTextRangeProviderWin* host_; +}; + AXPlatformNodeTextRangeProviderWin::AXPlatformNodeTextRangeProviderWin() { DVLOG(1) << __func__; } @@ -55,7 +107,7 @@ ITextRangeProvider* AXPlatformNodeTextRangeProviderWin::CreateTextRangeProvider( STDMETHODIMP AXPlatformNodeTextRangeProviderWin::Clone( ITextRangeProvider** clone) { WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXTRANGE_CLONE); - UIA_VALIDATE_TEXTRANGEPROVIDER_CALL(); + UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_OUT(clone); *clone = CreateTextRangeProvider(owner_, start_->Clone(), end_->Clone()); return S_OK; @@ -65,19 +117,16 @@ STDMETHODIMP AXPlatformNodeTextRangeProviderWin::Compare( ITextRangeProvider* other, BOOL* result) { WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXTRANGE_COMPARE); - UIA_VALIDATE_TEXTRANGEPROVIDER_CALL(); + UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_IN_1_OUT(other, result); CComPtr<AXPlatformNodeTextRangeProviderWin> other_provider; - if (other->QueryInterface(&other_provider) != S_OK) { + if (other->QueryInterface(&other_provider) != S_OK) return UIA_E_INVALIDOPERATION; - } - *result = FALSE; if (*start_ == *(other_provider->start_) && *end_ == *(other_provider->end_)) { *result = TRUE; } - return S_OK; } @@ -87,29 +136,30 @@ STDMETHODIMP AXPlatformNodeTextRangeProviderWin::CompareEndpoints( TextPatternRangeEndpoint other_endpoint, int* result) { WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXTRANGE_COMPAREENDPOINTS); - UIA_VALIDATE_TEXTRANGEPROVIDER_CALL(); + UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_IN_1_OUT(other, result); CComPtr<AXPlatformNodeTextRangeProviderWin> other_provider; - if (other->QueryInterface(&other_provider) != S_OK) { + if (other->QueryInterface(&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_; - if (*this_provider_endpoint < *other_provider_endpoint) { + base::Optional<int> comparison = + this_provider_endpoint->CompareTo(*other_provider_endpoint); + if (!comparison) + return UIA_E_INVALIDOPERATION; + + if (comparison.value() < 0) *result = -1; - } else if (*this_provider_endpoint > *other_provider_endpoint) { + else if (comparison.value() > 0) *result = 1; - } else { + else *result = 0; - } - return S_OK; } @@ -146,6 +196,8 @@ STDMETHODIMP AXPlatformNodeTextRangeProviderWin::ExpandToEnclosingUnit( AXBoundaryBehavior::CrossBoundary); DCHECK(!end_->IsNullPosition()); } + + NormalizeTextRange(); break; } case TextUnit_Format: @@ -157,14 +209,16 @@ STDMETHODIMP AXPlatformNodeTextRangeProviderWin::ExpandToEnclosingUnit( case TextUnit_Word: start_ = start_->CreatePreviousWordStartPosition( AXBoundaryBehavior::StopIfAlreadyAtBoundary); - // Since start_ is already located at a word boundary, we need to cross it - // in order to move to the next one; the only edge case here is the last - // word in the document, which will result in a null position. + // in order to move to the next one (stopping at the last anchor's end). end_ = start_->CreateNextWordStartPosition( - AXBoundaryBehavior::CrossBoundary); - if (end_->IsNullPosition()) - end_ = start_->CreatePositionAtEndOfDocument(); + AXBoundaryBehavior::StopAtLastAnchorBoundary); + // 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()) { + end_ = start_->CreatePositionAtEndOfAnchor(); + } break; case TextUnit_Line: start_ = start_->CreatePreviousLineStartPosition( @@ -179,10 +233,10 @@ STDMETHODIMP AXPlatformNodeTextRangeProviderWin::ExpandToEnclosingUnit( AXBoundaryBehavior::StopIfAlreadyAtBoundary); break; case TextUnit_Page: { - // If the document doesn't support pagination, default to document units - // per UIA spec - AXPositionInstance common_ancestor = start_->LowestCommonAncestor(*end_); - if (common_ancestor->GetAnchor()->tree()->HasPaginationSupport()) { + // 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()) { start_ = start_->CreatePreviousPageStartPosition( ui::AXBoundaryBehavior::StopIfAlreadyAtBoundary); end_ = start_->CreateNextPageEndPosition( @@ -198,21 +252,6 @@ STDMETHODIMP AXPlatformNodeTextRangeProviderWin::ExpandToEnclosingUnit( default: return UIA_E_NOTSUPPORTED; } - - // Some text positions are equal when compared, but they could be located at - // different anchors, affecting how `GetEnclosingElement` works. Normalize the - // endpoints to correctly enclose characters of the text representation. - AXPositionInstance normalized_start = start_->AsPositionBeforeCharacter(); - AXPositionInstance normalized_end = end_->AsPositionBeforeCharacter(); - - if (!normalized_start->IsNullPosition()) { - DCHECK_EQ(*start_, *normalized_start); - start_ = std::move(normalized_start); - } - if (!normalized_end->IsNullPosition()) { - DCHECK_EQ(*end_, *normalized_end); - end_ = std::move(normalized_end); - } return S_OK; } @@ -250,7 +289,8 @@ STDMETHODIMP AXPlatformNodeTextRangeProviderWin::FindAttribute( // 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); - UIA_VALIDATE_TEXTRANGEPROVIDER_CALL(); + UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_OUT(result); + NormalizeTextRange(); *result = nullptr; AXPositionInstance matched_range_start = nullptr; @@ -337,7 +377,6 @@ HRESULT AXPlatformNodeTextRangeProviderWin::FindAttributeRange( break; } } - return S_OK; } @@ -347,13 +386,9 @@ STDMETHODIMP AXPlatformNodeTextRangeProviderWin::FindText( BOOL ignore_case, ITextRangeProvider** result) { WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXTRANGE_FINDTEXT); - UIA_VALIDATE_TEXTRANGEPROVIDER_CALL(); - if (!result || !string) - return E_INVALIDARG; + UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_IN_1_OUT(string, result); - *result = nullptr; base::string16 search_string(string); - if (search_string.length() <= 0) return E_INVALIDARG; @@ -374,17 +409,23 @@ STDMETHODIMP AXPlatformNodeTextRangeProviderWin::GetAttributeValue( TEXTATTRIBUTEID attribute_id, VARIANT* value) { WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXTRANGE_GETATTRIBUTEVALUE); + UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_OUT(value); + NormalizeTextRange(); base::win::ScopedVariant attribute_value_variant; - AXNodeRange range(start_->Clone(), end_->Clone()); - for (const AXNodeRange& leaf_text_range : range) { - AXPositionInstanceType* anchor_start = leaf_text_range.anchor(); - AXPlatformNodeDelegate* delegate = GetDelegate(anchor_start); - DCHECK(anchor_start && delegate); + // The range is inclusive, so advance our endpoint to the next position + auto end = end_->CreateNextAnchorPosition(); + + // Iterate over anchor positions + for (auto it = start_->Clone(); + it->anchor_id() != end->anchor_id() || it->tree_id() != end->tree_id(); + it = it->CreateNextAnchorPosition()) { + AXPlatformNodeDelegate* delegate = GetDelegate(it.get()); + DCHECK(it && delegate); AXPlatformNodeWin* platform_node = static_cast<AXPlatformNodeWin*>( - delegate->GetFromNodeID(anchor_start->anchor_id())); + delegate->GetFromNodeID(it->anchor_id())); DCHECK(platform_node); base::win::ScopedVariant current_variant; @@ -412,11 +453,12 @@ STDMETHODIMP AXPlatformNodeTextRangeProviderWin::GetAttributeValue( STDMETHODIMP AXPlatformNodeTextRangeProviderWin::GetBoundingRectangles( SAFEARRAY** rectangles) { WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXTRANGE_GETBOUNDINGRECTANGLES); - UIA_VALIDATE_TEXTRANGEPROVIDER_CALL(); + UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_OUT(rectangles); *rectangles = nullptr; AXNodeRange range(start_->Clone(), end_->Clone()); - std::vector<gfx::Rect> rects = range.GetScreenRects(); + AXRangeScreenRectDelegateImpl rect_delegate(this); + std::vector<gfx::Rect> rects = range.GetScreenRects(&rect_delegate); // 4 array items per rect: left, top, width, height SAFEARRAY* safe_array = SafeArrayCreateVector( @@ -455,16 +497,33 @@ STDMETHODIMP AXPlatformNodeTextRangeProviderWin::GetBoundingRectangles( STDMETHODIMP AXPlatformNodeTextRangeProviderWin::GetEnclosingElement( IRawElementProviderSimple** element) { WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXTRANGE_GETENCLOSINGELEMENT); - UIA_VALIDATE_TEXTRANGEPROVIDER_CALL(); + UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_OUT(element); - AXPositionInstance common_ancestor = start_->LowestCommonAncestor(*end_); + AXNode* common_anchor = start_->LowestCommonAnchor(*end_); + DCHECK(common_anchor); + if (!common_anchor) + return UIA_E_ELEMENTNOTAVAILABLE; - AXPlatformNodeDelegate* delegate = GetDelegate(common_ancestor.get()); - DCHECK(delegate); + const AXTreeID tree_id = common_anchor->tree()->GetAXTreeID(); + const AXNode::AXID node_id = common_anchor->id(); + AXPlatformNodeWin* enclosing_node = + static_cast<AXPlatformNodeWin*>(AXPlatformNode::FromNativeViewAccessible( + GetDelegate(tree_id, node_id)->GetNativeViewAccessible())); + DCHECK(enclosing_node); + // If this node has an ancestor that is a control type, use that as the + // enclosing element. + enclosing_node = enclosing_node->GetLowestAccessibleElement(); + DCHECK(enclosing_node); + + while (ui::IsIgnored(enclosing_node->GetData())) { + AXPlatformNodeWin* parent = static_cast<AXPlatformNodeWin*>( + AXPlatformNode::FromNativeViewAccessible(enclosing_node->GetParent())); + DCHECK(parent); + enclosing_node = parent; + } - delegate->GetFromNodeID(common_ancestor->anchor_id()) - ->GetNativeViewAccessible() - ->QueryInterface(IID_PPV_ARGS(element)); + enclosing_node->GetNativeViewAccessible()->QueryInterface( + IID_PPV_ARGS(element)); DCHECK(*element); return S_OK; @@ -473,11 +532,11 @@ STDMETHODIMP AXPlatformNodeTextRangeProviderWin::GetEnclosingElement( STDMETHODIMP AXPlatformNodeTextRangeProviderWin::GetText(int max_count, BSTR* text) { WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXTRANGE_GETTEXT); - UIA_VALIDATE_TEXTRANGEPROVIDER_CALL(); + 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 invalid. - if (max_count < -1 || !text) + // Any other negative value is an invalid argument. + if (max_count < -1) return E_INVALIDARG; base::string16 full_text = GetString(max_count); @@ -491,7 +550,6 @@ STDMETHODIMP AXPlatformNodeTextRangeProviderWin::GetText(int max_count, } else { *text = SysAllocString(L""); } - return S_OK; } @@ -499,9 +557,7 @@ STDMETHODIMP AXPlatformNodeTextRangeProviderWin::Move(TextUnit unit, int count, int* units_moved) { WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXTRANGE_MOVE); - UIA_VALIDATE_TEXTRANGEPROVIDER_CALL(); - - *units_moved = 0; + UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_OUT(units_moved); // Per MSDN, move with zero count has no effect. if (count == 0) @@ -542,6 +598,14 @@ STDMETHODIMP AXPlatformNodeTextRangeProviderWin::Move(TextUnit unit, &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)) { + ExpandToEnclosingUnit(unit); + } } } @@ -563,7 +627,7 @@ STDMETHODIMP AXPlatformNodeTextRangeProviderWin::MoveEndpointByUnit( int count, int* units_moved) { WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXTRANGE_MOVEENDPOINTBYUNIT); - UIA_VALIDATE_TEXTRANGEPROVIDER_CALL(); + UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_OUT(units_moved); // Per MSDN, MoveEndpointByUnit with zero count has no effect. if (count == 0) { @@ -584,8 +648,7 @@ STDMETHODIMP AXPlatformNodeTextRangeProviderWin::MoveEndpointByUnit( new_position = MoveEndpointByFormat(position_to_move, count, units_moved); break; case TextUnit_Word: - new_position = MoveEndpointByWord(position_to_move, is_start_endpoint, - count, units_moved); + new_position = MoveEndpointByWord(position_to_move, count, units_moved); break; case TextUnit_Line: new_position = MoveEndpointByLine(position_to_move, is_start_endpoint, @@ -606,12 +669,11 @@ STDMETHODIMP AXPlatformNodeTextRangeProviderWin::MoveEndpointByUnit( default: return UIA_E_NOTSUPPORTED; } - position_to_move = 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. - if (*start_ > *end_) { + if (*end_->AsTreePosition() < *start_->AsTreePosition() || *end_ < *start_) { if (is_start_endpoint) end_ = start_->Clone(); else @@ -625,12 +687,11 @@ STDMETHODIMP AXPlatformNodeTextRangeProviderWin::MoveEndpointByRange( ITextRangeProvider* other, TextPatternRangeEndpoint other_endpoint) { WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXTRANGE_MOVEENPOINTBYRANGE); - UIA_VALIDATE_TEXTRANGEPROVIDER_CALL(); + UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_IN(other); CComPtr<AXPlatformNodeTextRangeProviderWin> other_provider; - if (other->QueryInterface(&other_provider) != S_OK) { + if (other->QueryInterface(&other_provider) != S_OK) return UIA_E_INVALIDOPERATION; - } const AXPositionInstance& other_provider_endpoint = (other_endpoint == TextPatternRangeEndpoint_Start) @@ -646,7 +707,6 @@ STDMETHODIMP AXPlatformNodeTextRangeProviderWin::MoveEndpointByRange( if (*start_ > *end_) start_ = end_->Clone(); } - return S_OK; } @@ -696,16 +756,23 @@ STDMETHODIMP AXPlatformNodeTextRangeProviderWin::ScrollIntoView( const AXTreeManager* ax_tree_manager = AXTreeManagerMap::GetInstance().GetManager(common_ancestor_tree_id); DCHECK(ax_tree_manager); - + AXNode* root_node = ax_tree_manager->GetRootAsAXNode(); + const AXPlatformNode* root_platform_node = + owner_->GetDelegate()->GetFromTreeIDAndNodeID(common_ancestor_tree_id, + root_node->id()); + DCHECK(root_platform_node); const AXPlatformNodeDelegate* root_delegate = - ax_tree_manager->GetRootDelegate(common_ancestor_tree_id); + root_platform_node->GetDelegate(); 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 = + owner_->GetDelegate()->GetFromTreeIDAndNodeID( + common_ancestor_tree_id, common_ancestor_anchor->id()); + DCHECK(common_ancestor_platform_node); AXPlatformNodeDelegate* common_ancestor_delegate = - ax_tree_manager->GetDelegate(common_ancestor_tree_id, - common_ancestor_anchor->id()); + common_ancestor_platform_node->GetDelegate(); DCHECK(common_ancestor_delegate); const gfx::Rect text_range_container_frame_bounds = common_ancestor_delegate->GetBoundsRect(AXCoordinateSystem::kFrame, @@ -758,21 +825,23 @@ STDMETHODIMP AXPlatformNodeTextRangeProviderWin::ScrollIntoView( STDMETHODIMP AXPlatformNodeTextRangeProviderWin::GetChildren( SAFEARRAY** children) { WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXTRANGE_GETCHILDREN); - UIA_VALIDATE_TEXTRANGEPROVIDER_CALL(); + UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_OUT(children); std::vector<gfx::NativeViewAccessible> descendants; - AXPositionInstance common_ancestor = - start_->LowestCommonAncestor(*end_.get()); - - if (!common_ancestor->GetAnchor()->children().empty()) { - AXPlatformNodeDelegate* delegate = GetDelegate(common_ancestor.get()); - DCHECK(delegate); - - descendants = delegate->GetFromNodeID(common_ancestor->anchor_id()) - ->GetDelegate() - ->GetDescendants(); + const AXNode* common_anchor = start_->LowestCommonAnchor(*end_); + const AXTreeID tree_id = common_anchor->tree()->GetAXTreeID(); + const AXNode::AXID node_id = common_anchor->id(); + AXPlatformNodeDelegate* delegate = GetDelegate(tree_id, node_id); + DCHECK(delegate); + while (ui::IsIgnored(delegate->GetData())) { + auto* node = static_cast<AXPlatformNodeWin*>( + AXPlatformNode::FromNativeViewAccessible(delegate->GetParent())); + DCHECK(node); + delegate = node->GetDelegate(); } + if (delegate->GetChildCount()) + descendants = delegate->GetDescendants(); SAFEARRAY* safe_array = SafeArrayCreateVector(VT_UNKNOWN, 0, descendants.size()); @@ -809,11 +878,18 @@ AXPlatformNodeWin* AXPlatformNodeTextRangeProviderWin::owner() const { AXPlatformNodeDelegate* AXPlatformNodeTextRangeProviderWin::GetDelegate( const AXPositionInstanceType* position) const { - AXTreeManager* manager = - AXTreeManagerMap::GetInstance().GetManager(position->tree_id()); - return manager - ? manager->GetDelegate(position->tree_id(), position->anchor_id()) - : owner()->GetDelegate(); + return GetDelegate(position->tree_id(), position->anchor_id()); +} + +AXPlatformNodeDelegate* AXPlatformNodeTextRangeProviderWin::GetDelegate( + const AXTreeID tree_id, + const AXNode::AXID node_id) const { + AXPlatformNode* platform_node = + owner_->GetDelegate()->GetFromTreeIDAndNodeID(tree_id, node_id); + if (!platform_node) + return nullptr; + + return platform_node->GetDelegate(); } AXPlatformNodeTextRangeProviderWin::AXPositionInstance @@ -821,39 +897,17 @@ AXPlatformNodeTextRangeProviderWin::MoveEndpointByCharacter( const AXPositionInstance& endpoint, const int count, int* units_moved) { - DCHECK_NE(count, 0); return MoveEndpointByUnitHelper( - std::move(endpoint), - (count > 0) ? &AXPositionInstanceType::CreateNextCharacterPosition - : &AXPositionInstanceType::CreatePreviousCharacterPosition, - count, units_moved); + std::move(endpoint), AXTextBoundary::kCharacter, count, units_moved); } AXPlatformNodeTextRangeProviderWin::AXPositionInstance AXPlatformNodeTextRangeProviderWin::MoveEndpointByWord( const AXPositionInstance& endpoint, - bool is_start_endpoint, const int count, int* units_moved) { - DCHECK_NE(count, 0); - - bool going_forward = count > 0; - AXPositionInstance new_position = MoveEndpointByUnitHelper( - std::move(endpoint), - going_forward ? &AXPositionInstanceType::CreateNextWordStartPosition - : &AXPositionInstanceType::CreatePreviousWordStartPosition, - count, units_moved); - - if (going_forward && *units_moved < count && - !new_position->AsPositionBeforeCharacter()->IsNullPosition()) { - AXPositionInstance next_word_position = - new_position->CreatePositionAtEndOfDocument(); - DCHECK(!next_word_position->IsNullPosition()); - - new_position = std::move(next_word_position); - ++*units_moved; - } - return new_position; + return MoveEndpointByUnitHelper( + std::move(endpoint), AXTextBoundary::kWordStart, count, units_moved); } AXPlatformNodeTextRangeProviderWin::AXPositionInstance @@ -862,21 +916,10 @@ AXPlatformNodeTextRangeProviderWin::MoveEndpointByLine( bool is_start_endpoint, const int count, int* units_moved) { - DCHECK_NE(count, 0); - - CreateNextPositionFunction create_next_position = nullptr; - if (count > 0) - create_next_position = - is_start_endpoint ? &AXPositionInstanceType::CreateNextLineStartPosition - : &AXPositionInstanceType::CreateNextLineEndPosition; - else - create_next_position = - is_start_endpoint - ? &AXPositionInstanceType::CreatePreviousLineStartPosition - : &AXPositionInstanceType::CreatePreviousLineEndPosition; - - return MoveEndpointByUnitHelper(std::move(endpoint), create_next_position, - count, units_moved); + return MoveEndpointByUnitHelper( + std::move(endpoint), + is_start_endpoint ? AXTextBoundary::kLineStart : AXTextBoundary::kLineEnd, + count, units_moved); } AXPlatformNodeTextRangeProviderWin::AXPositionInstance @@ -884,106 +927,39 @@ AXPlatformNodeTextRangeProviderWin::MoveEndpointByFormat( const AXPositionInstance& endpoint, const int count, int* units_moved) { - DCHECK_NE(count, 0); - - CreateNextPositionFunction create_next_position = nullptr; - if (count > 0) - create_next_position = &AXPositionInstanceType::CreateNextFormatEndPosition; - else - create_next_position = - &AXPositionInstanceType::CreatePreviousFormatStartPosition; - - return MoveEndpointByUnitHelper(std::move(endpoint), create_next_position, - count, units_moved); + return MoveEndpointByUnitHelper( + std::move(endpoint), AXTextBoundary::kFormatChange, count, units_moved); } AXPlatformNodeTextRangeProviderWin::AXPositionInstance AXPlatformNodeTextRangeProviderWin::MoveEndpointByParagraph( - const AXNodePosition::AXPositionInstance& endpoint, + const AXPositionInstance& endpoint, const bool is_start_endpoint, const int count, - int* count_moved) { - auto current_endpoint = endpoint->Clone(); - const bool forwards = count > 0; - const int count_abs = std::abs(count); - const auto behavior = AXBoundaryBehavior::CrossBoundary; - int iteration = 0; - for (iteration = 0; iteration < count_abs; ++iteration) { - AXPositionInstance next_endpoint; - if (forwards) { - next_endpoint = - is_start_endpoint - ? current_endpoint->CreateNextParagraphStartPosition(behavior) - : current_endpoint->CreateNextParagraphEndPosition(behavior); - } else { - next_endpoint = - is_start_endpoint - ? current_endpoint->CreatePreviousParagraphStartPosition(behavior) - : current_endpoint->CreatePreviousParagraphEndPosition(behavior); - } - - // End of document - if (next_endpoint->IsNullPosition()) { - int document_moved; - next_endpoint = MoveEndpointByDocument(endpoint, count, &document_moved); - if (*endpoint != *next_endpoint && !next_endpoint->IsNullPosition()) { - ++iteration; - current_endpoint = std::move(next_endpoint); - } - break; - } - current_endpoint = std::move(next_endpoint); - } - - *count_moved = (forwards) ? iteration : -iteration; - return current_endpoint; + int* units_moved) { + return MoveEndpointByUnitHelper(std::move(endpoint), + is_start_endpoint + ? AXTextBoundary::kParagraphStart + : AXTextBoundary::kParagraphEnd, + count, units_moved); } AXPlatformNodeTextRangeProviderWin::AXPositionInstance AXPlatformNodeTextRangeProviderWin::MoveEndpointByPage( - const AXNodePosition::AXPositionInstance& endpoint, + const AXPositionInstance& endpoint, const bool is_start_endpoint, const int count, - int* count_moved) { - // If the document doesn't support pagination, default to document navigation - // per UIA spec + int* units_moved) { + // Per UIA spec, if the document containing the current endpoint doesn't + // support pagination, default to document navigation. AXPositionInstance common_ancestor = start_->LowestCommonAncestor(*end_); if (!common_ancestor->GetAnchor()->tree()->HasPaginationSupport()) - return MoveEndpointByDocument(std::move(endpoint), count, count_moved); - - auto current_endpoint = endpoint->Clone(); - const bool forwards = count > 0; - const int count_abs = std::abs(count); - const auto behavior = ui::AXBoundaryBehavior::CrossBoundary; - int iteration = 0; - for (iteration = 0; iteration < count_abs; ++iteration) { - AXPositionInstance next_endpoint; - if (forwards) { - next_endpoint = - is_start_endpoint - ? current_endpoint->CreateNextPageStartPosition(behavior) - : current_endpoint->CreateNextPageEndPosition(behavior); - } else { - next_endpoint = - is_start_endpoint - ? current_endpoint->CreatePreviousPageStartPosition(behavior) - : current_endpoint->CreatePreviousPageEndPosition(behavior); - } - // End of document - if (next_endpoint->IsNullPosition()) { - int document_moved; - next_endpoint = MoveEndpointByDocument(endpoint, count, &document_moved); - if (*endpoint != *next_endpoint && !next_endpoint->IsNullPosition()) { - ++iteration; - current_endpoint = std::move(next_endpoint); - } - break; - } - current_endpoint = std::move(next_endpoint); - } + return MoveEndpointByDocument(std::move(endpoint), count, units_moved); - *count_moved = (forwards) ? iteration : -iteration; - return current_endpoint; + return MoveEndpointByUnitHelper( + std::move(endpoint), + is_start_endpoint ? AXTextBoundary::kPageStart : AXTextBoundary::kPageEnd, + count, units_moved); } AXPlatformNodeTextRangeProviderWin::AXPositionInstance @@ -993,39 +969,39 @@ AXPlatformNodeTextRangeProviderWin::MoveEndpointByDocument( int* units_moved) { DCHECK_NE(count, 0); - *units_moved = 0; - AXPositionInstance current_endpoint = endpoint->Clone(); - const bool forwards = count > 0; - - if (forwards && !current_endpoint->AtEndOfDocument()) { - current_endpoint = endpoint->CreatePositionAtEndOfDocument(); - *units_moved = 1; - } else if (!forwards && !current_endpoint->AtStartOfDocument()) { - current_endpoint = endpoint->CreatePositionAtStartOfDocument(); - *units_moved = -1; + if (count < 0) { + *units_moved = !endpoint->AtStartOfDocument() ? -1 : 0; + return endpoint->CreatePositionAtStartOfDocument(); } - - return current_endpoint; + *units_moved = !endpoint->AtEndOfDocument() ? 1 : 0; + return endpoint->CreatePositionAtEndOfDocument(); } AXPlatformNodeTextRangeProviderWin::AXPositionInstance AXPlatformNodeTextRangeProviderWin::MoveEndpointByUnitHelper( const AXPositionInstance& endpoint, - CreateNextPositionFunction create_next_position, + const AXTextBoundary boundary_type, const int count, int* units_moved) { DCHECK_NE(count, 0); + const bool going_forward = count > 0; AXPositionInstance current_endpoint = endpoint->Clone(); - for (int iteration = 0; iteration < std::abs(count); ++iteration) { AXPositionInstance next_endpoint = - (current_endpoint.get()->*create_next_position)( - AXBoundaryBehavior::CrossBoundary); - - // We've reached either the start or the end of the document. - if (next_endpoint->IsNullPosition()) { - *units_moved = (count > 0) ? iteration : -iteration; + current_endpoint->CreatePositionAtTextBoundary( + boundary_type, + going_forward ? AXTextBoundaryDirection::kForwards + : AXTextBoundaryDirection::kBackwards, + AXBoundaryBehavior::StopAtLastAnchorBoundary); + DCHECK(!next_endpoint->IsNullPosition()); + + // Since AXBoundaryBehavior::StopAtLastAnchorBoundary 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. + if (*current_endpoint == *next_endpoint) { + *units_moved = going_forward ? iteration : -iteration; return current_endpoint; } current_endpoint = std::move(next_endpoint); @@ -1035,4 +1011,17 @@ AXPlatformNodeTextRangeProviderWin::MoveEndpointByUnitHelper( return current_endpoint; } +void AXPlatformNodeTextRangeProviderWin::NormalizeTextRange() { + // Only normalize non-degenerate ranges. + if (*start_ != *end_) { + AXPositionInstance normalized_start = start_->AsPositionBeforeCharacter(); + AXPositionInstance normalized_end = end_->AsPositionAfterCharacter(); + DCHECK_EQ(*start_, *normalized_start); + DCHECK_EQ(*end_, *normalized_end); + + start_ = std::move(normalized_start); + end_ = std::move(normalized_end); + } +} + } // namespace ui diff --git a/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win.h b/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win.h index 2db00331378..b4749b02f7a 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win.h +++ b/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win.h @@ -12,6 +12,7 @@ #include "ui/accessibility/ax_node_position.h" #include "ui/accessibility/ax_position.h" #include "ui/accessibility/ax_range.h" +#include "ui/accessibility/ax_text_boundary.h" #include "ui/accessibility/platform/ax_platform_node_win.h" namespace ui { @@ -83,15 +84,17 @@ class AX_EXPORT __declspec(uuid("3071e40d-a10d-45ff-a59f-6e8e1138e2c1")) using AXPositionInstance = AXNodePosition::AXPositionInstance; using AXPositionInstanceType = typename AXPositionInstance::element_type; using AXNodeRange = AXRange<AXPositionInstanceType>; - using CreateNextPositionFunction = - AXPositionInstance (AXPositionInstanceType::*)(AXBoundaryBehavior) const; friend class AXPlatformNodeTextRangeProviderTest; friend class AXPlatformNodeTextProviderTest; + friend class AXRangeScreenRectDelegateImpl; + base::string16 GetString(int max_count); AXPlatformNodeWin* owner() const; AXPlatformNodeDelegate* GetDelegate( const AXPositionInstanceType* position) const; + AXPlatformNodeDelegate* GetDelegate(const AXTreeID tree_id, + const AXNode::AXID node_id) const; template <typename AnchorIterator, typename ExpandMatchLambda> HRESULT FindAttributeRange(const TEXTATTRIBUTEID text_attribute_id, @@ -104,7 +107,6 @@ class AX_EXPORT __declspec(uuid("3071e40d-a10d-45ff-a59f-6e8e1138e2c1")) const int count, int* units_moved); AXPositionInstance MoveEndpointByWord(const AXPositionInstance& endpoint, - bool endpoint_is_start, const int count, int* units_moved); AXPositionInstance MoveEndpointByLine(const AXPositionInstance& endpoint, @@ -114,11 +116,11 @@ class AX_EXPORT __declspec(uuid("3071e40d-a10d-45ff-a59f-6e8e1138e2c1")) AXPositionInstance MoveEndpointByParagraph(const AXPositionInstance& endpoint, const bool is_start_endpoint, const int count, - int* count_moved); + int* units_moved); AXPositionInstance MoveEndpointByPage(const AXPositionInstance& endpoint, const bool is_start_endpoint, const int count, - int* count_moved); + int* units_moved); AXPositionInstance MoveEndpointByFormat(const AXPositionInstance& endpoint, const int count, int* units_moved); @@ -128,10 +130,12 @@ class AX_EXPORT __declspec(uuid("3071e40d-a10d-45ff-a59f-6e8e1138e2c1")) AXPositionInstance MoveEndpointByUnitHelper( const AXPositionInstance& endpoint, - CreateNextPositionFunction create_next_position, + const AXTextBoundary boundary_type, const int count, int* units_moved); + void NormalizeTextRange(); + CComPtr<AXPlatformNodeWin> owner_; AXPositionInstance start_; AXPositionInstance end_; diff --git a/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win_unittest.cc b/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win_unittest.cc index 48a1608068d..e3eca56c27d 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win_unittest.cc +++ b/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win_unittest.cc @@ -167,6 +167,18 @@ namespace ui { EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, expected_text); \ } +#define EXPECT_ENCLOSING_ELEMENT(ax_node_given, ax_node_expected) \ + { \ + ComPtr<ITextRangeProvider> text_range_provider; \ + GetTextRangeProviderFromTextNode(text_range_provider, ax_node_given); \ + ComPtr<IRawElementProviderSimple> enclosing_element; \ + ASSERT_HRESULT_SUCCEEDED( \ + text_range_provider->GetEnclosingElement(&enclosing_element)); \ + ComPtr<IRawElementProviderSimple> expected_text_provider = \ + QueryInterfaceFromNode<IRawElementProviderSimple>(ax_node_expected); \ + EXPECT_EQ(expected_text_provider.Get(), enclosing_element.Get()); \ + } + class AXPlatformNodeTextRangeProviderTest : public ui::AXPlatformNodeWinTest { public: const AXNodePosition::AXPositionInstance& GetStart( @@ -492,16 +504,24 @@ class AXPlatformNodeTextRangeProviderTest : public ui::AXPlatformNodeWinTest { ax::mojom::IntListAttribute::kWordEnds, word_end_offsets); paragraph1_data.AddBoolAttribute( ax::mojom::BoolAttribute::kIsLineBreakingObject, true); - paragraph1_data.child_ids = {10}; + + ui::AXNodeData ignored_text_data; + ignored_text_data.id = 11; + ignored_text_data.role = ax::mojom::Role::kStaticText; + ignored_text_data.AddState(ax::mojom::State::kIgnored); + text_content = "ignored text"; + ignored_text_data.SetName(text_content); + + paragraph1_data.child_ids = {10, 11}; ui::AXNodeData paragraph2_data; - paragraph2_data.id = 11; + paragraph2_data.id = 12; paragraph2_data.role = ax::mojom::Role::kParagraph; paragraph2_data.AddBoolAttribute( ax::mojom::BoolAttribute::kIsLineBreakingObject, true); ui::AXNodeData paragraph2_text_data; - paragraph2_text_data.id = 12; + paragraph2_text_data.id = 13; paragraph2_text_data.role = ax::mojom::Role::kStaticText; text_content = "Paragraph 2"; paragraph2_text_data.SetName(text_content); @@ -513,28 +533,23 @@ class AXPlatformNodeTextRangeProviderTest : public ui::AXPlatformNodeWinTest { ax::mojom::IntListAttribute::kWordEnds, word_end_offsets); paragraph1_data.AddBoolAttribute( ax::mojom::BoolAttribute::kIsLineBreakingObject, true); - paragraph2_data.child_ids = {12}; + paragraph2_data.child_ids = {13}; ui::AXNodeData root_data; root_data.id = 1; root_data.role = ax::mojom::Role::kRootWebArea; - root_data.child_ids = {2, 4, 8, 9, 11}; + root_data.child_ids = {2, 4, 8, 9, 12}; ui::AXTreeUpdate update; update.has_tree_data = true; update.root_id = root_data.id; - update.nodes.push_back(root_data); - update.nodes.push_back(group1_data); - update.nodes.push_back(text_data); - update.nodes.push_back(group2_data); - update.nodes.push_back(line_break1_data); - update.nodes.push_back(standalone_text_data); - update.nodes.push_back(line_break2_data); - update.nodes.push_back(bold_text_data); - update.nodes.push_back(paragraph1_data); - update.nodes.push_back(paragraph1_text_data); - update.nodes.push_back(paragraph2_data); - update.nodes.push_back(paragraph2_text_data); + update.nodes = {root_data, group1_data, + text_data, group2_data, + line_break1_data, standalone_text_data, + line_break2_data, bold_text_data, + paragraph1_data, paragraph1_text_data, + ignored_text_data, paragraph2_data, + paragraph2_text_data}; update.tree_data.tree_id = ui::AXTreeID::CreateNewAXTreeID(); return update; } @@ -644,29 +659,26 @@ class AXPlatformNodeTextRangeProviderTest : public ui::AXPlatformNodeWinTest { root_data.child_ids = {2, 4, 8, 10, 12, 14, 16}; 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(group1_data); - update.nodes.push_back(text_data); - update.nodes.push_back(group2_data); - update.nodes.push_back(group3_data); - update.nodes.push_back(line_break1_data); - update.nodes.push_back(standalone_text_data); - update.nodes.push_back(line_break2_data); - update.nodes.push_back(bold_text_data); - update.nodes.push_back(paragraph1_data); - update.nodes.push_back(paragraph1_text_data); - update.nodes.push_back(paragraph2_data); - update.nodes.push_back(paragraph2_text_data); - update.nodes.push_back(paragraph3_data); - update.nodes.push_back(paragraph3_text_data); - update.nodes.push_back(paragraph4_data); - update.nodes.push_back(paragraph4_text_data); - + update.nodes = {root_data, + group1_data, + text_data, + group2_data, + group3_data, + line_break1_data, + standalone_text_data, + line_break2_data, + bold_text_data, + paragraph1_data, + paragraph1_text_data, + paragraph2_data, + paragraph2_text_data, + paragraph3_data, + paragraph3_text_data, + paragraph4_data, + paragraph4_text_data}; + update.tree_data.tree_id = ui::AXTreeID::CreateNewAXTreeID(); return update; } @@ -719,15 +731,12 @@ class AXPlatformNodeTextRangeProviderTest : public ui::AXPlatformNodeWinTest { root_data.child_ids = {2, 4, 6}; 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 = {root_data, page_1_data, page_1_text_data, page_2_data, page_2_text_data, page_3_data, page_3_text_data}; - + update.tree_data.tree_id = ui::AXTreeID::CreateNewAXTreeID(); return update; } }; @@ -948,8 +957,11 @@ TEST_F(AXPlatformNodeTextRangeProviderTest, TEST_F(AXPlatformNodeTextRangeProviderTest, TestITextRangeProviderExpandToEnclosingCharacter) { - Init(BuildTextDocument({"some text", "more text"})); + ui::AXTreeUpdate update = BuildTextDocument({"some text", "more text"}); + Init(update); AXNodePosition::SetTree(tree_.get()); + AXTreeManagerMap::GetInstance().AddTreeManager(update.tree_data.tree_id, + this); AXNode* root_node = GetRootNode(); ComPtr<ITextRangeProvider> text_range_provider; @@ -1146,7 +1158,7 @@ TEST_F(AXPlatformNodeTextRangeProviderTest, EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"First line of text\n"); // Moving the start by two lines will create a degenerate range positioned - // at the next paragraph (skipping the newline) + // at the next paragraph (skipping the newline). ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit( TextPatternRangeEndpoint_Start, TextUnit_Line, /*count*/ 2, &count)); EXPECT_EQ(2, count); @@ -1169,10 +1181,10 @@ TEST_F(AXPlatformNodeTextRangeProviderTest, /*expected_count*/ 1); ASSERT_HRESULT_SUCCEEDED( text_range_provider->ExpandToEnclosingUnit(TextUnit_Paragraph)); - EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"bold text\n"); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"bold text"); // Create a degenerate range at the end of the document, then expand by - // paragraph + // paragraph. ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit( TextPatternRangeEndpoint_Start, TextUnit_Document, /*count*/ 1, &count)); EXPECT_EQ(1, count); @@ -1422,11 +1434,11 @@ TEST_F(AXPlatformNodeTextRangeProviderTest, /*expected_count*/ 1); EXPECT_UIA_MOVE(text_range_provider, TextUnit_Character, /*count*/ 18, - /*expected_text*/ L"\nS", + /*expected_text*/ L"S", /*expected_count*/ 18); EXPECT_UIA_MOVE(text_range_provider, TextUnit_Character, /*count*/ 16, - /*expected_text*/ L"\nb", + /*expected_text*/ L"b", /*expected_count*/ 16); EXPECT_UIA_MOVE(text_range_provider, TextUnit_Character, /*count*/ 60, @@ -1656,7 +1668,7 @@ TEST_F(AXPlatformNodeTextRangeProviderTest, TestITextRangeProviderMovePage) { /*count*/ 5, /*expected_text*/ L"some text on page 1\nsome text on page 2some more text on page 3", - /*expected_count*/ 4); + /*expected_count*/ 3); // Range moves EXPECT_UIA_MOVE(text_range_provider, TextUnit_Page, @@ -1720,11 +1732,11 @@ TEST_F(AXPlatformNodeTextRangeProviderTest, TestITextRangeProviderMoveWord) { /*expected_count*/ 1); EXPECT_UIA_MOVE(text_range_provider, TextUnit_Word, /*count*/ 2, - /*expected_text*/ L"text\n", + /*expected_text*/ L"text", /*expected_count*/ 2); EXPECT_UIA_MOVE(text_range_provider, TextUnit_Word, /*count*/ 2, - /*expected_text*/ L"line\n", + /*expected_text*/ L"line", /*expected_count*/ 2); EXPECT_UIA_MOVE(text_range_provider, TextUnit_Word, /*count*/ 3, @@ -1748,11 +1760,11 @@ TEST_F(AXPlatformNodeTextRangeProviderTest, TestITextRangeProviderMoveWord) { /*expected_count*/ -3); EXPECT_UIA_MOVE(text_range_provider, TextUnit_Word, /*count*/ -3, - /*expected_text*/ L"line\n", + /*expected_text*/ L"line", /*expected_count*/ -3); EXPECT_UIA_MOVE(text_range_provider, TextUnit_Word, /*count*/ -2, - /*expected_text*/ L"text\n", + /*expected_text*/ L"text", /*expected_count*/ -2); EXPECT_UIA_MOVE(text_range_provider, TextUnit_Word, /*count*/ -6, @@ -1922,9 +1934,9 @@ TEST_F(AXPlatformNodeTextRangeProviderTest, /*expected_text*/ L"Standalone line\n", /*expected_count*/ -3); EXPECT_UIA_MOVE(text_range_provider, TextUnit_Paragraph, - /*count*/ -2, + /*count*/ -1, /*expected_text*/ L"First line of text\n", - /*expected_count*/ -2); + /*expected_count*/ -1); // Moving backward by any number of paragraphs at the start of document // should have no effect. @@ -1976,7 +1988,7 @@ TEST_F(AXPlatformNodeTextRangeProviderTest, EXPECT_UIA_MOVE(text_range_provider, TextUnit_Paragraph, /*count*/ -7, /*expected_text*/ L"First line of text\n", - /*expected_count*/ -5); + /*expected_count*/ -4); EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( text_range_provider, TextPatternRangeEndpoint_End, TextUnit_Paragraph, /*count*/ -1, @@ -2738,28 +2750,109 @@ TEST_F(AXPlatformNodeTextRangeProviderTest, TEST_F(AXPlatformNodeTextRangeProviderTest, TestITextRangeProviderGetEnclosingElement) { - Init(BuildTextDocument({"some text", "more text"})); + // Set up ax tree with the following structure: + // + // root + // | + // paragraph_______ + // | | + // static_text link + // | | + // text_node static_text + // | + // text_node + + ui::AXNodeData root_data; + root_data.id = 1; + root_data.role = ax::mojom::Role::kRootWebArea; + + ui::AXNodeData paragraph_data; + paragraph_data.id = 2; + paragraph_data.role = ax::mojom::Role::kParagraph; + root_data.child_ids.push_back(paragraph_data.id); + + ui::AXNodeData static_text_data1; + static_text_data1.id = 3; + static_text_data1.role = ax::mojom::Role::kStaticText; + paragraph_data.child_ids.push_back(static_text_data1.id); + + ui::AXNodeData inline_text_data1; + inline_text_data1.id = 4; + inline_text_data1.role = ax::mojom::Role::kInlineTextBox; + static_text_data1.child_ids.push_back(inline_text_data1.id); + + ui::AXNodeData link_data; + link_data.id = 5; + link_data.role = ax::mojom::Role::kLink; + paragraph_data.child_ids.push_back(link_data.id); + + ui::AXNodeData static_text_data2; + static_text_data2.id = 6; + static_text_data2.role = ax::mojom::Role::kStaticText; + link_data.child_ids.push_back(static_text_data2.id); + + ui::AXNodeData inline_text_data2; + inline_text_data2.id = 7; + inline_text_data2.role = ax::mojom::Role::kInlineTextBox; + static_text_data2.child_ids.push_back(inline_text_data2.id); + + 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(paragraph_data); + update.nodes.push_back(static_text_data1); + update.nodes.push_back(inline_text_data1); + update.nodes.push_back(link_data); + update.nodes.push_back(static_text_data2); + update.nodes.push_back(inline_text_data2); + + Init(update); + + // Set up variables from the tree for testing. + AXNode* paragraph_node = GetRootNode()->children()[0]; AXNodePosition::SetTree(tree_.get()); - AXNode* root_node = GetRootNode(); + AXNode* static_text_node1 = paragraph_node->children()[0]; + AXNode* link_node = paragraph_node->children()[1]; + AXNode* inline_text_node1 = static_text_node1->children()[0]; + AXNode* static_text_node2 = link_node->children()[0]; + AXNode* inline_text_node2 = static_text_node2->children()[0]; + + ComPtr<IRawElementProviderSimple> link_node_raw = + QueryInterfaceFromNode<IRawElementProviderSimple>(link_node); + ComPtr<IRawElementProviderSimple> inline_text_node_raw1 = + QueryInterfaceFromNode<IRawElementProviderSimple>(inline_text_node1); + ComPtr<IRawElementProviderSimple> inline_text_node_raw2 = + QueryInterfaceFromNode<IRawElementProviderSimple>(inline_text_node2); + + // Test GetEnclosingElement for the two leaves text nodes. The enclosing + // element of the first one should be itself and the enclosing element for the + // text node that is grandchild of the link node should return the link node. + ComPtr<ITextProvider> text_provider; + EXPECT_HRESULT_SUCCEEDED(inline_text_node_raw1->GetPatternProvider( + UIA_TextPatternId, &text_provider)); - // Test GetEnclosingElement for each child text node. - for (auto* child : root_node->children()) { - ComPtr<IRawElementProviderSimple> text_node_raw = - QueryInterfaceFromNode<IRawElementProviderSimple>(child); + ComPtr<ITextRangeProvider> text_range_provider; + EXPECT_HRESULT_SUCCEEDED( + text_provider->get_DocumentRange(&text_range_provider)); - ComPtr<ITextProvider> text_provider; - EXPECT_HRESULT_SUCCEEDED( - text_node_raw->GetPatternProvider(UIA_TextPatternId, &text_provider)); + ComPtr<IRawElementProviderSimple> enclosing_element; + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->GetEnclosingElement(&enclosing_element)); + EXPECT_EQ(inline_text_node_raw1.Get(), enclosing_element.Get()); - ComPtr<ITextRangeProvider> text_range_provider; - EXPECT_HRESULT_SUCCEEDED( - text_provider->get_DocumentRange(&text_range_provider)); + EXPECT_HRESULT_SUCCEEDED(inline_text_node_raw2->GetPatternProvider( + UIA_TextPatternId, &text_provider)); - ComPtr<IRawElementProviderSimple> enclosing_element; - EXPECT_HRESULT_SUCCEEDED( - text_range_provider->GetEnclosingElement(&enclosing_element)); - EXPECT_EQ(text_node_raw.Get(), enclosing_element.Get()); - } + EXPECT_HRESULT_SUCCEEDED( + text_provider->get_DocumentRange(&text_range_provider)); + + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->GetEnclosingElement(&enclosing_element)); + EXPECT_EQ(link_node_raw.Get(), enclosing_element.Get()); } TEST_F(AXPlatformNodeTextRangeProviderTest, @@ -2973,32 +3066,46 @@ TEST_F(AXPlatformNodeTextRangeProviderTest, TEST_F(AXPlatformNodeTextRangeProviderTest, TestITextRangeProviderGetChildren) { // Set up ax tree with the following structure: // - // root______________________ - // | | - // text_node1___ text_node2 + // root + // | + // document(ignored)_________________________ + // | | | + // text_node1___ text_node2 ignored_text // | | // text_node3 text_node4 ui::AXNodeData root_data; root_data.id = 1; root_data.role = ax::mojom::Role::kRootWebArea; + ui::AXNodeData document_data; + document_data.id = 2; + document_data.role = ax::mojom::Role::kDocument; + document_data.AddState(ax::mojom::State::kIgnored); + root_data.child_ids.push_back(document_data.id); + ui::AXNodeData text_node1; - text_node1.id = 2; + text_node1.id = 3; text_node1.role = ax::mojom::Role::kStaticText; - root_data.child_ids.push_back(text_node1.id); + document_data.child_ids.push_back(text_node1.id); ui::AXNodeData text_node2; - text_node2.id = 3; + text_node2.id = 4; text_node2.role = ax::mojom::Role::kStaticText; - root_data.child_ids.push_back(text_node2.id); + document_data.child_ids.push_back(text_node2.id); + + ui::AXNodeData ignored_text; + ignored_text.id = 5; + ignored_text.role = ax::mojom::Role::kStaticText; + ignored_text.AddState(ax::mojom::State::kIgnored); + document_data.child_ids.push_back(ignored_text.id); ui::AXNodeData text_node3; - text_node3.id = 4; + text_node3.id = 6; text_node3.role = ax::mojom::Role::kStaticText; text_node1.child_ids.push_back(text_node3.id); ui::AXNodeData text_node4; - text_node4.id = 5; + text_node4.id = 7; text_node4.role = ax::mojom::Role::kStaticText; text_node1.child_ids.push_back(text_node4.id); @@ -3009,23 +3116,25 @@ TEST_F(AXPlatformNodeTextRangeProviderTest, TestITextRangeProviderGetChildren) { update.has_tree_data = true; update.root_id = root_data.id; update.nodes.push_back(root_data); + update.nodes.push_back(document_data); update.nodes.push_back(text_node1); update.nodes.push_back(text_node2); + update.nodes.push_back(ignored_text); update.nodes.push_back(text_node3); update.nodes.push_back(text_node4); Init(update); // Set up variables from the tree for testing. - AXNode* rootnode = GetRootNode(); + AXNode* document_node = GetRootNode()->children()[0]; AXNodePosition::SetTree(tree_.get()); - AXNode* node1 = rootnode->children()[0]; - AXNode* node2 = rootnode->children()[1]; + AXNode* node1 = document_node->children()[0]; + AXNode* node2 = document_node->children()[1]; AXNode* node3 = node1->children()[0]; AXNode* node4 = node1->children()[1]; - ComPtr<IRawElementProviderSimple> root_node_raw = - QueryInterfaceFromNode<IRawElementProviderSimple>(rootnode); + ComPtr<IRawElementProviderSimple> document_node_raw = + QueryInterfaceFromNode<IRawElementProviderSimple>(document_node); ComPtr<IRawElementProviderSimple> text_node_raw1 = QueryInterfaceFromNode<IRawElementProviderSimple>(node1); ComPtr<IRawElementProviderSimple> text_node_raw2 = @@ -3081,7 +3190,7 @@ TEST_F(AXPlatformNodeTextRangeProviderTest, TestITextRangeProviderGetChildren) { // Test root_node - children should include the entire left subtree and // the entire right subtree. EXPECT_HRESULT_SUCCEEDED( - root_node_raw->GetPatternProvider(UIA_TextPatternId, &text_provider)); + document_node_raw->GetPatternProvider(UIA_TextPatternId, &text_provider)); EXPECT_HRESULT_SUCCEEDED( text_provider->get_DocumentRange(&text_range_provider)); @@ -3121,6 +3230,12 @@ TEST_F(AXPlatformNodeTextRangeProviderTest, heading_data.id = 3; heading_data.role = ax::mojom::Role::kHeading; heading_data.AddIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel, 6); + heading_data.AddIntAttribute(ax::mojom::IntAttribute::kBackgroundColor, + 0xDEADBEEFU); + heading_data.AddIntAttribute(ax::mojom::IntAttribute::kColor, 0xDEADC0DEU); + heading_data.SetTextDirection(ax::mojom::TextDirection::kRtl); + heading_data.SetTextPosition(ax::mojom::TextPosition::kSuperscript); + heading_data.AddState(ax::mojom::State::kEditable); heading_data.child_ids = {4}; ui::AXNodeData heading_text_data; @@ -3139,6 +3254,10 @@ TEST_F(AXPlatformNodeTextRangeProviderTest, ui::AXNodeData mark_data; mark_data.id = 5; mark_data.role = ax::mojom::Role::kMark; + mark_data.AddIntAttribute(ax::mojom::IntAttribute::kBackgroundColor, + 0xDEADBEEFU); + mark_data.AddIntAttribute(ax::mojom::IntAttribute::kColor, 0xDEADC0DEU); + mark_data.SetTextDirection(ax::mojom::TextDirection::kRtl); mark_data.child_ids = {6}; ui::AXNodeData mark_text_data; @@ -3154,6 +3273,9 @@ TEST_F(AXPlatformNodeTextRangeProviderTest, list_data.id = 7; list_data.role = ax::mojom::Role::kList; list_data.child_ids = {8, 10}; + list_data.AddIntAttribute(ax::mojom::IntAttribute::kBackgroundColor, + 0xDEADBEEFU); + list_data.AddIntAttribute(ax::mojom::IntAttribute::kColor, 0xDEADC0DEU); ui::AXNodeData list_item_data; list_item_data.id = 8; @@ -3162,6 +3284,9 @@ TEST_F(AXPlatformNodeTextRangeProviderTest, list_item_data.AddIntAttribute( ax::mojom::IntAttribute::kListStyle, static_cast<int>(ax::mojom::ListStyle::kOther)); + list_item_data.AddIntAttribute(ax::mojom::IntAttribute::kBackgroundColor, + 0xDEADBEEFU); + list_item_data.AddIntAttribute(ax::mojom::IntAttribute::kColor, 0xDEADC0DEU); ui::AXNodeData list_item_text_data; list_item_text_data.id = 9; @@ -3170,6 +3295,7 @@ TEST_F(AXPlatformNodeTextRangeProviderTest, 0xDEADBEEFU); list_item_text_data.AddIntAttribute(ax::mojom::IntAttribute::kColor, 0xDEADC0DEU); + list_item_text_data.SetName("list item"); ui::AXNodeData list_item2_data; list_item2_data.id = 10; @@ -3178,6 +3304,9 @@ TEST_F(AXPlatformNodeTextRangeProviderTest, list_item2_data.AddIntAttribute( ax::mojom::IntAttribute::kListStyle, static_cast<int>(ax::mojom::ListStyle::kDisc)); + list_item2_data.AddIntAttribute(ax::mojom::IntAttribute::kBackgroundColor, + 0xDEADBEEFU); + list_item2_data.AddIntAttribute(ax::mojom::IntAttribute::kColor, 0xDEADC0DEU); ui::AXNodeData list_item2_text_data; list_item2_text_data.id = 11; @@ -3186,6 +3315,7 @@ TEST_F(AXPlatformNodeTextRangeProviderTest, ax::mojom::IntAttribute::kBackgroundColor, 0xDEADBEEFU); list_item2_text_data.AddIntAttribute(ax::mojom::IntAttribute::kColor, 0xDEADC0DEU); + list_item2_text_data.SetName("list item 2"); ui::AXNodeData root_data; root_data.id = 1; @@ -3396,6 +3526,47 @@ TEST_F(AXPlatformNodeTextRangeProviderTest, EXPECT_UIA_TEXTATTRIBUTE_MIXED(document_range_provider, UIA_TextFlowDirectionsAttributeId); expected_variant.Reset(); + + // Move the start endpoint back and forth one character to force such endpoint + // to be located at the end of the previous anchor, this shouldn't cause + // GetAttributeValue to include the previous anchor's attributes. + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT(mark_text_range_provider, + TextPatternRangeEndpoint_Start, + TextUnit_Character, + /*count*/ -1, + /*expected_text*/ L"tmarked text", + /*expected_count*/ -1); + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT(mark_text_range_provider, + TextPatternRangeEndpoint_Start, + TextUnit_Character, + /*count*/ 1, + /*expected_text*/ L"marked text", + /*expected_count*/ 1); + expected_variant.Set(false); + EXPECT_UIA_TEXTATTRIBUTE_EQ(mark_text_range_provider, + UIA_IsSuperscriptAttributeId, expected_variant); + expected_variant.Reset(); + + // Same idea as above, but moving forth and back the end endpoint to force it + // to be located at the start of the next anchor. + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT(mark_text_range_provider, + TextPatternRangeEndpoint_End, + TextUnit_Character, + /*count*/ 1, + /*expected_text*/ L"marked textl", + /*expected_count*/ 1); + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT(mark_text_range_provider, + TextPatternRangeEndpoint_End, + TextUnit_Character, + /*count*/ -1, + /*expected_text*/ L"marked text", + /*expected_count*/ -1); + expected_variant.Set( + static_cast<int32_t>(FlowDirections::FlowDirections_RightToLeft)); + EXPECT_UIA_TEXTATTRIBUTE_EQ(mark_text_range_provider, + UIA_TextFlowDirectionsAttributeId, + expected_variant); + expected_variant.Reset(); } TEST_F(AXPlatformNodeTextRangeProviderTest, @@ -4194,4 +4365,140 @@ TEST_F(AXPlatformNodeTextRangeProviderTest, ElementNotAvailable) { ASSERT_EQ(static_cast<HRESULT>(UIA_E_ELEMENTNOTAVAILABLE), text_range_provider->ScrollIntoView(bool_arg)); } + +TEST_F(AXPlatformNodeTextRangeProviderTest, + TestITextRangeProviderIgnoredNodes) { + // Parent Tree + // 1 + // | + // 2(i) + // |________________________________ + // | | | | | | + // 3 4 5 6 7(i) 8(i) + // | |________ + // | | | + // 9(i) 10(i) 11 + // | |____ + // | | | + // 12 13 14 + + ui::AXTreeUpdate tree_update; + ui::AXTreeID tree_id = ui::AXTreeID::CreateNewAXTreeID(); + tree_update.tree_data.tree_id = tree_id; + tree_update.has_tree_data = true; + tree_update.root_id = 1; + tree_update.nodes.resize(14); + tree_update.nodes[0].id = 1; + tree_update.nodes[0].child_ids = {2}; + tree_update.nodes[0].role = ax::mojom::Role::kRootWebArea; + + tree_update.nodes[1].id = 2; + tree_update.nodes[1].child_ids = {3, 4, 5, 6, 7, 8}; + tree_update.nodes[1].AddState(ax::mojom::State::kIgnored); + tree_update.nodes[1].role = ax::mojom::Role::kDocument; + + tree_update.nodes[2].id = 3; + tree_update.nodes[2].role = ax::mojom::Role::kStaticText; + tree_update.nodes[2].SetName(".3."); + + tree_update.nodes[3].id = 4; + tree_update.nodes[3].role = ax::mojom::Role::kStaticText; + tree_update.nodes[3].SetName(".4."); + + tree_update.nodes[4].id = 5; + tree_update.nodes[4].role = ax::mojom::Role::kStaticText; + tree_update.nodes[4].SetName(".5."); + + tree_update.nodes[5].id = 6; + tree_update.nodes[5].role = ax::mojom::Role::kGenericContainer; + tree_update.nodes[5].child_ids = {9}; + + tree_update.nodes[6].id = 7; + tree_update.nodes[6].child_ids = {10, 11}; + tree_update.nodes[6].AddState(ax::mojom::State::kIgnored); + tree_update.nodes[6].role = ax::mojom::Role::kGenericContainer; + + tree_update.nodes[7].id = 8; + tree_update.nodes[7].AddState(ax::mojom::State::kIgnored); + tree_update.nodes[7].role = ax::mojom::Role::kStaticText; + tree_update.nodes[7].SetName(".8."); + + tree_update.nodes[8].id = 9; + tree_update.nodes[8].child_ids = {12}; + tree_update.nodes[8].AddState(ax::mojom::State::kIgnored); + tree_update.nodes[8].role = ax::mojom::Role::kGenericContainer; + + tree_update.nodes[9].id = 10; + tree_update.nodes[9].child_ids = {13, 14}; + tree_update.nodes[9].AddState(ax::mojom::State::kIgnored); + tree_update.nodes[8].role = ax::mojom::Role::kGenericContainer; + + tree_update.nodes[10].id = 11; + tree_update.nodes[10].role = ax::mojom::Role::kStaticText; + tree_update.nodes[10].SetName(".11."); + + tree_update.nodes[11].id = 12; + tree_update.nodes[11].role = ax::mojom::Role::kStaticText; + tree_update.nodes[11].AddState(ax::mojom::State::kIgnored); + tree_update.nodes[11].SetName(".12."); + + tree_update.nodes[12].id = 13; + tree_update.nodes[12].role = ax::mojom::Role::kStaticText; + tree_update.nodes[12].SetName(".13."); + + tree_update.nodes[13].id = 14; + tree_update.nodes[13].role = ax::mojom::Role::kStaticText; + tree_update.nodes[13].SetName(".14."); + + Init(tree_update); + AXNodePosition::SetTree(tree_.get()); + EXPECT_ENCLOSING_ELEMENT(GetNodeFromTree(tree_id, 1), + GetNodeFromTree(tree_id, 1)); + EXPECT_ENCLOSING_ELEMENT(GetNodeFromTree(tree_id, 2), + GetNodeFromTree(tree_id, 1)); + EXPECT_ENCLOSING_ELEMENT(GetNodeFromTree(tree_id, 3), + GetNodeFromTree(tree_id, 3)); + EXPECT_ENCLOSING_ELEMENT(GetNodeFromTree(tree_id, 4), + GetNodeFromTree(tree_id, 4)); + EXPECT_ENCLOSING_ELEMENT(GetNodeFromTree(tree_id, 5), + GetNodeFromTree(tree_id, 5)); + EXPECT_ENCLOSING_ELEMENT(GetNodeFromTree(tree_id, 8), + GetNodeFromTree(tree_id, 1)); + EXPECT_ENCLOSING_ELEMENT(GetNodeFromTree(tree_id, 11), + GetNodeFromTree(tree_id, 11)); + EXPECT_ENCLOSING_ELEMENT(GetNodeFromTree(tree_id, 12), + GetNodeFromTree(tree_id, 6)); + EXPECT_ENCLOSING_ELEMENT(GetNodeFromTree(tree_id, 13), + GetNodeFromTree(tree_id, 13)); + EXPECT_ENCLOSING_ELEMENT(GetNodeFromTree(tree_id, 14), + GetNodeFromTree(tree_id, 14)); + + // Test movement and GetText() + ComPtr<ITextRangeProvider> text_range_provider; + GetTextRangeProviderFromTextNode(text_range_provider, + GetNodeFromTree(tree_id, 1)); + + ASSERT_HRESULT_SUCCEEDED( + text_range_provider->ExpandToEnclosingUnit(TextUnit_Character)); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"."); + + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_End, TextUnit_Character, + /*count*/ 2, + /*expected_text*/ L".3.", + /*expected_count*/ 2); + + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_End, TextUnit_Character, + /*count*/ 6, + /*expected_text*/ L".3..4..5.", + /*expected_count*/ 6); + + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_End, TextUnit_Character, + /*count*/ 12, + /*expected_text*/ L".3..4..5..13..14..11.", + /*expected_count*/ 12); +} + } // namespace ui diff --git a/chromium/ui/accessibility/platform/ax_platform_node_unittest.cc b/chromium/ui/accessibility/platform/ax_platform_node_unittest.cc index e8d77a72c58..9950894e9fb 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_unittest.cc +++ b/chromium/ui/accessibility/platform/ax_platform_node_unittest.cc @@ -67,36 +67,6 @@ AXNode* AXPlatformNodeTest::GetNodeFromTree(const ui::AXTreeID tree_id, return nullptr; } -AXPlatformNodeDelegate* AXPlatformNodeTest::GetDelegate( - const ui::AXTreeID tree_id, - const int32_t node_id) const { - AXNode* node = GetNodeFromTree(tree_id, node_id); - - if (node) { - TestAXNodeWrapper* wrapper = - TestAXNodeWrapper::GetOrCreate(tree_.get(), node); - - return wrapper; - } - - return nullptr; -} - -AXPlatformNodeDelegate* AXPlatformNodeTest::GetRootDelegate( - const AXTreeID tree_id) const { - if (GetTreeID() == tree_id) { - AXNode* root_node = GetRootNode(); - - if (root_node) { - TestAXNodeWrapper* wrapper = - TestAXNodeWrapper::GetOrCreate(tree_.get(), root_node); - return wrapper; - } - } - - return nullptr; -} - AXTreeID AXPlatformNodeTest::GetTreeID() const { return tree_->data().tree_id; } diff --git a/chromium/ui/accessibility/platform/ax_platform_node_unittest.h b/chromium/ui/accessibility/platform/ax_platform_node_unittest.h index ecae3caf8bd..2fa8b660612 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_unittest.h +++ b/chromium/ui/accessibility/platform/ax_platform_node_unittest.h @@ -41,10 +41,6 @@ class AXPlatformNodeTest : public testing::Test, public AXTreeManager { // AXTreeManager implementation. AXNode* GetNodeFromTree(const ui::AXTreeID tree_id, const int32_t node_id) const override; - AXPlatformNodeDelegate* GetDelegate(const AXTreeID tree_id, - const int32_t node_id) const override; - AXPlatformNodeDelegate* GetRootDelegate( - const AXTreeID tree_id) const override; AXTreeID GetTreeID() const override; AXTreeID GetParentTreeID() const override; ui::AXNode* GetRootAsAXNode() const override; diff --git a/chromium/ui/accessibility/platform/ax_platform_node_win.cc b/chromium/ui/accessibility/platform/ax_platform_node_win.cc index 7d6d3817bfa..6331949e5bd 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_win.cc +++ b/chromium/ui/accessibility/platform/ax_platform_node_win.cc @@ -5,6 +5,7 @@ #include "ui/accessibility/platform/ax_platform_node_win.h" #include <wrl/client.h> +#include <wrl/implements.h> #include <algorithm> #include <map> @@ -591,9 +592,24 @@ void AXPlatformNodeWin::NotifyAccessibilityEvent(ax::mojom::Event event_type) { // role of button. auto* parent = static_cast<AXPlatformNodeWin*>(FromNativeViewAccessible(GetParent())); - if (MSAARole() == ROLE_SYSTEM_MENUITEM || - (parent && parent->MSAARole() == ROLE_SYSTEM_MENUPOPUP)) { + int role = MSAARole(); + if (role == ROLE_SYSTEM_MENUITEM) { event_type = ax::mojom::Event::kFocus; + } else if (role == ROLE_SYSTEM_LISTITEM) { + if (AXPlatformNodeBase* container = GetSelectionContainer()) { + const ui::AXNodeData& data = container->GetData(); + if (data.role == ax::mojom::Role::kListBox && + !data.HasState(ax::mojom::State::kMultiselectable) && + GetDelegate()->GetFocus() == GetNativeViewAccessible()) { + event_type = ax::mojom::Event::kFocus; + } + } + } else if (parent) { + int parent_role = parent->MSAARole(); + if (parent_role == ROLE_SYSTEM_MENUPOPUP || + parent_role == ROLE_SYSTEM_LIST) { + event_type = ax::mojom::Event::kFocus; + } } } @@ -636,6 +652,18 @@ int AXPlatformNodeWin::GetIndexInParent() { LONG child_count = 0; if (S_OK != parent_accessible->get_accChildCount(&child_count)) return -1; + + // Ask the delegate for the index in parent, and return it if it's plausible. + // + // Delegates are allowed to not implement this (AXPlatformNodeDelegateBase + // returns -1). Also, delegates may not know the correct answer if this + // node is the root of a tree that's embedded in another tree, in which + // case the delegate should return -1 and we'll compute it. + int index_in_parent = GetDelegate()->GetIndexInParent(); + if (index_in_parent >= 0 && index_in_parent < child_count) + return index_in_parent; + + // Otherwise, search the parent's children. for (LONG index = 1; index <= child_count; ++index) { base::win::ScopedVariant childid_index(index); Microsoft::WRL::ComPtr<IDispatch> child_dispatch; @@ -1169,16 +1197,14 @@ IFACEMETHODIMP AXPlatformNodeWin::get_accSelection(VARIANT* selected) { // Multiple items are selected. LONG selected_count = static_cast<LONG>(selected_nodes.size()); - auto* enum_variant = new base::win::EnumVariant(selected_count); - enum_variant->AddRef(); + Microsoft::WRL::ComPtr<base::win::EnumVariant> enum_variant = + Microsoft::WRL::Make<base::win::EnumVariant>(selected_count); for (LONG i = 0; i < selected_count; ++i) { enum_variant->ItemAt(i)->vt = VT_DISPATCH; enum_variant->ItemAt(i)->pdispVal = selected_nodes[i].Detach(); } selected->vt = VT_UNKNOWN; - HRESULT hr = enum_variant->QueryInterface(IID_PPV_ARGS(&V_UNKNOWN(selected))); - enum_variant->Release(); - return hr; + return enum_variant.CopyTo(IID_PPV_ARGS(&V_UNKNOWN(selected))); } IFACEMETHODIMP AXPlatformNodeWin::accSelect(LONG flagsSelect, VARIANT var_id) { @@ -1750,10 +1776,20 @@ IFACEMETHODIMP AXPlatformNodeWin::get_ExpandCollapseState( WIN_ACCESSIBILITY_API_HISTOGRAM( UMA_API_EXPANDCOLLAPSE_GET_EXPANDCOLLAPSESTATE); UIA_VALIDATE_CALL_1_ARG(result); + const AXNodeData& data = GetData(); - if (data.HasState(ax::mojom::State::kExpanded)) { + const bool is_menu_button = data.GetHasPopup() == ax::mojom::HasPopup::kMenu; + const bool is_expanded_menu_button = + is_menu_button && + data.GetCheckedState() == ax::mojom::CheckedState::kTrue; + const bool is_collapsed_menu_button = + is_menu_button && + data.GetCheckedState() != ax::mojom::CheckedState::kTrue; + + if (data.HasState(ax::mojom::State::kExpanded) || is_expanded_menu_button) { *result = ExpandCollapseState_Expanded; - } else if (data.HasState(ax::mojom::State::kCollapsed)) { + } else if (data.HasState(ax::mojom::State::kCollapsed) || + is_collapsed_menu_button) { *result = ExpandCollapseState_Collapsed; } else { *result = ExpandCollapseState_LeafNode; @@ -2025,9 +2061,6 @@ IFACEMETHODIMP AXPlatformNodeWin::get_VerticalViewSize(double* result) { HRESULT AXPlatformNodeWin::ISelectionItemProviderSetSelected(bool selected) { UIA_VALIDATE_CALL(); - if (!IsSelectionItemSupported()) - return UIA_E_INVALIDOPERATION; - int restriction; if (GetIntAttribute(ax::mojom::IntAttribute::kRestriction, &restriction)) { if (restriction == static_cast<int>(ax::mojom::Restriction::kDisabled)) @@ -2065,38 +2098,23 @@ IFACEMETHODIMP AXPlatformNodeWin::Select() { IFACEMETHODIMP AXPlatformNodeWin::get_IsSelected(BOOL* result) { WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_SELECTIONITEM_GET_ISSELECTED); UIA_VALIDATE_CALL_1_ARG(result); - - if (!IsSelectionItemSupported()) - return UIA_E_INVALIDOPERATION; - // https://www.w3.org/TR/core-aam-1.1/#mapping_state-property_table - // SelectionItem.IsSelected is exposed when aria-checked is True or False, - // for 'radio' and 'menuitemradio' roles + // SelectionItem.IsSelected is set according to the True or False value of + // aria-checked for 'radio' and 'menuitemradio' roles. if (GetData().role == ax::mojom::Role::kRadioButton || GetData().role == ax::mojom::Role::kMenuItemRadio) { - const auto checked_state = GetData().GetCheckedState(); - switch (checked_state) { - case ax::mojom::CheckedState::kTrue: - case ax::mojom::CheckedState::kFalse: { - *result = (checked_state == ax::mojom::CheckedState::kTrue); - return S_OK; - } - case ax::mojom::CheckedState::kMixed: - case ax::mojom::CheckedState::kNone: { - return UIA_E_INVALIDOPERATION; - } - } + *result = (GetData().GetCheckedState() == ax::mojom::CheckedState::kTrue); + return S_OK; } // https://www.w3.org/TR/wai-aria-1.1/#aria-selected - // Elements are not selectable when aria-selected is undefined + // SelectionItem.IsSelected is set according to the True or False value of + // aria-selected. bool is_selected; - if (GetBoolAttribute(ax::mojom::BoolAttribute::kSelected, &is_selected)) { + if (GetBoolAttribute(ax::mojom::BoolAttribute::kSelected, &is_selected)) *result = is_selected; - return S_OK; - } - return UIA_E_INVALIDOPERATION; + return S_OK; } IFACEMETHODIMP AXPlatformNodeWin::get_SelectionContainer( @@ -3470,10 +3488,41 @@ IFACEMETHODIMP AXPlatformNodeWin::get_offsetAtPoint( LONG y, enum IA2CoordinateType coord_type, LONG* offset) { + WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_OFFSET_AT_POINT); COM_OBJECT_VALIDATE_1_ARG(offset); - // We don't support this method, but we have to return something - // rather than E_NOTIMPL or screen readers will complain. - *offset = 0; + + *offset = -1; + + if (coord_type == IA2CoordinateType::IA2_COORDTYPE_PARENT_RELATIVE) { + // We don't support when the IA2 coordinate type is parent relative, but + // we have to return something rather than E_NOTIMPL or screen readers + // will complain. + NOTIMPLEMENTED_LOG_ONCE() << "See http://crbug.com/1010726"; + return S_FALSE; + } + + // We currently only handle IA2 screen relative coord type. + DCHECK_EQ(coord_type, IA2_COORDTYPE_SCREEN_RELATIVE); + + const AXPlatformNodeWin* hit_child = static_cast<AXPlatformNodeWin*>( + FromNativeViewAccessible(GetDelegate()->HitTestSync(x, y))); + + if (!hit_child || !hit_child->IsTextOnlyObject()) { + return S_FALSE; + } + + for (int i = 0, text_length = hit_child->GetInnerText().length(); + i < text_length; ++i) { + gfx::Rect char_bounds = + hit_child->GetDelegate()->GetInnerTextRangeBoundsRect( + i, i + 1, AXCoordinateSystem::kScreen, + AXClippingBehavior::kUnclipped); + if (char_bounds.Contains(x, y)) { + *offset = i; + break; + } + } + return S_OK; } @@ -3731,7 +3780,7 @@ IFACEMETHODIMP AXPlatformNodeWin::GetPropertyValue(PROPERTYID property_id, WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_PROPERTY_VALUE); constexpr LONG kFirstKnownUiaPropertyId = UIA_RuntimeIdPropertyId; - constexpr LONG kLastKnownUiaPropertyId = UIA_HeadingLevelPropertyId; + constexpr LONG kLastKnownUiaPropertyId = UIA_IsDialogPropertyId; if (property_id >= kFirstKnownUiaPropertyId && property_id <= kLastKnownUiaPropertyId) { base::UmaHistogramSparse("Accessibility.WinAPIs.GetPropertyValue", @@ -4475,6 +4524,13 @@ int AXPlatformNodeWin::MSAARole() { case ax::mojom::Role::kAnchor: return ROLE_SYSTEM_LINK; + case ax::mojom::Role::kAnnotationAttribution: + case ax::mojom::Role::kAnnotationCommentary: + case ax::mojom::Role::kAnnotationPresence: + case ax::mojom::Role::kAnnotationRevision: + case ax::mojom::Role::kAnnotationSuggestion: + return ROLE_SYSTEM_GROUPING; + case ax::mojom::Role::kApplication: return ROLE_SYSTEM_APPLICATION; @@ -4485,6 +4541,7 @@ int AXPlatformNodeWin::MSAARole() { return ROLE_SYSTEM_GROUPING; case ax::mojom::Role::kBanner: + case ax::mojom::Role::kHeader: return ROLE_SYSTEM_GROUPING; case ax::mojom::Role::kBlockquote: @@ -4636,6 +4693,10 @@ int AXPlatformNodeWin::MSAARole() { case ax::mojom::Role::kFeed: return ROLE_SYSTEM_GROUPING; + case ax::mojom::Role::kFooterAsNonLandmark: + case ax::mojom::Role::kHeaderAsNonLandmark: + return ROLE_SYSTEM_GROUPING; + case ax::mojom::Role::kForm: return ROLE_SYSTEM_GROUPING; @@ -4785,19 +4846,8 @@ int AXPlatformNodeWin::MSAARole() { case ax::mojom::Role::kRadioGroup: return ROLE_SYSTEM_GROUPING; - case ax::mojom::Role::kRegion: { - std::string html_tag = - GetData().GetStringAttribute(ax::mojom::StringAttribute::kHtmlTag); - if (html_tag == "section" && - GetData() - .GetString16Attribute(ax::mojom::StringAttribute::kName) - .empty()) { - // Do not use ARIA mapping for nameless <section>. - return ROLE_SYSTEM_GROUPING; - } - // Use ARIA mapping. + case ax::mojom::Role::kRegion: return ROLE_SYSTEM_PANE; - } case ax::mojom::Role::kRow: { // Role changes depending on whether row is inside a treegrid @@ -4811,6 +4861,17 @@ int AXPlatformNodeWin::MSAARole() { case ax::mojom::Role::kRuby: return ROLE_SYSTEM_TEXT; + case ax::mojom::Role::kSection: { + if (GetData() + .GetString16Attribute(ax::mojom::StringAttribute::kName) + .empty()) { + // Do not use ARIA mapping for nameless <section>. + return ROLE_SYSTEM_GROUPING; + } + // Use ARIA mapping. + return ROLE_SYSTEM_PANE; + } + case ax::mojom::Role::kScrollBar: return ROLE_SYSTEM_SCROLLBAR; @@ -4832,7 +4893,7 @@ int AXPlatformNodeWin::MSAARole() { case ax::mojom::Role::kSwitch: return ROLE_SYSTEM_CHECKBUTTON; - case ax::mojom::Role::kAnnotation: + case ax::mojom::Role::kRubyAnnotation: case ax::mojom::Role::kListMarker: case ax::mojom::Role::kStaticText: return ROLE_SYSTEM_STATICTEXT; @@ -5056,7 +5117,14 @@ int32_t AXPlatformNodeWin::ComputeIA2Role() { int32_t ia2_role = 0; switch (GetData().role) { + case ax::mojom::Role::kAnnotationAttribution: + case ax::mojom::Role::kAnnotationCommentary: + case ax::mojom::Role::kAnnotationPresence: + case ax::mojom::Role::kAnnotationRevision: + case ax::mojom::Role::kAnnotationSuggestion: + return IA2_ROLE_SECTION; case ax::mojom::Role::kBanner: + case ax::mojom::Role::kHeader: // CORE-AAM recommends LANDMARK instead of HEADER. ia2_role = IA2_ROLE_LANDMARK; break; @@ -5148,6 +5216,10 @@ int32_t AXPlatformNodeWin::ComputeIA2Role() { case ax::mojom::Role::kFigcaption: ia2_role = IA2_ROLE_CAPTION; break; + case ax::mojom::Role::kFooterAsNonLandmark: + case ax::mojom::Role::kHeaderAsNonLandmark: + ia2_role = IA2_ROLE_SECTION; + break; case ax::mojom::Role::kForm: ia2_role = IA2_ROLE_FORM; break; @@ -5197,11 +5269,17 @@ int32_t AXPlatformNodeWin::ComputeIA2Role() { case ax::mojom::Role::kPre: ia2_role = IA2_ROLE_PARAGRAPH; break; - case ax::mojom::Role::kRegion: { - std::string html_tag = - GetData().GetStringAttribute(ax::mojom::StringAttribute::kHtmlTag); - if (html_tag == "section" && - GetData() + case ax::mojom::Role::kRegion: + ia2_role = IA2_ROLE_LANDMARK; + break; + case ax::mojom::Role::kRuby: + ia2_role = IA2_ROLE_TEXT_FRAME; + break; + case ax::mojom::Role::kSearch: + ia2_role = IA2_ROLE_LANDMARK; + break; + case ax::mojom::Role::kSection: { + if (GetData() .GetString16Attribute(ax::mojom::StringAttribute::kName) .empty()) { // Do not use ARIA mapping for nameless <section>. @@ -5212,12 +5290,6 @@ int32_t AXPlatformNodeWin::ComputeIA2Role() { } break; } - case ax::mojom::Role::kRuby: - ia2_role = IA2_ROLE_TEXT_FRAME; - break; - case ax::mojom::Role::kSearch: - ia2_role = IA2_ROLE_LANDMARK; - break; case ax::mojom::Role::kSwitch: ia2_role = IA2_ROLE_TOGGLE_BUTTON; break; @@ -5264,6 +5336,13 @@ base::string16 AXPlatformNodeWin::UIAAriaRole() { case ax::mojom::Role::kAnchor: return L"link"; + case ax::mojom::Role::kAnnotationAttribution: + case ax::mojom::Role::kAnnotationCommentary: + case ax::mojom::Role::kAnnotationPresence: + case ax::mojom::Role::kAnnotationRevision: + case ax::mojom::Role::kAnnotationSuggestion: + return L"group"; + case ax::mojom::Role::kApplication: return L"application"; @@ -5274,6 +5353,7 @@ base::string16 AXPlatformNodeWin::UIAAriaRole() { return L"group"; case ax::mojom::Role::kBanner: + case ax::mojom::Role::kHeader: return L"banner"; case ax::mojom::Role::kBlockquote: @@ -5425,6 +5505,10 @@ base::string16 AXPlatformNodeWin::UIAAriaRole() { case ax::mojom::Role::kFigure: return L"group"; + case ax::mojom::Role::kFooterAsNonLandmark: + case ax::mojom::Role::kHeaderAsNonLandmark: + return L"group"; + case ax::mojom::Role::kForm: return L"form"; @@ -5577,19 +5661,8 @@ base::string16 AXPlatformNodeWin::UIAAriaRole() { case ax::mojom::Role::kRadioGroup: return L"radiogroup"; - case ax::mojom::Role::kRegion: { - std::string html_tag = - GetData().GetStringAttribute(ax::mojom::StringAttribute::kHtmlTag); - if (html_tag == "section" && - GetData() - .GetString16Attribute(ax::mojom::StringAttribute::kName) - .empty()) { - // Do not use ARIA mapping for nameless <section>. - return L"group"; - } - // Use ARIA mapping. + case ax::mojom::Role::kRegion: return L"region"; - } case ax::mojom::Role::kRow: { // Role changes depending on whether row is inside a treegrid @@ -5603,6 +5676,17 @@ base::string16 AXPlatformNodeWin::UIAAriaRole() { case ax::mojom::Role::kRuby: return L"region"; + case ax::mojom::Role::kSection: { + if (GetData() + .GetString16Attribute(ax::mojom::StringAttribute::kName) + .empty()) { + // Do not use ARIA mapping for nameless <section>. + return L"group"; + } + // Use ARIA mapping. + return L"region"; + } + case ax::mojom::Role::kScrollBar: return L"scrollbar"; @@ -5624,7 +5708,7 @@ base::string16 AXPlatformNodeWin::UIAAriaRole() { case ax::mojom::Role::kSwitch: return L"checkbox"; - case ax::mojom::Role::kAnnotation: + case ax::mojom::Role::kRubyAnnotation: case ax::mojom::Role::kListMarker: case ax::mojom::Role::kStaticText: return L"description"; @@ -5905,6 +5989,13 @@ LONG AXPlatformNodeWin::ComputeUIAControlType() { // NOLINT(runtime/int) case ax::mojom::Role::kAnchor: return UIA_HyperlinkControlTypeId; + case ax::mojom::Role::kAnnotationAttribution: + case ax::mojom::Role::kAnnotationCommentary: + case ax::mojom::Role::kAnnotationPresence: + case ax::mojom::Role::kAnnotationRevision: + case ax::mojom::Role::kAnnotationSuggestion: + return ROLE_SYSTEM_GROUPING; + case ax::mojom::Role::kApplication: return UIA_PaneControlTypeId; @@ -5915,6 +6006,7 @@ LONG AXPlatformNodeWin::ComputeUIAControlType() { // NOLINT(runtime/int) return UIA_GroupControlTypeId; case ax::mojom::Role::kBanner: + case ax::mojom::Role::kHeader: return UIA_GroupControlTypeId; case ax::mojom::Role::kBlockquote: @@ -6066,6 +6158,10 @@ LONG AXPlatformNodeWin::ComputeUIAControlType() { // NOLINT(runtime/int) case ax::mojom::Role::kFigure: return UIA_GroupControlTypeId; + case ax::mojom::Role::kFooterAsNonLandmark: + case ax::mojom::Role::kHeaderAsNonLandmark: + return UIA_GroupControlTypeId; + case ax::mojom::Role::kForm: return UIA_GroupControlTypeId; @@ -6215,19 +6311,8 @@ LONG AXPlatformNodeWin::ComputeUIAControlType() { // NOLINT(runtime/int) case ax::mojom::Role::kRadioGroup: return UIA_GroupControlTypeId; - case ax::mojom::Role::kRegion: { - std::string html_tag = - GetData().GetStringAttribute(ax::mojom::StringAttribute::kHtmlTag); - if (html_tag == "section" && - GetData() - .GetString16Attribute(ax::mojom::StringAttribute::kName) - .empty()) { - // Do not use ARIA mapping for nameless <section>. - return UIA_GroupControlTypeId; - } - // Use ARIA mapping. - return UIA_PaneControlTypeId; - } + case ax::mojom::Role::kRegion: + return UIA_GroupControlTypeId; case ax::mojom::Role::kRow: { // Role changes depending on whether row is inside a treegrid @@ -6242,6 +6327,9 @@ LONG AXPlatformNodeWin::ComputeUIAControlType() { // NOLINT(runtime/int) case ax::mojom::Role::kRuby: return UIA_PaneControlTypeId; + case ax::mojom::Role::kSection: + return UIA_GroupControlTypeId; + case ax::mojom::Role::kScrollBar: return UIA_ScrollBarControlTypeId; @@ -6263,7 +6351,7 @@ LONG AXPlatformNodeWin::ComputeUIAControlType() { // NOLINT(runtime/int) case ax::mojom::Role::kSwitch: return UIA_CheckBoxControlTypeId; - case ax::mojom::Role::kAnnotation: + case ax::mojom::Role::kRubyAnnotation: case ax::mojom::Role::kListMarker: case ax::mojom::Role::kStaticText: return UIA_TextControlTypeId; @@ -6414,6 +6502,7 @@ base::Optional<LONG> AXPlatformNodeWin::ComputeUIALandmarkType() const { case ax::mojom::Role::kComplementary: case ax::mojom::Role::kContentInfo: case ax::mojom::Role::kFooter: + case ax::mojom::Role::kHeader: return UIA_CustomLandmarkTypeId; case ax::mojom::Role::kForm: @@ -6429,6 +6518,7 @@ base::Optional<LONG> AXPlatformNodeWin::ComputeUIALandmarkType() const { return UIA_SearchLandmarkTypeId; case ax::mojom::Role::kRegion: + case ax::mojom::Role::kSection: if (data.HasStringAttribute(ax::mojom::StringAttribute::kName)) return UIA_CustomLandmarkTypeId; FALLTHROUGH; @@ -6438,6 +6528,34 @@ base::Optional<LONG> AXPlatformNodeWin::ComputeUIALandmarkType() const { } } +bool AXPlatformNodeWin::IsInaccessibleDueToAncestor() const { + AXPlatformNodeWin* parent = static_cast<AXPlatformNodeWin*>( + AXPlatformNode::FromNativeViewAccessible(GetParent())); + while (parent) { + if (parent->ShouldHideChildren()) + return true; + parent = static_cast<AXPlatformNodeWin*>( + FromNativeViewAccessible(parent->GetParent())); + } + return false; +} + +bool AXPlatformNodeWin::ShouldHideChildren() const { + switch (GetData().role) { + case ax::mojom::Role::kButton: + case ax::mojom::Role::kImage: + case ax::mojom::Role::kGraphicsSymbol: + case ax::mojom::Role::kLink: + case ax::mojom::Role::kMath: + case ax::mojom::Role::kProgressIndicator: + case ax::mojom::Role::kScrollBar: + case ax::mojom::Role::kSlider: + return true; + default: + return false; + } +} + base::string16 AXPlatformNodeWin::GetValue() const { base::string16 value = AXPlatformNodeBase::GetValue(); @@ -7167,4 +7285,40 @@ void AXPlatformNodeWin::FireLiveRegionChangeRecursive() { } } +AXPlatformNodeWin* AXPlatformNodeWin::GetLowestAccessibleElement() { + if (!IsInaccessibleDueToAncestor()) + return this; + + AXPlatformNodeWin* parent = static_cast<AXPlatformNodeWin*>( + AXPlatformNode::FromNativeViewAccessible(GetParent())); + while (parent) { + if (parent->ShouldHideChildren()) + return parent; + parent = static_cast<AXPlatformNodeWin*>( + AXPlatformNode::FromNativeViewAccessible(parent->GetParent())); + } + + NOTREACHED(); + return nullptr; +} + +void AXPlatformNodeWin::SanitizeTextAttributeValue(const std::string& input, + std::string* output) const { + SanitizeStringAttributeForIA2(input, output); +} + +// static +void AXPlatformNodeWin::SanitizeStringAttributeForIA2(const std::string& input, + std::string* output) { + DCHECK(output); + // According to the IA2 Spec, these characters need to be escaped with a + // backslash: backslash, colon, comma, equals and semicolon. + // Note that backslash must be replaced first. + base::ReplaceChars(input, "\\", "\\\\", output); + base::ReplaceChars(*output, ":", "\\:", output); + base::ReplaceChars(*output, ",", "\\,", output); + base::ReplaceChars(*output, "=", "\\=", output); + base::ReplaceChars(*output, ";", "\\;", output); +} + } // namespace ui diff --git a/chromium/ui/accessibility/platform/ax_platform_node_win.h b/chromium/ui/accessibility/platform/ax_platform_node_win.h index 0b0189c75ce..92ce5752ad3 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_win.h +++ b/chromium/ui/accessibility/platform/ax_platform_node_win.h @@ -18,6 +18,7 @@ #include <vector> #include "base/compiler_specific.h" +#include "base/gtest_prod_util.h" #include "base/metrics/histogram_macros.h" #include "base/observer_list.h" #include "base/win/atl.h" @@ -1031,6 +1032,9 @@ class AX_EXPORT __declspec(uuid("26f5641a-246d-457b-a96d-07f3fae6acf2")) // Helper to recursively find live-regions and fire a change event on them void FireLiveRegionChangeRecursive(); + // Returns the parent node that makes this node inaccessible. + AXPlatformNodeWin* GetLowestAccessibleElement(); + // Convert a mojo event to an MSAA event. Exposed for testing. static base::Optional<DWORD> MojoEventToMSAAEvent(ax::mojom::Event event); @@ -1070,6 +1074,10 @@ class AX_EXPORT __declspec(uuid("26f5641a-246d-457b-a96d-07f3fae6acf2")) base::Optional<LONG> ComputeUIALandmarkType() const; + bool IsInaccessibleDueToAncestor() const; + + bool ShouldHideChildren() const; + // AXPlatformNodeBase overrides. void Dispose() override; @@ -1102,6 +1110,17 @@ class AX_EXPORT __declspec(uuid("26f5641a-246d-457b-a96d-07f3fae6acf2")) const char* value, PlatformAttributeList* attributes) override; + // Escape special characters as specified by the IA2 spec. + void SanitizeTextAttributeValue(const std::string& input, + std::string* output) const override; + + // Escapes characters in string attributes as required by the IA2 Spec. + // It's okay for input to be the same as output. + static void SanitizeStringAttributeForIA2(const std::string& input, + std::string* output); + FRIEND_TEST_ALL_PREFIXES(AXPlatformNodeWinTest, + TestSanitizeStringAttributeForIA2); + private: bool IsWebAreaForPresentationalIframe(); bool ShouldNodeHaveFocusableState(const AXNodeData& data) const; diff --git a/chromium/ui/accessibility/platform/ax_platform_node_win_unittest.cc b/chromium/ui/accessibility/platform/ax_platform_node_win_unittest.cc index 0540c6687ec..d6078808784 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_win_unittest.cc +++ b/chromium/ui/accessibility/platform/ax_platform_node_win_unittest.cc @@ -9,6 +9,7 @@ #include <memory> +#include "base/auto_reset.h" #include "base/stl_util.h" #include "base/test/metrics/histogram_tester.h" #include "base/win/atl.h" @@ -18,6 +19,7 @@ #include "testing/gmock/include/gmock/gmock-matchers.h" #include "testing/gtest/include/gtest/gtest.h" #include "third_party/iaccessible2/ia2_api_all.h" +#include "ui/accessibility/ax_enums.mojom.h" #include "ui/accessibility/ax_node_data.h" #include "ui/accessibility/platform/ax_fragment_root_win.h" #include "ui/accessibility/platform/ax_platform_node_win.h" @@ -444,7 +446,7 @@ TEST_F(AXPlatformNodeWinTest, TestIAccessibleDetachedObject) { TEST_F(AXPlatformNodeWinTest, TestIAccessibleHitTest) { AXNodeData root; root.id = 1; - root.relative_bounds.bounds = gfx::RectF(0, 0, 30, 30); + root.relative_bounds.bounds = gfx::RectF(0, 0, 40, 40); AXNodeData node1; node1.id = 2; @@ -454,7 +456,7 @@ TEST_F(AXPlatformNodeWinTest, TestIAccessibleHitTest) { AXNodeData node2; node2.id = 3; - node2.relative_bounds.bounds = gfx::RectF(20, 20, 10, 10); + node2.relative_bounds.bounds = gfx::RectF(20, 20, 20, 20); node2.SetName("Name2"); root.child_ids.push_back(node2.id); @@ -462,17 +464,23 @@ TEST_F(AXPlatformNodeWinTest, TestIAccessibleHitTest) { ComPtr<IAccessible> root_obj(GetRootIAccessible()); - ScopedVariant obj; + // This is way outside of the root node. + ScopedVariant obj_1; + EXPECT_EQ(S_FALSE, root_obj->accHitTest(50, 50, obj_1.Receive())); + EXPECT_EQ(VT_EMPTY, obj_1.type()); - // This is way outside of the root node - EXPECT_EQ(S_FALSE, root_obj->accHitTest(50, 50, obj.Receive())); - EXPECT_EQ(VT_EMPTY, obj.type()); + // This is directly on node 1. + EXPECT_EQ(S_OK, root_obj->accHitTest(5, 5, obj_1.Receive())); + ASSERT_NE(nullptr, obj_1.ptr()); + CheckVariantHasName(obj_1, L"Name1"); - // this is directly on node 1. - EXPECT_EQ(S_OK, root_obj->accHitTest(5, 5, obj.Receive())); - ASSERT_NE(nullptr, obj.ptr()); - - CheckVariantHasName(obj, L"Name1"); + // This is directly on node 2 with a scale factor of 1.5. + ScopedVariant obj_2; + std::unique_ptr<base::AutoReset<float>> scale_factor_reset = + TestAXNodeWrapper::SetScaleFactor(1.5); + EXPECT_EQ(S_OK, root_obj->accHitTest(38, 38, obj_2.Receive())); + ASSERT_NE(nullptr, obj_2.ptr()); + CheckVariantHasName(obj_2, L"Name2"); } TEST_F(AXPlatformNodeWinTest, TestIAccessibleName) { @@ -2870,6 +2878,154 @@ TEST_F(AXPlatformNodeWinTest, TestIAccessibleTextGetNCharacters) { EXPECT_EQ(4, count); } +TEST_F(AXPlatformNodeWinTest, TestIAccessibleTextGetOffsetAtPoint) { + AXNodeData root; + root.id = 1; + root.role = ax::mojom::Role::kWebArea; + root.relative_bounds.bounds = gfx::RectF(0, 0, 300, 200); + root.child_ids = {2, 3}; + + AXNodeData button; + button.id = 2; + button.role = ax::mojom::Role::kButton; + button.SetName("button"); + button.relative_bounds.bounds = gfx::RectF(20, 0, 10, 10); + + AXNodeData static_text1; + static_text1.id = 3; + static_text1.role = ax::mojom::Role::kStaticText; + static_text1.SetName("line 1"); + static_text1.relative_bounds.bounds = gfx::RectF(0, 20, 30, 10); + static_text1.child_ids = {4}; + + AXNodeData inline_box1; + inline_box1.id = 4; + inline_box1.role = ax::mojom::Role::kInlineTextBox; + inline_box1.SetName("line 1"); + inline_box1.relative_bounds.bounds = gfx::RectF(0, 20, 30, 10); + std::vector<int32_t> character_offsets1; + // The width of each character is 5px, height is 10px. + character_offsets1.push_back(5); // "l" {0, 20, 5x10} + character_offsets1.push_back(10); // "i" {5, 20, 5x10} + character_offsets1.push_back(15); // "n" {10, 20, 5x10} + character_offsets1.push_back(20); // "e" {15, 20, 5x10} + character_offsets1.push_back(25); // " " {20, 20, 5x10} + character_offsets1.push_back(30); // "1" {25, 20, 5x10} + + inline_box1.AddIntListAttribute( + ax::mojom::IntListAttribute::kCharacterOffsets, character_offsets1); + inline_box1.AddIntListAttribute(ax::mojom::IntListAttribute::kWordStarts, + std::vector<int32_t>{0, 5}); + inline_box1.AddIntListAttribute(ax::mojom::IntListAttribute::kWordEnds, + std::vector<int32_t>{4, 6}); + + Init(root, button, static_text1, inline_box1); + + LONG offset_result; + ComPtr<IAccessible> root_iaccessible(IAccessibleFromNode(GetRootNode())); + ASSERT_NE(nullptr, root_iaccessible.Get()); + + ComPtr<IAccessibleText> root_text; + root_iaccessible.As(&root_text); + ASSERT_NE(nullptr, root_text.Get()); + + // Test point(0, 0) with coordinate parent relative, which is not supported. + // Expected result: S_FALSE. + EXPECT_EQ(S_FALSE, root_text->get_offsetAtPoint( + 0, 0, IA2CoordinateType::IA2_COORDTYPE_PARENT_RELATIVE, + &offset_result)); + EXPECT_EQ(-1, offset_result); + + // Test point(0, 0) retrieved from IAccessibleText of the root web area is + // outside of any text. Expected result: S_FALSE. + EXPECT_EQ(S_FALSE, root_text->get_offsetAtPoint( + 0, 0, IA2CoordinateType::IA2_COORDTYPE_SCREEN_RELATIVE, + &offset_result)); + EXPECT_EQ(-1, offset_result); + + // Test point(25, 5) retrieved from IAccessibleText of the root web area is + // on button, and outside of any text. Expected result: S_FALSE. + EXPECT_EQ(S_FALSE, + root_text->get_offsetAtPoint( + 25, 5, IA2CoordinateType::IA2_COORDTYPE_SCREEN_RELATIVE, + &offset_result)); + EXPECT_EQ(-1, offset_result); + + // Test point(0, 20) retrieved from IAccessibleText of the root web area is + // on "line 1", character="l". Expected result: S_OK, offset=0. + EXPECT_EQ(S_OK, root_text->get_offsetAtPoint( + 0, 20, IA2CoordinateType::IA2_COORDTYPE_SCREEN_RELATIVE, + &offset_result)); + EXPECT_EQ(0, offset_result); + + AXNode* static_text1_node = GetRootNode()->children()[1]; + ComPtr<IAccessible> text1_iaccessible(IAccessibleFromNode(static_text1_node)); + ASSERT_NE(nullptr, text1_iaccessible.Get()); + + ComPtr<IAccessibleText> text1; + text1_iaccessible.As(&text1); + ASSERT_NE(nullptr, text1.Get()); + + // "l" 4 points of bounds {(0, 20), (5, 20), (0, 30), (5, 30)} + // "i" 4 points of bounds {(5, 20), (10, 20), (5, 30), (10, 30)} + // "n" 4 points of bounds {(10, 20), (15, 20), (10, 30), (15, 30)} + // "e" 4 points of bounds {(15, 20), (20, 20), (15, 30), (20, 30)} + // " " 4 points of bounds {(20, 20), (25, 20), (20, 30), (25, 30)} + // "1" 4 points of bounds {(25, 20), (30, 20), (25, 30), (30, 30)} + + // Test point(0, 0) outside of any character bounds and text. + EXPECT_EQ(S_FALSE, text1->get_offsetAtPoint( + 0, 0, IA2CoordinateType::IA2_COORDTYPE_SCREEN_RELATIVE, + &offset_result)); + EXPECT_EQ(-1, offset_result); + + // Test point(30, 30) outside of any character bounds but on the text. + EXPECT_EQ(S_OK, text1->get_offsetAtPoint( + 30, 30, IA2CoordinateType::IA2_COORDTYPE_SCREEN_RELATIVE, + &offset_result)); + EXPECT_EQ(-1, offset_result); + + // Test point(0, 20) inside bounds of "l", text offset=0 + // character bounds={(0, 20), (5, 20), (0, 30), (5, 30)} + EXPECT_HRESULT_SUCCEEDED(text1->get_offsetAtPoint( + 0, 20, IA2CoordinateType::IA2_COORDTYPE_SCREEN_RELATIVE, &offset_result)); + EXPECT_EQ(0, offset_result); + + // Test point(9, 20) inside bounds of "i", text offset=1 + // character bounds={(5, 20), (10, 20), (5, 30), (10, 30)} + EXPECT_HRESULT_SUCCEEDED(text1->get_offsetAtPoint( + 9, 20, IA2CoordinateType::IA2_COORDTYPE_SCREEN_RELATIVE, &offset_result)); + EXPECT_EQ(1, offset_result); + + // Test point(10, 30) inside bounds of "n", text offset=2 + // character bounds={(10, 20), (15, 20), (10, 30), (15, 30)} + EXPECT_HRESULT_SUCCEEDED(text1->get_offsetAtPoint( + 10, 29, IA2CoordinateType::IA2_COORDTYPE_SCREEN_RELATIVE, + &offset_result)); + EXPECT_EQ(2, offset_result); + + // Test point(19, 29) inside bounds of "e", text offset=3 + // character bounds={(15, 20), (20, 20), (15, 30), (20, 30) + EXPECT_HRESULT_SUCCEEDED(text1->get_offsetAtPoint( + 19, 29, IA2CoordinateType::IA2_COORDTYPE_SCREEN_RELATIVE, + &offset_result)); + EXPECT_EQ(3, offset_result); + + // Test point(23, 25) inside bounds of " ", text offset=4 + // character bounds={(20, 20), (25, 20), (20, 30), (25, 30)} + EXPECT_HRESULT_SUCCEEDED(text1->get_offsetAtPoint( + 23, 25, IA2CoordinateType::IA2_COORDTYPE_SCREEN_RELATIVE, + &offset_result)); + EXPECT_EQ(4, offset_result); + + // Test point(25, 20) inside bounds of "1", text offset=5 + // character bounds={(25, 20), (30, 20), (25, 30), (30, 30)} + EXPECT_HRESULT_SUCCEEDED(text1->get_offsetAtPoint( + 25, 20, IA2CoordinateType::IA2_COORDTYPE_SCREEN_RELATIVE, + &offset_result)); + EXPECT_EQ(5, offset_result); +} + TEST_F(AXPlatformNodeWinTest, TestIAccessibleTextTextFieldRemoveSelection) { Init(BuildTextFieldWithSelectionRange(1, 2)); @@ -5185,16 +5341,25 @@ TEST_F(AXPlatformNodeWinTest, TestIExpandCollapsePatternProviderAction) { ui::AXNodeData combo_box_grouping_expanded; ui::AXNodeData combo_box_grouping_collapsed; ui::AXNodeData combo_box_grouping_disabled; + ui::AXNodeData button_has_popup_menu; + ui::AXNodeData button_has_popup_menu_pressed; + ui::AXNodeData button_has_popup_true; combo_box_grouping_has_popup.id = 2; combo_box_grouping_expanded.id = 3; combo_box_grouping_collapsed.id = 4; combo_box_grouping_disabled.id = 5; + button_has_popup_menu.id = 6; + button_has_popup_menu_pressed.id = 7; + button_has_popup_true.id = 8; root.child_ids.push_back(combo_box_grouping_has_popup.id); root.child_ids.push_back(combo_box_grouping_expanded.id); root.child_ids.push_back(combo_box_grouping_collapsed.id); root.child_ids.push_back(combo_box_grouping_disabled.id); + root.child_ids.push_back(button_has_popup_menu.id); + root.child_ids.push_back(button_has_popup_menu_pressed.id); + root.child_ids.push_back(button_has_popup_true.id); // combo_box_grouping HasPopup set to true, can collapse, can expand. // state is ExpandCollapseState_LeafNode. @@ -5216,9 +5381,23 @@ TEST_F(AXPlatformNodeWinTest, TestIExpandCollapsePatternProviderAction) { combo_box_grouping_disabled.role = ax::mojom::Role::kComboBoxGrouping; combo_box_grouping_disabled.SetRestriction(ax::mojom::Restriction::kDisabled); + // button_has_popup_menu HasPopup set to kMenu and is not STATE_PRESSED. + // state is ExpandCollapseState_Collapsed. + button_has_popup_menu.SetHasPopup(ax::mojom::HasPopup::kMenu); + + // button_has_popup_menu_pressed HasPopup set to kMenu and is STATE_PRESSED. + // state is ExpandCollapseState_Expanded. + button_has_popup_menu_pressed.SetHasPopup(ax::mojom::HasPopup::kMenu); + button_has_popup_menu_pressed.SetCheckedState(ax::mojom::CheckedState::kTrue); + + // button_has_popup_true HasPopup set to true but is not a menu. + // state is ExpandCollapseState_LeafNode. + button_has_popup_true.SetHasPopup(ax::mojom::HasPopup::kTrue); + Init(root, combo_box_grouping_has_popup, combo_box_grouping_disabled, combo_box_grouping_expanded, combo_box_grouping_collapsed, - combo_box_grouping_disabled); + combo_box_grouping_disabled, button_has_popup_menu, + button_has_popup_menu_pressed, button_has_popup_true); // combo_box_grouping HasPopup set to true, can collapse, can expand. // state is ExpandCollapseState_LeafNode. @@ -5270,6 +5449,36 @@ TEST_F(AXPlatformNodeWinTest, TestIExpandCollapsePatternProviderAction) { EXPECT_HRESULT_SUCCEEDED( expandcollapse_provider->get_ExpandCollapseState(&state)); EXPECT_EQ(ExpandCollapseState_LeafNode, state); + + // button_has_popup_menu HasPopup set to kMenu and is not STATE_PRESSED. + // state is ExpandCollapseState_Collapsed. + raw_element_provider_simple = GetIRawElementProviderSimpleFromChildIndex(4); + EXPECT_HRESULT_SUCCEEDED(raw_element_provider_simple->GetPatternProvider( + UIA_ExpandCollapsePatternId, &expandcollapse_provider)); + EXPECT_NE(nullptr, expandcollapse_provider.Get()); + EXPECT_HRESULT_SUCCEEDED( + expandcollapse_provider->get_ExpandCollapseState(&state)); + EXPECT_EQ(ExpandCollapseState_Collapsed, state); + + // button_has_popup_menu_pressed HasPopup set to kMenu and is STATE_PRESSED. + // state is ExpandCollapseState_Expanded. + raw_element_provider_simple = GetIRawElementProviderSimpleFromChildIndex(5); + EXPECT_HRESULT_SUCCEEDED(raw_element_provider_simple->GetPatternProvider( + UIA_ExpandCollapsePatternId, &expandcollapse_provider)); + EXPECT_NE(nullptr, expandcollapse_provider.Get()); + EXPECT_HRESULT_SUCCEEDED( + expandcollapse_provider->get_ExpandCollapseState(&state)); + EXPECT_EQ(ExpandCollapseState_Expanded, state); + + // button_has_popup_true HasPopup set to true but is not a menu. + // state is ExpandCollapseState_LeafNode. + raw_element_provider_simple = GetIRawElementProviderSimpleFromChildIndex(6); + EXPECT_HRESULT_SUCCEEDED(raw_element_provider_simple->GetPatternProvider( + UIA_ExpandCollapsePatternId, &expandcollapse_provider)); + EXPECT_NE(nullptr, expandcollapse_provider.Get()); + EXPECT_HRESULT_SUCCEEDED( + expandcollapse_provider->get_ExpandCollapseState(&state)); + EXPECT_EQ(ExpandCollapseState_LeafNode, state); } TEST_F(AXPlatformNodeWinTest, TestIInvokeProviderInvoke) { @@ -5320,15 +5529,12 @@ TEST_F(AXPlatformNodeWinTest, TestISelectionItemProviderNotSupported) { Init(root); - ComPtr<ISelectionItemProvider> provider = - QueryInterfaceFromNode<ISelectionItemProvider>(GetRootNode()); - - BOOL selected; - - EXPECT_UIA_INVALIDOPERATION(provider->AddToSelection()); - EXPECT_UIA_INVALIDOPERATION(provider->RemoveFromSelection()); - EXPECT_UIA_INVALIDOPERATION(provider->Select()); - EXPECT_UIA_INVALIDOPERATION(provider->get_IsSelected(&selected)); + ComPtr<IRawElementProviderSimple> raw_element_provider_simple = + GetRootIRawElementProviderSimple(); + ComPtr<ISelectionItemProvider> selection_item_provider; + EXPECT_HRESULT_SUCCEEDED(raw_element_provider_simple->GetPatternProvider( + UIA_SelectionItemPatternId, &selection_item_provider)); + ASSERT_EQ(nullptr, selection_item_provider.Get()); } TEST_F(AXPlatformNodeWinTest, TestISelectionItemProviderDisabled) { @@ -5341,15 +5547,19 @@ TEST_F(AXPlatformNodeWinTest, TestISelectionItemProviderDisabled) { Init(root); - ComPtr<ISelectionItemProvider> provider = - QueryInterfaceFromNode<ISelectionItemProvider>(GetRootNode()); + ComPtr<IRawElementProviderSimple> raw_element_provider_simple = + GetRootIRawElementProviderSimple(); + ComPtr<ISelectionItemProvider> selection_item_provider; + EXPECT_HRESULT_SUCCEEDED(raw_element_provider_simple->GetPatternProvider( + UIA_SelectionItemPatternId, &selection_item_provider)); + ASSERT_NE(nullptr, selection_item_provider.Get()); BOOL selected; - EXPECT_UIA_ELEMENTNOTENABLED(provider->AddToSelection()); - EXPECT_UIA_ELEMENTNOTENABLED(provider->RemoveFromSelection()); - EXPECT_UIA_ELEMENTNOTENABLED(provider->Select()); - EXPECT_HRESULT_SUCCEEDED(provider->get_IsSelected(&selected)); + EXPECT_UIA_ELEMENTNOTENABLED(selection_item_provider->AddToSelection()); + EXPECT_UIA_ELEMENTNOTENABLED(selection_item_provider->RemoveFromSelection()); + EXPECT_UIA_ELEMENTNOTENABLED(selection_item_provider->Select()); + EXPECT_HRESULT_SUCCEEDED(selection_item_provider->get_IsSelected(&selected)); EXPECT_TRUE(selected); } @@ -5360,15 +5570,12 @@ TEST_F(AXPlatformNodeWinTest, TestISelectionItemProviderNotSelectable) { Init(root); - ComPtr<ISelectionItemProvider> item_provider = - QueryInterfaceFromNode<ISelectionItemProvider>(GetRootNode()); - - BOOL selected; - - EXPECT_UIA_INVALIDOPERATION(item_provider->AddToSelection()); - EXPECT_UIA_INVALIDOPERATION(item_provider->RemoveFromSelection()); - EXPECT_UIA_INVALIDOPERATION(item_provider->Select()); - EXPECT_UIA_INVALIDOPERATION(item_provider->get_IsSelected(&selected)); + ComPtr<IRawElementProviderSimple> raw_element_provider_simple = + GetRootIRawElementProviderSimple(); + ComPtr<ISelectionItemProvider> selection_item_provider; + EXPECT_HRESULT_SUCCEEDED(raw_element_provider_simple->GetPatternProvider( + UIA_SelectionItemPatternId, &selection_item_provider)); + ASSERT_EQ(nullptr, selection_item_provider.Get()); } TEST_F(AXPlatformNodeWinTest, TestISelectionItemProviderSimple) { @@ -5384,9 +5591,12 @@ TEST_F(AXPlatformNodeWinTest, TestISelectionItemProviderSimple) { Init(root, option1); - const auto* root_node = GetRootNode(); - ComPtr<ISelectionItemProvider> option1_provider = - QueryInterfaceFromNode<ISelectionItemProvider>(root_node->children()[0]); + ComPtr<IRawElementProviderSimple> raw_element_provider_simple = + GetIRawElementProviderSimpleFromChildIndex(0); + ComPtr<ISelectionItemProvider> option1_provider; + EXPECT_HRESULT_SUCCEEDED(raw_element_provider_simple->GetPatternProvider( + UIA_SelectionItemPatternId, &option1_provider)); + ASSERT_NE(nullptr, option1_provider.Get()); BOOL selected; @@ -5467,31 +5677,41 @@ TEST_F(AXPlatformNodeWinTest, TestISelectionItemProviderRadioButtonIsSelected) { Init(root, option1, option2, option3, option4); - const auto* root_node = GetRootNode(); - // CheckedState::kNone - ComPtr<ISelectionItemProvider> option1_provider = - QueryInterfaceFromNode<ISelectionItemProvider>(root_node->children()[0]); - // CheckedState::kFalse - ComPtr<ISelectionItemProvider> option2_provider = - QueryInterfaceFromNode<ISelectionItemProvider>(root_node->children()[1]); - // CheckedState::kTrue - ComPtr<ISelectionItemProvider> option3_provider = - QueryInterfaceFromNode<ISelectionItemProvider>(root_node->children()[2]); - // CheckedState::kMixed - ComPtr<ISelectionItemProvider> option4_provider = - QueryInterfaceFromNode<ISelectionItemProvider>(root_node->children()[3]); - BOOL selected; - EXPECT_UIA_INVALIDOPERATION(option1_provider->get_IsSelected(&selected)); + // CheckedState::kNone + ComPtr<ISelectionItemProvider> option1_provider; + EXPECT_HRESULT_SUCCEEDED( + GetIRawElementProviderSimpleFromChildIndex(0)->GetPatternProvider( + UIA_SelectionItemPatternId, &option1_provider)); + ASSERT_EQ(nullptr, option1_provider.Get()); + + // CheckedState::kFalse + ComPtr<ISelectionItemProvider> option2_provider; + EXPECT_HRESULT_SUCCEEDED( + GetIRawElementProviderSimpleFromChildIndex(1)->GetPatternProvider( + UIA_SelectionItemPatternId, &option2_provider)); + ASSERT_NE(nullptr, option2_provider.Get()); EXPECT_HRESULT_SUCCEEDED(option2_provider->get_IsSelected(&selected)); EXPECT_FALSE(selected); + // CheckedState::kTrue + ComPtr<ISelectionItemProvider> option3_provider; + EXPECT_HRESULT_SUCCEEDED( + GetIRawElementProviderSimpleFromChildIndex(2)->GetPatternProvider( + UIA_SelectionItemPatternId, &option3_provider)); + ASSERT_NE(nullptr, option3_provider.Get()); + EXPECT_HRESULT_SUCCEEDED(option3_provider->get_IsSelected(&selected)); EXPECT_TRUE(selected); - EXPECT_UIA_INVALIDOPERATION(option4_provider->get_IsSelected(&selected)); + // CheckedState::kMixed + ComPtr<ISelectionItemProvider> option4_provider; + EXPECT_HRESULT_SUCCEEDED( + GetIRawElementProviderSimpleFromChildIndex(3)->GetPatternProvider( + UIA_SelectionItemPatternId, &option4_provider)); + ASSERT_EQ(nullptr, option4_provider.Get()); } TEST_F(AXPlatformNodeWinTest, TestISelectionItemProviderTable) { @@ -5512,16 +5732,11 @@ TEST_F(AXPlatformNodeWinTest, TestISelectionItemProviderTable) { Init(root, row1, cell1); - const auto* row = GetRootNode()->children()[0]; - ComPtr<ISelectionItemProvider> item_provider = - QueryInterfaceFromNode<ISelectionItemProvider>(row->children()[0]); - - BOOL selected; - - EXPECT_UIA_INVALIDOPERATION(item_provider->AddToSelection()); - EXPECT_UIA_INVALIDOPERATION(item_provider->RemoveFromSelection()); - EXPECT_UIA_INVALIDOPERATION(item_provider->Select()); - EXPECT_UIA_INVALIDOPERATION(item_provider->get_IsSelected(&selected)); + ComPtr<ISelectionItemProvider> selection_item_provider; + EXPECT_HRESULT_SUCCEEDED( + GetIRawElementProviderSimpleFromChildIndex(0)->GetPatternProvider( + UIA_SelectionItemPatternId, &selection_item_provider)); + ASSERT_EQ(nullptr, selection_item_provider.Get()); } TEST_F(AXPlatformNodeWinTest, TestISelectionItemProviderGrid) { @@ -5543,8 +5758,13 @@ TEST_F(AXPlatformNodeWinTest, TestISelectionItemProviderGrid) { Init(root, row1, cell1); const auto* row = GetRootNode()->children()[0]; - ComPtr<ISelectionItemProvider> item_provider = - QueryInterfaceFromNode<ISelectionItemProvider>(row->children()[0]); + ComPtr<IRawElementProviderSimple> raw_element_provider_simple = + QueryInterfaceFromNode<IRawElementProviderSimple>(row->children()[0]); + + ComPtr<ISelectionItemProvider> selection_item_provider; + EXPECT_HRESULT_SUCCEEDED(raw_element_provider_simple->GetPatternProvider( + UIA_SelectionItemPatternId, &selection_item_provider)); + ASSERT_NE(nullptr, selection_item_provider.Get()); BOOL selected; @@ -5552,37 +5772,37 @@ TEST_F(AXPlatformNodeWinTest, TestISelectionItemProviderGrid) { // flip kSelected for kCell when the kDoDefault action is fired. // Initial State - EXPECT_HRESULT_SUCCEEDED(item_provider->get_IsSelected(&selected)); + EXPECT_HRESULT_SUCCEEDED(selection_item_provider->get_IsSelected(&selected)); EXPECT_FALSE(selected); // AddToSelection should fire event when not selected - EXPECT_HRESULT_SUCCEEDED(item_provider->AddToSelection()); - EXPECT_HRESULT_SUCCEEDED(item_provider->get_IsSelected(&selected)); + EXPECT_HRESULT_SUCCEEDED(selection_item_provider->AddToSelection()); + EXPECT_HRESULT_SUCCEEDED(selection_item_provider->get_IsSelected(&selected)); EXPECT_TRUE(selected); // AddToSelection should not fire event when selected - EXPECT_HRESULT_SUCCEEDED(item_provider->AddToSelection()); - EXPECT_HRESULT_SUCCEEDED(item_provider->get_IsSelected(&selected)); + EXPECT_HRESULT_SUCCEEDED(selection_item_provider->AddToSelection()); + EXPECT_HRESULT_SUCCEEDED(selection_item_provider->get_IsSelected(&selected)); EXPECT_TRUE(selected); // RemoveFromSelection should fire event when selected - EXPECT_HRESULT_SUCCEEDED(item_provider->RemoveFromSelection()); - EXPECT_HRESULT_SUCCEEDED(item_provider->get_IsSelected(&selected)); + EXPECT_HRESULT_SUCCEEDED(selection_item_provider->RemoveFromSelection()); + EXPECT_HRESULT_SUCCEEDED(selection_item_provider->get_IsSelected(&selected)); EXPECT_FALSE(selected); // RemoveFromSelection should not fire event when not selected - EXPECT_HRESULT_SUCCEEDED(item_provider->RemoveFromSelection()); - EXPECT_HRESULT_SUCCEEDED(item_provider->get_IsSelected(&selected)); + EXPECT_HRESULT_SUCCEEDED(selection_item_provider->RemoveFromSelection()); + EXPECT_HRESULT_SUCCEEDED(selection_item_provider->get_IsSelected(&selected)); EXPECT_FALSE(selected); // Select should fire event when not selected - EXPECT_HRESULT_SUCCEEDED(item_provider->Select()); - EXPECT_HRESULT_SUCCEEDED(item_provider->get_IsSelected(&selected)); + EXPECT_HRESULT_SUCCEEDED(selection_item_provider->Select()); + EXPECT_HRESULT_SUCCEEDED(selection_item_provider->get_IsSelected(&selected)); EXPECT_TRUE(selected); // Select should not fire event when selected - EXPECT_HRESULT_SUCCEEDED(item_provider->Select()); - EXPECT_HRESULT_SUCCEEDED(item_provider->get_IsSelected(&selected)); + EXPECT_HRESULT_SUCCEEDED(selection_item_provider->Select()); + EXPECT_HRESULT_SUCCEEDED(selection_item_provider->get_IsSelected(&selected)); EXPECT_TRUE(selected); } @@ -5836,4 +6056,11 @@ TEST_F(AXPlatformNodeWinTest, IScrollProviderSetScrollPercent) { EXPECT_EQ(y_scroll_percent, 34); } +TEST_F(AXPlatformNodeWinTest, TestSanitizeStringAttributeForIA2) { + std::string input("\\:=,;"); + std::string output; + AXPlatformNodeWin::SanitizeStringAttributeForIA2(input, &output); + EXPECT_EQ("\\\\\\:\\=\\,\\;", output); +} + } // namespace ui diff --git a/chromium/ui/accessibility/platform/test_ax_node_wrapper.cc b/chromium/ui/accessibility/platform/test_ax_node_wrapper.cc index 7b4fc903580..c5c58bd5ca6 100644 --- a/chromium/ui/accessibility/platform/test_ax_node_wrapper.cc +++ b/chromium/ui/accessibility/platform/test_ax_node_wrapper.cc @@ -7,6 +7,7 @@ #include <unordered_map> #include <utility> +#include "base/numerics/ranges.h" #include "base/stl_util.h" #include "base/strings/utf_string_conversions.h" #include "ui/accessibility/ax_action_data.h" @@ -25,6 +26,9 @@ std::unordered_map<AXNode*, TestAXNodeWrapper*> g_node_to_wrapper_map; // A global coordinate offset. gfx::Vector2d g_offset; +// A global scale factor. +float g_scale_factor = 1.0; + // A global map that stores which node is focused on a determined tree. // - If a tree has no node being focused, there shouldn't be any entry on the // map associated with such tree, i.e. a pair {tree, nullptr} is invalid. @@ -87,6 +91,12 @@ const AXNode* TestAXNodeWrapper::GetNodeFromLastDefaultAction() { return g_node_from_last_default_action; } +// static +std::unique_ptr<base::AutoReset<float>> TestAXNodeWrapper::SetScaleFactor( + float value) { + return std::make_unique<base::AutoReset<float>>(&g_scale_factor, value); +} + TestAXNodeWrapper::~TestAXNodeWrapper() { platform_node_->Destroy(); } @@ -110,8 +120,13 @@ AXNodePosition::AXPositionInstance TestAXNodeWrapper::CreateTextPositionAt( ax::mojom::TextAffinity::kDownstream); } +gfx::NativeViewAccessible TestAXNodeWrapper::GetNativeViewAccessible() { + return ax_platform_node()->GetNativeViewAccessible(); +} + gfx::NativeViewAccessible TestAXNodeWrapper::GetParent() { - TestAXNodeWrapper* parent_wrapper = GetOrCreate(tree_, node_->parent()); + TestAXNodeWrapper* parent_wrapper = + GetOrCreate(tree_, node_->GetUnignoredParent()); return parent_wrapper ? parent_wrapper->ax_platform_node()->GetNativeViewAccessible() : nullptr; @@ -137,6 +152,14 @@ gfx::Rect TestAXNodeWrapper::GetBoundsRect( // We could optionally add clipping here if ever needed. gfx::RectF bounds = GetLocation(); bounds.Offset(g_offset); + + // For test behavior only, for bounds that are offscreen we currently do + // not apply clipping to the bounds but we still return the offscreen + // status. + if (offscreen_result) { + *offscreen_result = DetermineOffscreenResult(bounds); + } + return gfx::ToEnclosingRect(bounds); } case AXCoordinateSystem::kRootFrame: @@ -171,6 +194,14 @@ gfx::Rect TestAXNodeWrapper::GetInnerTextRangeBoundsRect( } bounds.Offset(g_offset); + + // For test behavior only, for bounds that are offscreen we currently do + // not apply clipping to the bounds but we still return the offscreen + // status. + if (offscreen_result) { + *offscreen_result = DetermineOffscreenResult(bounds); + } + return gfx::ToEnclosingRect(bounds); } case AXCoordinateSystem::kRootFrame: @@ -222,7 +253,8 @@ TestAXNodeWrapper* TestAXNodeWrapper::HitTestSyncInternal(int x, int y) { } gfx::NativeViewAccessible TestAXNodeWrapper::HitTestSync(int x, int y) { - TestAXNodeWrapper* wrapper = HitTestSyncInternal(x, y); + TestAXNodeWrapper* wrapper = + HitTestSyncInternal(x / g_scale_factor, y / g_scale_factor); return wrapper ? wrapper->ax_platform_node()->GetNativeViewAccessible() : nullptr; } @@ -269,6 +301,15 @@ AXPlatformNode* TestAXNodeWrapper::GetFromNodeID(int32_t id) { return nullptr; } +AXPlatformNode* TestAXNodeWrapper::GetFromTreeIDAndNodeID( + const ui::AXTreeID& ax_tree_id, + int32_t id) { + // TestAXNodeWrapper only supports one accessibility tree. + // Additional work would need to be done to support multiple trees. + CHECK_EQ(GetTreeData().tree_id, ax_tree_id); + return GetFromNodeID(id); +} + int TestAXNodeWrapper::GetIndexInParent() { return node_ ? int{node_->index_in_parent()} : -1; } @@ -486,9 +527,9 @@ bool TestAXNodeWrapper::AccessibilityPerformAction( int scroll_y_max = GetData().GetIntAttribute(ax::mojom::IntAttribute::kScrollYMax); int scroll_x = - std::max(scroll_x_min, std::min(data.target_point.x(), scroll_x_max)); + base::ClampToRange(data.target_point.x(), scroll_x_min, scroll_x_max); int scroll_y = - std::max(scroll_y_min, std::min(data.target_point.y(), scroll_y_max)); + base::ClampToRange(data.target_point.y(), scroll_y_min, scroll_y_max); ReplaceIntAttribute(node_->id(), ax::mojom::IntAttribute::kScrollX, scroll_x); @@ -556,6 +597,7 @@ base::string16 TestAXNodeWrapper::GetLocalizedStringForLandmarkType() const { const AXNodeData& data = GetData(); switch (data.role) { case ax::mojom::Role::kBanner: + case ax::mojom::Role::kHeader: return base::ASCIIToUTF16("banner"); case ax::mojom::Role::kComplementary: @@ -566,6 +608,7 @@ base::string16 TestAXNodeWrapper::GetLocalizedStringForLandmarkType() const { return base::ASCIIToUTF16("content information"); case ax::mojom::Role::kRegion: + case ax::mojom::Role::kSection: if (data.HasStringAttribute(ax::mojom::StringAttribute::kName)) return base::ASCIIToUTF16("region"); FALLTHROUGH; @@ -577,6 +620,7 @@ base::string16 TestAXNodeWrapper::GetLocalizedStringForLandmarkType() const { base::string16 TestAXNodeWrapper::GetLocalizedStringForRoleDescription() const { const AXNodeData& data = GetData(); + switch (data.role) { case ax::mojom::Role::kArticle: return base::ASCIIToUTF16("article"); @@ -612,12 +656,30 @@ base::string16 TestAXNodeWrapper::GetLocalizedStringForRoleDescription() const { case ax::mojom::Role::kFigure: return base::ASCIIToUTF16("figure"); + case ax::mojom::Role::kFooter: + case ax::mojom::Role::kFooterAsNonLandmark: + return base::ASCIIToUTF16("footer"); + + case ax::mojom::Role::kHeader: + case ax::mojom::Role::kHeaderAsNonLandmark: + return base::ASCIIToUTF16("header"); + + case ax::mojom::Role::kMark: + return base::ASCIIToUTF16("highlight"); + case ax::mojom::Role::kMeter: return base::ASCIIToUTF16("meter"); case ax::mojom::Role::kSearchBox: return base::ASCIIToUTF16("search box"); + case ax::mojom::Role::kSection: { + if (data.HasStringAttribute(ax::mojom::StringAttribute::kName)) + return base::ASCIIToUTF16("section"); + + return {}; + } + case ax::mojom::Role::kStatus: return base::ASCIIToUTF16("output"); @@ -747,13 +809,13 @@ TestAXNodeWrapper* TestAXNodeWrapper::InternalGetChild(int index) const { void TestAXNodeWrapper::Descendants( const AXNode* node, std::vector<gfx::NativeViewAccessible>* descendants) const { - std::vector<AXNode*> child_nodes = node->children(); - for (AXNode* child : child_nodes) { + for (auto it = node->UnignoredChildrenBegin(); + it != node->UnignoredChildrenEnd(); ++it) { descendants->emplace_back(ax_platform_node() ->GetDelegate() - ->GetFromNodeID(child->id()) + ->GetFromNodeID(it->id()) ->GetNativeViewAccessible()); - Descendants(child, descendants); + Descendants(it.get(), descendants); } } @@ -792,4 +854,28 @@ gfx::RectF TestAXNodeWrapper::GetInlineTextRect(const int start_offset, return bounds; } +AXOffscreenResult TestAXNodeWrapper::DetermineOffscreenResult( + gfx::RectF bounds) const { + if (!tree_ || !tree_->root()) + return AXOffscreenResult::kOnscreen; + + const AXNodeData& root_web_area_node_data = tree_->root()->data(); + gfx::RectF root_web_area_bounds = + root_web_area_node_data.relative_bounds.bounds; + + // For testing, we only look at the current node's bound relative to the root + // web area bounds to determine offscreen status. We currently do not look at + // the bounds of the immediate parent of the node for determining offscreen + // status. + // We only determine offscreen result if the root web area bounds is actually + // set in the test. We default the offscreen result of every other situation + // to AXOffscreenResult::kOnscreen. + if (!root_web_area_bounds.IsEmpty()) { + bounds.Intersect(root_web_area_bounds); + if (bounds.IsEmpty()) + return AXOffscreenResult::kOffscreen; + } + return AXOffscreenResult::kOnscreen; +} + } // namespace ui diff --git a/chromium/ui/accessibility/platform/test_ax_node_wrapper.h b/chromium/ui/accessibility/platform/test_ax_node_wrapper.h index eff377809eb..acb54628444 100644 --- a/chromium/ui/accessibility/platform/test_ax_node_wrapper.h +++ b/chromium/ui/accessibility/platform/test_ax_node_wrapper.h @@ -9,6 +9,7 @@ #include <string> #include <vector> +#include "base/auto_reset.h" #include "build/build_config.h" #include "ui/accessibility/ax_node.h" #include "ui/accessibility/ax_tree.h" @@ -40,6 +41,9 @@ class TestAXNodeWrapper : public AXPlatformNodeDelegateBase { // called from for testing. static const AXNode* GetNodeFromLastDefaultAction(); + // Set a global scale factor for testing. + static std::unique_ptr<base::AutoReset<float>> SetScaleFactor(float value); + ~TestAXNodeWrapper() override; AXPlatformNode* ax_platform_node() const { return platform_node_; } @@ -55,6 +59,7 @@ class TestAXNodeWrapper : public AXPlatformNodeDelegateBase { const AXTree::Selection GetUnignoredSelection() const override; AXNodePosition::AXPositionInstance CreateTextPositionAt( int offset) const override; + gfx::NativeViewAccessible GetNativeViewAccessible() override; gfx::NativeViewAccessible GetParent() override; int GetChildCount() override; gfx::NativeViewAccessible ChildAtIndex(int index) override; @@ -77,6 +82,8 @@ class TestAXNodeWrapper : public AXPlatformNodeDelegateBase { gfx::NativeViewAccessible GetFocus() override; bool IsMinimized() const override; AXPlatformNode* GetFromNodeID(int32_t id) override; + AXPlatformNode* GetFromTreeIDAndNodeID(const ui::AXTreeID& ax_tree_id, + int32_t id) override; int GetIndexInParent() override; bool IsTable() const override; base::Optional<int> GetTableRowCount() const override; @@ -149,6 +156,9 @@ class TestAXNodeWrapper : public AXPlatformNodeDelegateBase { gfx::RectF GetInlineTextRect(const int start_offset, const int end_offset) const; + // Determine the offscreen status of a particular element given its bounds.. + AXOffscreenResult DetermineOffscreenResult(gfx::RectF bounds) const; + AXTree* tree_; AXNode* node_; ui::AXUniqueId unique_id_; |