blob: 84f173e03e16faa9db1a82107d345afa831a9209 [file] [log] [blame]
// Copyright (c) 2012 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 "chrome/browser/ui/views/accessibility/accessibility_event_router_views.h"
#include "base/basictypes.h"
#include "base/callback.h"
#include "base/memory/singleton.h"
#include "base/message_loop/message_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/accessibility/accessibility_extension_api.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/chrome_notification_types.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "content/public/browser/notification_service.h"
#include "ui/accessibility/ax_view_state.h"
#include "ui/views/controls/menu/menu_item_view.h"
#include "ui/views/controls/menu/submenu_view.h"
#include "ui/views/controls/tree/tree_view.h"
#include "ui/views/focus/view_storage.h"
#include "ui/views/view.h"
#include "ui/views/widget/widget.h"
using views::FocusManager;
using views::ViewStorage;
AccessibilityEventRouterViews::AccessibilityEventRouterViews()
: most_recent_profile_(NULL),
most_recent_view_id_(
ViewStorage::GetInstance()->CreateStorageID()) {
// Register for notification when profile is destroyed to ensure that all
// observers are detatched at that time.
registrar_.Add(this, chrome::NOTIFICATION_PROFILE_DESTROYED,
content::NotificationService::AllSources());
}
AccessibilityEventRouterViews::~AccessibilityEventRouterViews() {
}
// static
AccessibilityEventRouterViews* AccessibilityEventRouterViews::GetInstance() {
return Singleton<AccessibilityEventRouterViews>::get();
}
void AccessibilityEventRouterViews::HandleAccessibilityEvent(
views::View* view, ui::AXEvent event_type) {
if (!ExtensionAccessibilityEventRouter::GetInstance()->
IsAccessibilityEnabled()) {
return;
}
if (event_type == ui::AX_EVENT_TEXT_CHANGED ||
event_type == ui::AX_EVENT_TEXT_SELECTION_CHANGED) {
// These two events should only be sent for views that have focus. This
// enforces the invariant that we fire events triggered by user action and
// not by programmatic logic. For example, the location bar can be updated
// by javascript while the user focus is within some other part of the
// user interface. In contrast, the other supported events here do not
// depend on focus. For example, a menu within a menubar can open or close
// while focus is within the location bar or anywhere else as a result of
// user action. Note that the below logic can at some point be removed if
// we pass more information along to the listener such as focused state.
if (!view->GetFocusManager() ||
view->GetFocusManager()->GetFocusedView() != view)
return;
}
// Don't dispatch the accessibility event until the next time through the
// event loop, to handle cases where the view's state changes after
// the call to post the event. It's safe to use base::Unretained(this)
// because AccessibilityEventRouterViews is a singleton.
ViewStorage* view_storage = ViewStorage::GetInstance();
int view_storage_id = view_storage->CreateStorageID();
view_storage->StoreView(view_storage_id, view);
base::MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(
&AccessibilityEventRouterViews::DispatchEventOnViewStorageId,
view_storage_id,
event_type));
}
void AccessibilityEventRouterViews::HandleMenuItemFocused(
const base::string16& menu_name,
const base::string16& menu_item_name,
int item_index,
int item_count,
bool has_submenu) {
if (!ExtensionAccessibilityEventRouter::GetInstance()->
IsAccessibilityEnabled()) {
return;
}
if (!most_recent_profile_)
return;
AccessibilityMenuItemInfo info(most_recent_profile_,
base::UTF16ToUTF8(menu_item_name),
base::UTF16ToUTF8(menu_name),
has_submenu,
item_index,
item_count);
SendControlAccessibilityNotification(
ui::AX_EVENT_FOCUS, &info);
}
void AccessibilityEventRouterViews::Observe(
int type,
const content::NotificationSource& source,
const content::NotificationDetails& details) {
DCHECK_EQ(type, chrome::NOTIFICATION_PROFILE_DESTROYED);
Profile* profile = content::Source<Profile>(source).ptr();
if (profile == most_recent_profile_)
most_recent_profile_ = NULL;
}
//
// Private methods
//
void AccessibilityEventRouterViews::DispatchEventOnViewStorageId(
int view_storage_id,
ui::AXEvent type) {
ViewStorage* view_storage = ViewStorage::GetInstance();
views::View* view = view_storage->RetrieveView(view_storage_id);
view_storage->RemoveView(view_storage_id);
if (!view)
return;
AccessibilityEventRouterViews* instance =
AccessibilityEventRouterViews::GetInstance();
instance->DispatchAccessibilityEvent(view, type);
}
void AccessibilityEventRouterViews::DispatchAccessibilityEvent(
views::View* view, ui::AXEvent type) {
// Get the profile associated with this view. If it's not found, use
// the most recent profile where accessibility events were sent, or
// the default profile.
Profile* profile = NULL;
views::Widget* widget = view->GetWidget();
if (widget) {
profile = reinterpret_cast<Profile*>(
widget->GetNativeWindowProperty(Profile::kProfileKey));
}
if (!profile)
profile = most_recent_profile_;
if (!profile) {
if (g_browser_process->profile_manager())
profile = g_browser_process->profile_manager()->GetLastUsedProfile();
}
if (!profile) {
LOG(WARNING) << "Accessibility notification but no profile";
return;
}
most_recent_profile_ = profile;
if (type == ui::AX_EVENT_MENU_START ||
type == ui::AX_EVENT_MENU_POPUP_START ||
type == ui::AX_EVENT_MENU_END ||
type == ui::AX_EVENT_MENU_POPUP_END) {
SendMenuNotification(view, type, profile);
return;
}
view = FindFirstAccessibleAncestor(view);
// Since multiple items could share a highest focusable view, these items
// could all dispatch the same accessibility hover events, which isn't
// necessary.
if (type == ui::AX_EVENT_HOVER &&
ViewStorage::GetInstance()->RetrieveView(most_recent_view_id_) == view) {
return;
}
// If there was already a view stored here from before, it must be removed
// before storing a new view.
ViewStorage::GetInstance()->RemoveView(most_recent_view_id_);
ViewStorage::GetInstance()->StoreView(most_recent_view_id_, view);
ui::AXViewState state;
view->GetAccessibleState(&state);
if (type == ui::AX_EVENT_ALERT &&
!(state.role == ui::AX_ROLE_ALERT ||
state.role == ui::AX_ROLE_WINDOW)) {
SendAlertControlNotification(view, type, profile);
return;
}
switch (state.role) {
case ui::AX_ROLE_ALERT:
case ui::AX_ROLE_DIALOG:
case ui::AX_ROLE_WINDOW:
SendWindowNotification(view, type, profile);
break;
case ui::AX_ROLE_POP_UP_BUTTON:
case ui::AX_ROLE_MENU_BAR:
case ui::AX_ROLE_MENU_LIST_POPUP:
SendMenuNotification(view, type, profile);
break;
case ui::AX_ROLE_BUTTON_DROP_DOWN:
case ui::AX_ROLE_BUTTON:
SendButtonNotification(view, type, profile);
break;
case ui::AX_ROLE_CHECK_BOX:
SendCheckboxNotification(view, type, profile);
break;
case ui::AX_ROLE_COMBO_BOX:
SendComboboxNotification(view, type, profile);
break;
case ui::AX_ROLE_LINK:
SendLinkNotification(view, type, profile);
break;
case ui::AX_ROLE_LOCATION_BAR:
case ui::AX_ROLE_TEXT_FIELD:
SendTextfieldNotification(view, type, profile);
break;
case ui::AX_ROLE_MENU_ITEM:
SendMenuItemNotification(view, type, profile);
break;
case ui::AX_ROLE_RADIO_BUTTON:
// Not used anymore?
case ui::AX_ROLE_SLIDER:
SendSliderNotification(view, type, profile);
break;
case ui::AX_ROLE_STATIC_TEXT:
SendStaticTextNotification(view, type, profile);
break;
case ui::AX_ROLE_TREE:
SendTreeNotification(view, type, profile);
break;
case ui::AX_ROLE_TAB:
SendTabNotification(view, type, profile);
break;
case ui::AX_ROLE_TREE_ITEM:
SendTreeItemNotification(view, type, profile);
break;
default:
// Hover events can fire on literally any view, so it's safe to
// ignore ones we don't care about.
if (type == ui::AX_EVENT_HOVER)
break;
// If this is encountered, please file a bug with the role that wasn't
// caught so we can add accessibility extension API support.
NOTREACHED();
}
}
// static
void AccessibilityEventRouterViews::SendTabNotification(
views::View* view,
ui::AXEvent event,
Profile* profile) {
ui::AXViewState state;
view->GetAccessibleState(&state);
if (state.index == -1)
return;
std::string name = base::UTF16ToUTF8(state.name);
std::string context = GetViewContext(view);
AccessibilityTabInfo info(profile, name, context, state.index, state.count);
SendControlAccessibilityNotification(event, &info);
}
// static
void AccessibilityEventRouterViews::SendButtonNotification(
views::View* view,
ui::AXEvent event,
Profile* profile) {
AccessibilityButtonInfo info(
profile, GetViewName(view), GetViewContext(view));
SendControlAccessibilityNotification(event, &info);
}
// static
void AccessibilityEventRouterViews::SendStaticTextNotification(
views::View* view,
ui::AXEvent event,
Profile* profile) {
AccessibilityStaticTextInfo info(
profile, GetViewName(view), GetViewContext(view));
SendControlAccessibilityNotification(event, &info);
}
// static
void AccessibilityEventRouterViews::SendLinkNotification(
views::View* view,
ui::AXEvent event,
Profile* profile) {
AccessibilityLinkInfo info(profile, GetViewName(view), GetViewContext(view));
SendControlAccessibilityNotification(event, &info);
}
// static
void AccessibilityEventRouterViews::SendMenuNotification(
views::View* view,
ui::AXEvent event,
Profile* profile) {
AccessibilityMenuInfo info(profile, GetViewName(view));
SendMenuAccessibilityNotification(event, &info);
}
// static
void AccessibilityEventRouterViews::SendMenuItemNotification(
views::View* view,
ui::AXEvent event,
Profile* profile) {
std::string name = GetViewName(view);
std::string context = GetViewContext(view);
bool has_submenu = false;
int index = -1;
int count = -1;
if (!strcmp(view->GetClassName(), views::MenuItemView::kViewClassName))
has_submenu = static_cast<views::MenuItemView*>(view)->HasSubmenu();
views::View* parent_menu = view->parent();
while (parent_menu != NULL && strcmp(parent_menu->GetClassName(),
views::SubmenuView::kViewClassName)) {
parent_menu = parent_menu->parent();
}
if (parent_menu) {
count = 0;
RecursiveGetMenuItemIndexAndCount(parent_menu, view, &index, &count);
}
AccessibilityMenuItemInfo info(
profile, name, context, has_submenu, index, count);
SendControlAccessibilityNotification(event, &info);
}
// static
void AccessibilityEventRouterViews::SendTreeNotification(
views::View* view,
ui::AXEvent event,
Profile* profile) {
AccessibilityTreeInfo info(profile, GetViewName(view));
SendControlAccessibilityNotification(event, &info);
}
// static
void AccessibilityEventRouterViews::SendTreeItemNotification(
views::View* view,
ui::AXEvent event,
Profile* profile) {
std::string name = GetViewName(view);
std::string context = GetViewContext(view);
if (strcmp(view->GetClassName(), views::TreeView::kViewClassName) != 0) {
NOTREACHED();
return;
}
views::TreeView* tree = static_cast<views::TreeView*>(view);
ui::TreeModelNode* selected_node = tree->GetSelectedNode();
ui::TreeModel* model = tree->model();
int siblings_count = model->GetChildCount(model->GetRoot());
int children_count = -1;
int index = -1;
int depth = -1;
bool is_expanded = false;
if (selected_node) {
children_count = model->GetChildCount(selected_node);
is_expanded = tree->IsExpanded(selected_node);
ui::TreeModelNode* parent_node = model->GetParent(selected_node);
if (parent_node) {
index = model->GetIndexOf(parent_node, selected_node);
siblings_count = model->GetChildCount(parent_node);
}
// Get node depth.
depth = 0;
while (parent_node) {
depth++;
parent_node = model->GetParent(parent_node);
}
}
AccessibilityTreeItemInfo info(
profile, name, context, depth, index, siblings_count, children_count,
is_expanded);
SendControlAccessibilityNotification(event, &info);
}
// static
void AccessibilityEventRouterViews::SendTextfieldNotification(
views::View* view,
ui::AXEvent event,
Profile* profile) {
ui::AXViewState state;
view->GetAccessibleState(&state);
std::string name = base::UTF16ToUTF8(state.name);
std::string context = GetViewContext(view);
bool password = state.HasStateFlag(ui::AX_STATE_PROTECTED);
AccessibilityTextBoxInfo info(profile, name, context, password);
std::string value = base::UTF16ToUTF8(state.value);
info.SetValue(value, state.selection_start, state.selection_end);
SendControlAccessibilityNotification(event, &info);
}
// static
void AccessibilityEventRouterViews::SendComboboxNotification(
views::View* view,
ui::AXEvent event,
Profile* profile) {
ui::AXViewState state;
view->GetAccessibleState(&state);
std::string name = base::UTF16ToUTF8(state.name);
std::string value = base::UTF16ToUTF8(state.value);
std::string context = GetViewContext(view);
AccessibilityComboBoxInfo info(
profile, name, context, value, state.index, state.count);
SendControlAccessibilityNotification(event, &info);
}
// static
void AccessibilityEventRouterViews::SendCheckboxNotification(
views::View* view,
ui::AXEvent event,
Profile* profile) {
ui::AXViewState state;
view->GetAccessibleState(&state);
std::string name = base::UTF16ToUTF8(state.name);
std::string context = GetViewContext(view);
AccessibilityCheckboxInfo info(
profile,
name,
context,
state.HasStateFlag(ui::AX_STATE_CHECKED));
SendControlAccessibilityNotification(event, &info);
}
// static
void AccessibilityEventRouterViews::SendWindowNotification(
views::View* view,
ui::AXEvent event,
Profile* profile) {
ui::AXViewState state;
view->GetAccessibleState(&state);
std::string window_text;
// If it's an alert, try to get the text from the contents of the
// static text, not the window title.
if (state.role == ui::AX_ROLE_ALERT)
window_text = RecursiveGetStaticText(view);
// Otherwise get it from the window's accessible name.
if (window_text.empty())
window_text = base::UTF16ToUTF8(state.name);
AccessibilityWindowInfo info(profile, window_text);
SendWindowAccessibilityNotification(event, &info);
}
// static
void AccessibilityEventRouterViews::SendSliderNotification(
views::View* view,
ui::AXEvent event,
Profile* profile) {
ui::AXViewState state;
view->GetAccessibleState(&state);
std::string name = base::UTF16ToUTF8(state.name);
std::string value = base::UTF16ToUTF8(state.value);
std::string context = GetViewContext(view);
AccessibilitySliderInfo info(
profile,
name,
context,
value);
SendControlAccessibilityNotification(event, &info);
}
// static
void AccessibilityEventRouterViews::SendAlertControlNotification(
views::View* view,
ui::AXEvent event,
Profile* profile) {
ui::AXViewState state;
view->GetAccessibleState(&state);
std::string name = base::UTF16ToUTF8(state.name);
AccessibilityAlertInfo info(
profile,
name);
SendControlAccessibilityNotification(event, &info);
}
// static
std::string AccessibilityEventRouterViews::GetViewName(views::View* view) {
ui::AXViewState state;
view->GetAccessibleState(&state);
return base::UTF16ToUTF8(state.name);
}
// static
std::string AccessibilityEventRouterViews::GetViewContext(views::View* view) {
for (views::View* parent = view->parent();
parent;
parent = parent->parent()) {
ui::AXViewState state;
parent->GetAccessibleState(&state);
// Two cases are handled right now. More could be added in the future
// depending on how the UI evolves.
// A control inside of alert, toolbar or dialog should use that container's
// accessible name.
if ((state.role == ui::AX_ROLE_ALERT ||
state.role == ui::AX_ROLE_DIALOG ||
state.role == ui::AX_ROLE_TOOLBAR) &&
!state.name.empty()) {
return base::UTF16ToUTF8(state.name);
}
// A control inside of an alert or dialog (including an infobar)
// should grab the first static text descendant as the context;
// that's the prompt.
if (state.role == ui::AX_ROLE_ALERT ||
state.role == ui::AX_ROLE_DIALOG) {
views::View* static_text_child = FindDescendantWithAccessibleRole(
parent, ui::AX_ROLE_STATIC_TEXT);
if (static_text_child) {
ui::AXViewState state;
static_text_child->GetAccessibleState(&state);
if (!state.name.empty())
return base::UTF16ToUTF8(state.name);
}
return std::string();
}
}
return std::string();
}
// static
views::View* AccessibilityEventRouterViews::FindDescendantWithAccessibleRole(
views::View* view, ui::AXRole role) {
ui::AXViewState state;
view->GetAccessibleState(&state);
if (state.role == role)
return view;
for (int i = 0; i < view->child_count(); i++) {
views::View* child = view->child_at(i);
views::View* result = FindDescendantWithAccessibleRole(child, role);
if (result)
return result;
}
return NULL;
}
// static
void AccessibilityEventRouterViews::RecursiveGetMenuItemIndexAndCount(
views::View* menu,
views::View* item,
int* index,
int* count) {
for (int i = 0; i < menu->child_count(); ++i) {
views::View* child = menu->child_at(i);
if (!child->visible())
continue;
int previous_count = *count;
RecursiveGetMenuItemIndexAndCount(child, item, index, count);
ui::AXViewState state;
child->GetAccessibleState(&state);
if (state.role == ui::AX_ROLE_MENU_ITEM &&
*count == previous_count) {
if (item == child)
*index = *count;
(*count)++;
} else if (state.role == ui::AX_ROLE_BUTTON) {
if (item == child)
*index = *count;
(*count)++;
}
}
}
// static
std::string AccessibilityEventRouterViews::RecursiveGetStaticText(
views::View* view) {
ui::AXViewState state;
view->GetAccessibleState(&state);
if (state.role == ui::AX_ROLE_STATIC_TEXT)
return base::UTF16ToUTF8(state.name);
for (int i = 0; i < view->child_count(); ++i) {
views::View* child = view->child_at(i);
std::string result = RecursiveGetStaticText(child);
if (!result.empty())
return result;
}
return std::string();
}
// static
views::View* AccessibilityEventRouterViews::FindFirstAccessibleAncestor(
views::View* view) {
views::View* temp_view = view;
while (temp_view->parent() && !temp_view->IsAccessibilityFocusable())
temp_view = temp_view->parent();
if (temp_view->IsAccessibilityFocusable())
return temp_view;
return view;
}