blob: 10cc9408045d3a63967ecbc23e4f922c0f64e4f9 [file] [log] [blame]
// 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/platform/ax_platform_node.h"
#include "ui/events/event_utils.h"
#include "ui/gfx/native_widget_types.h"
#include "ui/views/controls/native/native_view_host.h"
#include "ui/views/view.h"
#include "ui/views/widget/widget.h"
namespace views {
namespace {
base::LazyInstance<std::map<int32_t, ui::AXPlatformNode*>>::Leaky
g_unique_id_to_ax_platform_node = LAZY_INSTANCE_INITIALIZER;
// Information required to fire a delayed accessibility event.
struct QueuedEvent {
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.
auto it = g_unique_id_to_ax_platform_node.Get().find(id);
if (it == g_unique_id_to_ax_platform_node.Get().end())
return nullptr;
return it->second;
}
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_node_ = ui::AXPlatformNode::Create(this);
DCHECK(ax_node_);
static bool first_time = true;
if (first_time) {
ui::AXPlatformNode::RegisterNativeWindowHandler(
base::BindRepeating(&FromNativeWindow));
first_time = false;
}
g_unique_id_to_ax_platform_node.Get()[GetUniqueId().Get()] = ax_node_;
}
ViewAXPlatformNodeDelegate::~ViewAXPlatformNodeDelegate() {
if (ui::AXPlatformNode::GetPopupFocusOverride() == GetNativeObject())
ui::AXPlatformNode::SetPopupFocusOverride(nullptr);
g_unique_id_to_ax_platform_node.Get().erase(GetUniqueId().Get());
ax_node_->Destroy();
}
gfx::NativeViewAccessible ViewAXPlatformNodeDelegate::GetNativeObject() {
return ax_node_->GetNativeViewAccessible();
}
void ViewAXPlatformNodeDelegate::NotifyAccessibilityEvent(
ax::mojom::Event event_type) {
if (g_is_queueing_events) {
g_event_queue.Get().push_back({event_type, GetUniqueId().Get()});
return;
}
ax_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_ && GetData().role == ax::mojom::Role::kMenuItem)
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_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_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 it, 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;
int child_count = view()->child_count();
std::vector<Widget*> child_widgets;
PopulateChildWidgetVector(&child_widgets);
child_count += child_widgets.size();
return child_count;
}
gfx::NativeViewAccessible ViewAXPlatformNodeDelegate::ChildAtIndex(int index) {
if (IsLeaf())
return nullptr;
// If this is a root view, our widget might have child widgets. Include
std::vector<Widget*> child_widgets;
PopulateChildWidgetVector(&child_widgets);
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::NativeWindow ViewAXPlatformNodeDelegate::GetTopLevelWidget() {
if (view()->GetWidget())
return view()->GetWidget()->GetTopLevelWidget()->GetNativeWindow();
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;
PopulateChildWidgetVector(&child_widgets);
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;
return focused_view ? focused_view->GetNativeViewAccessible() : nullptr;
}
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) {
// 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())
return;
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;
result_child_widgets->push_back(child_widget);
}
}
} // namespace views