accessibility_event_router_views.cc revision 9ab5563a3196760eb381d102cbb2bc0f7abc6a50
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 "content/public/browser/notification_source.h"
19#include "ui/base/accessibility/accessible_view_state.h"
20#include "ui/views/controls/menu/menu_item_view.h"
21#include "ui/views/controls/menu/submenu_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::AccessibilityTypes::Event event_type) {
46  if (!ExtensionAccessibilityEventRouter::GetInstance()->
47      IsAccessibilityEnabled()) {
48    return;
49  }
50
51  chrome::NotificationType notification_type;
52  switch (event_type) {
53    case ui::AccessibilityTypes::EVENT_FOCUS:
54      notification_type = chrome::NOTIFICATION_ACCESSIBILITY_CONTROL_FOCUSED;
55      break;
56    case ui::AccessibilityTypes::EVENT_MENUSTART:
57    case ui::AccessibilityTypes::EVENT_MENUPOPUPSTART:
58      notification_type = chrome::NOTIFICATION_ACCESSIBILITY_MENU_OPENED;
59      break;
60    case ui::AccessibilityTypes::EVENT_MENUEND:
61    case ui::AccessibilityTypes::EVENT_MENUPOPUPEND:
62      notification_type = chrome::NOTIFICATION_ACCESSIBILITY_MENU_CLOSED;
63      break;
64    case ui::AccessibilityTypes::EVENT_TEXT_CHANGED:
65    case ui::AccessibilityTypes::EVENT_SELECTION_CHANGED:
66      // These two events should only be sent for views that have focus. This
67      // enforces the invariant that we fire events triggered by user action and
68      // not by programmatic logic. For example, the location bar can be updated
69      // by javascript while the user focus is within some other part of the
70      // user interface. In contrast, the other supported events here do not
71      // depend on focus. For example, a menu within a menubar can open or close
72      // while focus is within the location bar or anywhere else as a result of
73      // user action. Note that the below logic can at some point be removed if
74      // we pass more information along to the listener such as focused state.
75      if (!view->GetFocusManager() ||
76          view->GetFocusManager()->GetFocusedView() != view)
77        return;
78      notification_type = chrome::NOTIFICATION_ACCESSIBILITY_TEXT_CHANGED;
79      break;
80    case ui::AccessibilityTypes::EVENT_VALUE_CHANGED:
81      notification_type = chrome::NOTIFICATION_ACCESSIBILITY_CONTROL_ACTION;
82      break;
83    case ui::AccessibilityTypes::EVENT_ALERT:
84      notification_type = chrome::NOTIFICATION_ACCESSIBILITY_WINDOW_OPENED;
85      break;
86    case ui::AccessibilityTypes::EVENT_NAME_CHANGED:
87    default:
88      NOTIMPLEMENTED();
89      return;
90  }
91
92  // Don't dispatch the accessibility event until the next time through the
93  // event loop, to handle cases where the view's state changes after
94  // the call to post the event. It's safe to use base::Unretained(this)
95  // because AccessibilityEventRouterViews is a singleton.
96  views::ViewStorage* view_storage = views::ViewStorage::GetInstance();
97  int view_storage_id = view_storage->CreateStorageID();
98  view_storage->StoreView(view_storage_id, view);
99  base::MessageLoop::current()->PostTask(
100      FROM_HERE,
101      base::Bind(
102          &AccessibilityEventRouterViews::DispatchNotificationOnViewStorageId,
103          view_storage_id,
104          notification_type));
105}
106
107void AccessibilityEventRouterViews::HandleMenuItemFocused(
108    const string16& menu_name,
109    const string16& menu_item_name,
110    int item_index,
111    int item_count,
112    bool has_submenu) {
113  if (!ExtensionAccessibilityEventRouter::GetInstance()->
114      IsAccessibilityEnabled()) {
115    return;
116  }
117
118  if (!most_recent_profile_)
119    return;
120
121  AccessibilityMenuItemInfo info(most_recent_profile_,
122                                 UTF16ToUTF8(menu_item_name),
123                                 UTF16ToUTF8(menu_name),
124                                 has_submenu,
125                                 item_index,
126                                 item_count);
127  SendAccessibilityNotification(
128      chrome::NOTIFICATION_ACCESSIBILITY_CONTROL_FOCUSED, &info);
129}
130
131void AccessibilityEventRouterViews::Observe(
132    int type,
133    const content::NotificationSource& source,
134    const content::NotificationDetails& details) {
135  DCHECK_EQ(type, chrome::NOTIFICATION_PROFILE_DESTROYED);
136  Profile* profile = content::Source<Profile>(source).ptr();
137  if (profile == most_recent_profile_)
138    most_recent_profile_ = NULL;
139}
140
141//
142// Private methods
143//
144
145void AccessibilityEventRouterViews::DispatchNotificationOnViewStorageId(
146    int view_storage_id,
147    chrome::NotificationType type) {
148  views::ViewStorage* view_storage = views::ViewStorage::GetInstance();
149  views::View* view = view_storage->RetrieveView(view_storage_id);
150  view_storage->RemoveView(view_storage_id);
151  if (!view)
152    return;
153
154  AccessibilityEventRouterViews* instance =
155      AccessibilityEventRouterViews::GetInstance();
156  instance->DispatchAccessibilityNotification(view, type);
157}
158
159void AccessibilityEventRouterViews::DispatchAccessibilityNotification(
160    views::View* view, chrome::NotificationType type) {
161  // Get the profile associated with this view. If it's not found, use
162  // the most recent profile where accessibility events were sent, or
163  // the default profile.
164  Profile* profile = NULL;
165  views::Widget* widget = view->GetWidget();
166  if (widget) {
167    profile = reinterpret_cast<Profile*>(
168        widget->GetNativeWindowProperty(Profile::kProfileKey));
169  }
170  if (!profile)
171    profile = most_recent_profile_;
172  if (!profile) {
173    if (g_browser_process->profile_manager())
174      profile = g_browser_process->profile_manager()->GetLastUsedProfile();
175  }
176  if (!profile) {
177    LOG(WARNING) << "Accessibility notification but no profile";
178    return;
179  }
180
181  most_recent_profile_ = profile;
182
183  if (type == chrome::NOTIFICATION_ACCESSIBILITY_MENU_OPENED ||
184      type == chrome::NOTIFICATION_ACCESSIBILITY_MENU_CLOSED) {
185    SendMenuNotification(view, type, profile);
186    return;
187  }
188
189  ui::AccessibleViewState state;
190  view->GetAccessibleState(&state);
191
192  switch (state.role) {
193  case ui::AccessibilityTypes::ROLE_ALERT:
194  case ui::AccessibilityTypes::ROLE_WINDOW:
195    SendWindowNotification(view, type, profile);
196    break;
197  case ui::AccessibilityTypes::ROLE_BUTTONMENU:
198  case ui::AccessibilityTypes::ROLE_MENUBAR:
199  case ui::AccessibilityTypes::ROLE_MENUPOPUP:
200    SendMenuNotification(view, type, profile);
201    break;
202  case ui::AccessibilityTypes::ROLE_BUTTONDROPDOWN:
203  case ui::AccessibilityTypes::ROLE_PUSHBUTTON:
204    SendButtonNotification(view, type, profile);
205    break;
206  case ui::AccessibilityTypes::ROLE_CHECKBUTTON:
207    SendCheckboxNotification(view, type, profile);
208    break;
209  case ui::AccessibilityTypes::ROLE_COMBOBOX:
210    SendComboboxNotification(view, type, profile);
211    break;
212  case ui::AccessibilityTypes::ROLE_LINK:
213    SendLinkNotification(view, type, profile);
214    break;
215  case ui::AccessibilityTypes::ROLE_LOCATION_BAR:
216  case ui::AccessibilityTypes::ROLE_TEXT:
217    SendTextfieldNotification(view, type, profile);
218    break;
219  case ui::AccessibilityTypes::ROLE_MENUITEM:
220    SendMenuItemNotification(view, type, profile);
221    break;
222  case ui::AccessibilityTypes::ROLE_RADIOBUTTON:
223    // Not used anymore?
224  case ui::AccessibilityTypes::ROLE_SLIDER:
225    SendSliderNotification(view, type, profile);
226    break;
227  default:
228    // If this is encountered, please file a bug with the role that wasn't
229    // caught so we can add accessibility extension API support.
230    NOTREACHED();
231  }
232}
233
234// static
235void AccessibilityEventRouterViews::SendButtonNotification(
236    views::View* view,
237    int type,
238    Profile* profile) {
239  AccessibilityButtonInfo info(
240      profile, GetViewName(view), GetViewContext(view));
241  SendAccessibilityNotification(type, &info);
242}
243
244// static
245void AccessibilityEventRouterViews::SendLinkNotification(
246    views::View* view,
247    int type,
248    Profile* profile) {
249  AccessibilityLinkInfo info(profile, GetViewName(view), GetViewContext(view));
250  SendAccessibilityNotification(type, &info);
251}
252
253// static
254void AccessibilityEventRouterViews::SendMenuNotification(
255    views::View* view,
256    int type,
257    Profile* profile) {
258  AccessibilityMenuInfo info(profile, GetViewName(view));
259  SendAccessibilityNotification(type, &info);
260}
261
262// static
263void AccessibilityEventRouterViews::SendMenuItemNotification(
264    views::View* view,
265    int type,
266    Profile* profile) {
267  std::string name = GetViewName(view);
268  std::string context = GetViewContext(view);
269
270  bool has_submenu = false;
271  int index = -1;
272  int count = -1;
273
274  if (!strcmp(view->GetClassName(), views::MenuItemView::kViewClassName))
275    has_submenu = static_cast<views::MenuItemView*>(view)->HasSubmenu();
276
277  views::View* parent_menu = view->parent();
278  while (parent_menu != NULL && strcmp(parent_menu->GetClassName(),
279                                       views::SubmenuView::kViewClassName)) {
280    parent_menu = parent_menu->parent();
281  }
282  if (parent_menu) {
283    count = 0;
284    RecursiveGetMenuItemIndexAndCount(parent_menu, view, &index, &count);
285  }
286
287  AccessibilityMenuItemInfo info(
288      profile, name, context, has_submenu, index, count);
289  SendAccessibilityNotification(type, &info);
290}
291
292// static
293void AccessibilityEventRouterViews::SendTextfieldNotification(
294    views::View* view,
295    int type,
296    Profile* profile) {
297  ui::AccessibleViewState state;
298  view->GetAccessibleState(&state);
299  std::string name = UTF16ToUTF8(state.name);
300  std::string context = GetViewContext(view);
301  bool password =
302      (state.state & ui::AccessibilityTypes::STATE_PROTECTED) != 0;
303  AccessibilityTextBoxInfo info(profile, name, context, password);
304  std::string value = UTF16ToUTF8(state.value);
305  info.SetValue(value, state.selection_start, state.selection_end);
306  SendAccessibilityNotification(type, &info);
307}
308
309// static
310void AccessibilityEventRouterViews::SendComboboxNotification(
311    views::View* view,
312    int type,
313    Profile* profile) {
314  ui::AccessibleViewState state;
315  view->GetAccessibleState(&state);
316  std::string name = UTF16ToUTF8(state.name);
317  std::string value = UTF16ToUTF8(state.value);
318  std::string context = GetViewContext(view);
319  AccessibilityComboBoxInfo info(
320      profile, name, context, value, state.index, state.count);
321  SendAccessibilityNotification(type, &info);
322}
323
324// static
325void AccessibilityEventRouterViews::SendCheckboxNotification(
326    views::View* view,
327    int type,
328    Profile* profile) {
329  ui::AccessibleViewState state;
330  view->GetAccessibleState(&state);
331  std::string name = UTF16ToUTF8(state.name);
332  std::string value = UTF16ToUTF8(state.value);
333  std::string context = GetViewContext(view);
334  AccessibilityCheckboxInfo info(
335      profile,
336      name,
337      context,
338      state.state == ui::AccessibilityTypes::STATE_CHECKED);
339  SendAccessibilityNotification(type, &info);
340}
341
342// static
343void AccessibilityEventRouterViews::SendWindowNotification(
344    views::View* view,
345    int type,
346    Profile* profile) {
347  ui::AccessibleViewState state;
348  view->GetAccessibleState(&state);
349  std::string window_text;
350
351  // If it's an alert, try to get the text from the contents of the
352  // static text, not the window title.
353  if (state.role == ui::AccessibilityTypes::ROLE_ALERT)
354    window_text = RecursiveGetStaticText(view);
355
356  // Otherwise get it from the window's accessible name.
357  if (window_text.empty())
358    window_text = UTF16ToUTF8(state.name);
359
360  AccessibilityWindowInfo info(profile, window_text);
361  SendAccessibilityNotification(type, &info);
362}
363
364// static
365void AccessibilityEventRouterViews::SendSliderNotification(
366    views::View* view,
367    int type,
368    Profile* profile) {
369  ui::AccessibleViewState state;
370  view->GetAccessibleState(&state);
371
372  std::string name = UTF16ToUTF8(state.name);
373  std::string value = UTF16ToUTF8(state.value);
374  std::string context = GetViewContext(view);
375  AccessibilitySliderInfo info(
376      profile,
377      name,
378      context,
379      value);
380  SendAccessibilityNotification(type, &info);
381}
382
383// static
384std::string AccessibilityEventRouterViews::GetViewName(views::View* view) {
385  ui::AccessibleViewState state;
386  view->GetAccessibleState(&state);
387  return UTF16ToUTF8(state.name);
388}
389
390// static
391std::string AccessibilityEventRouterViews::GetViewContext(views::View* view) {
392  for (views::View* parent = view->parent();
393       parent;
394       parent = parent->parent()) {
395    ui::AccessibleViewState state;
396    parent->GetAccessibleState(&state);
397
398    // Two cases are handled right now. More could be added in the future
399    // depending on how the UI evolves.
400
401    // A control in a toolbar should use the toolbar's accessible name
402    // as the context.
403    if (state.role == ui::AccessibilityTypes::ROLE_TOOLBAR &&
404        !state.name.empty()) {
405      return UTF16ToUTF8(state.name);
406    }
407
408    // A control inside of an alert or dialog (including an infobar)
409    // should grab the first static text descendant as the context;
410    // that's the prompt.
411    if (state.role == ui::AccessibilityTypes::ROLE_ALERT ||
412        state.role == ui::AccessibilityTypes::ROLE_DIALOG) {
413      views::View* static_text_child = FindDescendantWithAccessibleRole(
414          parent, ui::AccessibilityTypes::ROLE_STATICTEXT);
415      if (static_text_child) {
416        ui::AccessibleViewState state;
417        static_text_child->GetAccessibleState(&state);
418        if (!state.name.empty())
419          return UTF16ToUTF8(state.name);
420      }
421      return std::string();
422    }
423  }
424
425  return std::string();
426}
427
428// static
429views::View* AccessibilityEventRouterViews::FindDescendantWithAccessibleRole(
430    views::View* view, ui::AccessibilityTypes::Role role) {
431  ui::AccessibleViewState state;
432  view->GetAccessibleState(&state);
433  if (state.role == role)
434    return view;
435
436  for (int i = 0; i < view->child_count(); i++) {
437    views::View* child = view->child_at(i);
438    views::View* result = FindDescendantWithAccessibleRole(child, role);
439    if (result)
440      return result;
441  }
442
443  return NULL;
444}
445
446// static
447bool AccessibilityEventRouterViews::IsMenuEvent(
448    views::View* view,
449    int type) {
450  if (type == chrome::NOTIFICATION_ACCESSIBILITY_MENU_OPENED ||
451      type == chrome::NOTIFICATION_ACCESSIBILITY_MENU_CLOSED)
452    return true;
453
454  while (view) {
455    ui::AccessibleViewState state;
456    view->GetAccessibleState(&state);
457    ui::AccessibilityTypes::Role role = state.role;
458    if (role == ui::AccessibilityTypes::ROLE_MENUITEM ||
459        role == ui::AccessibilityTypes::ROLE_MENUPOPUP) {
460      return true;
461    }
462    view = view->parent();
463  }
464
465  return false;
466}
467
468// static
469void AccessibilityEventRouterViews::RecursiveGetMenuItemIndexAndCount(
470    views::View* menu,
471    views::View* item,
472    int* index,
473    int* count) {
474  for (int i = 0; i < menu->child_count(); ++i) {
475    views::View* child = menu->child_at(i);
476    int previous_count = *count;
477    RecursiveGetMenuItemIndexAndCount(child, item, index, count);
478    ui::AccessibleViewState state;
479    child->GetAccessibleState(&state);
480    if (state.role == ui::AccessibilityTypes::ROLE_MENUITEM &&
481        *count == previous_count) {
482      if (item == child)
483        *index = *count;
484      (*count)++;
485    } else if (state.role == ui::AccessibilityTypes::ROLE_PUSHBUTTON) {
486      if (item == child)
487        *index = *count;
488      (*count)++;
489    }
490  }
491}
492
493// static
494std::string AccessibilityEventRouterViews::RecursiveGetStaticText(
495    views::View* view) {
496  ui::AccessibleViewState state;
497  view->GetAccessibleState(&state);
498  if (state.role == ui::AccessibilityTypes::ROLE_STATICTEXT)
499    return UTF16ToUTF8(state.name);
500
501  for (int i = 0; i < view->child_count(); ++i) {
502    views::View* child = view->child_at(i);
503    std::string result = RecursiveGetStaticText(child);
504    if (!result.empty())
505      return result;
506  }
507  return std::string();
508}
509