| // Copyright 2013 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/views/accessibility/view_ax_platform_node_delegate.h" |
| |
| #include <algorithm> |
| #include <memory> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/check_op.h" |
| #include "base/containers/adapters.h" |
| #include "base/functional/bind.h" |
| #include "base/i18n/rtl.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/no_destructor.h" |
| #include "base/notimplemented.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "build/build_config.h" |
| #include "ui/accessibility/ax_action_data.h" |
| #include "ui/accessibility/ax_enums.mojom.h" |
| #include "ui/accessibility/ax_role_properties.h" |
| #include "ui/accessibility/ax_selection.h" |
| #include "ui/accessibility/ax_tree_id.h" |
| #include "ui/accessibility/platform/ax_platform_node.h" |
| #include "ui/accessibility/platform/ax_platform_node_base.h" |
| #include "ui/base/layout.h" |
| #include "ui/views/accessibility/atomic_view_ax_tree_manager.h" |
| #include "ui/views/accessibility/ax_virtual_view.h" |
| #include "ui/views/accessibility/view_accessibility.h" |
| #include "ui/views/accessibility/view_accessibility_utils.h" |
| #include "ui/views/cascading_property.h" |
| #include "ui/views/controls/native/native_view_host.h" |
| #include "ui/views/controls/table/table_view.h" |
| #include "ui/views/view.h" |
| #include "ui/views/widget/widget.h" |
| |
| namespace views { |
| |
| namespace { |
| |
| // Information required to fire a delayed accessibility event. |
| struct QueuedEvent { |
| QueuedEvent(ax::mojom::Event type, int32_t node_id) |
| : type(type), node_id(node_id) {} |
| |
| ax::mojom::Event type; |
| int32_t node_id; |
| }; |
| |
| std::vector<QueuedEvent>& GetEventQueue() { |
| static base::NoDestructor<std::vector<QueuedEvent>> event_queue; |
| return *event_queue; |
| } |
| |
| // g_is_queueing_events is set to true when we are in the "queueing events" |
| // state. It is set to true in PostFlushEventQueueTaskIfNecessary(), and |
| // is set to false once we start the async task FlushQueue(). |
| // This allows us to delay an event while preserving ordering by |
| // queueing events rather than firing them immediately, allowing us to |
| // queue any event that is fired after PostFlushEventQueueTaskIfNecessary() |
| // is called, until we begin to flush events. |
| bool g_is_queueing_events = false; |
| // g_is_flushing is true only when we are iterating over GetEventQueue() in |
| // FlushQueue(). While flushing, no new events should be added to the queue, see |
| // https://crbug.com/358404368 |
| bool g_is_flushing = false; |
| |
| ui::AXPlatformNode* FromNativeWindow(gfx::NativeWindow native_window) { |
| Widget* widget = Widget::GetWidgetForNativeWindow(native_window); |
| if (!widget) { |
| return nullptr; |
| } |
| |
| View* view = widget->GetRootView(); |
| if (!view) { |
| return nullptr; |
| } |
| |
| gfx::NativeViewAccessible native_view_accessible = |
| view->GetNativeViewAccessible(); |
| if (!native_view_accessible) { |
| return nullptr; |
| } |
| |
| return ui::AXPlatformNode::FromNativeViewAccessible(native_view_accessible); |
| } |
| |
| ui::AXPlatformNode* PlatformNodeFromNodeID(int32_t id) { |
| // Note: For Views, node IDs and unique IDs are the same - but that isn't |
| // necessarily true for all AXPlatformNodes. |
| return ui::AXPlatformNodeBase::GetFromUniqueId(id); |
| } |
| |
| void FireEvent(QueuedEvent event) { |
| ui::AXPlatformNode* node = PlatformNodeFromNodeID(event.node_id); |
| if (node) { |
| node->NotifyAccessibilityEvent(event.type); |
| } |
| } |
| |
| void FlushQueue() { |
| DCHECK(g_is_queueing_events); |
| g_is_queueing_events = false; |
| g_is_flushing = true; |
| for (QueuedEvent event : GetEventQueue()) { |
| FireEvent(event); |
| } |
| g_is_flushing = false; |
| GetEventQueue().clear(); |
| } |
| |
| void PostFlushEventQueueTaskIfNecessary() { |
| if (!g_is_queueing_events) { |
| g_is_queueing_events = true; |
| base::OnceCallback<void()> cb = base::BindOnce(&FlushQueue); |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(FROM_HERE, |
| std::move(cb)); |
| } |
| } |
| |
| } // namespace |
| |
| ViewAXPlatformNodeDelegate::ChildWidgetsResult::ChildWidgetsResult() = default; |
| |
| ViewAXPlatformNodeDelegate::ChildWidgetsResult::ChildWidgetsResult( |
| std::vector<raw_ptr<Widget, VectorExperimental>> child_widgets, |
| bool is_tab_modal_showing) |
| : child_widgets(child_widgets), |
| is_tab_modal_showing(is_tab_modal_showing) {} |
| ViewAXPlatformNodeDelegate::ChildWidgetsResult::ChildWidgetsResult( |
| const ViewAXPlatformNodeDelegate::ChildWidgetsResult& other) = default; |
| |
| ViewAXPlatformNodeDelegate::ChildWidgetsResult::~ChildWidgetsResult() = default; |
| |
| ViewAXPlatformNodeDelegate::ChildWidgetsResult& |
| ViewAXPlatformNodeDelegate::ChildWidgetsResult::operator=( |
| const ViewAXPlatformNodeDelegate::ChildWidgetsResult& other) = default; |
| |
| ViewAXPlatformNodeDelegate::ViewAXPlatformNodeDelegate(View* view) |
| : ViewAccessibility(view) {} |
| |
| void ViewAXPlatformNodeDelegate::Init() { |
| ax_platform_node_ = ui::AXPlatformNode::Create(*this); |
| DCHECK(ax_platform_node_); |
| |
| static bool first_time = true; |
| if (first_time) { |
| ui::AXPlatformNode::RegisterNativeWindowHandler( |
| base::BindRepeating(&FromNativeWindow)); |
| first_time = false; |
| } |
| } |
| |
| ViewAXPlatformNodeDelegate::~ViewAXPlatformNodeDelegate() { |
| if (ui::AXPlatformNode::GetPopupFocusOverride() == |
| ax_platform_node_->GetNativeViewAccessible()) { |
| ui::AXPlatformNode::SetPopupFocusOverride(gfx::NativeViewAccessible()); |
| } |
| } |
| |
| void ViewAXPlatformNodeDelegate::EnsureAtomicViewAXTreeManager() { |
| if (atomic_view_ax_tree_manager_) { |
| return; |
| } |
| |
| atomic_view_ax_tree_manager_ = |
| views::AtomicViewAXTreeManager::Create(this, GetData()); |
| } |
| |
| bool ViewAXPlatformNodeDelegate::IsAccessibilityFocusable() const { |
| return GetData().HasState(ax::mojom::State::kFocusable); |
| } |
| |
| bool ViewAXPlatformNodeDelegate::IsFocusedForTesting() const { |
| if (ui::AXPlatformNode::GetPopupFocusOverride()) { |
| return ui::AXPlatformNode::GetPopupFocusOverride() == |
| GetNativeViewAccessible(); |
| } |
| |
| return ViewAccessibility::IsFocusedForTesting(); |
| } |
| |
| void ViewAXPlatformNodeDelegate::SetPopupFocusOverride() { |
| ui::AXPlatformNode::SetPopupFocusOverride(GetNativeViewAccessible()); |
| } |
| |
| void ViewAXPlatformNodeDelegate::EndPopupFocusOverride() { |
| ui::AXPlatformNode::SetPopupFocusOverride(gfx::NativeViewAccessible()); |
| } |
| |
| void ViewAXPlatformNodeDelegate::FireFocusAfterMenuClose() { |
| ui::AXPlatformNodeBase* focused_node = |
| static_cast<ui::AXPlatformNodeBase*>(ax_platform_node_.get()); |
| // Continue to drill down focused nodes to get to the "deepest" node that is |
| // focused. This is not necessarily a view. It could be web content. |
| while (focused_node) { |
| ui::AXPlatformNodeBase* deeper_focus = static_cast<ui::AXPlatformNodeBase*>( |
| ui::AXPlatformNode::FromNativeViewAccessible(focused_node->GetFocus())); |
| if (!deeper_focus || deeper_focus == focused_node) { |
| break; |
| } |
| focused_node = deeper_focus; |
| } |
| if (focused_node) { |
| // Callback used for testing. |
| if (accessibility_events_callback_) { |
| accessibility_events_callback_.Run( |
| this, ax::mojom::Event::kFocusAfterMenuClose); |
| } |
| |
| focused_node->NotifyAccessibilityEvent( |
| ax::mojom::Event::kFocusAfterMenuClose); |
| } |
| } |
| |
| gfx::NativeViewAccessible ViewAXPlatformNodeDelegate::GetNativeObject() const { |
| DCHECK(ax_platform_node_); |
| return ax_platform_node_->GetNativeViewAccessible(); |
| } |
| |
| void ViewAXPlatformNodeDelegate::FireNativeEvent(ax::mojom::Event event_type) { |
| DCHECK(ax_platform_node_); |
| Widget* const widget = view()->GetWidget(); |
| if (!widget || widget->IsClosed()) { |
| return; |
| } |
| |
| if (g_is_flushing) { |
| // TODO(https://crbug.com/358404368): Add dump_will_be_check |
| // once all known instances where this would be hit are cleaned up. |
| // Do not fire new events while the queue is being flushed. |
| return; |
| } |
| |
| if (event_type == ax::mojom::Event::kAlert) { |
| // Do not queue alert events for later. They must be dealt with |
| // before the window potentially closes, and they can be fired |
| // out of order relative to other events. |
| ax_platform_node_->NotifyAccessibilityEvent(event_type); |
| return; |
| } |
| |
| if (accessibility_events_callback_) { |
| accessibility_events_callback_.Run(this, event_type); |
| } |
| |
| if (g_is_queueing_events) { |
| GetEventQueue().emplace_back(event_type, GetUniqueId()); |
| return; |
| } |
| |
| // Some events have special handling. |
| switch (event_type) { |
| case ax::mojom::Event::kFocusAfterMenuClose: { |
| DCHECK(!ui::AXPlatformNode::GetPopupFocusOverride()) |
| << "Must call ViewAccessibility::EndPopupFocusOverride() as menu " |
| "closes."; |
| break; |
| } |
| case ax::mojom::Event::kFocus: { |
| if (ui::AXPlatformNode::GetPopupFocusOverride()) { |
| DCHECK_EQ(ui::AXPlatformNode::GetPopupFocusOverride(), |
| GetNativeViewAccessible()) |
| << "If the popup focus override is on, then the kFocus event must " |
| "match it. Most likely the popup has closed, but did not call " |
| "ViewAccessibility::EndPopupFocusOverride(), and focus has " |
| "now moved on."; |
| } |
| break; |
| } |
| case ax::mojom::Event::kFocusContext: { |
| // A focus context event is intended to send a focus event and a delay |
| // before the next focus event. It makes sense to delay the entire next |
| // synchronous batch of next events so that ordering remains the same. |
| // Begin queueing subsequent events and flush queue asynchronously. |
| PostFlushEventQueueTaskIfNecessary(); |
| break; |
| } |
| case ax::mojom::Event::kLiveRegionChanged: { |
| // Fire after a delay so that screen readers don't wipe it out when |
| // another user-generated event fires simultaneously. |
| PostFlushEventQueueTaskIfNecessary(); |
| GetEventQueue().emplace_back(event_type, GetUniqueId()); |
| return; |
| } |
| default: |
| break; |
| } |
| |
| // Fire events here now that our internal state is up-to-date. |
| ax_platform_node_->NotifyAccessibilityEvent(event_type); |
| } |
| |
| #if BUILDFLAG(IS_MAC) |
| void ViewAXPlatformNodeDelegate::AnnounceTextAs( |
| const std::u16string& text, |
| ui::AXPlatformNode::AnnouncementType announcement_type) { |
| ax_platform_node_->AnnounceTextAs(text, announcement_type); |
| } |
| #endif // BUILDFLAG(IS_MAC) |
| |
| const ui::AXNodeData& ViewAXPlatformNodeDelegate::GetData() const { |
| // Clear computed node so AXNode::GetInnerText() value won't be stale. |
| if (atomic_view_ax_tree_manager_) { |
| // We can't call `GetRoot()->ClearComputedNodeData()` from here since |
| // `AtomicViewAXTreeManager::GetRoot` calls this function |
| // (`ViewAXplatformNodeDelegate::GetData`), which leads to an infinite loop. |
| // |
| // TODO(crbug.com/40924888): This code is temporary until the ViewsAX |
| // project is completed. |
| atomic_view_ax_tree_manager_->ClearComputedRootData(); |
| } |
| |
| // Clear the data, then populate it. |
| data_ = ui::AXNodeData(); |
| |
| if (!ignore_missing_widget_for_testing_ && !view()->GetWidget()) { |
| // This is to be consistent with what Views expect and what is being done in |
| // ViewAccessibility::GetAccessibleNodeData if the widget is null. |
| data_.role = ax::mojom::Role::kUnknown; |
| data_.SetRestriction(ax::mojom::Restriction::kDisabled); |
| return data_; |
| } |
| |
| GetAccessibleNodeData(&data_); |
| |
| return data_; |
| } |
| |
| size_t ViewAXPlatformNodeDelegate::GetChildCount() const { |
| // We call `ViewAccessibility::IsLeaf` here instead of our own override |
| // because our class has an expanded definition of what a leaf node is, which |
| // includes all nodes with zero unignored children. Calling our own override |
| // would create a circular definition of what a "leaf node" is. |
| if (ViewAccessibility::IsLeaf()) { |
| return 0; |
| } |
| |
| // If present, virtual view children override any real children. |
| if (!virtual_children().empty()) { |
| // Ignored virtual views are not exposed in any accessibility platform APIs. |
| // Remove all ignored virtual view children and recursively replace them |
| // with their unignored children count. |
| size_t virtual_child_count = 0; |
| for (const std::unique_ptr<AXVirtualView>& virtual_child : |
| virtual_children()) { |
| if (virtual_child->IsIgnored()) { |
| virtual_child_count += virtual_child->GetChildCount(); |
| } else { |
| ++virtual_child_count; |
| } |
| } |
| |
| // A virtual views subtree hides any real view children. |
| return virtual_child_count; |
| } |
| |
| const ChildWidgetsResult child_widgets_result = GetChildWidgets(); |
| if (child_widgets_result.is_tab_modal_showing) { |
| // In order to support the "read title (NVDAKey+T)" and "read window |
| // (NVDAKey+B)" commands in the NVDA screen reader, hide the rest of the UI |
| // from the accessibility tree when a modal dialog is showing. |
| DCHECK_EQ(child_widgets_result.child_widgets.size(), 1U); |
| return 1; |
| } |
| |
| // Ignored views are not exposed in any accessibility platform APIs. Remove |
| // all ignored view children and recursively replace them with their unignored |
| // children count. This matches how AXPlatformNodeDelegate::GetChildCount() |
| // behaves for Web content. |
| size_t view_child_count = 0; |
| for (View* child : view()->children()) { |
| const ViewAccessibility& view_accessibility = child->GetViewAccessibility(); |
| if (view_accessibility.GetIsIgnored()) { |
| const auto* child_view_delegate = |
| static_cast<const ViewAXPlatformNodeDelegate*>(&view_accessibility); |
| DCHECK(child_view_delegate); |
| view_child_count += child_view_delegate->GetChildCount(); |
| } else { |
| ++view_child_count; |
| } |
| } |
| |
| return view_child_count + child_widgets_result.child_widgets.size(); |
| } |
| |
| gfx::NativeViewAccessible ViewAXPlatformNodeDelegate::ChildAtIndex( |
| size_t index) const { |
| DCHECK_LT(index, GetChildCount()) |
| << "|index| should be less than the unignored child count."; |
| if (IsLeaf()) { |
| return gfx::NativeViewAccessible(); |
| } |
| |
| if (!virtual_children().empty()) { |
| // A virtual views subtree hides all the real view children. |
| for (const std::unique_ptr<AXVirtualView>& virtual_child : |
| virtual_children()) { |
| if (virtual_child->IsIgnored()) { |
| size_t virtual_child_count = virtual_child->GetChildCount(); |
| if (index < virtual_child_count) { |
| return virtual_child->ChildAtIndex(index); |
| } |
| index -= virtual_child_count; |
| } else { |
| if (index == 0) { |
| return virtual_child->GetNativeObject(); |
| } |
| --index; |
| } |
| } |
| |
| NOTREACHED() << "|index| should be less than the unignored child count."; |
| } |
| |
| // Our widget might have child widgets. If this is a root view, include those |
| // widgets in the list of the root view's children because this is the most |
| // opportune location in the accessibility tree to expose them. |
| const ChildWidgetsResult child_widgets_result = GetChildWidgets(); |
| const std::vector<raw_ptr<Widget, VectorExperimental>>& child_widgets = |
| child_widgets_result.child_widgets; |
| |
| // If a visible tab modal dialog is present, return the dialog's root view. |
| // |
| // This is in order to support the "read title (NVDAKey+T)" and "read window |
| // (NVDAKey+B)" commands in the NVDA screen reader. We need to hide the rest |
| // of the UI, other than the dialog, from the screen reader. |
| if (child_widgets_result.is_tab_modal_showing) { |
| DCHECK_EQ(index, 0u); |
| DCHECK_EQ(child_widgets.size(), 1U); |
| return child_widgets[0]->GetRootView()->GetNativeViewAccessible(); |
| } |
| |
| for (View* child : view()->children()) { |
| ViewAccessibility& view_accessibility = child->GetViewAccessibility(); |
| if (view_accessibility.GetIsIgnored()) { |
| auto* child_view_delegate = |
| static_cast<ViewAXPlatformNodeDelegate*>(&view_accessibility); |
| DCHECK(child_view_delegate); |
| size_t child_count = child_view_delegate->GetChildCount(); |
| if (index < child_count) { |
| return child_view_delegate->ChildAtIndex(index); |
| } |
| index -= child_count; |
| } else { |
| if (index == 0) { |
| return view_accessibility.view()->GetNativeViewAccessible(); |
| } |
| --index; |
| } |
| } |
| |
| CHECK_LT(index, child_widgets_result.child_widgets.size()) |
| << "|index| should be less than the unignored child count."; |
| return child_widgets[index]->GetRootView()->GetNativeViewAccessible(); |
| } |
| |
| bool ViewAXPlatformNodeDelegate::HasModalDialog() const { |
| return GetChildWidgets().is_tab_modal_showing; |
| } |
| |
| std::wstring ViewAXPlatformNodeDelegate::ComputeListItemNameFromContent() |
| const { |
| DCHECK_EQ(GetData().role, ax::mojom::Role::kListItem); |
| |
| std::string str = ""; |
| // The list item name will result in the concatenation of its children's |
| // accessible names, excluding the list item marker. |
| // TODO(accessibility): We're aware the accessible name might be computed |
| // incorrectly if there's a complex structure. Things might be missing for |
| // descendants of descendants. |
| for (size_t i = 0, child_count = GetChildCount(); i < child_count; ++i) { |
| auto* child = ui::AXPlatformNode::FromNativeViewAccessible(ChildAtIndex(i)); |
| if (GetData().role != ax::mojom::Role::kListMarker) { |
| str += child->GetDelegate()->GetName(); |
| } |
| } |
| |
| return base::UTF8ToWide(str); |
| } |
| |
| bool ViewAXPlatformNodeDelegate::IsChildOfLeaf() const { |
| return AXPlatformNodeDelegate::IsChildOfLeaf(); |
| } |
| |
| const ui::AXSelection ViewAXPlatformNodeDelegate::GetUnignoredSelection() |
| const { |
| const ui::AXNodeData& data = GetData(); |
| ui::AXSelection selection; |
| selection.is_backward = false; |
| selection.anchor_object_id = GetUniqueId(); |
| selection.anchor_offset = |
| data.GetIntAttribute(ax::mojom::IntAttribute::kTextSelStart); |
| selection.anchor_affinity = ax::mojom::TextAffinity::kDownstream; |
| selection.focus_object_id = GetUniqueId(); |
| selection.focus_offset = |
| data.GetIntAttribute(ax::mojom::IntAttribute::kTextSelEnd); |
| selection.focus_affinity = ax::mojom::TextAffinity::kDownstream; |
| return selection; |
| } |
| |
| // Since AtomicViewAXTreeManager only ever contains a single node, we can be |
| // sure that we are in a leaf node and only need to return a text position. |
| ui::AXNodePosition::AXPositionInstance |
| ViewAXPlatformNodeDelegate::CreatePositionAt( |
| int offset, |
| ax::mojom::TextAffinity affinity) const { |
| return CreateTextPositionAt(offset, affinity); |
| } |
| |
| ui::AXNodePosition::AXPositionInstance |
| ViewAXPlatformNodeDelegate::CreateTextPositionAt( |
| int offset, |
| ax::mojom::TextAffinity affinity) const { |
| if (!atomic_view_ax_tree_manager_) { |
| return ui::AXNodePosition::CreateNullPosition(); |
| } |
| |
| return ui::AXNodePosition::CreateTextPosition( |
| *atomic_view_ax_tree_manager_->GetRoot(), offset, affinity); |
| } |
| |
| gfx::NativeViewAccessible ViewAXPlatformNodeDelegate::GetNSWindow() { |
| NOTIMPLEMENTED() << "Should only be called on Mac."; |
| return gfx::NativeViewAccessible(); |
| } |
| |
| gfx::NativeViewAccessible ViewAXPlatformNodeDelegate::GetNativeViewAccessible() |
| const { |
| // TODO(nektar): Make "GetNativeViewAccessible" const throughout the codebase. |
| return const_cast<ViewAXPlatformNodeDelegate*>(this) |
| ->GetNativeViewAccessible(); |
| } |
| |
| gfx::NativeViewAccessible |
| ViewAXPlatformNodeDelegate::GetNativeViewAccessible() { |
| // The WebView class returns the BrowserAccessibility instance exposed by its |
| // WebContents child, not its own AXPlatformNode. This is done by overriding |
| // "GetNativeViewAccessible", so we can't simply call "GetNativeObject" here. |
| return view()->GetNativeViewAccessible(); |
| } |
| |
| gfx::NativeViewAccessible ViewAXPlatformNodeDelegate::GetParent() const { |
| if (View* parent_view = view()->parent()) { |
| ViewAccessibility& view_accessibility = parent_view->GetViewAccessibility(); |
| if (!view_accessibility.GetIsIgnored()) { |
| return parent_view->GetNativeViewAccessible(); |
| } |
| |
| auto* parent_view_delegate = |
| static_cast<ViewAXPlatformNodeDelegate*>(&view_accessibility); |
| DCHECK(parent_view_delegate); |
| return parent_view_delegate->GetParent(); |
| } |
| |
| if (Widget* widget = view()->GetWidget()) { |
| Widget* top_widget = widget->GetTopLevelWidget(); |
| if (top_widget && widget != top_widget && top_widget->GetRootView()) { |
| return top_widget->GetRootView()->GetNativeViewAccessible(); |
| } |
| } |
| |
| return gfx::NativeViewAccessible(); |
| } |
| |
| bool ViewAXPlatformNodeDelegate::IsLeaf() const { |
| return ViewAccessibility::IsLeaf() || AXPlatformNodeDelegate::IsLeaf(); |
| } |
| |
| bool ViewAXPlatformNodeDelegate::IsInvisibleOrIgnored() const { |
| return GetIsIgnored() || GetData().IsInvisible(); |
| } |
| |
| bool ViewAXPlatformNodeDelegate::IsFocused() const { |
| return GetFocus() == GetNativeObject(); |
| } |
| |
| gfx::Rect ViewAXPlatformNodeDelegate::GetBoundsRect( |
| const ui::AXCoordinateSystem coordinate_system, |
| const ui::AXClippingBehavior clipping_behavior, |
| ui::AXOffscreenResult* offscreen_result) const { |
| switch (coordinate_system) { |
| case ui::AXCoordinateSystem::kScreenDIPs: |
| // We could optionally add clipping here if ever needed. |
| return view()->GetBoundsInScreen(); |
| case ui::AXCoordinateSystem::kScreenPhysicalPixels: |
| case ui::AXCoordinateSystem::kRootFrame: |
| case ui::AXCoordinateSystem::kFrame: |
| NOTIMPLEMENTED(); |
| return gfx::Rect(); |
| } |
| } |
| |
| gfx::Rect ViewAXPlatformNodeDelegate::GetInnerTextRangeBoundsRect( |
| const int start_offset, |
| const int end_offset, |
| const ui::AXCoordinateSystem coordinate_system, |
| const ui::AXClippingBehavior clipping_behavior, |
| ui::AXOffscreenResult* offscreen_result) const { |
| switch (coordinate_system) { |
| case ui::AXCoordinateSystem::kScreenDIPs: { |
| gfx::Rect content_bounds = view()->GetContentsBounds(); |
| View::ConvertRectToScreen(view(), &content_bounds); |
| gfx::RectF bounds = GetInlineTextRect(start_offset, end_offset); |
| |
| if (ui::IsTextField(data_.role)) { |
| bounds = RelativeToContainerBounds(bounds, offscreen_result); |
| if (bounds.IsEmpty() && offscreen_result && |
| *offscreen_result == ui::AXOffscreenResult::kOnscreen) { |
| // Ensure we have a non-zero minimum width when in a text field so |
| // that the text cursor indicator will be represented correctly. |
| const int kMinimumWidth = 1; |
| bounds.set_width(kMinimumWidth); |
| bounds.set_height(content_bounds.height()); |
| if (base::i18n::IsRTL()) { |
| bounds.set_x(content_bounds.width() - kMinimumWidth); |
| } |
| } |
| } else if (offscreen_result) { |
| // TODO(accessibility): This is probably not always true, but we'll need |
| // to investigate if scrolling in Views -- other than TextFields -- is |
| // possible and, if so, adjust the condition. |
| *offscreen_result = ui::AXOffscreenResult::kOnscreen; |
| } |
| |
| bounds.Offset(content_bounds.x(), content_bounds.y()); |
| return gfx::ToEnclosingRect(bounds); |
| } |
| case ui::AXCoordinateSystem::kScreenPhysicalPixels: |
| case ui::AXCoordinateSystem::kRootFrame: |
| case ui::AXCoordinateSystem::kFrame: |
| NOTIMPLEMENTED(); |
| return gfx::Rect(); |
| } |
| } |
| |
| gfx::RectF ViewAXPlatformNodeDelegate::GetInlineTextRect( |
| const int start_offset, |
| const int end_offset) const { |
| DCHECK_GE(start_offset, 0); |
| DCHECK_GE(end_offset, 0); |
| DCHECK_LE(start_offset, end_offset); |
| const std::vector<int32_t>& character_offsets = |
| data_.GetIntListAttribute(ax::mojom::IntListAttribute::kCharacterOffsets); |
| if (character_offsets.empty()) { |
| return gfx::RectF(); |
| } |
| |
| const size_t adjusted_start = |
| std::min(static_cast<size_t>(start_offset), character_offsets.size() - 1); |
| const size_t adjusted_end = |
| std::min(static_cast<size_t>(end_offset), character_offsets.size() - 1); |
| |
| // Finding the left/right most offsets allow us to return the right bounds |
| // whether we're in LTR or RTL. |
| const int left_most_offset = std::min(character_offsets[adjusted_start], |
| character_offsets[adjusted_end]); |
| const int right_most_offset = std::max(character_offsets[adjusted_start], |
| character_offsets[adjusted_end]); |
| |
| if (left_most_offset == right_most_offset) { |
| return gfx::RectF(); |
| } |
| |
| return gfx::RectF(left_most_offset, 0, right_most_offset - left_most_offset, |
| view()->GetContentsBounds().height()); |
| } |
| |
| gfx::RectF ViewAXPlatformNodeDelegate::RelativeToContainerBounds( |
| const gfx::RectF& bounds, |
| ui::AXOffscreenResult* offscreen_result) const { |
| int scroll_x = data_.GetIntAttribute(ax::mojom::IntAttribute::kScrollX); |
| gfx::RectF relative_bounds = bounds; |
| relative_bounds.Offset(scroll_x, 0); |
| |
| gfx::RectF container_bounds = gfx::RectF(view()->GetBoundsInScreen()); |
| container_bounds.set_origin(gfx::PointF()); |
| gfx::RectF intersection = relative_bounds; |
| intersection.Intersect(container_bounds); |
| |
| if (offscreen_result) { |
| *offscreen_result = !bounds.IsEmpty() && intersection.IsEmpty() |
| ? ui::AXOffscreenResult::kOffscreen |
| : ui::AXOffscreenResult::kOnscreen; |
| } |
| return intersection; |
| } |
| |
| gfx::NativeViewAccessible ViewAXPlatformNodeDelegate::HitTestSync( |
| int screen_physical_pixel_x, |
| int screen_physical_pixel_y) const { |
| if (!view() || !view()->GetWidget()) { |
| return gfx::NativeViewAccessible(); |
| } |
| |
| if (IsLeaf()) { |
| return GetNativeViewAccessible(); |
| } |
| |
| gfx::Point point = ScreenToDIPPoint( |
| gfx::Point(screen_physical_pixel_x, screen_physical_pixel_y)); |
| |
| // Search child widgets first, since they're on top in the z-order. |
| for (Widget* child_widget : GetChildWidgets().child_widgets) { |
| View* child_root_view = child_widget->GetRootView(); |
| // Use a per-iteration copy; don't mutate `point` so later iterations and |
| // the view() hit test use the original ScreenToDIPPoint result. |
| gfx::Point point_for_child = point; |
| View::ConvertPointFromScreen(child_root_view, &point_for_child); |
| if (child_root_view->HitTestPoint(point_for_child)) { |
| return child_root_view->GetNativeViewAccessible(); |
| } |
| } |
| |
| View::ConvertPointFromScreen(view(), &point); |
| if (!view()->HitTestPoint(point)) { |
| return gfx::NativeViewAccessible(); |
| } |
| |
| // Check if the point is within any of the virtual children of this view. |
| // AXVirtualView's HitTestSync is a recursive function that will return the |
| // deepest child, since it does not support relative bounds. |
| if (!virtual_children().empty()) { |
| // Search the greater indices first, since they're on top in the z-order. |
| for (const std::unique_ptr<AXVirtualView>& child : |
| base::Reversed(virtual_children())) { |
| gfx::NativeViewAccessible result = |
| child->HitTestSync(screen_physical_pixel_x, screen_physical_pixel_y); |
| if (result) { |
| return result; |
| } |
| } |
| // If it's not inside any of our virtual children, it's inside this view. |
| return GetNativeViewAccessible(); |
| } |
| |
| // Check if the point is within any of the immediate children of this |
| // view. We don't have to search further because AXPlatformNode will |
| // do a recursive hit test if we return anything other than |this| or NULL. |
| View* v = view(); |
| const auto is_point_in_child = [point, v](View* child) { |
| if (!child->GetVisible()) { |
| return false; |
| } |
| ui::AXNodeData child_data; |
| child->GetViewAccessibility().GetAccessibleNodeData(&child_data); |
| if (child_data.IsInvisible()) { |
| return false; |
| } |
| gfx::Point point_in_child_coords = point; |
| v->ConvertPointToTarget(v, child, &point_in_child_coords); |
| return child->HitTestPoint(point_in_child_coords); |
| }; |
| const auto i = |
| std::ranges::find_if(base::Reversed(v->children()), is_point_in_child); |
| // If it's not inside any of our children, it's inside this view. |
| return (i == v->children().rend()) ? GetNativeViewAccessible() |
| : (*i)->GetNativeViewAccessible(); |
| } |
| |
| gfx::NativeViewAccessible ViewAXPlatformNodeDelegate::GetFocus() const { |
| gfx::NativeViewAccessible focus_override = |
| ui::AXPlatformNode::GetPopupFocusOverride(); |
| if (focus_override) { |
| return focus_override; |
| } |
| |
| FocusManager* focus_manager = view()->GetFocusManager(); |
| View* focused_view = |
| focus_manager ? focus_manager->GetFocusedView() : nullptr; |
| |
| if (!focused_view) { |
| return gfx::NativeViewAccessible(); |
| } |
| |
| // The accessibility focus will be either on the |focused_view| or on one of |
| // its virtual children. |
| return focused_view->GetViewAccessibility().GetFocusedDescendant(); |
| } |
| |
| ui::AXPlatformNode* ViewAXPlatformNodeDelegate::GetFromNodeID(int32_t id) { |
| return PlatformNodeFromNodeID(id); |
| } |
| |
| ui::AXPlatformNode* ViewAXPlatformNodeDelegate::GetFromTreeIDAndNodeID( |
| const ui::AXTreeID& ax_tree_id, |
| int32_t id) { |
| if (atomic_view_ax_tree_manager_) { |
| return atomic_view_ax_tree_manager_->GetPlatformNodeFromTree(id); |
| } |
| return nullptr; |
| } |
| |
| bool ViewAXPlatformNodeDelegate::AccessibilityPerformAction( |
| const ui::AXActionData& data) { |
| return view()->HandleAccessibleAction(data); |
| } |
| |
| bool ViewAXPlatformNodeDelegate::ShouldIgnoreHoveredStateForTesting() { |
| return false; |
| } |
| |
| bool ViewAXPlatformNodeDelegate::IsOffscreen() const { |
| // TODO(katydek): need to implement. |
| return false; |
| } |
| |
| std::u16string ViewAXPlatformNodeDelegate::GetAuthorUniqueId() const { |
| const View* v = view(); |
| if (v) { |
| const int view_id = v->GetID(); |
| if (view_id) { |
| return u"view_" + base::NumberToString16(view_id); |
| } |
| } |
| |
| return std::u16string(); |
| } |
| |
| bool ViewAXPlatformNodeDelegate::IsMinimized() const { |
| Widget* widget = view()->GetWidget(); |
| return widget && widget->IsMinimized(); |
| } |
| |
| // TODO(accessibility): This function should call AXNode::IsReadOnlySupported |
| // instead, just like in BrowserAccessibility, but ViewAccessibility objects |
| // don't have a corresponding AXNode yet. |
| bool ViewAXPlatformNodeDelegate::IsReadOnlySupported() const { |
| return ui::IsReadOnlySupported(GetData().role); |
| } |
| |
| // TODO(accessibility): This function should call AXNode::IsReadOnlyOrDisabled |
| // instead, just like in BrowserAccessibility, but ViewAccessibility objects |
| // don't have a corresponding AXNode yet. |
| bool ViewAXPlatformNodeDelegate::IsReadOnlyOrDisabled() const { |
| switch (GetData().GetRestriction()) { |
| case ax::mojom::Restriction::kReadOnly: |
| case ax::mojom::Restriction::kDisabled: |
| return true; |
| case ax::mojom::Restriction::kNone: { |
| if (HasState(ax::mojom::State::kEditable) || |
| HasState(ax::mojom::State::kRichlyEditable)) { |
| return false; |
| } |
| |
| if (ui::ShouldHaveReadonlyStateByDefault(GetData().role)) { |
| return true; |
| } |
| |
| // When readonly is not supported, we assume that the node is always |
| // read-only and mark it as such since this is the default behavior. |
| return !IsReadOnlySupported(); |
| } |
| } |
| } |
| |
| ui::AXPlatformNodeId ViewAXPlatformNodeDelegate::GetUniqueId() const { |
| return ViewAccessibility::GetUniqueId(); |
| } |
| |
| AtomicViewAXTreeManager* |
| ViewAXPlatformNodeDelegate::GetAtomicViewAXTreeManagerForTesting() const { |
| return atomic_view_ax_tree_manager_.get(); |
| } |
| |
| gfx::Point ViewAXPlatformNodeDelegate::ScreenToDIPPoint( |
| const gfx::Point& screen_point) const { |
| if (!view() || !view()->GetWidget()) { |
| return screen_point; |
| } |
| |
| gfx::NativeView native_view = view()->GetWidget()->GetNativeView(); |
| float scale_factor = 1.0; |
| if (native_view) { |
| scale_factor = ui::GetScaleFactorForNativeView(native_view); |
| scale_factor = scale_factor <= 0 ? 1.0 : scale_factor; |
| } |
| |
| return gfx::Point(screen_point.x() / scale_factor, |
| screen_point.y() / scale_factor); |
| } |
| |
| std::vector<int32_t> ViewAXPlatformNodeDelegate::GetColHeaderNodeIds() const { |
| std::vector<int32_t> col_header_ids; |
| if (!virtual_children().empty()) { |
| for (const std::unique_ptr<AXVirtualView>& header_cell : |
| virtual_children().front()->children()) { |
| const ui::AXNodeData& header_data = header_cell->GetData(); |
| if (header_data.role == ax::mojom::Role::kColumnHeader) { |
| col_header_ids.push_back(header_data.id); |
| } |
| } |
| } |
| return col_header_ids; |
| } |
| |
| std::vector<int32_t> ViewAXPlatformNodeDelegate::GetColHeaderNodeIds( |
| int col_index) const { |
| std::vector<int32_t> columns = GetColHeaderNodeIds(); |
| if (static_cast<size_t>(col_index) >= columns.size()) { |
| return {}; |
| } |
| return {columns[static_cast<size_t>(col_index)]}; |
| } |
| |
| std::optional<int32_t> ViewAXPlatformNodeDelegate::GetCellId( |
| int row_index, |
| int col_index) const { |
| if (virtual_children().empty() || !GetAncestorTableView()) { |
| return std::nullopt; |
| } |
| |
| AXVirtualView* ax_cell = GetAncestorTableView()->GetVirtualAccessibilityCell( |
| static_cast<size_t>(row_index), static_cast<size_t>(col_index)); |
| if (!ax_cell) { |
| return std::nullopt; |
| } |
| |
| const ui::AXNodeData& cell_data = ax_cell->GetData(); |
| if (cell_data.role == ax::mojom::Role::kCell || |
| cell_data.role == ax::mojom::Role::kGridCell) { |
| return cell_data.id; |
| } |
| |
| return std::nullopt; |
| } |
| |
| TableView* ViewAXPlatformNodeDelegate::GetAncestorTableView() const { |
| ui::AXNodeData data; |
| view()->GetViewAccessibility().GetAccessibleNodeData(&data); |
| |
| if (!ui::IsTableLike(data.role)) { |
| return nullptr; |
| } |
| |
| return static_cast<TableView*>(view()); |
| } |
| |
| bool ViewAXPlatformNodeDelegate::IsOrderedSetItem() const { |
| const ui::AXNodeData& data = GetData(); |
| return (view()->GetGroup() >= 0) || |
| (data.HasIntAttribute(ax::mojom::IntAttribute::kPosInSet) && |
| data.HasIntAttribute(ax::mojom::IntAttribute::kSetSize)); |
| } |
| |
| bool ViewAXPlatformNodeDelegate::IsOrderedSet() const { |
| return (view()->GetGroup() >= 0) || |
| GetData().HasIntAttribute(ax::mojom::IntAttribute::kSetSize); |
| } |
| |
| std::optional<int> ViewAXPlatformNodeDelegate::GetPosInSet() const { |
| // Consider overridable attributes first. |
| const ui::AXNodeData& data = GetData(); |
| if (data.HasIntAttribute(ax::mojom::IntAttribute::kPosInSet)) { |
| return data.GetIntAttribute(ax::mojom::IntAttribute::kPosInSet); |
| } |
| |
| std::vector<raw_ptr<View, VectorExperimental>> views_in_group; |
| GetViewsInGroupForSet(&views_in_group); |
| if (views_in_group.empty()) { |
| return std::nullopt; |
| } |
| // Check this is in views_in_group; it may be removed if it is ignored. |
| auto found_view = std::ranges::find(views_in_group, view()); |
| if (found_view == views_in_group.end()) { |
| return std::nullopt; |
| } |
| |
| int pos_in_set = base::checked_cast<int>( |
| std::distance(views_in_group.begin(), found_view)); |
| // pos_in_set is zero-based; users expect one-based, so increment. |
| return ++pos_in_set; |
| } |
| |
| std::optional<int> ViewAXPlatformNodeDelegate::GetSetSize() const { |
| // Consider overridable attributes first. |
| const ui::AXNodeData& data = GetData(); |
| if (data.HasIntAttribute(ax::mojom::IntAttribute::kSetSize)) { |
| return data.GetIntAttribute(ax::mojom::IntAttribute::kSetSize); |
| } |
| |
| std::vector<raw_ptr<View, VectorExperimental>> views_in_group; |
| GetViewsInGroupForSet(&views_in_group); |
| if (views_in_group.empty()) { |
| return std::nullopt; |
| } |
| // Check this is in views_in_group; it may be removed if it is ignored. |
| auto found_view = std::ranges::find(views_in_group, view()); |
| if (found_view == views_in_group.end()) { |
| return std::nullopt; |
| } |
| |
| return base::checked_cast<int>(views_in_group.size()); |
| } |
| |
| void ViewAXPlatformNodeDelegate::GetViewsInGroupForSet( |
| std::vector<raw_ptr<View, VectorExperimental>>* views_in_group) const { |
| const int group_id = view()->GetGroup(); |
| if (group_id < 0) { |
| return; |
| } |
| |
| View* view_to_check = view(); |
| // If the view is part of a cascading group, use that group. |
| if (View* parent_group_view = GetCascadingRadioGroupView(view())) { |
| view_to_check = parent_group_view; |
| } else if (view()->parent()) { |
| // If this view has a parent, check from the parent, to make sure we catch |
| // any siblings. |
| view_to_check = view()->parent(); |
| } |
| view_to_check->GetViewsInGroup(group_id, views_in_group); |
| |
| // Remove any views that are ignored in the accessibility tree. |
| std::erase_if(*views_in_group, [](View* view) { |
| return view->GetViewAccessibility().GetIsIgnored(); |
| }); |
| } |
| |
| bool ViewAXPlatformNodeDelegate::TableHasColumnOrRowHeaderNodeForTesting() |
| const { |
| if (!GetAncestorTableView()) { |
| return false; |
| } |
| return !GetAncestorTableView()->visible_columns().empty(); |
| } |
| |
| ViewAXPlatformNodeDelegate::ChildWidgetsResult |
| ViewAXPlatformNodeDelegate::GetChildWidgets() const { |
| // This method is used to create a parent / child relationship between the |
| // root view and any child widgets. Child widgets should only be exposed as |
| // the direct children of the root view. A root view should appear as the only |
| // child of a widget. |
| Widget* widget = view()->GetWidget(); |
| // Note that during window close, a Widget may exist in a state where it has |
| // no NativeView, but hasn't yet torn down its view hierarchy. |
| if (!widget || !widget->GetNativeView() || widget->GetRootView() != view()) { |
| return ChildWidgetsResult(); |
| } |
| |
| Widget::Widgets owned_widgets = |
| Widget::GetAllOwnedWidgets(widget->GetNativeView()); |
| |
| std::vector<raw_ptr<Widget, VectorExperimental>> visible_widgets; |
| std::ranges::copy_if(owned_widgets, std::back_inserter(visible_widgets), |
| &Widget::IsVisible); |
| |
| // Focused child widgets should take the place of the web page they cover in |
| // the accessibility tree. |
| const FocusManager* focus_manager = view()->GetFocusManager(); |
| const View* focused_view = |
| focus_manager ? focus_manager->GetFocusedView() : nullptr; |
| const auto is_focused_child = [focused_view](Widget* child_widget) { |
| return ViewAccessibilityUtils::IsFocusedChildWidget(child_widget, |
| focused_view); |
| }; |
| const auto i = std::ranges::find_if(visible_widgets, is_focused_child); |
| // In order to support the "read title (NVDAKey+T)" and "read window |
| // (NVDAKey+B)" commands in the NVDA screen reader, hide the rest of the UI |
| // from the accessibility tree when a modal dialog is showing. |
| if (i != visible_widgets.cend()) { |
| return ChildWidgetsResult({*i}, true /* is_tab_modal_showing */); |
| } |
| |
| return ChildWidgetsResult(visible_widgets, false /* is_tab_modal_showing */); |
| } |
| |
| } // namespace views |