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 26 using views::FocusManager; 27 using views::ViewStorage; 28 29 AccessibilityEventRouterViews::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 39 AccessibilityEventRouterViews::~AccessibilityEventRouterViews() { 40 } 41 42 // static 43 AccessibilityEventRouterViews* AccessibilityEventRouterViews::GetInstance() { 44 return Singleton<AccessibilityEventRouterViews>::get(); 45 } 46 47 void 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 85 void 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 109 void 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 123 void 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 137 void 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 254 void 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 270 void 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 281 void 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 292 void 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 302 void 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 312 void 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 343 void 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 353 void 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 399 void 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 416 void 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 432 void 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 450 void 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 473 void 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 493 void 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 509 std::string AccessibilityEventRouterViews::GetViewName(views::View* view) { 510 ui::AXViewState state; 511 view->GetAccessibleState(&state); 512 return base::UTF16ToUTF8(state.name); 513 } 514 515 // static 516 std::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 556 views::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 574 void 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 602 std::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 619 views::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