accessibility_event_router_views.cc revision effb81e5f8246d0db0270817048dc992db66e9fb
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.
4
5#include "chrome/browser/ui/views/accessibility/accessibility_event_router_views.h"
6
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"
25
26using views::FocusManager;
27
28AccessibilityEventRouterViews::AccessibilityEventRouterViews()
29    : most_recent_profile_(NULL) {
30  // Register for notification when profile is destroyed to ensure that all
31  // observers are detatched at that time.
32  registrar_.Add(this, chrome::NOTIFICATION_PROFILE_DESTROYED,
33                 content::NotificationService::AllSources());
34}
35
36AccessibilityEventRouterViews::~AccessibilityEventRouterViews() {
37}
38
39// static
40AccessibilityEventRouterViews* AccessibilityEventRouterViews::GetInstance() {
41  return Singleton<AccessibilityEventRouterViews>::get();
42}
43
44void AccessibilityEventRouterViews::HandleAccessibilityEvent(
45    views::View* view, ui::AXEvent event_type) {
46  if (!ExtensionAccessibilityEventRouter::GetInstance()->
47      IsAccessibilityEnabled()) {
48    return;
49  }
50
51  if (event_type == ui::AX_EVENT_TEXT_CHANGED ||
52      event_type == ui::AX_EVENT_SELECTION_CHANGED) {
53    // These two events should only be sent for views that have focus. This
54    // enforces the invariant that we fire events triggered by user action and
55    // not by programmatic logic. For example, the location bar can be updated
56    // by javascript while the user focus is within some other part of the
57    // user interface. In contrast, the other supported events here do not
58    // depend on focus. For example, a menu within a menubar can open or close
59    // while focus is within the location bar or anywhere else as a result of
60    // user action. Note that the below logic can at some point be removed if
61    // we pass more information along to the listener such as focused state.
62    if (!view->GetFocusManager() ||
63        view->GetFocusManager()->GetFocusedView() != view)
64      return;
65  }
66
67  // Don't dispatch the accessibility event until the next time through the
68  // event loop, to handle cases where the view's state changes after
69  // the call to post the event. It's safe to use base::Unretained(this)
70  // because AccessibilityEventRouterViews is a singleton.
71  views::ViewStorage* view_storage = views::ViewStorage::GetInstance();
72  int view_storage_id = view_storage->CreateStorageID();
73  view_storage->StoreView(view_storage_id, view);
74  base::MessageLoop::current()->PostTask(
75      FROM_HERE,
76      base::Bind(
77          &AccessibilityEventRouterViews::DispatchEventOnViewStorageId,
78          view_storage_id,
79          event_type));
80}
81
82void AccessibilityEventRouterViews::HandleMenuItemFocused(
83    const base::string16& menu_name,
84    const base::string16& menu_item_name,
85    int item_index,
86    int item_count,
87    bool has_submenu) {
88  if (!ExtensionAccessibilityEventRouter::GetInstance()->
89      IsAccessibilityEnabled()) {
90    return;
91  }
92
93  if (!most_recent_profile_)
94    return;
95
96  AccessibilityMenuItemInfo info(most_recent_profile_,
97                                 base::UTF16ToUTF8(menu_item_name),
98                                 base::UTF16ToUTF8(menu_name),
99                                 has_submenu,
100                                 item_index,
101                                 item_count);
102  SendControlAccessibilityNotification(
103      ui::AX_EVENT_FOCUS, &info);
104}
105
106void AccessibilityEventRouterViews::Observe(
107    int type,
108    const content::NotificationSource& source,
109    const content::NotificationDetails& details) {
110  DCHECK_EQ(type, chrome::NOTIFICATION_PROFILE_DESTROYED);
111  Profile* profile = content::Source<Profile>(source).ptr();
112  if (profile == most_recent_profile_)
113    most_recent_profile_ = NULL;
114}
115
116//
117// Private methods
118//
119
120void AccessibilityEventRouterViews::DispatchEventOnViewStorageId(
121    int view_storage_id,
122    ui::AXEvent type) {
123  views::ViewStorage* view_storage = views::ViewStorage::GetInstance();
124  views::View* view = view_storage->RetrieveView(view_storage_id);
125  view_storage->RemoveView(view_storage_id);
126  if (!view)
127    return;
128
129  AccessibilityEventRouterViews* instance =
130      AccessibilityEventRouterViews::GetInstance();
131  instance->DispatchAccessibilityEvent(view, type);
132}
133
134void AccessibilityEventRouterViews::DispatchAccessibilityEvent(
135    views::View* view, ui::AXEvent type) {
136  // Get the profile associated with this view. If it's not found, use
137  // the most recent profile where accessibility events were sent, or
138  // the default profile.
139  Profile* profile = NULL;
140  views::Widget* widget = view->GetWidget();
141  if (widget) {
142    profile = reinterpret_cast<Profile*>(
143        widget->GetNativeWindowProperty(Profile::kProfileKey));
144  }
145  if (!profile)
146    profile = most_recent_profile_;
147  if (!profile) {
148    if (g_browser_process->profile_manager())
149      profile = g_browser_process->profile_manager()->GetLastUsedProfile();
150  }
151  if (!profile) {
152    LOG(WARNING) << "Accessibility notification but no profile";
153    return;
154  }
155
156  most_recent_profile_ = profile;
157
158  if (type == ui::AX_EVENT_MENU_START ||
159      type == ui::AX_EVENT_MENU_POPUP_START ||
160      type == ui::AX_EVENT_MENU_END ||
161      type == ui::AX_EVENT_MENU_POPUP_END) {
162    SendMenuNotification(view, type, profile);
163    return;
164  }
165
166  ui::AXViewState state;
167  view->GetAccessibleState(&state);
168
169  if (type == ui::AX_EVENT_ALERT &&
170      !(state.role == ui::AX_ROLE_ALERT ||
171        state.role == ui::AX_ROLE_WINDOW)) {
172    SendAlertControlNotification(view, type, profile);
173    return;
174  }
175
176  switch (state.role) {
177  case ui::AX_ROLE_ALERT:
178  case ui::AX_ROLE_DIALOG:
179  case ui::AX_ROLE_WINDOW:
180    SendWindowNotification(view, type, profile);
181    break;
182  case ui::AX_ROLE_POP_UP_BUTTON:
183  case ui::AX_ROLE_MENU_BAR:
184  case ui::AX_ROLE_MENU_LIST_POPUP:
185    SendMenuNotification(view, type, profile);
186    break;
187  case ui::AX_ROLE_BUTTON_DROP_DOWN:
188  case ui::AX_ROLE_BUTTON:
189    SendButtonNotification(view, type, profile);
190    break;
191  case ui::AX_ROLE_CHECK_BOX:
192    SendCheckboxNotification(view, type, profile);
193    break;
194  case ui::AX_ROLE_COMBO_BOX:
195    SendComboboxNotification(view, type, profile);
196    break;
197  case ui::AX_ROLE_LINK:
198    SendLinkNotification(view, type, profile);
199    break;
200  case ui::AX_ROLE_LOCATION_BAR:
201  case ui::AX_ROLE_TEXT_FIELD:
202    SendTextfieldNotification(view, type, profile);
203    break;
204  case ui::AX_ROLE_MENU_ITEM:
205    SendMenuItemNotification(view, type, profile);
206    break;
207  case ui::AX_ROLE_RADIO_BUTTON:
208    // Not used anymore?
209  case ui::AX_ROLE_SLIDER:
210    SendSliderNotification(view, type, profile);
211    break;
212  case ui::AX_ROLE_TREE:
213    SendTreeNotification(view, type, profile);
214    break;
215  case ui::AX_ROLE_TREE_ITEM:
216    SendTreeItemNotification(view, type, profile);
217    break;
218  default:
219    // If this is encountered, please file a bug with the role that wasn't
220    // caught so we can add accessibility extension API support.
221    NOTREACHED();
222  }
223}
224
225// static
226void AccessibilityEventRouterViews::SendButtonNotification(
227    views::View* view,
228    ui::AXEvent event,
229    Profile* profile) {
230  AccessibilityButtonInfo info(
231      profile, GetViewName(view), GetViewContext(view));
232  SendControlAccessibilityNotification(event, &info);
233}
234
235// static
236void AccessibilityEventRouterViews::SendLinkNotification(
237    views::View* view,
238    ui::AXEvent event,
239    Profile* profile) {
240  AccessibilityLinkInfo info(profile, GetViewName(view), GetViewContext(view));
241  SendControlAccessibilityNotification(event, &info);
242}
243
244// static
245void AccessibilityEventRouterViews::SendMenuNotification(
246    views::View* view,
247    ui::AXEvent event,
248    Profile* profile) {
249  AccessibilityMenuInfo info(profile, GetViewName(view));
250  SendMenuAccessibilityNotification(event, &info);
251}
252
253// static
254void AccessibilityEventRouterViews::SendMenuItemNotification(
255    views::View* view,
256    ui::AXEvent event,
257    Profile* profile) {
258  std::string name = GetViewName(view);
259  std::string context = GetViewContext(view);
260
261  bool has_submenu = false;
262  int index = -1;
263  int count = -1;
264
265  if (!strcmp(view->GetClassName(), views::MenuItemView::kViewClassName))
266    has_submenu = static_cast<views::MenuItemView*>(view)->HasSubmenu();
267
268  views::View* parent_menu = view->parent();
269  while (parent_menu != NULL && strcmp(parent_menu->GetClassName(),
270                                       views::SubmenuView::kViewClassName)) {
271    parent_menu = parent_menu->parent();
272  }
273  if (parent_menu) {
274    count = 0;
275    RecursiveGetMenuItemIndexAndCount(parent_menu, view, &index, &count);
276  }
277
278  AccessibilityMenuItemInfo info(
279      profile, name, context, has_submenu, index, count);
280  SendControlAccessibilityNotification(event, &info);
281}
282
283// static
284void AccessibilityEventRouterViews::SendTreeNotification(
285    views::View* view,
286    ui::AXEvent event,
287    Profile* profile) {
288  AccessibilityTreeInfo info(profile, GetViewName(view));
289  SendControlAccessibilityNotification(event, &info);
290}
291
292// static
293void AccessibilityEventRouterViews::SendTreeItemNotification(
294    views::View* view,
295    ui::AXEvent event,
296    Profile* profile) {
297  std::string name = GetViewName(view);
298  std::string context = GetViewContext(view);
299
300  if (strcmp(view->GetClassName(), views::TreeView::kViewClassName) != 0) {
301    NOTREACHED();
302    return;
303  }
304
305  views::TreeView* tree = static_cast<views::TreeView*>(view);
306  ui::TreeModelNode* selected_node = tree->GetSelectedNode();
307  ui::TreeModel* model = tree->model();
308
309  int siblings_count = model->GetChildCount(model->GetRoot());
310  int children_count = -1;
311  int index = -1;
312  int depth = -1;
313  bool is_expanded = false;
314
315  if (selected_node) {
316    children_count = model->GetChildCount(selected_node);
317    is_expanded = tree->IsExpanded(selected_node);
318    ui::TreeModelNode* parent_node = model->GetParent(selected_node);
319    if (parent_node) {
320      index = model->GetIndexOf(parent_node, selected_node);
321      siblings_count = model->GetChildCount(parent_node);
322    }
323    // Get node depth.
324    depth = 0;
325    while (parent_node) {
326      depth++;
327      parent_node = model->GetParent(parent_node);
328    }
329  }
330
331  AccessibilityTreeItemInfo info(
332      profile, name, context, depth, index, siblings_count, children_count,
333      is_expanded);
334  SendControlAccessibilityNotification(event, &info);
335}
336
337// static
338void AccessibilityEventRouterViews::SendTextfieldNotification(
339    views::View* view,
340    ui::AXEvent event,
341    Profile* profile) {
342  ui::AXViewState state;
343  view->GetAccessibleState(&state);
344  std::string name = base::UTF16ToUTF8(state.name);
345  std::string context = GetViewContext(view);
346  bool password = state.HasStateFlag(ui::AX_STATE_PROTECTED);
347  AccessibilityTextBoxInfo info(profile, name, context, password);
348  std::string value = base::UTF16ToUTF8(state.value);
349  info.SetValue(value, state.selection_start, state.selection_end);
350  SendControlAccessibilityNotification(event, &info);
351}
352
353// static
354void AccessibilityEventRouterViews::SendComboboxNotification(
355    views::View* view,
356    ui::AXEvent event,
357    Profile* profile) {
358  ui::AXViewState state;
359  view->GetAccessibleState(&state);
360  std::string name = base::UTF16ToUTF8(state.name);
361  std::string value = base::UTF16ToUTF8(state.value);
362  std::string context = GetViewContext(view);
363  AccessibilityComboBoxInfo info(
364      profile, name, context, value, state.index, state.count);
365  SendControlAccessibilityNotification(event, &info);
366}
367
368// static
369void AccessibilityEventRouterViews::SendCheckboxNotification(
370    views::View* view,
371    ui::AXEvent event,
372    Profile* profile) {
373  ui::AXViewState state;
374  view->GetAccessibleState(&state);
375  std::string name = base::UTF16ToUTF8(state.name);
376  std::string context = GetViewContext(view);
377  AccessibilityCheckboxInfo info(
378      profile,
379      name,
380      context,
381      state.HasStateFlag(ui::AX_STATE_CHECKED));
382  SendControlAccessibilityNotification(event, &info);
383}
384
385// static
386void AccessibilityEventRouterViews::SendWindowNotification(
387    views::View* view,
388    ui::AXEvent event,
389    Profile* profile) {
390  ui::AXViewState state;
391  view->GetAccessibleState(&state);
392  std::string window_text;
393
394  // If it's an alert, try to get the text from the contents of the
395  // static text, not the window title.
396  if (state.role == ui::AX_ROLE_ALERT)
397    window_text = RecursiveGetStaticText(view);
398
399  // Otherwise get it from the window's accessible name.
400  if (window_text.empty())
401    window_text = base::UTF16ToUTF8(state.name);
402
403  AccessibilityWindowInfo info(profile, window_text);
404  SendWindowAccessibilityNotification(event, &info);
405}
406
407// static
408void AccessibilityEventRouterViews::SendSliderNotification(
409    views::View* view,
410    ui::AXEvent event,
411    Profile* profile) {
412  ui::AXViewState state;
413  view->GetAccessibleState(&state);
414
415  std::string name = base::UTF16ToUTF8(state.name);
416  std::string value = base::UTF16ToUTF8(state.value);
417  std::string context = GetViewContext(view);
418  AccessibilitySliderInfo info(
419      profile,
420      name,
421      context,
422      value);
423  SendControlAccessibilityNotification(event, &info);
424}
425
426// static
427void AccessibilityEventRouterViews::SendAlertControlNotification(
428    views::View* view,
429    ui::AXEvent event,
430    Profile* profile) {
431  ui::AXViewState state;
432  view->GetAccessibleState(&state);
433
434  std::string name = base::UTF16ToUTF8(state.name);
435  AccessibilityAlertInfo info(
436      profile,
437      name);
438  SendControlAccessibilityNotification(event, &info);
439}
440
441// static
442std::string AccessibilityEventRouterViews::GetViewName(views::View* view) {
443  ui::AXViewState state;
444  view->GetAccessibleState(&state);
445  return base::UTF16ToUTF8(state.name);
446}
447
448// static
449std::string AccessibilityEventRouterViews::GetViewContext(views::View* view) {
450  for (views::View* parent = view->parent();
451       parent;
452       parent = parent->parent()) {
453    ui::AXViewState state;
454    parent->GetAccessibleState(&state);
455
456    // Two cases are handled right now. More could be added in the future
457    // depending on how the UI evolves.
458
459    // A control inside of alert, toolbar or dialog should use that container's
460    // accessible name.
461    if ((state.role == ui::AX_ROLE_ALERT ||
462         state.role == ui::AX_ROLE_DIALOG ||
463         state.role == ui::AX_ROLE_TOOLBAR) &&
464        !state.name.empty()) {
465      return base::UTF16ToUTF8(state.name);
466    }
467
468    // A control inside of an alert or dialog (including an infobar)
469    // should grab the first static text descendant as the context;
470    // that's the prompt.
471    if (state.role == ui::AX_ROLE_ALERT ||
472        state.role == ui::AX_ROLE_DIALOG) {
473      views::View* static_text_child = FindDescendantWithAccessibleRole(
474          parent, ui::AX_ROLE_STATIC_TEXT);
475      if (static_text_child) {
476        ui::AXViewState state;
477        static_text_child->GetAccessibleState(&state);
478        if (!state.name.empty())
479          return base::UTF16ToUTF8(state.name);
480      }
481      return std::string();
482    }
483  }
484
485  return std::string();
486}
487
488// static
489views::View* AccessibilityEventRouterViews::FindDescendantWithAccessibleRole(
490    views::View* view, ui::AXRole role) {
491  ui::AXViewState state;
492  view->GetAccessibleState(&state);
493  if (state.role == role)
494    return view;
495
496  for (int i = 0; i < view->child_count(); i++) {
497    views::View* child = view->child_at(i);
498    views::View* result = FindDescendantWithAccessibleRole(child, role);
499    if (result)
500      return result;
501  }
502
503  return NULL;
504}
505
506// static
507void AccessibilityEventRouterViews::RecursiveGetMenuItemIndexAndCount(
508    views::View* menu,
509    views::View* item,
510    int* index,
511    int* count) {
512  for (int i = 0; i < menu->child_count(); ++i) {
513    views::View* child = menu->child_at(i);
514    if (!child->visible())
515      continue;
516
517    int previous_count = *count;
518    RecursiveGetMenuItemIndexAndCount(child, item, index, count);
519    ui::AXViewState state;
520    child->GetAccessibleState(&state);
521    if (state.role == ui::AX_ROLE_MENU_ITEM &&
522        *count == previous_count) {
523      if (item == child)
524        *index = *count;
525      (*count)++;
526    } else if (state.role == ui::AX_ROLE_BUTTON) {
527      if (item == child)
528        *index = *count;
529      (*count)++;
530    }
531  }
532}
533
534// static
535std::string AccessibilityEventRouterViews::RecursiveGetStaticText(
536    views::View* view) {
537  ui::AXViewState state;
538  view->GetAccessibleState(&state);
539  if (state.role == ui::AX_ROLE_STATIC_TEXT)
540    return base::UTF16ToUTF8(state.name);
541
542  for (int i = 0; i < view->child_count(); ++i) {
543    views::View* child = view->child_at(i);
544    std::string result = RecursiveGetStaticText(child);
545    if (!result.empty())
546      return result;
547  }
548  return std::string();
549}
550