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