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