Home | History | Annotate | Download | only in accessibility
      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 
     26 using views::FocusManager;
     27 
     28 AccessibilityEventRouterViews::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 
     36 AccessibilityEventRouterViews::~AccessibilityEventRouterViews() {
     37 }
     38 
     39 // static
     40 AccessibilityEventRouterViews* AccessibilityEventRouterViews::GetInstance() {
     41   return Singleton<AccessibilityEventRouterViews>::get();
     42 }
     43 
     44 void 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 
    107 void 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 
    131 void 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 
    145 void 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 
    159 void 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
    235 void 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
    245 void 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
    254 void 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
    263 void 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
    293 void 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
    310 void 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
    325 void 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
    343 void 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
    365 void 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
    384 std::string AccessibilityEventRouterViews::GetViewName(views::View* view) {
    385   ui::AccessibleViewState state;
    386   view->GetAccessibleState(&state);
    387   return UTF16ToUTF8(state.name);
    388 }
    389 
    390 // static
    391 std::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
    429 views::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
    447 bool 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
    469 void 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
    494 std::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