| // 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 "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/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->focus_behavior() != 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(); |
| } |
| |
| } // namespace |
| |
| // static |
| int ViewAXPlatformNodeDelegate::menu_depth_ = 0; |
| |
| 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() == GetNativeObject()) |
| ui::AXPlatformNode::SetPopupFocusOverride(nullptr); |
| ax_platform_node_->Destroy(); |
| } |
| |
| gfx::NativeViewAccessible ViewAXPlatformNodeDelegate::GetNativeObject() { |
| DCHECK(ax_platform_node_); |
| return ax_platform_node_->GetNativeViewAccessible(); |
| } |
| |
| void ViewAXPlatformNodeDelegate::NotifyAccessibilityEvent( |
| ax::mojom::Event event_type) { |
| DCHECK(ax_platform_node_); |
| if (g_is_queueing_events) { |
| g_event_queue.Get().emplace_back(event_type, GetUniqueId()); |
| return; |
| } |
| |
| ax_platform_node_->NotifyAccessibilityEvent(event_type); |
| |
| // Some events have special handling. |
| switch (event_type) { |
| case ax::mojom::Event::kMenuStart: |
| OnMenuStart(); |
| break; |
| case ax::mojom::Event::kMenuEnd: |
| OnMenuEnd(); |
| break; |
| case ax::mojom::Event::kSelection: |
| if (menu_depth_ && ui::IsMenuItem(GetData().role)) |
| OnMenuItemActive(); |
| 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. |
| g_is_queueing_events = true; |
| base::OnceCallback<void()> cb = base::BindOnce(&FlushQueue); |
| base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, std::move(cb)); |
| break; |
| } |
| default: |
| break; |
| } |
| } |
| |
| #if defined(OS_MACOSX) |
| void ViewAXPlatformNodeDelegate::AnnounceText(base::string16& text) { |
| ax_platform_node_->AnnounceText(text); |
| } |
| #endif |
| |
| void ViewAXPlatformNodeDelegate::OnMenuItemActive() { |
| // When a native menu is shown and has an item selected, treat it and the |
| // currently selected item as focused, even though the actual focus is in the |
| // browser's currently focused textfield. |
| ui::AXPlatformNode::SetPopupFocusOverride( |
| ax_platform_node_->GetNativeViewAccessible()); |
| } |
| |
| void ViewAXPlatformNodeDelegate::OnMenuStart() { |
| ++menu_depth_; |
| } |
| |
| void ViewAXPlatformNodeDelegate::OnMenuEnd() { |
| // When a native menu is hidden, restore accessibility focus to the current |
| // focus in the document. |
| if (menu_depth_ >= 1) |
| --menu_depth_; |
| if (menu_depth_ == 0) |
| ui::AXPlatformNode::SetPopupFocusOverride(nullptr); |
| } |
| |
| // 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_.role = ax::mojom::Role::kIgnored; |
| |
| return data_; |
| } |
| |
| int ViewAXPlatformNodeDelegate::GetChildCount() { |
| if (IsLeaf()) |
| return 0; |
| |
| if (virtual_child_count()) |
| return virtual_child_count(); |
| |
| int child_count = view()->child_count(); |
| std::vector<Widget*> child_widgets; |
| bool is_tab_modal_showing; |
| PopulateChildWidgetVector(&child_widgets, &is_tab_modal_showing); |
| if (is_tab_modal_showing) { |
| DCHECK_EQ(child_widgets.size(), 1ULL); |
| return 1; |
| } |
| child_count += child_widgets.size(); |
| |
| return child_count; |
| } |
| |
| gfx::NativeViewAccessible ViewAXPlatformNodeDelegate::ChildAtIndex(int index) { |
| DCHECK_GE(index, 0) << "Child indices should be greater or equal to 0."; |
| DCHECK_LT(index, GetChildCount()) |
| << "Child indices should be less than the child count."; |
| if (IsLeaf()) |
| return nullptr; |
| |
| if (virtual_child_count()) |
| return virtual_child_at(index)->GetNativeObject(); |
| |
| // If this is a root view, our widget might have child widgets. Include |
| std::vector<Widget*> child_widgets; |
| bool is_tab_modal_showing; |
| PopulateChildWidgetVector(&child_widgets, &is_tab_modal_showing); |
| |
| // If a visible tab modal dialog is present, ignore |index| and return the |
| // dialog. |
| if (is_tab_modal_showing) { |
| DCHECK_EQ(child_widgets.size(), 1ULL); |
| return child_widgets[0]->GetRootView()->GetNativeViewAccessible(); |
| } |
| |
| int child_widget_count = static_cast<int>(child_widgets.size()); |
| if (index < view()->child_count()) { |
| return view()->child_at(index)->GetNativeViewAccessible(); |
| } else if (index < view()->child_count() + child_widget_count) { |
| Widget* child_widget = child_widgets[index - view()->child_count()]; |
| return child_widget->GetRootView()->GetNativeViewAccessible(); |
| } |
| |
| return nullptr; |
| } |
| |
| gfx::NativeViewAccessible ViewAXPlatformNodeDelegate::GetNSWindow() { |
| NOTREACHED(); |
| return nullptr; |
| } |
| |
| gfx::NativeViewAccessible ViewAXPlatformNodeDelegate::GetParent() { |
| if (view()->parent()) |
| return view()->parent()->GetNativeViewAccessible(); |
| |
| 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; |
| } |
| |
| gfx::Rect ViewAXPlatformNodeDelegate::GetClippedScreenBoundsRect() const { |
| // We could optionally add clipping here if ever needed. |
| return view()->GetBoundsInScreen(); |
| } |
| |
| gfx::Rect ViewAXPlatformNodeDelegate::GetUnclippedScreenBoundsRect() const { |
| return view()->GetBoundsInScreen(); |
| } |
| |
| gfx::NativeViewAccessible ViewAXPlatformNodeDelegate::HitTestSync(int x, |
| int y) { |
| if (!view() || !view()->GetWidget()) |
| return nullptr; |
| |
| if (IsLeaf()) |
| return GetNativeObject(); |
| |
| // Search child widgets first, since they're on top in the z-order. |
| std::vector<Widget*> child_widgets; |
| bool is_tab_modal_showing; |
| PopulateChildWidgetVector(&child_widgets, &is_tab_modal_showing); |
| for (Widget* child_widget : child_widgets) { |
| View* child_root_view = child_widget->GetRootView(); |
| gfx::Point point(x, y); |
| View::ConvertPointFromScreen(child_root_view, &point); |
| if (child_root_view->HitTestPoint(point)) |
| return child_root_view->GetNativeViewAccessible(); |
| } |
| |
| gfx::Point point(x, y); |
| View::ConvertPointFromScreen(view(), &point); |
| if (!view()->HitTestPoint(point)) |
| return nullptr; |
| |
| // 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. |
| for (int i = view()->child_count() - 1; i >= 0; --i) { |
| View* child_view = view()->child_at(i); |
| if (!child_view->visible()) |
| continue; |
| |
| gfx::Point point_in_child_coords(point); |
| view()->ConvertPointToTarget(view(), child_view, &point_in_child_coords); |
| if (child_view->HitTestPoint(point_in_child_coords)) |
| return child_view->GetNativeViewAccessible(); |
| } |
| |
| // If it's not inside any of our children, it's inside this view. |
| return GetNativeObject(); |
| } |
| |
| gfx::NativeViewAccessible ViewAXPlatformNodeDelegate::GetFocus() { |
| 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); |
| } |
| |
| bool ViewAXPlatformNodeDelegate::AccessibilityPerformAction( |
| const ui::AXActionData& data) { |
| return view()->HandleAccessibleAction(data); |
| } |
| |
| bool ViewAXPlatformNodeDelegate::ShouldIgnoreHoveredStateForTesting() { |
| return false; |
| } |
| |
| bool ViewAXPlatformNodeDelegate::IsOffscreen() const { |
| // TODO: need to implement. |
| return false; |
| } |
| |
| const ui::AXUniqueId& ViewAXPlatformNodeDelegate::GetUniqueId() const { |
| return ViewAccessibility::GetUniqueId(); |
| } |
| |
| void ViewAXPlatformNodeDelegate::PopulateChildWidgetVector( |
| std::vector<Widget*>* result_child_widgets, |
| bool* is_tab_modal_showing) { |
| // Only attach child widgets to the root view. |
| 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()) { |
| *is_tab_modal_showing = false; |
| return; |
| } |
| |
| const views::FocusManager* focus_manager = view()->GetFocusManager(); |
| const views::View* focused_view = |
| focus_manager ? focus_manager->GetFocusedView() : nullptr; |
| |
| std::set<Widget*> child_widgets; |
| Widget::GetAllOwnedWidgets(widget->GetNativeView(), &child_widgets); |
| for (auto iter = child_widgets.begin(); iter != child_widgets.end(); ++iter) { |
| Widget* child_widget = *iter; |
| DCHECK_NE(widget, child_widget); |
| |
| if (!child_widget->IsVisible()) |
| continue; |
| |
| if (widget->GetNativeWindowProperty(kWidgetNativeViewHostKey)) |
| continue; |
| |
| // Focused child widgets should take the place of the web page they cover in |
| // the accessibility tree. |
| if (ViewAccessibilityUtils::IsFocusedChildWidget(child_widget, |
| focused_view)) { |
| result_child_widgets->clear(); |
| result_child_widgets->push_back(child_widget); |
| *is_tab_modal_showing = true; |
| return; |
| } |
| |
| result_child_widgets->push_back(child_widget); |
| } |
| *is_tab_modal_showing = false; |
| } |
| |
| } // namespace views |