1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "chrome/browser/ui/views/accessibility/accessibility_event_router_views.h"
7 #include "base/basictypes.h"
8 #include "base/callback.h"
9 #include "base/memory/singleton.h"
10 #include "base/message_loop/message_loop.h"
11 #include "base/strings/utf_string_conversions.h"
12 #include "chrome/browser/accessibility/accessibility_extension_api.h"
13 #include "chrome/browser/browser_process.h"
14 #include "chrome/browser/chrome_notification_types.h"
15 #include "chrome/browser/profiles/profile.h"
16 #include "chrome/browser/profiles/profile_manager.h"
17 #include "content/public/browser/notification_service.h"
18 #include "ui/accessibility/ax_view_state.h"
19 #include "ui/views/controls/menu/menu_item_view.h"
20 #include "ui/views/controls/menu/submenu_view.h"
21 #include "ui/views/controls/tree/tree_view.h"
22 #include "ui/views/focus/view_storage.h"
23 #include "ui/views/view.h"
24 #include "ui/views/widget/widget.h"
26 using views::FocusManager;
27 using views::ViewStorage;
29 AccessibilityEventRouterViews::AccessibilityEventRouterViews()
30 : most_recent_profile_(NULL),
32 ViewStorage::GetInstance()->CreateStorageID()) {
33 // Register for notification when profile is destroyed to ensure that all
34 // observers are detatched at that time.
35 registrar_.Add(this, chrome::NOTIFICATION_PROFILE_DESTROYED,
36 content::NotificationService::AllSources());
39 AccessibilityEventRouterViews::~AccessibilityEventRouterViews() {
43 AccessibilityEventRouterViews* AccessibilityEventRouterViews::GetInstance() {
44 return Singleton<AccessibilityEventRouterViews>::get();
47 void AccessibilityEventRouterViews::HandleAccessibilityEvent(
48 views::View* view, ui::AXEvent event_type) {
49 if (!ExtensionAccessibilityEventRouter::GetInstance()->
50 IsAccessibilityEnabled()) {
54 if (event_type == ui::AX_EVENT_TEXT_CHANGED ||
55 event_type == ui::AX_EVENT_TEXT_SELECTION_CHANGED) {
56 // These two events should only be sent for views that have focus. This
57 // enforces the invariant that we fire events triggered by user action and
58 // not by programmatic logic. For example, the location bar can be updated
59 // by javascript while the user focus is within some other part of the
60 // user interface. In contrast, the other supported events here do not
61 // depend on focus. For example, a menu within a menubar can open or close
62 // while focus is within the location bar or anywhere else as a result of
63 // user action. Note that the below logic can at some point be removed if
64 // we pass more information along to the listener such as focused state.
65 if (!view->GetFocusManager() ||
66 view->GetFocusManager()->GetFocusedView() != view)
70 // Don't dispatch the accessibility event until the next time through the
71 // event loop, to handle cases where the view's state changes after
72 // the call to post the event. It's safe to use base::Unretained(this)
73 // because AccessibilityEventRouterViews is a singleton.
74 ViewStorage* view_storage = ViewStorage::GetInstance();
75 int view_storage_id = view_storage->CreateStorageID();
76 view_storage->StoreView(view_storage_id, view);
77 base::MessageLoop::current()->PostTask(
80 &AccessibilityEventRouterViews::DispatchEventOnViewStorageId,
85 void AccessibilityEventRouterViews::HandleMenuItemFocused(
86 const base::string16& menu_name,
87 const base::string16& menu_item_name,
91 if (!ExtensionAccessibilityEventRouter::GetInstance()->
92 IsAccessibilityEnabled()) {
96 if (!most_recent_profile_)
99 AccessibilityMenuItemInfo info(most_recent_profile_,
100 base::UTF16ToUTF8(menu_item_name),
101 base::UTF16ToUTF8(menu_name),
105 SendControlAccessibilityNotification(
106 ui::AX_EVENT_FOCUS, &info);
109 void AccessibilityEventRouterViews::Observe(
111 const content::NotificationSource& source,
112 const content::NotificationDetails& details) {
113 DCHECK_EQ(type, chrome::NOTIFICATION_PROFILE_DESTROYED);
114 Profile* profile = content::Source<Profile>(source).ptr();
115 if (profile == most_recent_profile_)
116 most_recent_profile_ = NULL;
123 void AccessibilityEventRouterViews::DispatchEventOnViewStorageId(
126 ViewStorage* view_storage = ViewStorage::GetInstance();
127 views::View* view = view_storage->RetrieveView(view_storage_id);
128 view_storage->RemoveView(view_storage_id);
132 AccessibilityEventRouterViews* instance =
133 AccessibilityEventRouterViews::GetInstance();
134 instance->DispatchAccessibilityEvent(view, type);
137 void AccessibilityEventRouterViews::DispatchAccessibilityEvent(
138 views::View* view, ui::AXEvent type) {
139 // Get the profile associated with this view. If it's not found, use
140 // the most recent profile where accessibility events were sent, or
141 // the default profile.
142 Profile* profile = NULL;
143 views::Widget* widget = view->GetWidget();
145 profile = reinterpret_cast<Profile*>(
146 widget->GetNativeWindowProperty(Profile::kProfileKey));
149 profile = most_recent_profile_;
151 if (g_browser_process->profile_manager())
152 profile = g_browser_process->profile_manager()->GetLastUsedProfile();
155 LOG(WARNING) << "Accessibility notification but no profile";
159 most_recent_profile_ = profile;
161 if (type == ui::AX_EVENT_MENU_START ||
162 type == ui::AX_EVENT_MENU_POPUP_START ||
163 type == ui::AX_EVENT_MENU_END ||
164 type == ui::AX_EVENT_MENU_POPUP_END) {
165 SendMenuNotification(view, type, profile);
169 view = FindFirstAccessibleAncestor(view);
171 // Since multiple items could share a highest focusable view, these items
172 // could all dispatch the same accessibility hover events, which isn't
174 if (type == ui::AX_EVENT_HOVER &&
175 ViewStorage::GetInstance()->RetrieveView(most_recent_view_id_) == view) {
178 // If there was already a view stored here from before, it must be removed
179 // before storing a new view.
180 ViewStorage::GetInstance()->RemoveView(most_recent_view_id_);
181 ViewStorage::GetInstance()->StoreView(most_recent_view_id_, view);
183 ui::AXViewState state;
184 view->GetAccessibleState(&state);
186 if (type == ui::AX_EVENT_ALERT &&
187 !(state.role == ui::AX_ROLE_ALERT ||
188 state.role == ui::AX_ROLE_WINDOW)) {
189 SendAlertControlNotification(view, type, profile);
193 switch (state.role) {
194 case ui::AX_ROLE_ALERT:
195 case ui::AX_ROLE_DIALOG:
196 case ui::AX_ROLE_WINDOW:
197 SendWindowNotification(view, type, profile);
199 case ui::AX_ROLE_POP_UP_BUTTON:
200 case ui::AX_ROLE_MENU_BAR:
201 case ui::AX_ROLE_MENU_LIST_POPUP:
202 SendMenuNotification(view, type, profile);
204 case ui::AX_ROLE_BUTTON_DROP_DOWN:
205 case ui::AX_ROLE_BUTTON:
206 SendButtonNotification(view, type, profile);
208 case ui::AX_ROLE_CHECK_BOX:
209 SendCheckboxNotification(view, type, profile);
211 case ui::AX_ROLE_COMBO_BOX:
212 SendComboboxNotification(view, type, profile);
214 case ui::AX_ROLE_LINK:
215 SendLinkNotification(view, type, profile);
217 case ui::AX_ROLE_LOCATION_BAR:
218 case ui::AX_ROLE_TEXT_FIELD:
219 SendTextfieldNotification(view, type, profile);
221 case ui::AX_ROLE_MENU_ITEM:
222 SendMenuItemNotification(view, type, profile);
224 case ui::AX_ROLE_RADIO_BUTTON:
226 case ui::AX_ROLE_SLIDER:
227 SendSliderNotification(view, type, profile);
229 case ui::AX_ROLE_STATIC_TEXT:
230 SendStaticTextNotification(view, type, profile);
232 case ui::AX_ROLE_TREE:
233 SendTreeNotification(view, type, profile);
235 case ui::AX_ROLE_TAB:
236 SendTabNotification(view, type, profile);
238 case ui::AX_ROLE_TREE_ITEM:
239 SendTreeItemNotification(view, type, profile);
242 // Hover events can fire on literally any view, so it's safe to
243 // ignore ones we don't care about.
244 if (type == ui::AX_EVENT_HOVER)
247 // If this is encountered, please file a bug with the role that wasn't
248 // caught so we can add accessibility extension API support.
254 void AccessibilityEventRouterViews::SendTabNotification(
258 ui::AXViewState state;
259 view->GetAccessibleState(&state);
260 if (state.index == -1)
262 std::string name = base::UTF16ToUTF8(state.name);
263 std::string context = GetViewContext(view);
264 AccessibilityTabInfo info(profile, name, context, state.index, state.count);
265 SendControlAccessibilityNotification(event, &info);
269 void AccessibilityEventRouterViews::SendButtonNotification(
273 AccessibilityButtonInfo info(
274 profile, GetViewName(view), GetViewContext(view));
275 SendControlAccessibilityNotification(event, &info);
279 void AccessibilityEventRouterViews::SendStaticTextNotification(
283 AccessibilityStaticTextInfo info(
284 profile, GetViewName(view), GetViewContext(view));
285 SendControlAccessibilityNotification(event, &info);
289 void AccessibilityEventRouterViews::SendLinkNotification(
293 AccessibilityLinkInfo info(profile, GetViewName(view), GetViewContext(view));
294 SendControlAccessibilityNotification(event, &info);
298 void AccessibilityEventRouterViews::SendMenuNotification(
302 AccessibilityMenuInfo info(profile, GetViewName(view));
303 SendMenuAccessibilityNotification(event, &info);
307 void AccessibilityEventRouterViews::SendMenuItemNotification(
311 std::string name = GetViewName(view);
312 std::string context = GetViewContext(view);
314 bool has_submenu = false;
318 if (!strcmp(view->GetClassName(), views::MenuItemView::kViewClassName))
319 has_submenu = static_cast<views::MenuItemView*>(view)->HasSubmenu();
321 views::View* parent_menu = view->parent();
322 while (parent_menu != NULL && strcmp(parent_menu->GetClassName(),
323 views::SubmenuView::kViewClassName)) {
324 parent_menu = parent_menu->parent();
328 RecursiveGetMenuItemIndexAndCount(parent_menu, view, &index, &count);
331 AccessibilityMenuItemInfo info(
332 profile, name, context, has_submenu, index, count);
333 SendControlAccessibilityNotification(event, &info);
337 void AccessibilityEventRouterViews::SendTreeNotification(
341 AccessibilityTreeInfo info(profile, GetViewName(view));
342 SendControlAccessibilityNotification(event, &info);
346 void AccessibilityEventRouterViews::SendTreeItemNotification(
350 std::string name = GetViewName(view);
351 std::string context = GetViewContext(view);
353 if (strcmp(view->GetClassName(), views::TreeView::kViewClassName) != 0) {
358 views::TreeView* tree = static_cast<views::TreeView*>(view);
359 ui::TreeModelNode* selected_node = tree->GetSelectedNode();
360 ui::TreeModel* model = tree->model();
362 int siblings_count = model->GetChildCount(model->GetRoot());
363 int children_count = -1;
366 bool is_expanded = false;
369 children_count = model->GetChildCount(selected_node);
370 is_expanded = tree->IsExpanded(selected_node);
371 ui::TreeModelNode* parent_node = model->GetParent(selected_node);
373 index = model->GetIndexOf(parent_node, selected_node);
374 siblings_count = model->GetChildCount(parent_node);
378 while (parent_node) {
380 parent_node = model->GetParent(parent_node);
384 AccessibilityTreeItemInfo info(
385 profile, name, context, depth, index, siblings_count, children_count,
387 SendControlAccessibilityNotification(event, &info);
391 void AccessibilityEventRouterViews::SendTextfieldNotification(
395 ui::AXViewState state;
396 view->GetAccessibleState(&state);
397 std::string name = base::UTF16ToUTF8(state.name);
398 std::string context = GetViewContext(view);
399 bool password = state.HasStateFlag(ui::AX_STATE_PROTECTED);
400 AccessibilityTextBoxInfo info(profile, name, context, password);
401 std::string value = base::UTF16ToUTF8(state.value);
402 info.SetValue(value, state.selection_start, state.selection_end);
403 SendControlAccessibilityNotification(event, &info);
407 void AccessibilityEventRouterViews::SendComboboxNotification(
411 ui::AXViewState state;
412 view->GetAccessibleState(&state);
413 std::string name = base::UTF16ToUTF8(state.name);
414 std::string value = base::UTF16ToUTF8(state.value);
415 std::string context = GetViewContext(view);
416 AccessibilityComboBoxInfo info(
417 profile, name, context, value, state.index, state.count);
418 SendControlAccessibilityNotification(event, &info);
422 void AccessibilityEventRouterViews::SendCheckboxNotification(
426 ui::AXViewState state;
427 view->GetAccessibleState(&state);
428 std::string name = base::UTF16ToUTF8(state.name);
429 std::string context = GetViewContext(view);
430 AccessibilityCheckboxInfo info(
434 state.HasStateFlag(ui::AX_STATE_CHECKED));
435 SendControlAccessibilityNotification(event, &info);
439 void AccessibilityEventRouterViews::SendWindowNotification(
443 ui::AXViewState state;
444 view->GetAccessibleState(&state);
445 std::string window_text;
447 // If it's an alert, try to get the text from the contents of the
448 // static text, not the window title.
449 if (state.role == ui::AX_ROLE_ALERT)
450 window_text = RecursiveGetStaticText(view);
452 // Otherwise get it from the window's accessible name.
453 if (window_text.empty())
454 window_text = base::UTF16ToUTF8(state.name);
456 AccessibilityWindowInfo info(profile, window_text);
457 SendWindowAccessibilityNotification(event, &info);
461 void AccessibilityEventRouterViews::SendSliderNotification(
465 ui::AXViewState state;
466 view->GetAccessibleState(&state);
468 std::string name = base::UTF16ToUTF8(state.name);
469 std::string value = base::UTF16ToUTF8(state.value);
470 std::string context = GetViewContext(view);
471 AccessibilitySliderInfo info(
476 SendControlAccessibilityNotification(event, &info);
480 void AccessibilityEventRouterViews::SendAlertControlNotification(
484 ui::AXViewState state;
485 view->GetAccessibleState(&state);
487 std::string name = base::UTF16ToUTF8(state.name);
488 AccessibilityAlertInfo info(
491 SendControlAccessibilityNotification(event, &info);
495 std::string AccessibilityEventRouterViews::GetViewName(views::View* view) {
496 ui::AXViewState state;
497 view->GetAccessibleState(&state);
498 return base::UTF16ToUTF8(state.name);
502 std::string AccessibilityEventRouterViews::GetViewContext(views::View* view) {
503 for (views::View* parent = view->parent();
505 parent = parent->parent()) {
506 ui::AXViewState state;
507 parent->GetAccessibleState(&state);
509 // Two cases are handled right now. More could be added in the future
510 // depending on how the UI evolves.
512 // A control inside of alert, toolbar or dialog should use that container's
514 if ((state.role == ui::AX_ROLE_ALERT ||
515 state.role == ui::AX_ROLE_DIALOG ||
516 state.role == ui::AX_ROLE_TOOLBAR) &&
517 !state.name.empty()) {
518 return base::UTF16ToUTF8(state.name);
521 // A control inside of an alert or dialog (including an infobar)
522 // should grab the first static text descendant as the context;
523 // that's the prompt.
524 if (state.role == ui::AX_ROLE_ALERT ||
525 state.role == ui::AX_ROLE_DIALOG) {
526 views::View* static_text_child = FindDescendantWithAccessibleRole(
527 parent, ui::AX_ROLE_STATIC_TEXT);
528 if (static_text_child) {
529 ui::AXViewState state;
530 static_text_child->GetAccessibleState(&state);
531 if (!state.name.empty())
532 return base::UTF16ToUTF8(state.name);
534 return std::string();
538 return std::string();
542 views::View* AccessibilityEventRouterViews::FindDescendantWithAccessibleRole(
543 views::View* view, ui::AXRole role) {
544 ui::AXViewState state;
545 view->GetAccessibleState(&state);
546 if (state.role == role)
549 for (int i = 0; i < view->child_count(); i++) {
550 views::View* child = view->child_at(i);
551 views::View* result = FindDescendantWithAccessibleRole(child, role);
560 void AccessibilityEventRouterViews::RecursiveGetMenuItemIndexAndCount(
565 for (int i = 0; i < menu->child_count(); ++i) {
566 views::View* child = menu->child_at(i);
567 if (!child->visible())
570 int previous_count = *count;
571 RecursiveGetMenuItemIndexAndCount(child, item, index, count);
572 ui::AXViewState state;
573 child->GetAccessibleState(&state);
574 if (state.role == ui::AX_ROLE_MENU_ITEM &&
575 *count == previous_count) {
579 } else if (state.role == ui::AX_ROLE_BUTTON) {
588 std::string AccessibilityEventRouterViews::RecursiveGetStaticText(
590 ui::AXViewState state;
591 view->GetAccessibleState(&state);
592 if (state.role == ui::AX_ROLE_STATIC_TEXT)
593 return base::UTF16ToUTF8(state.name);
595 for (int i = 0; i < view->child_count(); ++i) {
596 views::View* child = view->child_at(i);
597 std::string result = RecursiveGetStaticText(child);
601 return std::string();
605 views::View* AccessibilityEventRouterViews::FindFirstAccessibleAncestor(
607 views::View* temp_view = view;
608 while (temp_view->parent() && !temp_view->IsAccessibilityFocusable())
609 temp_view = temp_view->parent();
610 if (temp_view->IsAccessibilityFocusable())