| // Copyright 2014 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ui/accessibility/platform/ax_platform_node_base.h" |
| |
| #include <algorithm> |
| #include <iomanip> |
| #include <limits> |
| #include <set> |
| #include <sstream> |
| #include <string> |
| #include <utility> |
| |
| #include "base/check.h" |
| #include "base/check_deref.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/no_destructor.h" |
| #include "base/numerics/checked_math.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/to_string.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "base/trace_event/memory_allocator_dump.h" |
| #include "base/trace_event/memory_dump_manager.h" |
| #include "base/trace_event/memory_dump_provider.h" |
| #include "base/trace_event/process_memory_dump.h" |
| #include "build/build_config.h" |
| #include "third_party/abseil-cpp/absl/container/flat_hash_map.h" |
| #include "third_party/skia/include/core/SkColor.h" |
| #include "ui/accessibility/ax_action_data.h" |
| #include "ui/accessibility/ax_enums.mojom-shared-internal.h" |
| #include "ui/accessibility/ax_enums.mojom-shared.h" |
| #include "ui/accessibility/ax_enums.mojom.h" |
| #include "ui/accessibility/ax_node_data.h" |
| #include "ui/accessibility/ax_role_properties.h" |
| #include "ui/accessibility/ax_selection.h" |
| #include "ui/accessibility/ax_tree_data.h" |
| #include "ui/accessibility/platform/ax_platform_node_delegate.h" |
| #include "ui/accessibility/platform/compute_attributes.h" |
| #include "ui/gfx/geometry/rect_conversions.h" |
| |
| namespace ui { |
| |
| namespace { |
| |
| using OnNotifyEventCallbackMap = |
| std::map<ax::mojom::Event, |
| // A function to call when focus changes, for testing only. |
| base::RepeatingClosure>; |
| |
| OnNotifyEventCallbackMap& GetOnNotifyEventCallbackMap() { |
| static base::NoDestructor<OnNotifyEventCallbackMap> |
| on_notify_event_for_testing; |
| return *on_notify_event_for_testing; |
| } |
| |
| // Check for descendant comment, using limited depth first search. |
| bool FindDescendantRoleWithMaxDepth(const AXPlatformNodeBase* node, |
| ax::mojom::Role descendant_role, |
| size_t max_depth, |
| size_t max_children_to_check) { |
| if (node->GetRole() == descendant_role) |
| return true; |
| if (max_depth <= 1) |
| return false; |
| |
| size_t num_children_to_check = |
| std::min(node->GetChildCount(), max_children_to_check); |
| for (size_t index = 0; index < num_children_to_check; index++) { |
| auto* child = static_cast<AXPlatformNodeBase*>( |
| AXPlatformNode::FromNativeViewAccessible(node->ChildAtIndex(index))); |
| if (child && |
| FindDescendantRoleWithMaxDepth(child, descendant_role, max_depth - 1, |
| max_children_to_check)) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| // Map from each AXPlatformNode's unique id to its instance. |
| using UniqueIdMap = |
| absl::flat_hash_map<int32_t, raw_ptr<AXPlatformNode, CtnExperimental>>; |
| UniqueIdMap& GetUniqueIdMap() { |
| static base::NoDestructor<UniqueIdMap> map; |
| return *map; |
| } |
| |
| // Adds process-wide statistics about accessibility objects to traces. |
| class AXPlatformNodeMemoryDumpProvider |
| : public base::trace_event::MemoryDumpProvider { |
| public: |
| AXPlatformNodeMemoryDumpProvider(const AXPlatformNodeMemoryDumpProvider&) = |
| delete; |
| AXPlatformNodeMemoryDumpProvider& operator=( |
| const AXPlatformNodeMemoryDumpProvider&) = delete; |
| |
| // base::trace_event::MemoryDumpProvider: |
| bool OnMemoryDump(const base::trace_event::MemoryDumpArgs& args, |
| base::trace_event::ProcessMemoryDump* pmd) override; |
| |
| private: |
| friend class base::NoDestructor<AXPlatformNodeMemoryDumpProvider>; |
| |
| explicit AXPlatformNodeMemoryDumpProvider(const UniqueIdMap& id_to_node); |
| ~AXPlatformNodeMemoryDumpProvider() override = default; |
| |
| const raw_ref<const UniqueIdMap> id_to_node_; |
| }; |
| |
| bool AXPlatformNodeMemoryDumpProvider::OnMemoryDump( |
| const base::trace_event::MemoryDumpArgs& args, |
| base::trace_event::ProcessMemoryDump* pmd) { |
| auto* const dump = pmd->CreateAllocatorDump("accessibility/ax_platform_node"); |
| dump->AddScalar(base::trace_event::MemoryAllocatorDump::kNameObjectCount, |
| base::trace_event::MemoryAllocatorDump::kUnitsObjects, |
| id_to_node_->size()); |
| return true; |
| } |
| |
| AXPlatformNodeMemoryDumpProvider::AXPlatformNodeMemoryDumpProvider( |
| const UniqueIdMap& id_to_node) |
| : id_to_node_(id_to_node) { |
| // Skip this in tests that don't set up a task runner on the main thread. |
| if (base::SingleThreadTaskRunner::HasCurrentDefault()) { |
| base::trace_event::MemoryDumpManager::GetInstance()->RegisterDumpProvider( |
| this, "AXPlatformNode", |
| base::SingleThreadTaskRunner::GetCurrentDefault()); |
| } |
| } |
| |
| } // namespace |
| |
| const char16_t AXPlatformNodeBase::kEmbeddedCharacter = u'\xfffc'; |
| const std::string AXPlatformNodeBase::kAriaActionsPrefix = "custom"; |
| |
| // TODO(fxbug.dev/91030): Remove the !BUILDFLAG(IS_FUCHSIA) condition once |
| // fuchsia has native accessibility. |
| #if !BUILDFLAG(HAS_NATIVE_ACCESSIBILITY) && !BUILDFLAG(IS_FUCHSIA) |
| // static |
| AXPlatformNode::Pointer AXPlatformNode::Create( |
| AXPlatformNodeDelegate& delegate) { |
| AXPlatformNodeBase* node = new AXPlatformNodeBase(); |
| node->Init(delegate); |
| return Pointer(node); |
| } |
| #endif |
| |
| // static |
| AXPlatformNode* AXPlatformNodeBase::GetFromUniqueId(int32_t unique_id) { |
| UniqueIdMap& unique_ids = GetUniqueIdMap(); |
| auto iter = unique_ids.find(unique_id); |
| if (iter != unique_ids.end()) { |
| return iter->second; |
| } |
| |
| return nullptr; |
| } |
| |
| // static |
| size_t AXPlatformNodeBase::GetInstanceCount() { |
| return GetUniqueIdMap().size(); |
| } |
| |
| // static |
| size_t AXPlatformNodeBase::ResetInstanceCountForTesting() { |
| auto& id_map = GetUniqueIdMap(); |
| const auto result = id_map.size(); |
| id_map.clear(); |
| return result; |
| } |
| |
| // static |
| void AXPlatformNodeBase::SetOnNotifyEventCallbackForTesting( |
| ax::mojom::Event event_type, |
| base::RepeatingClosure callback) { |
| OnNotifyEventCallbackMap& callback_map = GetOnNotifyEventCallbackMap(); |
| callback_map[event_type] = std::move(callback); |
| } |
| |
| AXPlatformNodeBase::AXPlatformNodeBase() = default; |
| |
| AXPlatformNodeBase::~AXPlatformNodeBase() = default; |
| |
| void AXPlatformNodeBase::Init(AXPlatformNodeDelegate& delegate) { |
| CHECK(!delegate_); |
| delegate_ = &delegate; |
| |
| // This must be called after assigning our delegate. |
| GetUniqueIdMap()[GetUniqueId()] = this; |
| |
| static base::NoDestructor<AXPlatformNodeMemoryDumpProvider> dump_provider( |
| GetUniqueIdMap()); |
| } |
| |
| const AXNodeData& AXPlatformNodeBase::GetData() const { |
| return GetDelegate()->GetData(); |
| } |
| |
| gfx::NativeViewAccessible AXPlatformNodeBase::GetFocus() const { |
| return GetDelegate()->GetFocus(); |
| } |
| |
| gfx::NativeViewAccessible AXPlatformNodeBase::GetParent() const { |
| return GetDelegate()->GetParent(); |
| } |
| |
| AXPlatformNodeBase* AXPlatformNodeBase::GetPlatformParent() const { |
| return FromNativeViewAccessible(GetDelegate()->GetParent()); |
| } |
| |
| AXPlatformNodeBase* AXPlatformNodeBase::GetPlatformTextFieldAncestor() const { |
| return FromNativeViewAccessible(GetDelegate()->GetTextFieldAncestor()); |
| } |
| |
| size_t AXPlatformNodeBase::GetChildCount() const { |
| return GetDelegate()->GetChildCount(); |
| } |
| |
| gfx::NativeViewAccessible AXPlatformNodeBase::ChildAtIndex(size_t index) const { |
| return GetDelegate()->ChildAtIndex(index); |
| } |
| |
| std::string AXPlatformNodeBase::GetName() const { |
| AXPlatformNodeDelegate* const delegate = GetDelegate(); |
| std::string name = delegate->GetName(); |
| |
| // Compute extra name based on the image annotation (generated alt text) |
| // results. |
| std::string extra_text; |
| ax::mojom::ImageAnnotationStatus status = |
| GetData().GetImageAnnotationStatus(); |
| switch (status) { |
| case ax::mojom::ImageAnnotationStatus::kEligibleForAnnotation: |
| case ax::mojom::ImageAnnotationStatus::kAnnotationPending: |
| case ax::mojom::ImageAnnotationStatus::kAnnotationEmpty: |
| case ax::mojom::ImageAnnotationStatus::kAnnotationAdult: |
| case ax::mojom::ImageAnnotationStatus::kAnnotationProcessFailed: |
| extra_text = base::UTF16ToUTF8( |
| delegate->GetLocalizedStringForImageAnnotationStatus(status)); |
| break; |
| |
| case ax::mojom::ImageAnnotationStatus::kAnnotationSucceeded: |
| extra_text = |
| GetStringAttribute(ax::mojom::StringAttribute::kImageAnnotation); |
| break; |
| |
| case ax::mojom::ImageAnnotationStatus::kNone: |
| case ax::mojom::ImageAnnotationStatus::kWillNotAnnotateDueToScheme: |
| case ax::mojom::ImageAnnotationStatus::kIneligibleForAnnotation: |
| case ax::mojom::ImageAnnotationStatus::kSilentlyEligibleForAnnotation: |
| break; |
| } |
| |
| if (!extra_text.empty()) { |
| if (!name.empty()) { |
| name += ". "; |
| } |
| name += extra_text; |
| } |
| |
| DCHECK(base::IsStringUTF8AllowingNoncharacters(name)) << "Invalid UTF8"; |
| return name; |
| } |
| |
| std::optional<size_t> AXPlatformNodeBase::GetIndexInParent() { |
| AXPlatformNodeBase* parent = FromNativeViewAccessible(GetParent()); |
| if (!parent) { |
| return std::nullopt; |
| } |
| |
| // If this is the webview, it is not in the child in the list of its parent's |
| // child. |
| // TODO(jkim): Check if we could remove this after making WebView ignored. |
| AXPlatformNodeDelegate* const delegate = GetDelegate(); |
| if (delegate->GetNativeViewAccessible() != GetNativeViewAccessible()) { |
| return std::nullopt; |
| } |
| |
| size_t child_count = parent->GetChildCount(); |
| if (child_count == 0) { |
| // |child_count| could be 0 if the parent is IsLeaf. |
| DCHECK(parent->IsLeaf()); |
| return std::nullopt; |
| } |
| |
| // Ask the delegate for the index in parent, and return it if it's plausible. |
| // |
| // Delegates are allowed to not implement this (ViewsAXPlatformNodeDelegate |
| // 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. |
| auto index = delegate->GetIndexInParent(); |
| if (index.has_value() && index.value() < child_count) |
| return index; |
| |
| // Otherwise, search the parent's children. |
| gfx::NativeViewAccessible current = GetNativeViewAccessible(); |
| for (size_t i = 0; i < child_count; i++) { |
| if (parent->ChildAtIndex(i) == current) |
| return i; |
| } |
| |
| // If the parent has a modal dialog, it doesn't count other children. |
| if (parent->GetDelegate()->HasModalDialog()) { |
| return std::nullopt; |
| } |
| |
| DCHECK(false) |
| << "Unable to find the child in the list of its parent's children."; |
| return std::nullopt; |
| } |
| |
| base::stack<gfx::NativeViewAccessible> AXPlatformNodeBase::GetAncestors() { |
| base::stack<gfx::NativeViewAccessible> ancestors; |
| gfx::NativeViewAccessible current_node = GetNativeViewAccessible(); |
| while (current_node) { |
| AXPlatformNodeBase* current_platform_node = |
| FromNativeViewAccessible(current_node); |
| if (!current_platform_node) { |
| break; |
| } |
| ancestors.push(current_node); |
| current_node = current_platform_node->GetParent(); |
| } |
| |
| return ancestors; |
| } |
| |
| std::optional<int> AXPlatformNodeBase::CompareTo(AXPlatformNodeBase& other) { |
| // We define two node's relative positions in the following way: |
| // 1. this->CompareTo(other) == 0: |
| // - |this| and |other| are the same node. |
| // 2. this->CompareTo(other) < 0: |
| // - |this| is an ancestor of |other|. |
| // - |this|'s first uncommon ancestor comes before |other|'s first uncommon |
| // ancestor. The first uncommon ancestor is defined as the immediate child |
| // of the lowest common ancestor of the two nodes. The first uncommon |
| // ancestor of |this| and |other| share the same parent (i.e. lowest common |
| // ancestor), so we can just compare the first uncommon ancestors' child |
| // indices to determine their relative positions. |
| // 3. this->CompareTo(other) == nullopt: |
| // - |this| and |other| are not comparable. E.g. they do not have a common |
| // ancestor. |
| // |
| // Another way to look at the nodes' relative positions/logical orders is that |
| // they are equivalent to pre-order traversal of the tree. If we pre-order |
| // traverse from the root, the node that we visited earlier is always going to |
| // be before (logically less) the node we visit later. |
| |
| if (this == &other) { |
| return std::optional<int>(0); |
| } |
| |
| // Compute the ancestor stacks of both positions and traverse them from the |
| // top most ancestor down, so we can discover the first uncommon ancestors. |
| // The first uncommon ancestor is the immediate child of the lowest common |
| // ancestor. |
| gfx::NativeViewAccessible common_ancestor = gfx::NativeViewAccessible(); |
| base::stack<gfx::NativeViewAccessible> our_ancestors = GetAncestors(); |
| base::stack<gfx::NativeViewAccessible> other_ancestors = other.GetAncestors(); |
| |
| // Start at the root and traverse down. Keep going until the |this|'s ancestor |
| // chain and |other|'s ancestor chain disagree. The last node before they |
| // disagree is the lowest common ancestor. |
| while (!our_ancestors.empty() && !other_ancestors.empty() && |
| our_ancestors.top() == other_ancestors.top()) { |
| common_ancestor = our_ancestors.top(); |
| our_ancestors.pop(); |
| other_ancestors.pop(); |
| } |
| |
| // Nodes do not have a common ancestor, they are not comparable. |
| if (!common_ancestor) { |
| return std::nullopt; |
| } |
| |
| // Compute the logical order when the common ancestor is |this| or |other|. |
| auto* common_ancestor_platform_node = |
| FromNativeViewAccessible(common_ancestor); |
| if (common_ancestor_platform_node == this) { |
| return std::optional<int>(-1); |
| } |
| if (common_ancestor_platform_node == &other) { |
| return std::optional<int>(1); |
| } |
| |
| // Compute the logical order of |this| and |other| by using their first |
| // uncommon ancestors. |
| if (!our_ancestors.empty() && !other_ancestors.empty()) { |
| std::optional<int> this_index_in_parent = |
| FromNativeViewAccessible(our_ancestors.top())->GetIndexInParent(); |
| std::optional<int> other_index_in_parent = |
| FromNativeViewAccessible(other_ancestors.top())->GetIndexInParent(); |
| |
| if (!this_index_in_parent || !other_index_in_parent) { |
| return std::nullopt; |
| } |
| |
| int this_uncommon_ancestor_index = this_index_in_parent.value(); |
| int other_uncommon_ancestor_index = other_index_in_parent.value(); |
| DCHECK_NE(this_uncommon_ancestor_index, other_uncommon_ancestor_index) |
| << "Deepest uncommon ancestors should truly be uncommon, i.e. not " |
| "the same."; |
| |
| return std::optional<int>(this_uncommon_ancestor_index - |
| other_uncommon_ancestor_index); |
| } |
| |
| return std::nullopt; |
| } |
| |
| AXNodeID AXPlatformNodeBase::GetNodeId() const { |
| return GetDelegate()->GetData().id; |
| } |
| |
| AXPlatformNodeBase* AXPlatformNodeBase::GetActiveDescendant() const { |
| AXNodeID active_descendant_id; |
| AXPlatformNodeBase* active_descendant = nullptr; |
| if (GetIntAttribute(ax::mojom::IntAttribute::kActivedescendantId, |
| &active_descendant_id)) { |
| active_descendant = static_cast<AXPlatformNodeBase*>( |
| GetDelegate()->GetFromNodeID(active_descendant_id)); |
| } |
| |
| if (GetRole() == ax::mojom::Role::kComboBoxSelect) { |
| AXPlatformNodeBase* child = GetFirstChild(); |
| if (child && child->GetRole() == ax::mojom::Role::kMenuListPopup && |
| !child->IsInvisibleOrIgnored()) { |
| // The active descendant is found on the menu list popup, i.e. on the |
| // actual list and not on the button that opens it. |
| // If there is no active descendant, focus should stay on the button so |
| // that Windows screen readers would enable their virtual cursor. |
| // Do not expose an activedescendant in a hidden/collapsed list, as |
| // screen readers expect the focus event to go to the button itself. |
| // Note that the AX hierarchy in this case is strange -- the active |
| // option is the only visible option, and is inside an invisible list. |
| if (child->GetIntAttribute(ax::mojom::IntAttribute::kActivedescendantId, |
| &active_descendant_id)) { |
| active_descendant = static_cast<AXPlatformNodeBase*>( |
| child->GetDelegate()->GetFromNodeID(active_descendant_id)); |
| } |
| } |
| } |
| |
| if (active_descendant && !active_descendant->IsInvisibleOrIgnored()) |
| return active_descendant; |
| |
| return nullptr; |
| } |
| |
| // AXPlatformNode overrides. |
| |
| void AXPlatformNodeBase::Destroy() { |
| GetUniqueIdMap().erase(GetUniqueId()); |
| delegate_ = nullptr; |
| Dispose(); |
| } |
| |
| bool AXPlatformNodeBase::IsDestroyed() const { |
| return !delegate_; |
| } |
| |
| void AXPlatformNodeBase::Dispose() { |
| delete this; |
| } |
| |
| gfx::NativeViewAccessible AXPlatformNodeBase::GetNativeViewAccessible() { |
| return gfx::NativeViewAccessible(); |
| } |
| |
| void AXPlatformNodeBase::NotifyAccessibilityEvent(ax::mojom::Event event_type) { |
| if (event_type == ax::mojom::Event::kAlert) { |
| CHECK(IsAlert(GetRole())) |
| << "On some platforms, the alert event does not work correctly unless " |
| "it is fired on an object with an alert role. Role was " |
| << GetRole(); |
| } |
| |
| OnNotifyEventCallbackMap& callback_map = GetOnNotifyEventCallbackMap(); |
| if (callback_map.find(event_type) != callback_map.end() && |
| callback_map[event_type]) { |
| callback_map[event_type].Run(); |
| } |
| } |
| |
| #if BUILDFLAG(IS_APPLE) |
| void AXPlatformNodeBase::AnnounceTextAs(const std::u16string& text, |
| AnnouncementType announcement_type) {} |
| #endif |
| |
| std::string AXPlatformNodeBase::GetRootURL() const { |
| return GetDelegate()->GetRootURL(); |
| } |
| |
| bool AXPlatformNodeBase::IsWebContent() const { |
| return GetDelegate()->IsWebContent(); |
| } |
| |
| AXPlatformNodeDelegate* AXPlatformNodeBase::GetDelegate() const { |
| return &CHECK_DEREF(delegate_.get()); |
| } |
| |
| bool AXPlatformNodeBase::IsDescendantOf(AXPlatformNode* ancestor) const { |
| if (!ancestor) |
| return false; |
| |
| if (this == ancestor) |
| return true; |
| |
| AXPlatformNodeBase* parent = FromNativeViewAccessible(GetParent()); |
| if (!parent) |
| return false; |
| |
| return parent->IsDescendantOf(ancestor); |
| } |
| |
| AXPlatformNodeBase::AXPlatformNodeChildIterator |
| AXPlatformNodeBase::AXPlatformNodeChildrenBegin() const { |
| return AXPlatformNodeChildIterator(this, GetFirstChild()); |
| } |
| |
| AXPlatformNodeBase::AXPlatformNodeChildIterator |
| AXPlatformNodeBase::AXPlatformNodeChildrenEnd() const { |
| return AXPlatformNodeChildIterator(this, nullptr); |
| } |
| // Helpers. |
| |
| AXPlatformNodeBase* AXPlatformNodeBase::GetPreviousSibling() const { |
| return FromNativeViewAccessible(GetDelegate()->GetPreviousSibling()); |
| } |
| |
| AXPlatformNodeBase* AXPlatformNodeBase::GetNextSibling() const { |
| return FromNativeViewAccessible(GetDelegate()->GetNextSibling()); |
| } |
| |
| AXPlatformNodeBase* AXPlatformNodeBase::GetFirstChild() const { |
| return FromNativeViewAccessible(GetDelegate()->GetFirstChild()); |
| } |
| |
| AXPlatformNodeBase* AXPlatformNodeBase::GetLastChild() const { |
| return FromNativeViewAccessible(GetDelegate()->GetLastChild()); |
| } |
| |
| ax::mojom::Role AXPlatformNodeBase::GetRole() const { |
| return GetDelegate()->GetRole(); |
| } |
| |
| bool AXPlatformNodeBase::HasBoolAttribute( |
| ax::mojom::BoolAttribute attribute) const { |
| return GetDelegate()->HasBoolAttribute(attribute); |
| } |
| |
| bool AXPlatformNodeBase::GetBoolAttribute( |
| ax::mojom::BoolAttribute attribute) const { |
| return GetDelegate()->GetBoolAttribute(attribute); |
| } |
| |
| bool AXPlatformNodeBase::GetBoolAttribute(ax::mojom::BoolAttribute attribute, |
| bool* value) const { |
| return GetDelegate()->GetBoolAttribute(attribute, value); |
| } |
| |
| bool AXPlatformNodeBase::HasFloatAttribute( |
| ax::mojom::FloatAttribute attribute) const { |
| return GetDelegate()->HasFloatAttribute(attribute); |
| } |
| |
| float AXPlatformNodeBase::GetFloatAttribute( |
| ax::mojom::FloatAttribute attribute) const { |
| return GetDelegate()->GetFloatAttribute(attribute); |
| } |
| |
| bool AXPlatformNodeBase::GetFloatAttribute(ax::mojom::FloatAttribute attribute, |
| float* value) const { |
| return GetDelegate()->GetFloatAttribute(attribute, value); |
| } |
| |
| const AXIntAttributes& AXPlatformNodeBase::GetIntAttributes() const { |
| return GetDelegate()->GetIntAttributes(); |
| } |
| |
| bool AXPlatformNodeBase::HasIntAttribute( |
| ax::mojom::IntAttribute attribute) const { |
| return GetDelegate()->HasIntAttribute(attribute); |
| } |
| |
| int AXPlatformNodeBase::GetIntAttribute( |
| ax::mojom::IntAttribute attribute) const { |
| return GetDelegate()->GetIntAttribute(attribute); |
| } |
| |
| bool AXPlatformNodeBase::GetIntAttribute(ax::mojom::IntAttribute attribute, |
| int* value) const { |
| return GetDelegate()->GetIntAttribute(attribute, value); |
| } |
| |
| const AXStringAttributes& AXPlatformNodeBase::GetStringAttributes() const { |
| return GetDelegate()->GetStringAttributes(); |
| } |
| |
| bool AXPlatformNodeBase::HasStringAttribute( |
| ax::mojom::StringAttribute attribute) const { |
| return GetDelegate()->HasStringAttribute(attribute); |
| } |
| |
| const std::string& AXPlatformNodeBase::GetStringAttribute( |
| ax::mojom::StringAttribute attribute) const { |
| return GetDelegate()->GetStringAttribute(attribute); |
| } |
| |
| bool AXPlatformNodeBase::GetStringAttribute( |
| ax::mojom::StringAttribute attribute, |
| std::string* value) const { |
| return GetDelegate()->GetStringAttribute(attribute, value); |
| } |
| |
| std::u16string AXPlatformNodeBase::GetString16Attribute( |
| ax::mojom::StringAttribute attribute) const { |
| return GetDelegate()->GetString16Attribute(attribute); |
| } |
| |
| bool AXPlatformNodeBase::GetString16Attribute( |
| ax::mojom::StringAttribute attribute, |
| std::u16string* value) const { |
| return GetDelegate()->GetString16Attribute(attribute, value); |
| } |
| |
| bool AXPlatformNodeBase::HasInheritedStringAttribute( |
| ax::mojom::StringAttribute attribute) const { |
| const AXPlatformNodeBase* current_node = this; |
| |
| do { |
| if (current_node->HasStringAttribute(attribute)) { |
| return true; |
| } |
| |
| current_node = FromNativeViewAccessible(current_node->GetParent()); |
| } while (current_node); |
| |
| return false; |
| } |
| |
| const std::string& AXPlatformNodeBase::GetInheritedStringAttribute( |
| ax::mojom::StringAttribute attribute) const { |
| // TODO(nektar): Switch to using `AXNode::GetInheritedStringAttribute` after |
| // it has been modified to cross tree boundaries. |
| const AXPlatformNodeBase* current_node = this; |
| |
| do { |
| if (current_node->HasStringAttribute(attribute)) { |
| return current_node->GetStringAttribute(attribute); |
| } |
| |
| current_node = FromNativeViewAccessible(current_node->GetParent()); |
| } while (current_node); |
| |
| return base::EmptyString(); |
| } |
| |
| bool AXPlatformNodeBase::GetInheritedStringAttribute( |
| ax::mojom::StringAttribute attribute, |
| std::string* value) const { |
| // TODO(nektar): Switch to using `AXNode::GetInheritedStringAttribute` after |
| // it has been modified to cross tree boundaries. |
| const AXPlatformNodeBase* current_node = this; |
| |
| do { |
| if (current_node->GetStringAttribute(attribute, value)) { |
| return true; |
| } |
| |
| current_node = FromNativeViewAccessible(current_node->GetParent()); |
| } while (current_node); |
| |
| return false; |
| } |
| |
| std::u16string AXPlatformNodeBase::GetInheritedString16Attribute( |
| ax::mojom::StringAttribute attribute) const { |
| // TODO(nektar): Switch to using `AXNode::GetInheritedString16Attribute` after |
| // it has been modified to cross tree boundaries. |
| return base::UTF8ToUTF16(GetInheritedStringAttribute(attribute)); |
| } |
| |
| bool AXPlatformNodeBase::GetInheritedString16Attribute( |
| ax::mojom::StringAttribute attribute, |
| std::u16string* value) const { |
| // TODO(nektar): Switch to using `AXNode::GetInheritedString16Attribute` after |
| // it has been modified to cross tree boundaries. |
| std::string value_utf8; |
| if (!GetInheritedStringAttribute(attribute, &value_utf8)) |
| return false; |
| *value = base::UTF8ToUTF16(value_utf8); |
| return true; |
| } |
| |
| const AXIntListAttributes& AXPlatformNodeBase::GetIntListAttributes() const { |
| return GetDelegate()->GetIntListAttributes(); |
| } |
| |
| bool AXPlatformNodeBase::HasIntListAttribute( |
| ax::mojom::IntListAttribute attribute) const { |
| return GetDelegate()->HasIntListAttribute(attribute); |
| } |
| |
| const std::vector<int32_t>& AXPlatformNodeBase::GetIntListAttribute( |
| ax::mojom::IntListAttribute attribute) const { |
| return GetDelegate()->GetIntListAttribute(attribute); |
| } |
| |
| bool AXPlatformNodeBase::GetIntListAttribute( |
| ax::mojom::IntListAttribute attribute, |
| std::vector<int32_t>* value) const { |
| return GetDelegate()->GetIntListAttribute(attribute, value); |
| } |
| |
| bool AXPlatformNodeBase::HasStringListAttribute( |
| ax::mojom::StringListAttribute attribute) const { |
| return GetDelegate()->HasStringListAttribute(attribute); |
| } |
| |
| const std::vector<std::string>& AXPlatformNodeBase::GetStringListAttribute( |
| ax::mojom::StringListAttribute attribute) const { |
| return GetDelegate()->GetStringListAttribute(attribute); |
| } |
| |
| bool AXPlatformNodeBase::GetStringListAttribute( |
| ax::mojom::StringListAttribute attribute, |
| std::vector<std::string>* value) const { |
| return GetDelegate()->GetStringListAttribute(attribute, value); |
| } |
| |
| const base::StringPairs& AXPlatformNodeBase::GetHtmlAttributes() const { |
| return GetDelegate()->GetHtmlAttributes(); |
| } |
| |
| AXTextAttributes AXPlatformNodeBase::GetTextAttributes() const { |
| return GetDelegate()->GetTextAttributes(); |
| } |
| |
| bool AXPlatformNodeBase::HasState(ax::mojom::State state) const { |
| return GetDelegate()->HasState(state); |
| } |
| |
| bool AXPlatformNodeBase::HasAction(ax::mojom::Action action) const { |
| return GetDelegate()->HasAction(action); |
| } |
| |
| bool AXPlatformNodeBase::HasTextStyle(ax::mojom::TextStyle text_style) const { |
| return GetDelegate()->HasTextStyle(text_style); |
| } |
| |
| ax::mojom::NameFrom AXPlatformNodeBase::GetNameFrom() const { |
| return GetDelegate()->GetNameFrom(); |
| } |
| |
| bool AXPlatformNodeBase::HasNameFromOtherElement() const { |
| ax::mojom::NameFrom nameFrom = GetNameFrom(); |
| return nameFrom == ax::mojom::NameFrom::kCaption || |
| nameFrom == ax::mojom::NameFrom::kRelatedElement; |
| } |
| |
| // static |
| AXPlatformNodeBase* AXPlatformNodeBase::FromNativeViewAccessible( |
| gfx::NativeViewAccessible accessible) { |
| return static_cast<AXPlatformNodeBase*>( |
| AXPlatformNode::FromNativeViewAccessible(accessible)); |
| } |
| |
| bool AXPlatformNodeBase::SetHypertextSelection(int start_offset, |
| int end_offset) { |
| return GetDelegate()->SetHypertextSelection(start_offset, end_offset); |
| } |
| |
| bool AXPlatformNodeBase::IsPlatformDocument() const { |
| return GetDelegate()->IsPlatformDocument(); |
| } |
| |
| bool AXPlatformNodeBase::IsStructuredAnnotation() const { |
| // The node represents a structured annotation if it can trace back to a |
| // target node that is being annotated. |
| std::vector<AXPlatformNode*> reverse_relations = |
| GetDelegate()->GetSourceNodesForReverseRelations( |
| ax::mojom::IntListAttribute::kDetailsIds); |
| |
| return !reverse_relations.empty(); |
| } |
| |
| bool AXPlatformNodeBase::IsTextField() const { |
| return GetData().IsTextField(); |
| } |
| |
| bool AXPlatformNodeBase::IsAtomicTextField() const { |
| return GetData().IsAtomicTextField(); |
| } |
| |
| bool AXPlatformNodeBase::IsNonAtomicTextField() const { |
| return GetData().IsNonAtomicTextField(); |
| } |
| |
| bool AXPlatformNodeBase::IsText() const { |
| return GetDelegate()->IsText(); |
| } |
| |
| std::u16string AXPlatformNodeBase::GetHypertext() const { |
| // Hypertext of platform leaves, which internally are composite objects, are |
| // represented with the text content of the internal composite object. These |
| // don't exist on non-web content. |
| if (IsChildOfLeaf()) |
| return GetTextContentUTF16(); |
| |
| if (hypertext_.needs_update) |
| UpdateComputedHypertext(); |
| return hypertext_.hypertext; |
| } |
| |
| std::u16string AXPlatformNodeBase::GetTextContentUTF16() const { |
| return GetDelegate()->GetTextContentUTF16(); |
| } |
| |
| int AXPlatformNodeBase::GetTextContentLengthUTF16() const { |
| return GetDelegate()->GetTextContentLengthUTF16(); |
| } |
| |
| std::u16string |
| AXPlatformNodeBase::GetRoleDescriptionFromImageAnnotationStatusOrFromAttribute() |
| const { |
| if (GetRole() == ax::mojom::Role::kImage && |
| (GetData().GetImageAnnotationStatus() == |
| ax::mojom::ImageAnnotationStatus::kEligibleForAnnotation || |
| GetData().GetImageAnnotationStatus() == |
| ax::mojom::ImageAnnotationStatus::kSilentlyEligibleForAnnotation)) { |
| return GetDelegate()->GetLocalizedRoleDescriptionForUnlabeledImage(); |
| } |
| |
| return GetString16Attribute(ax::mojom::StringAttribute::kRoleDescription); |
| } |
| |
| std::u16string AXPlatformNodeBase::GetRoleDescription() const { |
| std::u16string role_description = |
| GetRoleDescriptionFromImageAnnotationStatusOrFromAttribute(); |
| |
| if (!role_description.empty()) { |
| return role_description; |
| } |
| |
| return GetDelegate()->GetLocalizedStringForRoleDescription(); |
| } |
| |
| bool AXPlatformNodeBase::IsImageWithMap() const { |
| DCHECK_EQ(GetRole(), ax::mojom::Role::kImage) |
| << "Only call IsImageWithMap() on an image"; |
| return GetChildCount(); |
| } |
| |
| AXPlatformNodeBase* AXPlatformNodeBase::GetSelectionContainer() const { |
| return FromNativeViewAccessible(GetDelegate()->GetSelectionContainer()); |
| } |
| |
| AXPlatformNodeBase* AXPlatformNodeBase::GetTable() const { |
| return FromNativeViewAccessible(GetDelegate()->GetTableAncestor()); |
| } |
| |
| AXPlatformNodeBase* AXPlatformNodeBase::GetTableCaption() const { |
| return static_cast<AXPlatformNodeBase*>(GetDelegate()->GetTableCaption()); |
| } |
| |
| AXPlatformNodeBase* AXPlatformNodeBase::GetTableCell(int index) const { |
| std::optional<int32_t> cell_id = GetDelegate()->CellIndexToId(index); |
| if (!cell_id) |
| return nullptr; |
| |
| return static_cast<AXPlatformNodeBase*>( |
| GetDelegate()->GetFromNodeID(*cell_id)); |
| } |
| |
| AXPlatformNodeBase* AXPlatformNodeBase::GetTableCell(int row, |
| int column) const { |
| std::optional<int32_t> cell_id = GetDelegate()->GetCellId(row, column); |
| if (!cell_id) |
| return nullptr; |
| |
| return static_cast<AXPlatformNodeBase*>( |
| GetDelegate()->GetFromNodeID(*cell_id)); |
| } |
| |
| AXPlatformNodeBase* AXPlatformNodeBase::GetAriaTableCell(int aria_row, |
| int aria_column) const { |
| std::optional<int32_t> cell_id = |
| GetDelegate()->GetCellIdAriaCoords(aria_row, aria_column); |
| if (!cell_id) { |
| return nullptr; |
| } |
| return static_cast<AXPlatformNodeBase*>( |
| GetDelegate()->GetFromNodeID(*cell_id)); |
| } |
| |
| std::optional<int> AXPlatformNodeBase::GetTableCellIndex() const { |
| return GetDelegate()->GetTableCellIndex(); |
| } |
| |
| std::optional<int> AXPlatformNodeBase::GetTableColumn() const { |
| return GetDelegate()->GetTableCellColIndex(); |
| } |
| |
| std::optional<int> AXPlatformNodeBase::GetTableColumnCount() const { |
| return GetDelegate()->GetTableColCount(); |
| } |
| |
| std::optional<int> AXPlatformNodeBase::GetTableAriaColumnCount() const { |
| return GetDelegate()->GetTableAriaColCount(); |
| } |
| |
| std::optional<int> AXPlatformNodeBase::GetTableColumnSpan() const { |
| return GetDelegate()->GetTableCellColSpan(); |
| } |
| |
| std::optional<int> AXPlatformNodeBase::GetTableRow() const { |
| AXPlatformNodeDelegate* const delegate = GetDelegate(); |
| if (delegate->IsTableRow()) { |
| return delegate->GetTableRowRowIndex(); |
| } |
| if (delegate->IsTableCellOrHeader()) { |
| return delegate->GetTableCellRowIndex(); |
| } |
| return std::nullopt; |
| } |
| |
| std::optional<int> AXPlatformNodeBase::GetTableRowCount() const { |
| return GetDelegate()->GetTableRowCount(); |
| } |
| |
| std::optional<int> AXPlatformNodeBase::GetTableAriaRowCount() const { |
| return GetDelegate()->GetTableAriaRowCount(); |
| } |
| |
| std::optional<int> AXPlatformNodeBase::GetTableRowSpan() const { |
| return GetDelegate()->GetTableCellRowSpan(); |
| } |
| |
| std::optional<float> AXPlatformNodeBase::GetFontSizeInPoints() const { |
| 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; |
| return points; |
| } |
| return std::nullopt; |
| } |
| |
| bool AXPlatformNodeBase::HasVisibleCaretOrSelection() const { |
| return GetDelegate()->HasVisibleCaretOrSelection(); |
| } |
| |
| bool AXPlatformNodeBase::IsLeaf() const { |
| return GetDelegate()->IsLeaf(); |
| } |
| |
| bool AXPlatformNodeBase::IsChildOfLeaf() const { |
| return GetDelegate()->IsChildOfLeaf(); |
| } |
| |
| bool AXPlatformNodeBase::IsInvisibleOrIgnored() const { |
| if (!GetData().IsInvisibleOrIgnored()) |
| return false; |
| |
| // Never marked a focused node as invisible or ignored, otherwise screen |
| // reader users will not hear an announcement for it when it receives focus. |
| if (IsFocused()) |
| return false; |
| |
| return !HasVisibleCaretOrSelection(); |
| } |
| |
| bool AXPlatformNodeBase::IsFocused() const { |
| return FromNativeViewAccessible(GetDelegate()->GetFocus()) == this; |
| } |
| |
| bool AXPlatformNodeBase::IsFocusable() const { |
| return GetDelegate()->IsFocusable(); |
| } |
| |
| bool AXPlatformNodeBase::IsScrollable() const { |
| return (HasIntAttribute(ax::mojom::IntAttribute::kScrollXMin) && |
| HasIntAttribute(ax::mojom::IntAttribute::kScrollXMax) && |
| HasIntAttribute(ax::mojom::IntAttribute::kScrollX)) || |
| (HasIntAttribute(ax::mojom::IntAttribute::kScrollYMin) && |
| HasIntAttribute(ax::mojom::IntAttribute::kScrollYMax) && |
| HasIntAttribute(ax::mojom::IntAttribute::kScrollY)); |
| } |
| |
| bool AXPlatformNodeBase::IsHorizontallyScrollable() const { |
| DCHECK_GE(GetIntAttribute(ax::mojom::IntAttribute::kScrollXMin), 0) |
| << "Pixel sizes should be non-negative."; |
| DCHECK_GE(GetIntAttribute(ax::mojom::IntAttribute::kScrollXMax), 0) |
| << "Pixel sizes should be non-negative."; |
| return IsScrollable() && |
| GetIntAttribute(ax::mojom::IntAttribute::kScrollXMin) < |
| GetIntAttribute(ax::mojom::IntAttribute::kScrollXMax); |
| } |
| |
| bool AXPlatformNodeBase::IsVerticallyScrollable() const { |
| DCHECK_GE(GetIntAttribute(ax::mojom::IntAttribute::kScrollYMin), 0) |
| << "Pixel sizes should be non-negative."; |
| DCHECK_GE(GetIntAttribute(ax::mojom::IntAttribute::kScrollYMax), 0) |
| << "Pixel sizes should be non-negative."; |
| return IsScrollable() && |
| GetIntAttribute(ax::mojom::IntAttribute::kScrollYMin) < |
| GetIntAttribute(ax::mojom::IntAttribute::kScrollYMax); |
| } |
| |
| std::u16string AXPlatformNodeBase::GetValueForControl() const { |
| return GetDelegate()->GetValueForControl(); |
| } |
| |
| void AXPlatformNodeBase::ComputeAttributes(PlatformAttributeList* attributes) { |
| // Expose some HTML and ARIA attributes in the IAccessible2 attributes string |
| // "display", "tag", and "xml-roles" have somewhat unusual names for |
| // historical reasons. Aside from that, virtually every ARIA attribute |
| // is exposed in a really straightforward way, i.e. "aria-foo" is exposed |
| // as "foo". |
| AddAttributeToList(ax::mojom::StringAttribute::kDisplay, "display", |
| attributes); |
| AddAttributeToList(ax::mojom::StringAttribute::kHtmlTag, "tag", attributes); |
| AddAttributeToList(ax::mojom::StringAttribute::kRole, "xml-roles", |
| attributes); |
| AddAttributeToList(ax::mojom::StringAttribute::kPlaceholder, "placeholder", |
| attributes); |
| |
| AddAttributeToList(ax::mojom::StringAttribute::kAutoComplete, "autocomplete", |
| attributes); |
| if (!HasStringAttribute(ax::mojom::StringAttribute::kAutoComplete) && |
| HasState(ax::mojom::State::kAutofillAvailable)) { |
| AddAttributeToList("autocomplete", "list", attributes); |
| } |
| |
| if (HasState(ax::mojom::State::kHasActions)) { |
| AddAttributeToList("has-actions", "true", attributes); |
| } |
| |
| std::u16string role_description = |
| GetRoleDescriptionFromImageAnnotationStatusOrFromAttribute(); |
| if (!role_description.empty() || |
| HasStringAttribute(ax::mojom::StringAttribute::kRoleDescription)) { |
| AddAttributeToList("roledescription", base::UTF16ToUTF8(role_description), |
| attributes); |
| } |
| |
| // Expose description-from and description. |
| int desc_from; |
| if (GetIntAttribute(ax::mojom::IntAttribute::kDescriptionFrom, &desc_from)) { |
| std::string from; |
| switch (static_cast<ax::mojom::DescriptionFrom>(desc_from)) { |
| case ax::mojom::DescriptionFrom::kAriaDescription: |
| // Descriptions are exposed via each platform's usual description field. |
| // Also, only aria-description is exposed via the "description" object |
| // attribute, in order to match Firefox. |
| AddAttributeToList(ax::mojom::StringAttribute::kDescription, |
| "description", attributes); |
| from = "aria-description"; |
| break; |
| case ax::mojom::DescriptionFrom::kButtonLabel: |
| from = "button-label"; |
| break; |
| case ax::mojom::DescriptionFrom::kProhibitedNameRepair: |
| from = "prohibited-name-repair"; |
| break; |
| case ax::mojom::DescriptionFrom::kRelatedElement: |
| // aria-describedby=tooltip is mapped to "tooltip". |
| from = IsDescribedByTooltip() ? "tooltip" : "aria-describedby"; |
| break; |
| case ax::mojom::DescriptionFrom::kRubyAnnotation: |
| from = "ruby-annotation"; |
| break; |
| case ax::mojom::DescriptionFrom::kSummary: |
| from = "summary"; |
| break; |
| case ax::mojom::DescriptionFrom::kSvgDescElement: |
| from = "svg-desc-element"; |
| break; |
| case ax::mojom::DescriptionFrom::kTableCaption: |
| from = "table-caption"; |
| break; |
| case ax::mojom::DescriptionFrom::kTitle: |
| case ax::mojom::DescriptionFrom::kPopoverTarget: |
| case ax::mojom::DescriptionFrom::kInterestFor: |
| // The following types of markup are mapped to "tooltip": |
| // * The title attribute. |
| // * A popover=something related via the `popovertarget` attribute. |
| // * A tooltip related via aria-describedby (see kRelatedElement above). |
| // * An interestfor pointing to plain content. |
| from = "tooltip"; |
| break; |
| case ax::mojom::DescriptionFrom::kNone: |
| case ax::mojom::DescriptionFrom::kAttributeExplicitlyEmpty: |
| break; |
| } |
| if (!from.empty()) { |
| AddAttributeToList("description-from", from, attributes); |
| } |
| } |
| |
| AddAttributeToList(ax::mojom::StringAttribute::kAriaBrailleLabel, |
| "braillelabel", attributes); |
| AddAttributeToList(ax::mojom::StringAttribute::kAriaBrailleRoleDescription, |
| "brailleroledescription", attributes); |
| |
| AddAttributeToList(ax::mojom::StringAttribute::kKeyShortcuts, "keyshortcuts", |
| attributes); |
| AddAttributeToList(ax::mojom::IntAttribute::kHierarchicalLevel, "level", |
| attributes); |
| AddAttributeToList(ax::mojom::IntAttribute::kSetSize, "setsize", attributes); |
| AddAttributeToList(ax::mojom::IntAttribute::kPosInSet, "posinset", |
| attributes); |
| |
| if (IsPlatformCheckable()) |
| AddAttributeToList("checkable", "true", attributes); |
| |
| if (IsInvisibleOrIgnored()) // Note: NVDA prefers this over INVISIBLE state. |
| AddAttributeToList("hidden", "true", attributes); |
| |
| // Expose live region attributes. |
| AddAttributeToList(ax::mojom::StringAttribute::kLiveStatus, "live", |
| attributes); |
| AddAttributeToList(ax::mojom::StringAttribute::kLiveRelevant, "relevant", |
| attributes); |
| AddAttributeToList(ax::mojom::BoolAttribute::kLiveAtomic, "atomic", |
| attributes); |
| // Busy is usually associated with live regions but can occur anywhere: |
| AddAttributeToList(ax::mojom::BoolAttribute::kBusy, "busy", attributes); |
| |
| // Expose container live region attributes. |
| AddAttributeToList(ax::mojom::StringAttribute::kContainerLiveStatus, |
| "container-live", attributes); |
| AddAttributeToList(ax::mojom::StringAttribute::kContainerLiveRelevant, |
| "container-relevant", attributes); |
| AddAttributeToList(ax::mojom::BoolAttribute::kContainerLiveAtomic, |
| "container-atomic", attributes); |
| AddAttributeToList(ax::mojom::BoolAttribute::kContainerLiveBusy, |
| "container-busy", attributes); |
| |
| // Expose name-from. |
| ax::mojom::NameFrom name_from = GetNameFrom(); |
| std::string from; |
| bool is_explicit_name = true; |
| switch (static_cast<ax::mojom::NameFrom>(name_from)) { |
| case ax::mojom::NameFrom::kAttribute: |
| from = "attribute"; |
| DCHECK(!GetName().empty()); |
| break; |
| case ax::mojom::NameFrom::kCaption: |
| from = "caption"; |
| DCHECK(!GetName().empty()); |
| break; |
| case ax::mojom::NameFrom::kContents: |
| is_explicit_name = false; |
| from = "contents"; |
| DCHECK(!GetName().empty()); |
| break; |
| case ax::mojom::NameFrom::kCssAltText: |
| from = "CSS alt text"; |
| DCHECK(!GetName().empty()); |
| break; |
| case ax::mojom::NameFrom::kPlaceholder: |
| from = "placeholder"; |
| DCHECK(!GetName().empty()); |
| break; |
| case ax::mojom::NameFrom::kProhibited: |
| case ax::mojom::NameFrom::kProhibitedAndRedundant: |
| is_explicit_name = false; |
| from = "prohibited"; |
| DCHECK(GetName().empty()); |
| break; |
| case ax::mojom::NameFrom::kRelatedElement: |
| from = "related-element"; |
| DCHECK(!GetName().empty()); |
| break; |
| case ax::mojom::NameFrom::kPopoverTarget: |
| case ax::mojom::NameFrom::kInterestFor: |
| case ax::mojom::NameFrom::kTitle: |
| from = "tooltip"; |
| DCHECK(!GetName().empty()); |
| break; |
| case ax::mojom::NameFrom::kValue: |
| from = "value"; |
| DCHECK(!GetName().empty()); |
| break; |
| case ax::mojom::NameFrom::kAttributeExplicitlyEmpty: |
| break; |
| case ax::mojom::NameFrom::kNone: |
| is_explicit_name = false; |
| break; // Not exposed. |
| } |
| if (!from.empty()) { |
| AddAttributeToList("name-from", from, attributes); |
| } |
| // Expose the non-standard explicit-name IA2 attribute. |
| if (is_explicit_name) { |
| AddAttributeToList("explicit-name", "true", attributes); |
| } |
| |
| // Expose the aria-haspopup attribute. |
| int32_t has_popup; |
| if (GetIntAttribute(ax::mojom::IntAttribute::kHasPopup, &has_popup)) { |
| switch (static_cast<ax::mojom::HasPopup>(has_popup)) { |
| case ax::mojom::HasPopup::kFalse: |
| break; |
| case ax::mojom::HasPopup::kTrue: |
| AddAttributeToList("haspopup", "true", attributes); |
| break; |
| case ax::mojom::HasPopup::kMenu: |
| AddAttributeToList("haspopup", "menu", attributes); |
| break; |
| case ax::mojom::HasPopup::kListbox: |
| AddAttributeToList("haspopup", "listbox", attributes); |
| break; |
| case ax::mojom::HasPopup::kTree: |
| AddAttributeToList("haspopup", "tree", attributes); |
| break; |
| case ax::mojom::HasPopup::kGrid: |
| AddAttributeToList("haspopup", "grid", attributes); |
| break; |
| case ax::mojom::HasPopup::kDialog: |
| AddAttributeToList("haspopup", "dialog", attributes); |
| break; |
| } |
| } else if (HasState(ax::mojom::State::kAutofillAvailable)) { |
| AddAttributeToList("haspopup", "menu", attributes); |
| } |
| |
| if (HasState(ax::mojom::State::kHasInterestFor)) { |
| AddAttributeToList("has-interest-for", "true", attributes); |
| } |
| |
| // Expose the aria-ispopup attribute. |
| int32_t is_popup; |
| if (GetIntAttribute(ax::mojom::IntAttribute::kIsPopup, &is_popup)) { |
| switch (static_cast<ax::mojom::IsPopup>(is_popup)) { |
| case ax::mojom::IsPopup::kNone: |
| break; |
| case ax::mojom::IsPopup::kManual: |
| AddAttributeToList("ispopup", "manual", attributes); |
| break; |
| case ax::mojom::IsPopup::kAuto: |
| AddAttributeToList("ispopup", "auto", attributes); |
| break; |
| case ax::mojom::IsPopup::kHint: |
| AddAttributeToList("ispopup", "hint", attributes); |
| break; |
| } |
| } |
| |
| // Expose the aria-current attribute. |
| int32_t aria_current_state; |
| if (GetIntAttribute(ax::mojom::IntAttribute::kAriaCurrentState, |
| &aria_current_state)) { |
| switch (static_cast<ax::mojom::AriaCurrentState>(aria_current_state)) { |
| case ax::mojom::AriaCurrentState::kNone: |
| break; |
| case ax::mojom::AriaCurrentState::kFalse: |
| AddAttributeToList("current", "false", attributes); |
| break; |
| case ax::mojom::AriaCurrentState::kTrue: |
| AddAttributeToList("current", "true", attributes); |
| break; |
| case ax::mojom::AriaCurrentState::kPage: |
| AddAttributeToList("current", "page", attributes); |
| break; |
| case ax::mojom::AriaCurrentState::kStep: |
| AddAttributeToList("current", "step", attributes); |
| break; |
| case ax::mojom::AriaCurrentState::kLocation: |
| AddAttributeToList("current", "location", attributes); |
| break; |
| case ax::mojom::AriaCurrentState::kDate: |
| AddAttributeToList("current", "date", attributes); |
| break; |
| case ax::mojom::AriaCurrentState::kTime: |
| AddAttributeToList("current", "time", attributes); |
| break; |
| } |
| } |
| |
| AXPlatformNodeDelegate* const delegate = GetDelegate(); |
| |
| // Expose table cell index. |
| if (IsCellOrTableHeader(GetRole())) { |
| std::optional<int> index = delegate->GetTableCellIndex(); |
| if (index) { |
| std::string str_index(base::NumberToString(*index)); |
| AddAttributeToList("table-cell-index", str_index, attributes); |
| } |
| } |
| if (GetRole() == ax::mojom::Role::kLayoutTable) |
| AddAttributeToList("layout-guess", "true", attributes); |
| |
| // Expose aria-colcount and aria-rowcount in a table, grid or treegrid if they |
| // are different from its physical dimensions. |
| if (IsTableLike(GetRole()) && |
| (delegate->GetTableAriaRowCount() != delegate->GetTableRowCount() || |
| delegate->GetTableAriaColCount() != delegate->GetTableColCount())) { |
| AddAttributeToList(ax::mojom::IntAttribute::kAriaColumnCount, "colcount", |
| attributes); |
| AddAttributeToList(ax::mojom::IntAttribute::kAriaRowCount, "rowcount", |
| attributes); |
| } |
| |
| if (IsCellOrTableHeader(GetRole()) || IsTableRow(GetRole())) { |
| // Expose aria-colindex and aria-rowindex in a cell or row only if they are |
| // different from the table's physical coordinates. |
| // Note: aria-col/rowindex is 1 based where as table's physical coordinates |
| // are 0 based, so we subtract aria-col/rowindex by 1 to compare with |
| // table's physical coordinates. |
| std::optional<int> aria_rowindex = delegate->GetTableCellAriaRowIndex(); |
| std::optional<int> physical_rowindex = delegate->GetTableCellRowIndex(); |
| std::optional<int> aria_colindex = delegate->GetTableCellAriaColIndex(); |
| std::optional<int> physical_colindex = delegate->GetTableCellColIndex(); |
| |
| if (aria_rowindex && physical_rowindex && |
| aria_rowindex.value() - 1 != physical_rowindex.value()) { |
| std::string str_value = base::NumberToString(*aria_rowindex); |
| AddAttributeToList("rowindex", str_value, attributes); |
| } |
| |
| if (!IsTableRow(GetRole()) && aria_colindex && physical_colindex && |
| aria_colindex.value() - 1 != physical_colindex.value()) { |
| AddAttributeToList(ax::mojom::IntAttribute::kAriaCellColumnIndex, |
| "colindex", attributes); |
| } |
| } |
| |
| // Expose row or column header sort direction. |
| int32_t sort_direction; |
| if (IsTableHeader(GetRole()) && |
| GetIntAttribute(ax::mojom::IntAttribute::kSortDirection, |
| &sort_direction)) { |
| switch (static_cast<ax::mojom::SortDirection>(sort_direction)) { |
| case ax::mojom::SortDirection::kNone: |
| break; |
| case ax::mojom::SortDirection::kUnsorted: |
| AddAttributeToList("sort", "none", attributes); |
| break; |
| case ax::mojom::SortDirection::kAscending: |
| AddAttributeToList("sort", "ascending", attributes); |
| break; |
| case ax::mojom::SortDirection::kDescending: |
| AddAttributeToList("sort", "descending", attributes); |
| break; |
| case ax::mojom::SortDirection::kOther: |
| AddAttributeToList("sort", "other", attributes); |
| break; |
| } |
| } |
| |
| if (IsCellOrTableHeader(GetRole())) { |
| // These are the older, backwards compatible names that work with JAWS/NVDA: |
| AddAttributeToList(ax::mojom::StringAttribute::kAriaCellColumnIndexText, |
| "coltext", attributes); |
| AddAttributeToList(ax::mojom::StringAttribute::kAriaCellRowIndexText, |
| "rowtext", attributes); |
| // These newer names are consistent with the ARIA attribute names: |
| AddAttributeToList(ax::mojom::StringAttribute::kAriaCellColumnIndexText, |
| "colindextext", attributes); |
| AddAttributeToList(ax::mojom::StringAttribute::kAriaCellRowIndexText, |
| "rowindextext", attributes); |
| |
| AddAttributeToList(ax::mojom::IntAttribute::kAriaCellColumnSpan, "colspan", |
| attributes); |
| AddAttributeToList(ax::mojom::IntAttribute::kAriaCellRowSpan, "rowspan", |
| attributes); |
| } |
| |
| // Expose the value of a progress bar, slider, scroll bar or <select> element. |
| if (GetData().IsRangeValueSupported() || |
| GetRole() == ax::mojom::Role::kComboBoxMenuButton) { |
| std::string value = base::UTF16ToUTF8(GetValueForControl()); |
| if (!value.empty()) |
| AddAttributeToList("valuetext", value, attributes); |
| } |
| |
| // Expose dropeffect attribute. |
| // aria-dropeffect is deprecated in WAI-ARIA 1.1. |
| if (delegate->HasIntAttribute( |
| ax::mojom::IntAttribute::kDropeffectDeprecated)) { |
| NOTREACHED(); |
| } |
| |
| // Expose class attribute. |
| std::string class_attr; |
| if (delegate->GetStringAttribute(ax::mojom::StringAttribute::kClassName, |
| &class_attr)) { |
| AddAttributeToList("class", class_attr, attributes); |
| } |
| |
| // Expose machine-readable datetime attribute on <time>, <ins> and <del>. |
| AddAttributeToList(ax::mojom::StringAttribute::kDateTime, "datetime", |
| attributes); |
| |
| std::string id; |
| if (delegate->GetStringAttribute(ax::mojom::StringAttribute::kHtmlId, &id)) { |
| AddAttributeToList("id", id, attributes); |
| } |
| |
| std::string input_name; |
| if (delegate->GetStringAttribute(ax::mojom::StringAttribute::kHtmlInputName, |
| &input_name)) { |
| AddAttributeToList("html-input-name", input_name, attributes); |
| } |
| |
| std::string src; |
| if (IsImage(GetRole()) && |
| GetStringAttribute(ax::mojom::StringAttribute::kUrl, &src)) { |
| AddAttributeToList("src", src, attributes); |
| } |
| |
| if (delegate->HasIntAttribute(ax::mojom::IntAttribute::kTextAlign)) { |
| auto text_align = static_cast<ax::mojom::TextAlign>( |
| delegate->GetIntAttribute(ax::mojom::IntAttribute::kTextAlign)); |
| switch (text_align) { |
| case ax::mojom::TextAlign::kNone: |
| break; |
| case ax::mojom::TextAlign::kLeft: |
| AddAttributeToList("text-align", "left", attributes); |
| break; |
| case ax::mojom::TextAlign::kRight: |
| AddAttributeToList("text-align", "right", attributes); |
| break; |
| case ax::mojom::TextAlign::kCenter: |
| AddAttributeToList("text-align", "center", attributes); |
| break; |
| case ax::mojom::TextAlign::kJustify: |
| AddAttributeToList("text-align", "justify", attributes); |
| break; |
| } |
| } |
| |
| float text_indent; |
| if (GetFloatAttribute(ax::mojom::FloatAttribute::kTextIndent, &text_indent) != |
| 0.0f) { |
| // Round value to two decimal places. |
| std::stringstream value; |
| value << std::fixed << std::setprecision(2) << text_indent << "mm"; |
| AddAttributeToList("text-indent", value.str(), attributes); |
| } |
| |
| // Text fields need to report the attribute "text-model:a1" to instruct |
| // screen readers to use IAccessible2 APIs to handle text editing in this |
| // object (as opposed to treating it like a native Windows text box). |
| // The text-model:a1 attribute is documented here: |
| // http://www.linuxfoundation.org/collaborate/workgroups/accessibility/ia2/ia2_implementation_guide |
| if (IsTextField()) |
| AddAttributeToList("text-model", "a1", attributes); |
| |
| // Expose input-text type attribute. |
| if (IsAtomicTextField() || IsDateOrTimeInput(GetRole())) { |
| AddAttributeToList(ax::mojom::StringAttribute::kInputType, |
| "text-input-type", attributes); |
| } |
| |
| // Expose details-from. |
| int details_from; |
| if (GetIntAttribute(ax::mojom::IntAttribute::kDetailsFrom, &details_from)) { |
| switch (static_cast<ax::mojom::DetailsFrom>(details_from)) { |
| case ax::mojom::DetailsFrom::kAriaDetails: |
| AddAttributeToList("details-from", "aria-details", attributes); |
| break; |
| case ax::mojom::DetailsFrom::kCssAnchor: |
| AddAttributeToList("details-from", "css-anchor", attributes); |
| break; |
| case ax::mojom::DetailsFrom::kPopoverTarget: |
| AddAttributeToList("details-from", "popover-target", attributes); |
| break; |
| case ax::mojom::DetailsFrom::kInterestFor: |
| AddAttributeToList("details-from", "interest-for", attributes); |
| break; |
| case ax::mojom::DetailsFrom::kCommandfor: |
| AddAttributeToList("details-from", "command-for", attributes); |
| break; |
| case ax::mojom::DetailsFrom::kCssScrollMarkerPseudoElement: |
| AddAttributeToList("details-from", "css-scroll-marker-pseudo-element", |
| attributes); |
| break; |
| } |
| } |
| |
| std::string details_roles = ComputeDetailsRoles(); |
| if (!details_roles.empty()) |
| AddAttributeToList("details-roles", details_roles, attributes); |
| |
| if (IsLink(GetRole())) { |
| AddAttributeToList(ax::mojom::StringAttribute::kLinkTarget, "link-target", |
| attributes); |
| } |
| |
| // MathML content. |
| AddAttributeToList(ax::mojom::StringAttribute::kMathContent, "math", |
| attributes); |
| |
| // The maxlength of an input. |
| // TODO(https://github.com/w3c/aria/issues/1119): consider aria-maxlength. |
| if (int max_length = GetIntAttribute(ax::mojom::IntAttribute::kMaxLength)) { |
| AddAttributeToList("maxlength", base::NumberToString(max_length), |
| attributes); |
| } |
| } |
| |
| void AXPlatformNodeBase::AddAttributeToList( |
| const ax::mojom::StringAttribute attribute, |
| const char* name, |
| PlatformAttributeList* attributes) { |
| DCHECK(attributes); |
| std::string value; |
| if (GetStringAttribute(attribute, &value)) { |
| AddAttributeToList(name, value, attributes); |
| } |
| } |
| |
| void AXPlatformNodeBase::AddAttributeToList( |
| const ax::mojom::BoolAttribute attribute, |
| const char* name, |
| PlatformAttributeList* attributes) { |
| DCHECK(attributes); |
| bool value; |
| if (GetBoolAttribute(attribute, &value)) { |
| AddAttributeToList(name, base::ToString(value), attributes); |
| } |
| } |
| |
| void AXPlatformNodeBase::AddAttributeToList( |
| const ax::mojom::IntAttribute attribute, |
| const char* name, |
| PlatformAttributeList* attributes) { |
| DCHECK(attributes); |
| |
| auto maybe_value = ComputeAttribute(GetDelegate(), attribute); |
| if (maybe_value.has_value()) { |
| std::string str_value = base::NumberToString(maybe_value.value()); |
| AddAttributeToList(name, str_value, attributes); |
| } |
| } |
| |
| void AXPlatformNodeBase::AddAttributeToList(const char* name, |
| const std::string& value, |
| PlatformAttributeList* attributes) { |
| AddAttributeToList(name, value.c_str(), attributes); |
| } |
| |
| AXLegacyHypertext::AXLegacyHypertext() = default; |
| AXLegacyHypertext::~AXLegacyHypertext() = default; |
| AXLegacyHypertext::AXLegacyHypertext(const AXLegacyHypertext& other) = default; |
| AXLegacyHypertext& AXLegacyHypertext::operator=( |
| const AXLegacyHypertext& other) = default; |
| AXLegacyHypertext::AXLegacyHypertext(AXLegacyHypertext&& other) noexcept |
| : needs_update(std::exchange(other.needs_update, true)), |
| hyperlink_offset_to_index(std::move(other.hyperlink_offset_to_index)), |
| hyperlinks(std::move(other.hyperlinks)), |
| hypertext(std::move(other.hypertext)) {} |
| AXLegacyHypertext& AXLegacyHypertext::operator=(AXLegacyHypertext&& other) { |
| needs_update = std::exchange(other.needs_update, true); |
| hyperlink_offset_to_index = std::move(other.hyperlink_offset_to_index); |
| hyperlinks = std::move(other.hyperlinks); |
| hypertext = std::move(other.hypertext); |
| return *this; |
| } |
| |
| void AXLegacyHypertext::Clear() { |
| needs_update = true; |
| hyperlink_offset_to_index.clear(); |
| hyperlinks.clear(); |
| hypertext.clear(); |
| } |
| |
| // TODO(nektar): To be able to use AXNode in Views, move this logic to AXNode. |
| void AXPlatformNodeBase::UpdateComputedHypertext() const { |
| hypertext_.Clear(); |
| |
| if (GetData().IsIgnored() || IsLeaf()) { |
| hypertext_.hypertext = GetTextContentUTF16(); |
| hypertext_.needs_update = false; |
| return; |
| } |
| |
| // Construct the hypertext for this node, which contains the concatenation |
| // of all of the static text and whitespace from this node's children, and an |
| // embedded object character for all the other children. Build up a map from |
| // the character index of each embedded object character to the id of the |
| // child object it points to. |
| AXLegacyHypertext::OffsetToIndex::container_type indices; |
| std::u16string hypertext; |
| for (AXPlatformNodeChildIterator child_iter = AXPlatformNodeChildrenBegin(), |
| child_end = AXPlatformNodeChildrenEnd(); |
| child_iter != child_end; ++child_iter) { |
| // Similar to Firefox, we don't expose text nodes in IAccessible2 and ATK |
| // hypertext with the embedded object character. We copy all of their text |
| // instead. |
| if (child_iter->IsText()) { |
| hypertext_.hypertext += child_iter->GetTextContentUTF16(); |
| } else { |
| int32_t char_offset = static_cast<int32_t>(hypertext_.hypertext.size()); |
| int32_t child_unique_id = child_iter->GetUniqueId(); |
| int32_t index = static_cast<int32_t>(hypertext_.hyperlinks.size()); |
| indices.emplace_back(char_offset, index); |
| hypertext_.hyperlinks.push_back(child_unique_id); |
| hypertext_.hypertext += kEmbeddedCharacter; |
| } |
| } |
| |
| hypertext_.hyperlink_offset_to_index.replace(std::move(indices)); |
| hypertext_.needs_update = false; |
| } |
| |
| void AXPlatformNodeBase::AddAttributeToList(const char* name, |
| const char* value, |
| PlatformAttributeList* attributes) { |
| } |
| |
| std::optional<int> AXPlatformNodeBase::GetPosInSet() const { |
| return GetDelegate()->GetPosInSet(); |
| } |
| |
| std::optional<int> AXPlatformNodeBase::GetSetSize() const { |
| return GetDelegate()->GetSetSize(); |
| } |
| |
| bool AXPlatformNodeBase::ScrollToNode(ScrollType scroll_type) { |
| // ax::mojom::Action::kScrollToMakeVisible wants a target rect in *local* |
| // coords. |
| gfx::Rect r = gfx::ToEnclosingRect(GetData().relative_bounds.bounds); |
| r -= r.OffsetFromOrigin(); |
| switch (scroll_type) { |
| case ScrollType::TopLeft: |
| r = gfx::Rect(r.x(), r.y(), 0, 0); |
| break; |
| case ScrollType::BottomRight: |
| r = gfx::Rect(r.right(), r.bottom(), 0, 0); |
| break; |
| case ScrollType::TopEdge: |
| r = gfx::Rect(r.x(), r.y(), r.width(), 0); |
| break; |
| case ScrollType::BottomEdge: |
| r = gfx::Rect(r.x(), r.bottom(), r.width(), 0); |
| break; |
| case ScrollType::LeftEdge: |
| r = gfx::Rect(r.x(), r.y(), 0, r.height()); |
| break; |
| case ScrollType::RightEdge: |
| r = gfx::Rect(r.right(), r.y(), 0, r.height()); |
| break; |
| case ScrollType::Anywhere: |
| break; |
| } |
| |
| AXActionData action_data; |
| action_data.target_node_id = GetData().id; |
| action_data.action = ax::mojom::Action::kScrollToMakeVisible; |
| action_data.horizontal_scroll_alignment = |
| ax::mojom::ScrollAlignment::kScrollAlignmentCenter; |
| action_data.vertical_scroll_alignment = |
| ax::mojom::ScrollAlignment::kScrollAlignmentCenter; |
| action_data.scroll_behavior = |
| ax::mojom::ScrollBehavior::kDoNotScrollIfVisible; |
| action_data.target_rect = r; |
| GetDelegate()->AccessibilityPerformAction(action_data); |
| return true; |
| } |
| |
| // static |
| void AXPlatformNodeBase::SanitizeStringAttribute(const std::string& input, |
| std::string* output) { |
| DCHECK(output); |
| // According to the IA2 spec and AT-SPI2, 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); |
| base::ReplaceChars(*output, "\r", " ", output); |
| base::ReplaceChars(*output, "\n", " ", output); |
| } |
| |
| int32_t AXPlatformNodeBase::GetHyperlinkIndexFromChild( |
| AXPlatformNodeBase* child) { |
| if (hypertext_.hyperlinks.empty()) |
| return -1; |
| |
| auto iterator = |
| std::ranges::find(hypertext_.hyperlinks, child->GetUniqueId()); |
| if (iterator == hypertext_.hyperlinks.end()) |
| return -1; |
| |
| return static_cast<int32_t>(iterator - hypertext_.hyperlinks.begin()); |
| } |
| |
| int32_t AXPlatformNodeBase::GetHypertextOffsetFromHyperlinkIndex( |
| int32_t hyperlink_index) { |
| for (auto& offset_index : hypertext_.hyperlink_offset_to_index) { |
| if (offset_index.second == hyperlink_index) |
| return offset_index.first; |
| } |
| return -1; |
| } |
| |
| int32_t AXPlatformNodeBase::GetHypertextOffsetFromChild( |
| AXPlatformNodeBase* child) { |
| // TODO(dougt) DCHECK(child.owner()->PlatformGetParent() == owner()); |
| |
| if (IsLeaf()) |
| return -1; |
| |
| // Handle the case when we are dealing with a text-only child. |
| // Text-only children should not be present at tree roots and so no |
| // cross-tree traversal is necessary. |
| if (child->IsText()) { |
| int32_t hypertext_offset = 0; |
| for (auto child_iter = AXPlatformNodeChildrenBegin(), |
| child_end = AXPlatformNodeChildrenEnd(); |
| child_iter != child_end && child_iter.get() != child; ++child_iter) { |
| if (child_iter->IsText()) { |
| hypertext_offset += |
| static_cast<int32_t>(child_iter->GetHypertext().size()); |
| } else { |
| ++hypertext_offset; |
| } |
| } |
| return hypertext_offset; |
| } |
| |
| int32_t hyperlink_index = GetHyperlinkIndexFromChild(child); |
| if (hyperlink_index < 0) |
| return -1; |
| |
| return GetHypertextOffsetFromHyperlinkIndex(hyperlink_index); |
| } |
| |
| int AXPlatformNodeBase::HypertextOffsetFromChildIndex(int child_index) const { |
| DCHECK_GE(child_index, 0); |
| DCHECK_LE(child_index, static_cast<int>(GetChildCount())); |
| |
| // Use both a child index and an iterator to avoid an O(n^2) complexity which |
| // would be the case if we were to call GetChildAtIndex on each child. |
| int hypertext_offset = 0; |
| int endpoint_child_index = 0; |
| for (AXPlatformNodeChildIterator child_iter = AXPlatformNodeChildrenBegin(), |
| child_end = AXPlatformNodeChildrenEnd(); |
| child_iter != child_end; ++child_iter) { |
| if (endpoint_child_index >= child_index) { |
| break; |
| } |
| |
| int child_text_len = 1; |
| if (child_iter->IsText()) |
| child_text_len = |
| base::checked_cast<int>(child_iter->GetHypertext().size()); |
| |
| endpoint_child_index++; |
| hypertext_offset += child_text_len; |
| } |
| return hypertext_offset; |
| } |
| |
| int32_t AXPlatformNodeBase::GetHypertextOffsetFromDescendant( |
| AXPlatformNodeBase* descendant) { |
| auto* parent_object = static_cast<AXPlatformNodeBase*>( |
| FromNativeViewAccessible(descendant->GetDelegate()->GetParent())); |
| while (parent_object && parent_object != this) { |
| descendant = parent_object; |
| parent_object = static_cast<AXPlatformNodeBase*>( |
| FromNativeViewAccessible(descendant->GetParent())); |
| } |
| if (!parent_object) |
| return -1; |
| |
| return parent_object->GetHypertextOffsetFromChild(descendant); |
| } |
| |
| int AXPlatformNodeBase::GetHypertextOffsetFromEndpoint( |
| AXPlatformNodeBase* endpoint_object, |
| int endpoint_offset) { |
| DCHECK_GE(endpoint_offset, 0); |
| |
| // There are three cases: |
| // 1. The selection endpoint is this object itself: endpoint_offset should be |
| // returned, possibly adjusted from a child offset to a hypertext offset. |
| // 2. The selection endpoint is an ancestor of this object. If endpoint_offset |
| // points out after this object, then this object text length is returned, |
| // otherwise 0. |
| // 3. The selection endpoint is a descendant of this object. The offset of the |
| // character in this object's hypertext corresponding to the subtree in which |
| // the endpoint is located should be returned. |
| // 4. The selection endpoint is in a completely different part of the tree. |
| // Either 0 or hypertext length should be returned depending on the direction |
| // that one needs to travel to find the endpoint. |
| // |
| // TODO(nektar): Replace all this logic with the use of AXNodePosition. |
| |
| // Case 1. Is the endpoint object equal to this object |
| if (endpoint_object == this) { |
| if (endpoint_object->IsLeaf()) |
| return endpoint_offset; |
| return HypertextOffsetFromChildIndex(endpoint_offset); |
| } |
| |
| // Case 2. Is the endpoint an ancestor of this object. |
| if (IsDescendantOf(endpoint_object)) { |
| DCHECK_LE(endpoint_offset, |
| static_cast<int>(endpoint_object->GetChildCount())); |
| |
| AXPlatformNodeBase* closest_ancestor = this; |
| while (closest_ancestor) { |
| AXPlatformNodeBase* parent = static_cast<AXPlatformNodeBase*>( |
| FromNativeViewAccessible(closest_ancestor->GetParent())); |
| if (parent == endpoint_object) |
| break; |
| closest_ancestor = parent; |
| } |
| |
| // If the endpoint is after this node, then return the node's |
| // hypertext length, otherwise 0 as the endpoint points before the node. |
| std::optional<size_t> index_in_parent = |
| closest_ancestor->GetIndexInParent(); |
| DCHECK(index_in_parent) |
| << "No index in parent for ancestor: " << *closest_ancestor; |
| if (index_in_parent && |
| endpoint_offset > static_cast<int>(*index_in_parent)) { |
| return static_cast<int>(GetHypertext().size()); |
| } |
| return 0; |
| } |
| |
| AXPlatformNodeBase* common_parent = this; |
| std::optional<size_t> index_in_common_parent = GetIndexInParent(); |
| while (common_parent && !endpoint_object->IsDescendantOf(common_parent)) { |
| index_in_common_parent = common_parent->GetIndexInParent(); |
| common_parent = static_cast<AXPlatformNodeBase*>( |
| FromNativeViewAccessible(common_parent->GetParent())); |
| } |
| if (!common_parent) |
| return -1; |
| |
| DCHECK(!(common_parent->IsText())); |
| |
| // Case 2. Is the selection endpoint inside a descendant of this object? |
| // |
| // We already checked in case 1 if our endpoint object is equal to this |
| // object. We can safely assume that it is a descendant or in a completely |
| // different part of the tree. |
| if (common_parent == this) { |
| int32_t hypertext_offset = |
| GetHypertextOffsetFromDescendant(endpoint_object); |
| auto* parent = static_cast<AXPlatformNodeBase*>( |
| FromNativeViewAccessible(endpoint_object->GetParent())); |
| if (parent == this && endpoint_object->IsText()) { |
| // Due to a historical design decision, the hypertext of the immediate |
| // parents of text objects includes all their text. We therefore need to |
| // adjust the hypertext offset in the parent by adding any text offset. |
| hypertext_offset += endpoint_offset; |
| } |
| |
| return hypertext_offset; |
| } |
| |
| // Case 3. Selection endpoint is in a completely different part of the tree: |
| // - Return 0 if it's in an earlier part of the tree. |
| // - Return GetHypertext.size() if it's in a later part of the tree. |
| // We can safely assume that the endpoint is in another part of the tree or |
| // at common parent, and that this object is a descendant of common parent. |
| std::optional<size_t> endpoint_index_in_common_parent; |
| for (auto child_iter = common_parent->AXPlatformNodeChildrenBegin(), |
| child_end = common_parent->AXPlatformNodeChildrenEnd(); |
| child_iter != child_end; ++child_iter) { |
| if (endpoint_object->IsDescendantOf(child_iter.get())) { |
| endpoint_index_in_common_parent = child_iter->GetIndexInParent(); |
| break; |
| } |
| } |
| |
| if (endpoint_index_in_common_parent < index_in_common_parent) { |
| // In earlier point in tree than endpoint_object. |
| return 0; |
| } |
| if (endpoint_index_in_common_parent > index_in_common_parent) { |
| // In later point in the tree than endpoint_object. |
| return static_cast<int>(GetHypertext().size()); |
| } |
| |
| // TODO(crbug.com/40897578): Make sure this doesn't fire then turn the last |
| // conditional into a CHECK_GT(endpoint_index_in_common_parent, |
| // index_in_common_parent); and remove this code path. |
| DUMP_WILL_BE_NOTREACHED() |
| << "Was not in descendant, so the endpoint_index_in_common_parent should " |
| "be < or > than the index_in_common_parent:\n" |
| << "\n* This: " << this << "\n* Endpoint object: " << endpoint_object |
| << "\n* Endpoint offset: " << endpoint_offset |
| << "\n* Common parent: " << common_parent |
| << "\n* Index in common parent: " << index_in_common_parent.value_or(-99) |
| << "\n* Endpoint in common parent: " |
| << endpoint_index_in_common_parent.value_or(-99); |
| return -1; |
| } |
| |
| AXPlatformNodeBase::AXPosition AXPlatformNodeBase::HypertextOffsetToEndpoint( |
| int hypertext_offset) const { |
| DCHECK_GE(hypertext_offset, 0); |
| // The offset can be equal to the length when it is past the end. |
| DCHECK_LE(hypertext_offset, static_cast<int>(GetHypertext().size())); |
| |
| if (IsLeaf()) { |
| if (IsText()) { |
| return GetDelegate()->CreateTextPositionAt(hypertext_offset); |
| } |
| return GetDelegate()->CreatePositionAt(hypertext_offset); |
| } |
| |
| int current_hypertext_offset = hypertext_offset; |
| for (auto child_iter = AXPlatformNodeChildrenBegin(), |
| child_end = AXPlatformNodeChildrenEnd(); |
| child_iter != child_end && current_hypertext_offset >= 0; ++child_iter) { |
| int child_text_len = 1; |
| if (child_iter->IsText()) |
| child_text_len = |
| base::checked_cast<int>(child_iter->GetHypertext().size()); |
| |
| if (current_hypertext_offset <= child_text_len) { |
| int endpoint_offset = current_hypertext_offset; |
| if (child_iter->IsText()) |
| return child_iter->GetDelegate()->CreateTextPositionAt(endpoint_offset); |
| return child_iter->GetDelegate()->CreatePositionAt(endpoint_offset); |
| } |
| current_hypertext_offset -= child_text_len; |
| } |
| return AXNodePosition::CreateNullPosition(); |
| } |
| |
| int AXPlatformNodeBase::GetSelectionAnchor(const AXSelection* selection) { |
| DCHECK(selection); |
| AXNodeID anchor_id = selection->anchor_object_id; |
| AXPlatformNodeBase* anchor_object = |
| static_cast<AXPlatformNodeBase*>(GetDelegate()->GetFromNodeID(anchor_id)); |
| if (!anchor_object) |
| return -1; |
| |
| return GetHypertextOffsetFromEndpoint(anchor_object, |
| selection->anchor_offset); |
| } |
| |
| int AXPlatformNodeBase::GetSelectionFocus(const AXSelection* selection) { |
| DCHECK(selection); |
| AXNodeID focus_id = selection->focus_object_id; |
| AXPlatformNodeBase* focus_object = |
| static_cast<AXPlatformNodeBase*>(GetDelegate()->GetFromNodeID(focus_id)); |
| if (!focus_object) |
| return -1; |
| |
| return GetHypertextOffsetFromEndpoint(focus_object, selection->focus_offset); |
| } |
| |
| void AXPlatformNodeBase::GetSelectionOffsets(int* selection_start, |
| int* selection_end) { |
| GetSelectionOffsets(nullptr, selection_start, selection_end); |
| } |
| |
| void AXPlatformNodeBase::GetSelectionOffsets(const AXSelection* selection, |
| int* selection_start, |
| int* selection_end) { |
| DCHECK(selection_start && selection_end); |
| |
| if (IsAtomicTextField() && |
| GetIntAttribute(ax::mojom::IntAttribute::kTextSelStart, |
| selection_start) && |
| GetIntAttribute(ax::mojom::IntAttribute::kTextSelEnd, selection_end)) { |
| return; |
| } |
| |
| // If the unignored selection has not been computed yet, compute it now. |
| AXSelection unignored_selection; |
| if (!selection) { |
| unignored_selection = GetDelegate()->GetUnignoredSelection(); |
| selection = &unignored_selection; |
| } |
| DCHECK(selection); |
| GetSelectionOffsetsFromTree(selection, selection_start, selection_end); |
| } |
| |
| int AXPlatformNodeBase::GetCaretOffset() { |
| if (IsAtomicTextField()) { |
| return GetIntAttribute(ax::mojom::IntAttribute::kTextSelEnd); |
| } |
| |
| // If the unignored selection has not been computed yet, compute it now. |
| AXSelection unignored_selection = GetDelegate()->GetUnignoredSelection(); |
| int selection_start, selection_end; |
| GetSelectionOffsetsFromTree(&unignored_selection, &selection_start, |
| &selection_end, /*caret_only*/ true); |
| |
| return selection_end; |
| } |
| |
| void AXPlatformNodeBase::GetSelectionOffsetsFromTree( |
| const AXSelection* selection, |
| int* selection_start, |
| int* selection_end, |
| bool caret_only) { |
| DCHECK(selection_start && selection_end); |
| |
| *selection_start = GetSelectionAnchor(selection); |
| *selection_end = GetSelectionFocus(selection); |
| if (*selection_start < 0 || *selection_end < 0) |
| return; |
| |
| // There are three cases when a selection would start and end on the same |
| // character: |
| // 1. Anchor and focus are both in a subtree that is to the right of this |
| // object. |
| // 2. Anchor and focus are both in a subtree that is to the left of this |
| // object. |
| // 3. Anchor and focus are in a subtree represented by a single embedded |
| // object character. |
| // Only case 3 refers to a valid selection because cases 1 and 2 fall |
| // outside this object in their entirety. |
| // Selections that span more than one character are by definition inside |
| // this object, so checking them is not necessary. |
| if (*selection_start == *selection_end && !HasVisibleCaretOrSelection()) { |
| *selection_start = -1; |
| *selection_end = -1; |
| return; |
| } |
| |
| if (caret_only) { |
| // Just return the offsets, skipping the below computation that returns |
| // and end offset after an embedded object character when the selection |
| // ends wihin the descendant subtree. |
| return; |
| } |
| |
| // The IA2 Spec says that if the largest of the two offsets falls on an |
| // embedded object character and if there is a selection in that embedded |
| // object, it should be incremented by one so that it points after the |
| // embedded object character. |
| // This is a signal to AT software that the embedded object is also part of |
| // the selection. |
| int* largest_offset = |
| (*selection_start <= *selection_end) ? selection_end : selection_start; |
| const std::map<int, int>& offset_to_child_index = |
| GetDelegate()->GetHypertextOffsetToHyperlinkChildIndex(); |
| auto index_iter = offset_to_child_index.find(*largest_offset); |
| if (index_iter == offset_to_child_index.end()) |
| return; |
| |
| int child_index = index_iter->second; |
| DCHECK_GE(child_index, 0); |
| DCHECK_LT(static_cast<size_t>(child_index), GetChildCount()); |
| AXPlatformNodeBase* hyperlink = static_cast<AXPlatformNodeBase*>( |
| AXPlatformNode::FromNativeViewAccessible(ChildAtIndex(child_index))); |
| if (!hyperlink) |
| return; |
| |
| int hyperlink_selection_start, hyperlink_selection_end; |
| hyperlink->GetSelectionOffsets(selection, &hyperlink_selection_start, |
| &hyperlink_selection_end); |
| if (hyperlink_selection_start >= 0 && hyperlink_selection_end >= 0 && |
| hyperlink_selection_start != hyperlink_selection_end) { |
| ++(*largest_offset); |
| } |
| } |
| |
| bool AXPlatformNodeBase::IsSameHypertextCharacter( |
| const AXLegacyHypertext& old_hypertext, |
| size_t old_char_index, |
| size_t new_char_index) { |
| if (old_char_index >= old_hypertext.hypertext.size() || |
| new_char_index >= hypertext_.hypertext.size()) { |
| return false; |
| } |
| |
| // For anything other than the "embedded character", we just compare the |
| // characters directly. |
| char16_t old_ch = old_hypertext.hypertext[old_char_index]; |
| char16_t new_ch = hypertext_.hypertext[new_char_index]; |
| if (old_ch != new_ch) |
| return false; |
| if (new_ch != kEmbeddedCharacter) |
| return true; |
| |
| // If it's an embedded character, they're only identical if the child id |
| // the hyperlink points to is the same. |
| const auto& old_offset_to_index = old_hypertext.hyperlink_offset_to_index; |
| const std::vector<int32_t>& old_hyperlinks = old_hypertext.hyperlinks; |
| int32_t old_hyperlinkscount = static_cast<int32_t>(old_hyperlinks.size()); |
| auto iter = old_offset_to_index.find(static_cast<int32_t>(old_char_index)); |
| int old_index = (iter != old_offset_to_index.end()) ? iter->second : -1; |
| int old_child_id = (old_index >= 0 && old_index < old_hyperlinkscount) |
| ? old_hyperlinks[old_index] |
| : -1; |
| |
| const auto& new_offset_to_index = hypertext_.hyperlink_offset_to_index; |
| const std::vector<int32_t>& new_hyperlinks = hypertext_.hyperlinks; |
| int32_t new_hyperlinkscount = static_cast<int32_t>(new_hyperlinks.size()); |
| iter = new_offset_to_index.find(static_cast<int32_t>(new_char_index)); |
| int new_index = (iter != new_offset_to_index.end()) ? iter->second : -1; |
| int new_child_id = (new_index >= 0 && new_index < new_hyperlinkscount) |
| ? new_hyperlinks[new_index] |
| : -1; |
| |
| return old_child_id == new_child_id; |
| } |
| |
| // Return true if the index represents a text character. |
| bool AXPlatformNodeBase::IsText(const std::u16string& text, |
| size_t index, |
| bool is_indexed_from_end) { |
| size_t text_len = text.size(); |
| if (index == text_len) |
| return false; |
| auto ch = text[is_indexed_from_end ? text_len - index - 1 : index]; |
| return ch != kEmbeddedCharacter; |
| } |
| |
| bool AXPlatformNodeBase::IsPlatformCheckable() const { |
| return GetData().HasCheckedState(); |
| } |
| |
| void AXPlatformNodeBase::ComputeHypertextRemovedAndInserted( |
| const AXLegacyHypertext& old_hypertext, |
| size_t* start, |
| size_t* old_len, |
| size_t* new_len) { |
| *start = 0; |
| *old_len = 0; |
| *new_len = 0; |
| |
| // Do not compute for text objects, otherwise redundant text change |
| // announcements will occur in live regions, as the parent hypertext also |
| // changes. |
| if (IsText()) |
| return; |
| |
| const std::u16string& old_text = old_hypertext.hypertext; |
| const std::u16string& new_text = hypertext_.hypertext; |
| |
| // TODO(accessibility) Plumb through which part of text changed so we don't |
| // have to guess what changed based on character differences. This can be |
| // wrong in some cases as follows: |
| // -- EDITABLE -- |
| // If editable: when part of the text node changes, assume only that part |
| // changed, and not the entire thing. For example, if "car" changes to |
| // "cat", assume only 1 letter changed. This code compares common characters |
| // to guess what has changed. |
| // -- NOT EDITABLE -- |
| // When part of the text changes, assume the entire node's text changed. For |
| // example, if "car" changes to "cat" then assume all 3 letters changed. |
| // Note, it is possible (though rare) that CharacterData methods are used to |
| // remove, insert, replace or append a substring. |
| bool allow_partial_text_node_changes = HasState(ax::mojom::State::kEditable); |
| size_t prefix_index = 0; |
| size_t common_prefix = 0; |
| while (prefix_index < old_text.size() && prefix_index < new_text.size() && |
| IsSameHypertextCharacter(old_hypertext, prefix_index, prefix_index)) { |
| ++prefix_index; |
| if (allow_partial_text_node_changes || |
| (!IsText(old_text, prefix_index) && !IsText(new_text, prefix_index))) { |
| common_prefix = prefix_index; |
| } |
| } |
| |
| size_t suffix_index = 0; |
| size_t common_suffix = 0; |
| while (common_prefix + suffix_index < old_text.size() && |
| common_prefix + suffix_index < new_text.size() && |
| IsSameHypertextCharacter(old_hypertext, |
| old_text.size() - suffix_index - 1, |
| new_text.size() - suffix_index - 1)) { |
| ++suffix_index; |
| if (allow_partial_text_node_changes || |
| (!IsText(old_text, suffix_index, true) && |
| !IsText(new_text, suffix_index, true))) { |
| common_suffix = suffix_index; |
| } |
| } |
| |
| *start = common_prefix; |
| *old_len = old_text.size() - common_prefix - common_suffix; |
| *new_len = new_text.size() - common_prefix - common_suffix; |
| } |
| |
| int AXPlatformNodeBase::FindTextBoundary( |
| ax::mojom::TextBoundary boundary, |
| int offset, |
| ax::mojom::MoveDirection direction, |
| ax::mojom::TextAffinity affinity) const { |
| DCHECK_NE(boundary, ax::mojom::TextBoundary::kNone); |
| |
| const AXPosition position = |
| GetDelegate()->CreateTextPositionAt(offset, affinity); |
| // On Windows and Linux ATK, searching for a text boundary should always stop |
| // at the boundary of the current object. |
| AXMovementOptions options{AXBoundaryBehavior::kStopAtAnchorBoundary, |
| AXBoundaryDetection::kDontCheckInitialPosition}; |
| // On Windows and Linux ATK, it is standard text navigation behavior to stop |
| // if we are searching in the backwards direction and the current position is |
| // already at the required text boundary. |
| if (direction == ax::mojom::MoveDirection::kBackward) { |
| options.boundary_detection = AXBoundaryDetection::kCheckInitialPosition; |
| } |
| |
| const AXPosition boundary_position = |
| position->CreatePositionAtTextBoundary(boundary, direction, options); |
| if (boundary_position->IsNullPosition()) |
| return -1; |
| DCHECK_EQ(boundary_position->GetAnchor(), position->GetAnchor()); |
| DCHECK_GE(boundary_position->text_offset(), 0); |
| return boundary_position->text_offset(); |
| } |
| |
| AXPlatformNodeBase* AXPlatformNodeBase::NearestLeafToPoint( |
| gfx::Point point) const { |
| // First, scope the search to the node that contains point. |
| AXPlatformNodeBase* nearest_node = |
| static_cast<AXPlatformNodeBase*>(AXPlatformNode::FromNativeViewAccessible( |
| GetDelegate()->HitTestSync(point.x(), point.y()))); |
| |
| if (!nearest_node) |
| return nullptr; |
| |
| AXPlatformNodeBase* parent = nearest_node; |
| // GetFirstChild does not consider if the parent is a leaf. |
| AXPlatformNodeBase* current_descendant = |
| parent->GetChildCount() ? parent->GetFirstChild() : nullptr; |
| AXPlatformNodeBase* nearest_descendant = nullptr; |
| float shortest_distance; |
| while (parent && current_descendant) { |
| // Manhattan Distance is used to provide faster distance estimates. |
| float current_distance = current_descendant->GetDelegate() |
| ->GetClippedScreenBoundsRect() |
| .ManhattanDistanceToPoint(point); |
| |
| if (!nearest_descendant || current_distance < shortest_distance) { |
| shortest_distance = current_distance; |
| nearest_descendant = current_descendant; |
| } |
| |
| // Traverse |
| AXPlatformNodeBase* next_sibling = current_descendant->GetNextSibling(); |
| if (next_sibling) { |
| current_descendant = next_sibling; |
| } else { |
| // We have gone through all siblings, update nearest and descend if |
| // possible. |
| if (nearest_descendant) { |
| nearest_node = nearest_descendant; |
| // If the nearest node is a leaf that does not have a child tree, break. |
| if (!nearest_node->GetChildCount()) |
| break; |
| |
| parent = nearest_node; |
| current_descendant = parent->GetFirstChild(); |
| |
| // Reset nearest_descendant to force the nearest node to be a descendant |
| // of "parent". |
| nearest_descendant = nullptr; |
| } |
| } |
| } |
| return nearest_node; |
| } |
| |
| int AXPlatformNodeBase::NearestTextIndexToPoint(gfx::Point point) { |
| // For text objects, find the text position nearest to the point.The nearest |
| // index of a non-text object is implicitly 0. Text fields such as textarea |
| // have an embedded div inside them that holds all the text, |
| // GetRangeBoundsRect will correctly handle these nodes |
| int nearest_index = 0; |
| const AXCoordinateSystem coordinate_system = AXCoordinateSystem::kScreenDIPs; |
| const AXClippingBehavior clipping_behavior = AXClippingBehavior::kUnclipped; |
| |
| // Manhattan Distance is used to provide faster distance estimates. |
| // get the distance from the point to the bounds of each character. |
| float shortest_distance = GetDelegate() |
| ->GetInnerTextRangeBoundsRect( |
| 0, 1, coordinate_system, clipping_behavior) |
| .ManhattanDistanceToPoint(point); |
| for (int i = 1, text_length = GetTextContentLengthUTF16(); i < text_length; |
| ++i) { |
| float current_distance = |
| GetDelegate() |
| ->GetInnerTextRangeBoundsRect(i, i + 1, coordinate_system, |
| clipping_behavior) |
| .ManhattanDistanceToPoint(point); |
| if (current_distance < shortest_distance) { |
| shortest_distance = current_distance; |
| nearest_index = i; |
| } |
| } |
| return nearest_index; |
| } |
| |
| TextAttributeList AXPlatformNodeBase::ComputeTextAttributes() const { |
| TextAttributeList attributes; |
| |
| // From the IA2 Spec: |
| // Occasionally, word processors will automatically generate characters which |
| // appear on a line along with editable text. The characters are not |
| // themselves editable, but are part of the document. The most common examples |
| // of automatically inserted characters are in bulleted and numbered lists. |
| if (HasBoolAttribute(ax::mojom::BoolAttribute::kNotUserSelectableStyle)) { |
| // From IA2 text attribute guide: |
| // this attribute's value is “true” for list bullet/numbering prefix text or |
| // layout-inserted text such as via the CSS pseudo styles :before or :after. |
| attributes.emplace_back("auto-generated", "true"); |
| } |
| |
| AXPlatformNodeDelegate* const delegate = GetDelegate(); |
| int color; |
| if ((color = delegate->GetBackgroundColor())) { |
| unsigned int red = SkColorGetR(color); |
| unsigned int green = SkColorGetG(color); |
| unsigned int blue = SkColorGetB(color); |
| std::string color_value = "rgb(" + base::NumberToString(red) + ',' + |
| base::NumberToString(green) + ',' + |
| base::NumberToString(blue) + ')'; |
| SanitizeTextAttributeValue(color_value, &color_value); |
| attributes.emplace_back(std::make_pair("background-color", color_value)); |
| } |
| |
| if ((color = delegate->GetColor())) { |
| unsigned int red = SkColorGetR(color); |
| unsigned int green = SkColorGetG(color); |
| unsigned int blue = SkColorGetB(color); |
| std::string color_value = "rgb(" + base::NumberToString(red) + ',' + |
| base::NumberToString(green) + ',' + |
| base::NumberToString(blue) + ')'; |
| SanitizeTextAttributeValue(color_value, &color_value); |
| attributes.emplace_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(delegate->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.emplace_back(std::make_pair("font-family", font_family)); |
| } |
| |
| std::optional<float> font_size_in_points = GetFontSizeInPoints(); |
| // Attribute has no default value. |
| if (font_size_in_points) { |
| attributes.emplace_back(std::make_pair( |
| "font-size", base::NumberToString(*font_size_in_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 (HasTextStyle(ax::mojom::TextStyle::kBold)) |
| attributes.emplace_back(std::make_pair("font-weight", "bold")); |
| if (HasTextStyle(ax::mojom::TextStyle::kItalic)) |
| attributes.emplace_back(std::make_pair("font-style", "italic")); |
| if (HasTextStyle(ax::mojom::TextStyle::kLineThrough)) { |
| // TODO(nektar): Figure out a more specific value. |
| attributes.emplace_back( |
| std::make_pair("text-line-through-style", "solid")); |
| } |
| if (HasTextStyle(ax::mojom::TextStyle::kUnderline)) { |
| // TODO(nektar): Figure out a more specific value. |
| attributes.emplace_back(std::make_pair("text-underline-style", "solid")); |
| } |
| } |
| |
| std::string language = delegate->GetLanguage(); |
| if (!language.empty()) { |
| SanitizeTextAttributeValue(language, &language); |
| attributes.emplace_back(std::make_pair("language", language)); |
| } |
| |
| auto text_direction = static_cast<ax::mojom::WritingDirection>( |
| GetIntAttribute(ax::mojom::IntAttribute::kTextDirection)); |
| switch (text_direction) { |
| case ax::mojom::WritingDirection::kNone: |
| break; |
| case ax::mojom::WritingDirection::kLtr: |
| attributes.emplace_back(std::make_pair("writing-mode", "lr")); |
| break; |
| case ax::mojom::WritingDirection::kRtl: |
| attributes.emplace_back(std::make_pair("writing-mode", "rl")); |
| break; |
| case ax::mojom::WritingDirection::kTtb: |
| attributes.emplace_back(std::make_pair("writing-mode", "tb")); |
| break; |
| case ax::mojom::WritingDirection::kBtt: |
| // Not listed in the IA2 Spec. |
| attributes.emplace_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.emplace_back(std::make_pair("text-position", "sub")); |
| break; |
| case ax::mojom::TextPosition::kSuperscript: |
| attributes.emplace_back(std::make_pair("text-position", "super")); |
| break; |
| } |
| |
| return attributes; |
| } |
| |
| int AXPlatformNodeBase::GetSelectionCount() const { |
| int max_items = GetMaxSelectableItems(); |
| if (!max_items) |
| return 0; |
| return GetSelectedItems(max_items); |
| } |
| |
| AXPlatformNodeBase* AXPlatformNodeBase::GetSelectedItem( |
| int selected_index) const { |
| DCHECK_GE(selected_index, 0); |
| int max_items = GetMaxSelectableItems(); |
| if (max_items == 0) |
| return nullptr; |
| if (selected_index >= max_items) |
| return nullptr; |
| |
| std::vector<AXPlatformNodeBase*> selected_children; |
| int requested_count = selected_index + 1; |
| int returned_count = GetSelectedItems(requested_count, &selected_children); |
| |
| if (returned_count <= selected_index) |
| return nullptr; |
| |
| DCHECK(!selected_children.empty()); |
| DCHECK_LT(selected_index, static_cast<int>(selected_children.size())); |
| return selected_children[selected_index]; |
| } |
| |
| int AXPlatformNodeBase::GetSelectedItems( |
| int max_items, |
| std::vector<AXPlatformNodeBase*>* out_selected_items) const { |
| int selected_count = 0; |
| for (auto child_iter = AXPlatformNodeChildrenBegin(), |
| child_end = AXPlatformNodeChildrenEnd(); |
| child_iter != child_end && selected_count < max_items; ++child_iter) { |
| if (!IsItemLike(child_iter->GetRole())) { |
| selected_count += child_iter->GetSelectedItems(max_items - selected_count, |
| out_selected_items); |
| } else if (child_iter->GetBoolAttribute( |
| ax::mojom::BoolAttribute::kSelected)) { |
| selected_count++; |
| if (out_selected_items) |
| out_selected_items->emplace_back(child_iter.get()); |
| } |
| } |
| return selected_count; |
| } |
| |
| void AXPlatformNodeBase::SanitizeTextAttributeValue(const std::string& input, |
| std::string* output) const { |
| DCHECK(output); |
| } |
| |
| bool AXPlatformNodeBase::IsDescribedByTooltip() const { |
| const std::vector<int32_t>& description_ids = |
| GetIntListAttribute(ax::mojom::IntListAttribute::kDescribedbyIds); |
| |
| std::string description_from; |
| |
| for (int id : description_ids) { |
| AXPlatformNodeBase* description_object = |
| static_cast<AXPlatformNodeBase*>(GetDelegate()->GetFromNodeID(id)); |
| if (description_object && |
| description_object->GetRole() == ax::mojom::Role::kTooltip) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| std::string AXPlatformNodeBase::ComputeDetailsRoles() const { |
| const std::vector<int32_t>& details_ids = |
| GetIntListAttribute(ax::mojom::IntListAttribute::kDetailsIds); |
| if (details_ids.empty()) |
| return std::string(); |
| |
| std::set<std::string> details_roles_set; |
| |
| AXPlatformNodeDelegate* const delegate = GetDelegate(); |
| for (int id : details_ids) { |
| AXPlatformNodeBase* detail_object = |
| static_cast<AXPlatformNodeBase*>(delegate->GetFromNodeID(id)); |
| if (!detail_object) |
| continue; |
| switch (detail_object->GetRole()) { |
| case ax::mojom::Role::kComment: |
| details_roles_set.insert("comment"); |
| break; |
| case ax::mojom::Role::kDefinition: |
| details_roles_set.insert("definition"); |
| break; |
| case ax::mojom::Role::kDocEndnote: |
| details_roles_set.insert("doc-endnote"); |
| break; |
| case ax::mojom::Role::kDocFootnote: |
| details_roles_set.insert("doc-footnote"); |
| break; |
| case ax::mojom::Role::kGroup: |
| case ax::mojom::Role::kRegion: { |
| if (DescendantHasComment(detail_object)) { |
| details_roles_set.insert("comment"); |
| break; |
| } |
| [[fallthrough]]; |
| } |
| default: |
| // If a popover of any kind, use "popover" -- technically this is not a |
| // role, and therefore, details-roles is more of a hints field. Use * to |
| // indicate some other role. |
| if (detail_object->GetDelegate()->node()->HasIntAttribute( |
| ax::mojom::IntAttribute::kIsPopup)) { |
| details_roles_set.insert("popover"); |
| } else { |
| details_roles_set.insert("*"); |
| } |
| break; |
| } |
| } |
| |
| // Create space delimited list of types. The set will not be large, as there |
| // are not very many possible types. |
| std::vector<std::string> details_roles_vector(details_roles_set.begin(), |
| details_roles_set.end()); |
| return base::JoinString(details_roles_vector, " "); |
| } |
| |
| // static |
| bool AXPlatformNodeBase::DescendantHasComment(const AXPlatformNodeBase* node) { |
| // These should still report comment if there are comments inside them. |
| constexpr size_t kMaxChildrenToCheck = 8; |
| constexpr size_t kMaxDepthToCheck = 4; |
| if (FindDescendantRoleWithMaxDepth(node, ax::mojom::Role::kComment, |
| kMaxDepthToCheck, kMaxChildrenToCheck)) { |
| return true; |
| } |
| return false; |
| } |
| |
| int AXPlatformNodeBase::GetMaxSelectableItems() const { |
| if (IsLeaf()) |
| return 0; |
| |
| if (!IsContainerWithSelectableChildren(GetRole())) |
| return 0; |
| |
| int max_items = 1; |
| if (HasState(ax::mojom::State::kMultiselectable)) |
| max_items = std::numeric_limits<int>::max(); |
| return max_items; |
| } |
| |
| } // namespace ui |