accessibility_event_router_views.cc revision 5f1c94371a64b3196d4be9466099bb892df9b88e
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;
27using views::ViewStorage;
28
29AccessibilityEventRouterViews::AccessibilityEventRouterViews()
30    : most_recent_profile_(NULL),
31      most_recent_view_id_(
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());
37}
38
39AccessibilityEventRouterViews::~AccessibilityEventRouterViews() {
40}
41
42// static
43AccessibilityEventRouterViews* AccessibilityEventRouterViews::GetInstance() {
44  return Singleton<AccessibilityEventRouterViews>::get();
45}
46
47void AccessibilityEventRouterViews::HandleAccessibilityEvent(
48    views::View* view, ui::AXEvent event_type) {
49  if (!ExtensionAccessibilityEventRouter::GetInstance()->
50      IsAccessibilityEnabled()) {
51    return;
52  }
53
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)
67      return;
68  }
69
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(
78      FROM_HERE,
79      base::Bind(
80          &AccessibilityEventRouterViews::DispatchEventOnViewStorageId,
81          view_storage_id,
82          event_type));
83}
84
85void AccessibilityEventRouterViews::HandleMenuItemFocused(
86    const base::string16& menu_name,
87    const base::string16& menu_item_name,
88    int item_index,
89    int item_count,
90    bool has_submenu) {
91  if (!ExtensionAccessibilityEventRouter::GetInstance()->
92      IsAccessibilityEnabled()) {
93    return;
94  }
95
96  if (!most_recent_profile_)
97    return;
98
99  AccessibilityMenuItemInfo info(most_recent_profile_,
100                                 base::UTF16ToUTF8(menu_item_name),
101                                 base::UTF16ToUTF8(menu_name),
102                                 has_submenu,
103                                 item_index,
104                                 item_count);
105  SendControlAccessibilityNotification(
106      ui::AX_EVENT_FOCUS, &info);
107}
108
109void AccessibilityEventRouterViews::Observe(
110    int type,
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;
117}
118
119//
120// Private methods
121//
122
123void AccessibilityEventRouterViews::DispatchEventOnViewStorageId(
124    int view_storage_id,
125    ui::AXEvent type) {
126  ViewStorage* view_storage = ViewStorage::GetInstance();
127  views::View* view = view_storage->RetrieveView(view_storage_id);
128  view_storage->RemoveView(view_storage_id);
129  if (!view)
130    return;
131
132  AccessibilityEventRouterViews* instance =
133      AccessibilityEventRouterViews::GetInstance();
134  instance->DispatchAccessibilityEvent(view, type);
135}
136
137void 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();
144  if (widget) {
145    profile = reinterpret_cast<Profile*>(
146        widget->GetNativeWindowProperty(Profile::kProfileKey));
147  }
148  if (!profile)
149    profile = most_recent_profile_;
150  if (!profile) {
151    if (g_browser_process->profile_manager())
152      profile = g_browser_process->profile_manager()->GetLastUsedProfile();
153  }
154  if (!profile) {
155    LOG(WARNING) << "Accessibility notification but no profile";
156    return;
157  }
158
159  most_recent_profile_ = profile;
160
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);
166    return;
167  }
168
169  view = FindFirstAccessibleAncestor(view);
170
171  // Since multiple items could share a highest focusable view, these items
172  // could all dispatch the same accessibility hover events, which isn't
173  // necessary.
174  if (type == ui::AX_EVENT_HOVER &&
175      ViewStorage::GetInstance()->RetrieveView(most_recent_view_id_) == view) {
176    return;
177  }
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);
182
183  ui::AXViewState state;
184  view->GetAccessibleState(&state);
185
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);
190    return;
191  }
192
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);
198    break;
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);
203    break;
204  case ui::AX_ROLE_BUTTON_DROP_DOWN:
205  case ui::AX_ROLE_BUTTON:
206    SendButtonNotification(view, type, profile);
207    break;
208  case ui::AX_ROLE_CHECK_BOX:
209    SendCheckboxNotification(view, type, profile);
210    break;
211  case ui::AX_ROLE_COMBO_BOX:
212    SendComboboxNotification(view, type, profile);
213    break;
214  case ui::AX_ROLE_LINK:
215    SendLinkNotification(view, type, profile);
216    break;
217  case ui::AX_ROLE_LOCATION_BAR:
218  case ui::AX_ROLE_TEXT_FIELD:
219    SendTextfieldNotification(view, type, profile);
220    break;
221  case ui::AX_ROLE_MENU_ITEM:
222    SendMenuItemNotification(view, type, profile);
223    break;
224  case ui::AX_ROLE_RADIO_BUTTON:
225    // Not used anymore?
226  case ui::AX_ROLE_SLIDER:
227    SendSliderNotification(view, type, profile);
228    break;
229  case ui::AX_ROLE_STATIC_TEXT:
230    SendStaticTextNotification(view, type, profile);
231    break;
232  case ui::AX_ROLE_TREE:
233    SendTreeNotification(view, type, profile);
234    break;
235  case ui::AX_ROLE_TAB:
236    SendTabNotification(view, type, profile);
237    break;
238  case ui::AX_ROLE_TREE_ITEM:
239    SendTreeItemNotification(view, type, profile);
240    break;
241  default:
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)
245      break;
246
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.
249    NOTREACHED();
250  }
251}
252
253// static
254void AccessibilityEventRouterViews::SendTabNotification(
255    views::View* view,
256    ui::AXEvent event,
257    Profile* profile) {
258  ui::AXViewState state;
259  view->GetAccessibleState(&state);
260  if (state.index == -1)
261    return;
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);
266}
267
268// static
269void AccessibilityEventRouterViews::SendButtonNotification(
270    views::View* view,
271    ui::AXEvent event,
272    Profile* profile) {
273  AccessibilityButtonInfo info(
274      profile, GetViewName(view), GetViewContext(view));
275  SendControlAccessibilityNotification(event, &info);
276}
277
278// static
279void AccessibilityEventRouterViews::SendStaticTextNotification(
280    views::View* view,
281    ui::AXEvent event,
282    Profile* profile) {
283  AccessibilityStaticTextInfo info(
284      profile, GetViewName(view), GetViewContext(view));
285  SendControlAccessibilityNotification(event, &info);
286}
287
288// static
289void AccessibilityEventRouterViews::SendLinkNotification(
290    views::View* view,
291    ui::AXEvent event,
292    Profile* profile) {
293  AccessibilityLinkInfo info(profile, GetViewName(view), GetViewContext(view));
294  SendControlAccessibilityNotification(event, &info);
295}
296
297// static
298void AccessibilityEventRouterViews::SendMenuNotification(
299    views::View* view,
300    ui::AXEvent event,
301    Profile* profile) {
302  AccessibilityMenuInfo info(profile, GetViewName(view));
303  SendMenuAccessibilityNotification(event, &info);
304}
305
306// static
307void AccessibilityEventRouterViews::SendMenuItemNotification(
308    views::View* view,
309    ui::AXEvent event,
310    Profile* profile) {
311  std::string name = GetViewName(view);
312  std::string context = GetViewContext(view);
313
314  bool has_submenu = false;
315  int index = -1;
316  int count = -1;
317
318  if (!strcmp(view->GetClassName(), views::MenuItemView::kViewClassName))
319    has_submenu = static_cast<views::MenuItemView*>(view)->HasSubmenu();
320
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();
325  }
326  if (parent_menu) {
327    count = 0;
328    RecursiveGetMenuItemIndexAndCount(parent_menu, view, &index, &count);
329  }
330
331  AccessibilityMenuItemInfo info(
332      profile, name, context, has_submenu, index, count);
333  SendControlAccessibilityNotification(event, &info);
334}
335
336// static
337void AccessibilityEventRouterViews::SendTreeNotification(
338    views::View* view,
339    ui::AXEvent event,
340    Profile* profile) {
341  AccessibilityTreeInfo info(profile, GetViewName(view));
342  SendControlAccessibilityNotification(event, &info);
343}
344
345// static
346void AccessibilityEventRouterViews::SendTreeItemNotification(
347    views::View* view,
348    ui::AXEvent event,
349    Profile* profile) {
350  std::string name = GetViewName(view);
351  std::string context = GetViewContext(view);
352
353  if (strcmp(view->GetClassName(), views::TreeView::kViewClassName) != 0) {
354    NOTREACHED();
355    return;
356  }
357
358  views::TreeView* tree = static_cast<views::TreeView*>(view);
359  ui::TreeModelNode* selected_node = tree->GetSelectedNode();
360  ui::TreeModel* model = tree->model();
361
362  int siblings_count = model->GetChildCount(model->GetRoot());
363  int children_count = -1;
364  int index = -1;
365  int depth = -1;
366  bool is_expanded = false;
367
368  if (selected_node) {
369    children_count = model->GetChildCount(selected_node);
370    is_expanded = tree->IsExpanded(selected_node);
371    ui::TreeModelNode* parent_node = model->GetParent(selected_node);
372    if (parent_node) {
373      index = model->GetIndexOf(parent_node, selected_node);
374      siblings_count = model->GetChildCount(parent_node);
375    }
376    // Get node depth.
377    depth = 0;
378    while (parent_node) {
379      depth++;
380      parent_node = model->GetParent(parent_node);
381    }
382  }
383
384  AccessibilityTreeItemInfo info(
385      profile, name, context, depth, index, siblings_count, children_count,
386      is_expanded);
387  SendControlAccessibilityNotification(event, &info);
388}
389
390// static
391void AccessibilityEventRouterViews::SendTextfieldNotification(
392    views::View* view,
393    ui::AXEvent event,
394    Profile* profile) {
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);
404}
405
406// static
407void AccessibilityEventRouterViews::SendComboboxNotification(
408    views::View* view,
409    ui::AXEvent event,
410    Profile* profile) {
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);
419}
420
421// static
422void AccessibilityEventRouterViews::SendCheckboxNotification(
423    views::View* view,
424    ui::AXEvent event,
425    Profile* profile) {
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(
431      profile,
432      name,
433      context,
434      state.HasStateFlag(ui::AX_STATE_CHECKED));
435  SendControlAccessibilityNotification(event, &info);
436}
437
438// static
439void AccessibilityEventRouterViews::SendWindowNotification(
440    views::View* view,
441    ui::AXEvent event,
442    Profile* profile) {
443  ui::AXViewState state;
444  view->GetAccessibleState(&state);
445  std::string window_text;
446
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);
451
452  // Otherwise get it from the window's accessible name.
453  if (window_text.empty())
454    window_text = base::UTF16ToUTF8(state.name);
455
456  AccessibilityWindowInfo info(profile, window_text);
457  SendWindowAccessibilityNotification(event, &info);
458}
459
460// static
461void AccessibilityEventRouterViews::SendSliderNotification(
462    views::View* view,
463    ui::AXEvent event,
464    Profile* profile) {
465  ui::AXViewState state;
466  view->GetAccessibleState(&state);
467
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(
472      profile,
473      name,
474      context,
475      value);
476  SendControlAccessibilityNotification(event, &info);
477}
478
479// static
480void AccessibilityEventRouterViews::SendAlertControlNotification(
481    views::View* view,
482    ui::AXEvent event,
483    Profile* profile) {
484  ui::AXViewState state;
485  view->GetAccessibleState(&state);
486
487  std::string name = base::UTF16ToUTF8(state.name);
488  AccessibilityAlertInfo info(
489      profile,
490      name);
491  SendControlAccessibilityNotification(event, &info);
492}
493
494// static
495std::string AccessibilityEventRouterViews::GetViewName(views::View* view) {
496  ui::AXViewState state;
497  view->GetAccessibleState(&state);
498  return base::UTF16ToUTF8(state.name);
499}
500
501// static
502std::string AccessibilityEventRouterViews::GetViewContext(views::View* view) {
503  for (views::View* parent = view->parent();
504       parent;
505       parent = parent->parent()) {
506    ui::AXViewState state;
507    parent->GetAccessibleState(&state);
508
509    // Two cases are handled right now. More could be added in the future
510    // depending on how the UI evolves.
511
512    // A control inside of alert, toolbar or dialog should use that container's
513    // accessible name.
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);
519    }
520
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);
533      }
534      return std::string();
535    }
536  }
537
538  return std::string();
539}
540
541// static
542views::View* AccessibilityEventRouterViews::FindDescendantWithAccessibleRole(
543    views::View* view, ui::AXRole role) {
544  ui::AXViewState state;
545  view->GetAccessibleState(&state);
546  if (state.role == role)
547    return view;
548
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);
552    if (result)
553      return result;
554  }
555
556  return NULL;
557}
558
559// static
560void AccessibilityEventRouterViews::RecursiveGetMenuItemIndexAndCount(
561    views::View* menu,
562    views::View* item,
563    int* index,
564    int* count) {
565  for (int i = 0; i < menu->child_count(); ++i) {
566    views::View* child = menu->child_at(i);
567    if (!child->visible())
568      continue;
569
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) {
576      if (item == child)
577        *index = *count;
578      (*count)++;
579    } else if (state.role == ui::AX_ROLE_BUTTON) {
580      if (item == child)
581        *index = *count;
582      (*count)++;
583    }
584  }
585}
586
587// static
588std::string AccessibilityEventRouterViews::RecursiveGetStaticText(
589    views::View* view) {
590  ui::AXViewState state;
591  view->GetAccessibleState(&state);
592  if (state.role == ui::AX_ROLE_STATIC_TEXT)
593    return base::UTF16ToUTF8(state.name);
594
595  for (int i = 0; i < view->child_count(); ++i) {
596    views::View* child = view->child_at(i);
597    std::string result = RecursiveGetStaticText(child);
598    if (!result.empty())
599      return result;
600  }
601  return std::string();
602}
603
604// static
605views::View* AccessibilityEventRouterViews::FindFirstAccessibleAncestor(
606    views::View* view) {
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())
611    return temp_view;
612  return view;
613}
614