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