accessibility_event_router_views.cc revision f2477e01787aa58f445919b809d89e252beef54f
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  if (type == ui::AccessibilityTypes::EVENT_ALERT &&
169      !(state.role == ui::AccessibilityTypes::ROLE_ALERT ||
170        state.role == ui::AccessibilityTypes::ROLE_WINDOW)) {
171    SendAlertControlNotification(view, type, profile);
172    return;
173  }
174
175  switch (state.role) {
176  case ui::AccessibilityTypes::ROLE_ALERT:
177  case ui::AccessibilityTypes::ROLE_DIALOG:
178  case ui::AccessibilityTypes::ROLE_WINDOW:
179    SendWindowNotification(view, type, profile);
180    break;
181  case ui::AccessibilityTypes::ROLE_BUTTONMENU:
182  case ui::AccessibilityTypes::ROLE_MENUBAR:
183  case ui::AccessibilityTypes::ROLE_MENUPOPUP:
184    SendMenuNotification(view, type, profile);
185    break;
186  case ui::AccessibilityTypes::ROLE_BUTTONDROPDOWN:
187  case ui::AccessibilityTypes::ROLE_PUSHBUTTON:
188    SendButtonNotification(view, type, profile);
189    break;
190  case ui::AccessibilityTypes::ROLE_CHECKBUTTON:
191    SendCheckboxNotification(view, type, profile);
192    break;
193  case ui::AccessibilityTypes::ROLE_COMBOBOX:
194    SendComboboxNotification(view, type, profile);
195    break;
196  case ui::AccessibilityTypes::ROLE_LINK:
197    SendLinkNotification(view, type, profile);
198    break;
199  case ui::AccessibilityTypes::ROLE_LOCATION_BAR:
200  case ui::AccessibilityTypes::ROLE_TEXT:
201    SendTextfieldNotification(view, type, profile);
202    break;
203  case ui::AccessibilityTypes::ROLE_MENUITEM:
204    SendMenuItemNotification(view, type, profile);
205    break;
206  case ui::AccessibilityTypes::ROLE_RADIOBUTTON:
207    // Not used anymore?
208  case ui::AccessibilityTypes::ROLE_SLIDER:
209    SendSliderNotification(view, type, profile);
210    break;
211  default:
212    // If this is encountered, please file a bug with the role that wasn't
213    // caught so we can add accessibility extension API support.
214    NOTREACHED();
215  }
216}
217
218// static
219void AccessibilityEventRouterViews::SendButtonNotification(
220    views::View* view,
221    ui::AccessibilityTypes::Event event,
222    Profile* profile) {
223  AccessibilityButtonInfo info(
224      profile, GetViewName(view), GetViewContext(view));
225  SendControlAccessibilityNotification(event, &info);
226}
227
228// static
229void AccessibilityEventRouterViews::SendLinkNotification(
230    views::View* view,
231    ui::AccessibilityTypes::Event event,
232    Profile* profile) {
233  AccessibilityLinkInfo info(profile, GetViewName(view), GetViewContext(view));
234  SendControlAccessibilityNotification(event, &info);
235}
236
237// static
238void AccessibilityEventRouterViews::SendMenuNotification(
239    views::View* view,
240    ui::AccessibilityTypes::Event event,
241    Profile* profile) {
242  AccessibilityMenuInfo info(profile, GetViewName(view));
243  SendMenuAccessibilityNotification(event, &info);
244}
245
246// static
247void AccessibilityEventRouterViews::SendMenuItemNotification(
248    views::View* view,
249    ui::AccessibilityTypes::Event event,
250    Profile* profile) {
251  std::string name = GetViewName(view);
252  std::string context = GetViewContext(view);
253
254  bool has_submenu = false;
255  int index = -1;
256  int count = -1;
257
258  if (!strcmp(view->GetClassName(), views::MenuItemView::kViewClassName))
259    has_submenu = static_cast<views::MenuItemView*>(view)->HasSubmenu();
260
261  views::View* parent_menu = view->parent();
262  while (parent_menu != NULL && strcmp(parent_menu->GetClassName(),
263                                       views::SubmenuView::kViewClassName)) {
264    parent_menu = parent_menu->parent();
265  }
266  if (parent_menu) {
267    count = 0;
268    RecursiveGetMenuItemIndexAndCount(parent_menu, view, &index, &count);
269  }
270
271  AccessibilityMenuItemInfo info(
272      profile, name, context, has_submenu, index, count);
273  SendControlAccessibilityNotification(event, &info);
274}
275
276// static
277void AccessibilityEventRouterViews::SendTextfieldNotification(
278    views::View* view,
279    ui::AccessibilityTypes::Event event,
280    Profile* profile) {
281  ui::AccessibleViewState state;
282  view->GetAccessibleState(&state);
283  std::string name = UTF16ToUTF8(state.name);
284  std::string context = GetViewContext(view);
285  bool password =
286      (state.state & ui::AccessibilityTypes::STATE_PROTECTED) != 0;
287  AccessibilityTextBoxInfo info(profile, name, context, password);
288  std::string value = UTF16ToUTF8(state.value);
289  info.SetValue(value, state.selection_start, state.selection_end);
290  SendControlAccessibilityNotification(event, &info);
291}
292
293// static
294void AccessibilityEventRouterViews::SendComboboxNotification(
295    views::View* view,
296    ui::AccessibilityTypes::Event event,
297    Profile* profile) {
298  ui::AccessibleViewState state;
299  view->GetAccessibleState(&state);
300  std::string name = UTF16ToUTF8(state.name);
301  std::string value = UTF16ToUTF8(state.value);
302  std::string context = GetViewContext(view);
303  AccessibilityComboBoxInfo info(
304      profile, name, context, value, state.index, state.count);
305  SendControlAccessibilityNotification(event, &info);
306}
307
308// static
309void AccessibilityEventRouterViews::SendCheckboxNotification(
310    views::View* view,
311    ui::AccessibilityTypes::Event event,
312    Profile* profile) {
313  ui::AccessibleViewState state;
314  view->GetAccessibleState(&state);
315  std::string name = UTF16ToUTF8(state.name);
316  std::string context = GetViewContext(view);
317  AccessibilityCheckboxInfo info(
318      profile,
319      name,
320      context,
321      state.state == ui::AccessibilityTypes::STATE_CHECKED);
322  SendControlAccessibilityNotification(event, &info);
323}
324
325// static
326void AccessibilityEventRouterViews::SendWindowNotification(
327    views::View* view,
328    ui::AccessibilityTypes::Event event,
329    Profile* profile) {
330  ui::AccessibleViewState state;
331  view->GetAccessibleState(&state);
332  std::string window_text;
333
334  // If it's an alert, try to get the text from the contents of the
335  // static text, not the window title.
336  if (state.role == ui::AccessibilityTypes::ROLE_ALERT)
337    window_text = RecursiveGetStaticText(view);
338
339  // Otherwise get it from the window's accessible name.
340  if (window_text.empty())
341    window_text = UTF16ToUTF8(state.name);
342
343  AccessibilityWindowInfo info(profile, window_text);
344  SendWindowAccessibilityNotification(event, &info);
345}
346
347// static
348void AccessibilityEventRouterViews::SendSliderNotification(
349    views::View* view,
350    ui::AccessibilityTypes::Event event,
351    Profile* profile) {
352  ui::AccessibleViewState state;
353  view->GetAccessibleState(&state);
354
355  std::string name = UTF16ToUTF8(state.name);
356  std::string value = UTF16ToUTF8(state.value);
357  std::string context = GetViewContext(view);
358  AccessibilitySliderInfo info(
359      profile,
360      name,
361      context,
362      value);
363  SendControlAccessibilityNotification(event, &info);
364}
365
366// static
367void AccessibilityEventRouterViews::SendAlertControlNotification(
368    views::View* view,
369    ui::AccessibilityTypes::Event event,
370    Profile* profile) {
371  ui::AccessibleViewState state;
372  view->GetAccessibleState(&state);
373
374  std::string name = UTF16ToUTF8(state.name);
375  AccessibilityAlertInfo info(
376      profile,
377      name);
378  SendControlAccessibilityNotification(event, &info);
379}
380
381// static
382std::string AccessibilityEventRouterViews::GetViewName(views::View* view) {
383  ui::AccessibleViewState state;
384  view->GetAccessibleState(&state);
385  return UTF16ToUTF8(state.name);
386}
387
388// static
389std::string AccessibilityEventRouterViews::GetViewContext(views::View* view) {
390  for (views::View* parent = view->parent();
391       parent;
392       parent = parent->parent()) {
393    ui::AccessibleViewState state;
394    parent->GetAccessibleState(&state);
395
396    // Two cases are handled right now. More could be added in the future
397    // depending on how the UI evolves.
398
399    // A control inside of alert, toolbar or dialog should use that container's
400    // accessible name.
401    if ((state.role == ui::AccessibilityTypes::ROLE_ALERT ||
402         state.role == ui::AccessibilityTypes::ROLE_DIALOG ||
403         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
447void AccessibilityEventRouterViews::RecursiveGetMenuItemIndexAndCount(
448    views::View* menu,
449    views::View* item,
450    int* index,
451    int* count) {
452  for (int i = 0; i < menu->child_count(); ++i) {
453    views::View* child = menu->child_at(i);
454    if (!child->visible())
455      continue;
456
457    int previous_count = *count;
458    RecursiveGetMenuItemIndexAndCount(child, item, index, count);
459    ui::AccessibleViewState state;
460    child->GetAccessibleState(&state);
461    if (state.role == ui::AccessibilityTypes::ROLE_MENUITEM &&
462        *count == previous_count) {
463      if (item == child)
464        *index = *count;
465      (*count)++;
466    } else if (state.role == ui::AccessibilityTypes::ROLE_PUSHBUTTON) {
467      if (item == child)
468        *index = *count;
469      (*count)++;
470    }
471  }
472}
473
474// static
475std::string AccessibilityEventRouterViews::RecursiveGetStaticText(
476    views::View* view) {
477  ui::AccessibleViewState state;
478  view->GetAccessibleState(&state);
479  if (state.role == ui::AccessibilityTypes::ROLE_STATICTEXT)
480    return UTF16ToUTF8(state.name);
481
482  for (int i = 0; i < view->child_count(); ++i) {
483    views::View* child = view->child_at(i);
484    std::string result = RecursiveGetStaticText(child);
485    if (!result.empty())
486      return result;
487  }
488  return std::string();
489}
490