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  info.set_bounds(view->GetBoundsInScreen());
266  SendControlAccessibilityNotification(event, &info);
267}
268
269// static
270void AccessibilityEventRouterViews::SendButtonNotification(
271    views::View* view,
272    ui::AXEvent event,
273    Profile* profile) {
274  AccessibilityButtonInfo info(
275      profile, GetViewName(view), GetViewContext(view));
276  info.set_bounds(view->GetBoundsInScreen());
277  SendControlAccessibilityNotification(event, &info);
278}
279
280// static
281void AccessibilityEventRouterViews::SendStaticTextNotification(
282    views::View* view,
283    ui::AXEvent event,
284    Profile* profile) {
285  AccessibilityStaticTextInfo info(
286      profile, GetViewName(view), GetViewContext(view));
287  info.set_bounds(view->GetBoundsInScreen());
288  SendControlAccessibilityNotification(event, &info);
289}
290
291// static
292void AccessibilityEventRouterViews::SendLinkNotification(
293    views::View* view,
294    ui::AXEvent event,
295    Profile* profile) {
296  AccessibilityLinkInfo info(profile, GetViewName(view), GetViewContext(view));
297  info.set_bounds(view->GetBoundsInScreen());
298  SendControlAccessibilityNotification(event, &info);
299}
300
301// static
302void AccessibilityEventRouterViews::SendMenuNotification(
303    views::View* view,
304    ui::AXEvent event,
305    Profile* profile) {
306  AccessibilityMenuInfo info(profile, GetViewName(view));
307  info.set_bounds(view->GetBoundsInScreen());
308  SendMenuAccessibilityNotification(event, &info);
309}
310
311// static
312void AccessibilityEventRouterViews::SendMenuItemNotification(
313    views::View* view,
314    ui::AXEvent event,
315    Profile* profile) {
316  std::string name = GetViewName(view);
317  std::string context = GetViewContext(view);
318
319  bool has_submenu = false;
320  int index = -1;
321  int count = -1;
322
323  if (!strcmp(view->GetClassName(), views::MenuItemView::kViewClassName))
324    has_submenu = static_cast<views::MenuItemView*>(view)->HasSubmenu();
325
326  views::View* parent_menu = view->parent();
327  while (parent_menu != NULL && strcmp(parent_menu->GetClassName(),
328                                       views::SubmenuView::kViewClassName)) {
329    parent_menu = parent_menu->parent();
330  }
331  if (parent_menu) {
332    count = 0;
333    RecursiveGetMenuItemIndexAndCount(parent_menu, view, &index, &count);
334  }
335
336  AccessibilityMenuItemInfo info(
337      profile, name, context, has_submenu, index, count);
338  info.set_bounds(view->GetBoundsInScreen());
339  SendControlAccessibilityNotification(event, &info);
340}
341
342// static
343void AccessibilityEventRouterViews::SendTreeNotification(
344    views::View* view,
345    ui::AXEvent event,
346    Profile* profile) {
347  AccessibilityTreeInfo info(profile, GetViewName(view));
348  info.set_bounds(view->GetBoundsInScreen());
349  SendControlAccessibilityNotification(event, &info);
350}
351
352// static
353void AccessibilityEventRouterViews::SendTreeItemNotification(
354    views::View* view,
355    ui::AXEvent event,
356    Profile* profile) {
357  std::string name = GetViewName(view);
358  std::string context = GetViewContext(view);
359
360  if (strcmp(view->GetClassName(), views::TreeView::kViewClassName) != 0) {
361    NOTREACHED();
362    return;
363  }
364
365  views::TreeView* tree = static_cast<views::TreeView*>(view);
366  ui::TreeModelNode* selected_node = tree->GetSelectedNode();
367  ui::TreeModel* model = tree->model();
368
369  int siblings_count = model->GetChildCount(model->GetRoot());
370  int children_count = -1;
371  int index = -1;
372  int depth = -1;
373  bool is_expanded = false;
374
375  if (selected_node) {
376    children_count = model->GetChildCount(selected_node);
377    is_expanded = tree->IsExpanded(selected_node);
378    ui::TreeModelNode* parent_node = model->GetParent(selected_node);
379    if (parent_node) {
380      index = model->GetIndexOf(parent_node, selected_node);
381      siblings_count = model->GetChildCount(parent_node);
382    }
383    // Get node depth.
384    depth = 0;
385    while (parent_node) {
386      depth++;
387      parent_node = model->GetParent(parent_node);
388    }
389  }
390
391  AccessibilityTreeItemInfo info(
392      profile, name, context, depth, index, siblings_count, children_count,
393      is_expanded);
394  info.set_bounds(view->GetBoundsInScreen());
395  SendControlAccessibilityNotification(event, &info);
396}
397
398// static
399void AccessibilityEventRouterViews::SendTextfieldNotification(
400    views::View* view,
401    ui::AXEvent event,
402    Profile* profile) {
403  ui::AXViewState state;
404  view->GetAccessibleState(&state);
405  std::string name = base::UTF16ToUTF8(state.name);
406  std::string context = GetViewContext(view);
407  bool password = state.HasStateFlag(ui::AX_STATE_PROTECTED);
408  AccessibilityTextBoxInfo info(profile, name, context, password);
409  std::string value = base::UTF16ToUTF8(state.value);
410  info.SetValue(value, state.selection_start, state.selection_end);
411  info.set_bounds(view->GetBoundsInScreen());
412  SendControlAccessibilityNotification(event, &info);
413}
414
415// static
416void AccessibilityEventRouterViews::SendComboboxNotification(
417    views::View* view,
418    ui::AXEvent event,
419    Profile* profile) {
420  ui::AXViewState state;
421  view->GetAccessibleState(&state);
422  std::string name = base::UTF16ToUTF8(state.name);
423  std::string value = base::UTF16ToUTF8(state.value);
424  std::string context = GetViewContext(view);
425  AccessibilityComboBoxInfo info(
426      profile, name, context, value, state.index, state.count);
427  info.set_bounds(view->GetBoundsInScreen());
428  SendControlAccessibilityNotification(event, &info);
429}
430
431// static
432void AccessibilityEventRouterViews::SendCheckboxNotification(
433    views::View* view,
434    ui::AXEvent event,
435    Profile* profile) {
436  ui::AXViewState state;
437  view->GetAccessibleState(&state);
438  std::string name = base::UTF16ToUTF8(state.name);
439  std::string context = GetViewContext(view);
440  AccessibilityCheckboxInfo info(
441      profile,
442      name,
443      context,
444      state.HasStateFlag(ui::AX_STATE_CHECKED));
445  info.set_bounds(view->GetBoundsInScreen());
446  SendControlAccessibilityNotification(event, &info);
447}
448
449// static
450void AccessibilityEventRouterViews::SendWindowNotification(
451    views::View* view,
452    ui::AXEvent event,
453    Profile* profile) {
454  ui::AXViewState state;
455  view->GetAccessibleState(&state);
456  std::string window_text;
457
458  // If it's an alert, try to get the text from the contents of the
459  // static text, not the window title.
460  if (state.role == ui::AX_ROLE_ALERT)
461    window_text = RecursiveGetStaticText(view);
462
463  // Otherwise get it from the window's accessible name.
464  if (window_text.empty())
465    window_text = base::UTF16ToUTF8(state.name);
466
467  AccessibilityWindowInfo info(profile, window_text);
468  info.set_bounds(view->GetBoundsInScreen());
469  SendWindowAccessibilityNotification(event, &info);
470}
471
472// static
473void AccessibilityEventRouterViews::SendSliderNotification(
474    views::View* view,
475    ui::AXEvent event,
476    Profile* profile) {
477  ui::AXViewState state;
478  view->GetAccessibleState(&state);
479
480  std::string name = base::UTF16ToUTF8(state.name);
481  std::string value = base::UTF16ToUTF8(state.value);
482  std::string context = GetViewContext(view);
483  AccessibilitySliderInfo info(
484      profile,
485      name,
486      context,
487      value);
488  info.set_bounds(view->GetBoundsInScreen());
489  SendControlAccessibilityNotification(event, &info);
490}
491
492// static
493void AccessibilityEventRouterViews::SendAlertControlNotification(
494    views::View* view,
495    ui::AXEvent event,
496    Profile* profile) {
497  ui::AXViewState state;
498  view->GetAccessibleState(&state);
499
500  std::string name = base::UTF16ToUTF8(state.name);
501  AccessibilityAlertInfo info(
502      profile,
503      name);
504  info.set_bounds(view->GetBoundsInScreen());
505  SendControlAccessibilityNotification(event, &info);
506}
507
508// static
509std::string AccessibilityEventRouterViews::GetViewName(views::View* view) {
510  ui::AXViewState state;
511  view->GetAccessibleState(&state);
512  return base::UTF16ToUTF8(state.name);
513}
514
515// static
516std::string AccessibilityEventRouterViews::GetViewContext(views::View* view) {
517  for (views::View* parent = view->parent();
518       parent;
519       parent = parent->parent()) {
520    ui::AXViewState state;
521    parent->GetAccessibleState(&state);
522
523    // Two cases are handled right now. More could be added in the future
524    // depending on how the UI evolves.
525
526    // A control inside of alert, toolbar or dialog should use that container's
527    // accessible name.
528    if ((state.role == ui::AX_ROLE_ALERT ||
529         state.role == ui::AX_ROLE_DIALOG ||
530         state.role == ui::AX_ROLE_TOOLBAR) &&
531        !state.name.empty()) {
532      return base::UTF16ToUTF8(state.name);
533    }
534
535    // A control inside of an alert or dialog (including an infobar)
536    // should grab the first static text descendant as the context;
537    // that's the prompt.
538    if (state.role == ui::AX_ROLE_ALERT ||
539        state.role == ui::AX_ROLE_DIALOG) {
540      views::View* static_text_child = FindDescendantWithAccessibleRole(
541          parent, ui::AX_ROLE_STATIC_TEXT);
542      if (static_text_child) {
543        ui::AXViewState state;
544        static_text_child->GetAccessibleState(&state);
545        if (!state.name.empty())
546          return base::UTF16ToUTF8(state.name);
547      }
548      return std::string();
549    }
550  }
551
552  return std::string();
553}
554
555// static
556views::View* AccessibilityEventRouterViews::FindDescendantWithAccessibleRole(
557    views::View* view, ui::AXRole role) {
558  ui::AXViewState state;
559  view->GetAccessibleState(&state);
560  if (state.role == role)
561    return view;
562
563  for (int i = 0; i < view->child_count(); i++) {
564    views::View* child = view->child_at(i);
565    views::View* result = FindDescendantWithAccessibleRole(child, role);
566    if (result)
567      return result;
568  }
569
570  return NULL;
571}
572
573// static
574void AccessibilityEventRouterViews::RecursiveGetMenuItemIndexAndCount(
575    views::View* menu,
576    views::View* item,
577    int* index,
578    int* count) {
579  for (int i = 0; i < menu->child_count(); ++i) {
580    views::View* child = menu->child_at(i);
581    if (!child->visible())
582      continue;
583
584    int previous_count = *count;
585    RecursiveGetMenuItemIndexAndCount(child, item, index, count);
586    ui::AXViewState state;
587    child->GetAccessibleState(&state);
588    if (state.role == ui::AX_ROLE_MENU_ITEM &&
589        *count == previous_count) {
590      if (item == child)
591        *index = *count;
592      (*count)++;
593    } else if (state.role == ui::AX_ROLE_BUTTON) {
594      if (item == child)
595        *index = *count;
596      (*count)++;
597    }
598  }
599}
600
601// static
602std::string AccessibilityEventRouterViews::RecursiveGetStaticText(
603    views::View* view) {
604  ui::AXViewState state;
605  view->GetAccessibleState(&state);
606  if (state.role == ui::AX_ROLE_STATIC_TEXT)
607    return base::UTF16ToUTF8(state.name);
608
609  for (int i = 0; i < view->child_count(); ++i) {
610    views::View* child = view->child_at(i);
611    std::string result = RecursiveGetStaticText(child);
612    if (!result.empty())
613      return result;
614  }
615  return std::string();
616}
617
618// static
619views::View* AccessibilityEventRouterViews::FindFirstAccessibleAncestor(
620    views::View* view) {
621  views::View* temp_view = view;
622  while (temp_view->parent() && !temp_view->IsAccessibilityFocusable())
623    temp_view = temp_view->parent();
624  if (temp_view->IsAccessibilityFocusable())
625    return temp_view;
626  return view;
627}
628