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