| // Copyright (c) 2013 The Chromium Authors. All rights reserved. |
| // 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 <map> |
| #include <memory> |
| #include <set> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/containers/adapters.h" |
| #include "base/lazy_instance.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "ui/accessibility/ax_action_data.h" |
| #include "ui/accessibility/ax_role_properties.h" |
| #include "ui/accessibility/ax_tree_data.h" |
| #include "ui/accessibility/platform/ax_platform_node.h" |
| #include "ui/accessibility/platform/ax_platform_node_base.h" |
| #include "ui/accessibility/platform/ax_unique_id.h" |
| #include "ui/base/layout.h" |
| #include "ui/events/event_utils.h" |
| #include "ui/views/accessibility/view_accessibility_utils.h" |
| #include "ui/views/controls/native/native_view_host.h" |
| #include "ui/views/view.h" |
| #include "ui/views/widget/widget.h" |
| #include "ui/views/widget/widget_delegate.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; |
| }; |
| |
| base::LazyInstance<std::vector<QueuedEvent>>::Leaky g_event_queue = |
| LAZY_INSTANCE_INITIALIZER; |
| |
| bool g_is_queueing_events = false; |
| |
| bool IsAccessibilityFocusableWhenEnabled(View* view) { |
| return view->GetFocusBehavior() != View::FocusBehavior::NEVER && |
| view->IsDrawn(); |
| } |
| |
| // Used to determine if a View should be ignored by accessibility clients by |
| // being a non-keyboard-focusable child of a keyboard-focusable ancestor. E.g., |
| // LabelButtons contain Labels, but a11y should just show that there's a button. |
| bool IsViewUnfocusableDescendantOfFocusableAncestor(View* view) { |
| if (IsAccessibilityFocusableWhenEnabled(view)) |
| return false; |
| |
| while (view->parent()) { |
| view = view->parent(); |
| if (IsAccessibilityFocusableWhenEnabled(view)) |
| return true; |
| } |
| return 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); |
| for (QueuedEvent event : g_event_queue.Get()) |
| FireEvent(event); |
| g_is_queueing_events = false; |
| g_event_queue.Get().clear(); |
| } |
| |
| void PostFlushEventQueueTaskIfNecessary() { |
| if (!g_is_queueing_events) { |
| g_is_queueing_events = true; |
| base::OnceCallback<void()> cb = base::BindOnce(&FlushQueue); |
| base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, std::move(cb)); |
| } |
| } |
| |
| } // namespace |
| |
| ViewAXPlatformNodeDelegate::ChildWidgetsResult::ChildWidgetsResult() = default; |
| |
| ViewAXPlatformNodeDelegate::ChildWidgetsResult::ChildWidgetsResult( |
| std::vector<Widget*> 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) { |
| 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(nullptr); |
| } |
| ax_platform_node_->Destroy(); |
| ax_platform_node_ = nullptr; |
| } |
| |
| void ViewAXPlatformNodeDelegate::SetPopupFocusOverride() { |
| ui::AXPlatformNode::SetPopupFocusOverride(GetNativeViewAccessible()); |
| } |
| |
| void ViewAXPlatformNodeDelegate::EndPopupFocusOverride() { |
| ui::AXPlatformNode::SetPopupFocusOverride(nullptr); |
| } |
| |
| bool ViewAXPlatformNodeDelegate::IsFocusedForTesting() { |
| if (ui::AXPlatformNode::GetPopupFocusOverride()) |
| return ui::AXPlatformNode::GetPopupFocusOverride() == |
| GetNativeViewAccessible(); |
| |
| return ViewAccessibility::IsFocusedForTesting(); |
| } |
| |
| gfx::NativeViewAccessible ViewAXPlatformNodeDelegate::GetNativeObject() const { |
| DCHECK(ax_platform_node_); |
| return ax_platform_node_->GetNativeViewAccessible(); |
| } |
| |
| void ViewAXPlatformNodeDelegate::NotifyAccessibilityEvent( |
| ax::mojom::Event event_type) { |
| DCHECK(ax_platform_node_); |
| if (accessibility_events_callback_) |
| accessibility_events_callback_.Run(this, event_type); |
| if (g_is_queueing_events) { |
| g_event_queue.Get().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(); |
| g_event_queue.Get().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 defined(OS_APPLE) |
| void ViewAXPlatformNodeDelegate::AnnounceText(const base::string16& text) { |
| ax_platform_node_->AnnounceText(text); |
| } |
| #endif |
| |
| void ViewAXPlatformNodeDelegate::FireFocusAfterMenuClose() { |
| ui::AXPlatformNodeBase* focused_node = |
| static_cast<ui::AXPlatformNodeBase*>(ax_platform_node_); |
| // 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); |
| } |
| } |
| |
| // ui::AXPlatformNodeDelegate |
| |
| const ui::AXNodeData& ViewAXPlatformNodeDelegate::GetData() const { |
| // Clear the data, then populate it. |
| data_ = ui::AXNodeData(); |
| GetAccessibleNodeData(&data_); |
| |
| // View::IsDrawn is true if a View is visible and all of its ancestors are |
| // visible too, since invisibility inherits. |
| // |
| // TODO(dmazzoni): Maybe consider moving this to ViewAccessibility? |
| // This will require ensuring that Chrome OS invalidates the whole |
| // subtree when a View changes its visibility state. |
| if (!view()->IsDrawn()) |
| data_.AddState(ax::mojom::State::kInvisible); |
| |
| // Make sure this element is excluded from the a11y tree if there's a |
| // focusable parent. All keyboard focusable elements should be leaf nodes. |
| // Exceptions to this rule will themselves be accessibility focusable. |
| // |
| // TODO(dmazzoni): this code was added to support MacViews acccessibility, |
| // because we needed a way to mark a View as a leaf node in the |
| // accessibility tree. We need to replace this with a cross-platform |
| // solution that works for ChromeVox, too, and move it to ViewAccessibility. |
| if (IsViewUnfocusableDescendantOfFocusableAncestor(view())) |
| data_.AddState(ax::mojom::State::kIgnored); |
| |
| return data_; |
| } |
| |
| int ViewAXPlatformNodeDelegate::GetChildCount() const { |
| // We do not call "IsLeaf" here because "AXPlatformNodeDelegateBase::IsLeaf" |
| // calls "GetChildCount", and this will result in an infinit loop. |
| 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. |
| int 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. |
| int view_child_count = 0; |
| for (View* child : view()->children()) { |
| const ViewAccessibility& view_accessibility = child->GetViewAccessibility(); |
| if (view_accessibility.IsIgnored()) { |
| 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 + int{child_widgets_result.child_widgets.size()}; |
| } |
| |
| gfx::NativeViewAccessible ViewAXPlatformNodeDelegate::ChildAtIndex(int index) { |
| DCHECK_GE(index, 0) << "|index| should be greater or equal to 0."; |
| DCHECK_LT(index, GetChildCount()) |
| << "|index| should be less than the unignored child count."; |
| if (IsLeaf()) |
| return nullptr; |
| |
| 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()) { |
| int 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; |
| } |
| |
| DCHECK_GE(index, 0); |
| } |
| |
| NOTREACHED() << "|index| should be less than the unignored child count."; |
| return nullptr; |
| } |
| |
| // 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<Widget*>& 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, 0); |
| 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.IsIgnored()) { |
| auto* child_view_delegate = |
| static_cast<ViewAXPlatformNodeDelegate*>(&view_accessibility); |
| DCHECK(child_view_delegate); |
| int 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; |
| } |
| |
| DCHECK_GE(index, 0); |
| } |
| |
| if (index < int{child_widgets_result.child_widgets.size()}) |
| return child_widgets[index]->GetRootView()->GetNativeViewAccessible(); |
| |
| NOTREACHED() << "|index| should be less than the unignored child count."; |
| return nullptr; |
| } |
| |
| bool ViewAXPlatformNodeDelegate::HasModalDialog() const { |
| return GetChildWidgets().is_tab_modal_showing; |
| } |
| |
| gfx::NativeViewAccessible ViewAXPlatformNodeDelegate::GetNSWindow() { |
| NOTREACHED(); |
| return nullptr; |
| } |
| |
| 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() { |
| if (View* parent_view = view()->parent()) { |
| ViewAccessibility& view_accessibility = parent_view->GetViewAccessibility(); |
| if (!view_accessibility.IsIgnored()) |
| 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 nullptr; |
| } |
| |
| bool ViewAXPlatformNodeDelegate::IsChildOfLeaf() const { |
| // Needed to prevent endless loops, see: http://crbug.com/1100047 |
| return false; |
| } |
| |
| bool ViewAXPlatformNodeDelegate::IsLeaf() const { |
| return ViewAccessibility::IsLeaf() || AXPlatformNodeDelegateBase::IsLeaf(); |
| } |
| |
| bool ViewAXPlatformNodeDelegate::IsIgnored() const { |
| return ViewAccessibility::IsIgnored() || GetData().IsIgnored(); |
| } |
| |
| bool ViewAXPlatformNodeDelegate::IsFocused() const { |
| return GetFocus() == GetNativeObject(); |
| } |
| |
| bool ViewAXPlatformNodeDelegate::IsToplevelBrowserWindow() { |
| // Note: only used on Desktop Linux. Other platforms don't have an application |
| // node so this would never return true. |
| ui::AXNodeData data = GetData(); |
| if (data.role != ax::mojom::Role::kWindow) |
| return false; |
| |
| AXPlatformNodeDelegate* parent = GetParentDelegate(); |
| return parent && parent->GetData().role == ax::mojom::Role::kApplication; |
| } |
| |
| 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::NativeViewAccessible ViewAXPlatformNodeDelegate::HitTestSync( |
| int screen_physical_pixel_x, |
| int screen_physical_pixel_y) const { |
| if (!view() || !view()->GetWidget()) |
| return nullptr; |
| |
| if (IsLeaf()) |
| return GetNativeViewAccessible(); |
| |
| 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; |
| } |
| int screen_dips_x = screen_physical_pixel_x / scale_factor; |
| int screen_dips_y = screen_physical_pixel_y / scale_factor; |
| |
| // 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(); |
| gfx::Point point(screen_dips_x, screen_dips_y); |
| View::ConvertPointFromScreen(child_root_view, &point); |
| if (child_root_view->HitTestPoint(point)) |
| return child_root_view->GetNativeViewAccessible(); |
| } |
| |
| gfx::Point point(screen_dips_x, screen_dips_y); |
| View::ConvertPointFromScreen(view(), &point); |
| if (!view()->HitTestPoint(point)) |
| return nullptr; |
| |
| // 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.HasState(ax::mojom::State::kInvisible)) |
| 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::find_if(v->children().rbegin(), v->children().rend(), |
| 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 nullptr; |
| |
| // 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) { |
| 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; |
| } |
| |
| base::string16 ViewAXPlatformNodeDelegate::GetAuthorUniqueId() const { |
| const View* v = view(); |
| if (v) { |
| const int view_id = v->GetID(); |
| if (view_id) |
| return base::WideToUTF16(L"view_") + base::NumberToString16(view_id); |
| } |
| |
| return base::string16(); |
| } |
| |
| bool ViewAXPlatformNodeDelegate::IsMinimized() const { |
| Widget* widget = view()->GetWidget(); |
| return widget && widget->IsMinimized(); |
| } |
| |
| const ui::AXUniqueId& ViewAXPlatformNodeDelegate::GetUniqueId() const { |
| return ViewAccessibility::GetUniqueId(); |
| } |
| |
| base::Optional<bool> |
| ViewAXPlatformNodeDelegate::GetTableHasColumnOrRowHeaderNode() const { |
| if (!GetAncestorTableView()) |
| return false; |
| return !GetAncestorTableView()->visible_columns().empty(); |
| } |
| |
| 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 (columns.size() <= size_t{col_index}) { |
| return {}; |
| } |
| return {columns[col_index]}; |
| } |
| |
| base::Optional<int32_t> ViewAXPlatformNodeDelegate::GetCellId( |
| int row_index, |
| int col_index) const { |
| if (virtual_children().empty() || !GetAncestorTableView()) |
| return base::nullopt; |
| |
| AXVirtualView* ax_cell = |
| GetAncestorTableView()->GetVirtualAccessibilityCell(row_index, col_index); |
| if (!ax_cell) |
| return base::nullopt; |
| |
| const ui::AXNodeData& cell_data = ax_cell->GetData(); |
| if (cell_data.role == ax::mojom::Role::kCell) |
| return cell_data.id; |
| |
| return base::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); |
| } |
| |
| base::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<View*> views_in_group; |
| GetViewsInGroupForSet(&views_in_group); |
| if (views_in_group.empty()) |
| return base::nullopt; |
| // Check this is in views_in_group; it may be removed if it is ignored. |
| auto found_view = |
| std::find(views_in_group.begin(), views_in_group.end(), view()); |
| if (found_view == views_in_group.end()) |
| return base::nullopt; |
| |
| int posInSet = std::distance(views_in_group.begin(), found_view); |
| // posInSet is zero-based; users expect one-based, so increment. |
| return ++posInSet; |
| } |
| |
| base::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<View*> views_in_group; |
| GetViewsInGroupForSet(&views_in_group); |
| if (views_in_group.empty()) |
| return base::nullopt; |
| // Check this is in views_in_group; it may be removed if it is ignored. |
| auto found_view = |
| std::find(views_in_group.begin(), views_in_group.end(), view()); |
| if (found_view == views_in_group.end()) |
| return base::nullopt; |
| |
| return views_in_group.size(); |
| } |
| |
| void ViewAXPlatformNodeDelegate::GetViewsInGroupForSet( |
| std::vector<View*>* views_in_group) const { |
| const int group_id = view()->GetGroup(); |
| if (group_id < 0) |
| return; |
| |
| View* view_to_check = view(); |
| // If this view has a parent, check from the parent, to make sure we catch any |
| // siblings. |
| if (view()->parent()) |
| view_to_check = view()->parent(); |
| view_to_check->GetViewsInGroup(group_id, views_in_group); |
| |
| // Remove any views that are ignored in the accessibility tree. |
| views_in_group->erase( |
| std::remove_if( |
| views_in_group->begin(), views_in_group->end(), |
| [](View* view) { |
| ViewAccessibility& view_accessibility = |
| view->GetViewAccessibility(); |
| return view_accessibility.IsIgnored(); |
| }), |
| views_in_group->end()); |
| } |
| |
| 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(); |
| |
| std::set<Widget*> owned_widgets; |
| Widget::GetAllOwnedWidgets(widget->GetNativeView(), &owned_widgets); |
| |
| std::vector<Widget*> visible_widgets; |
| std::copy_if(owned_widgets.cbegin(), owned_widgets.cend(), |
| std::back_inserter(visible_widgets), |
| [](const Widget* child) { return child->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::find_if(visible_widgets.cbegin(), visible_widgets.cend(), |
| 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 |