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; 27 28AccessibilityEventRouterViews::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 36AccessibilityEventRouterViews::~AccessibilityEventRouterViews() { 37} 38 39// static 40AccessibilityEventRouterViews* AccessibilityEventRouterViews::GetInstance() { 41 return Singleton<AccessibilityEventRouterViews>::get(); 42} 43 44void AccessibilityEventRouterViews::HandleAccessibilityEvent( 45 views::View* view, ui::AXEvent event_type) { 46 if (!ExtensionAccessibilityEventRouter::GetInstance()-> 47 IsAccessibilityEnabled()) { 48 return; 49 } 50 51 if (event_type == ui::AX_EVENT_TEXT_CHANGED || 52 event_type == ui::AX_EVENT_SELECTION_CHANGED) { 53 // These two events should only be sent for views that have focus. This 54 // enforces the invariant that we fire events triggered by user action and 55 // not by programmatic logic. For example, the location bar can be updated 56 // by javascript while the user focus is within some other part of the 57 // user interface. In contrast, the other supported events here do not 58 // depend on focus. For example, a menu within a menubar can open or close 59 // while focus is within the location bar or anywhere else as a result of 60 // user action. Note that the below logic can at some point be removed if 61 // we pass more information along to the listener such as focused state. 62 if (!view->GetFocusManager() || 63 view->GetFocusManager()->GetFocusedView() != view) 64 return; 65 } 66 67 // Don't dispatch the accessibility event until the next time through the 68 // event loop, to handle cases where the view's state changes after 69 // the call to post the event. It's safe to use base::Unretained(this) 70 // because AccessibilityEventRouterViews is a singleton. 71 views::ViewStorage* view_storage = views::ViewStorage::GetInstance(); 72 int view_storage_id = view_storage->CreateStorageID(); 73 view_storage->StoreView(view_storage_id, view); 74 base::MessageLoop::current()->PostTask( 75 FROM_HERE, 76 base::Bind( 77 &AccessibilityEventRouterViews::DispatchEventOnViewStorageId, 78 view_storage_id, 79 event_type)); 80} 81 82void AccessibilityEventRouterViews::HandleMenuItemFocused( 83 const base::string16& menu_name, 84 const base::string16& menu_item_name, 85 int item_index, 86 int item_count, 87 bool has_submenu) { 88 if (!ExtensionAccessibilityEventRouter::GetInstance()-> 89 IsAccessibilityEnabled()) { 90 return; 91 } 92 93 if (!most_recent_profile_) 94 return; 95 96 AccessibilityMenuItemInfo info(most_recent_profile_, 97 base::UTF16ToUTF8(menu_item_name), 98 base::UTF16ToUTF8(menu_name), 99 has_submenu, 100 item_index, 101 item_count); 102 SendControlAccessibilityNotification( 103 ui::AX_EVENT_FOCUS, &info); 104} 105 106void AccessibilityEventRouterViews::Observe( 107 int type, 108 const content::NotificationSource& source, 109 const content::NotificationDetails& details) { 110 DCHECK_EQ(type, chrome::NOTIFICATION_PROFILE_DESTROYED); 111 Profile* profile = content::Source<Profile>(source).ptr(); 112 if (profile == most_recent_profile_) 113 most_recent_profile_ = NULL; 114} 115 116// 117// Private methods 118// 119 120void AccessibilityEventRouterViews::DispatchEventOnViewStorageId( 121 int view_storage_id, 122 ui::AXEvent type) { 123 views::ViewStorage* view_storage = views::ViewStorage::GetInstance(); 124 views::View* view = view_storage->RetrieveView(view_storage_id); 125 view_storage->RemoveView(view_storage_id); 126 if (!view) 127 return; 128 129 AccessibilityEventRouterViews* instance = 130 AccessibilityEventRouterViews::GetInstance(); 131 instance->DispatchAccessibilityEvent(view, type); 132} 133 134void AccessibilityEventRouterViews::DispatchAccessibilityEvent( 135 views::View* view, ui::AXEvent type) { 136 // Get the profile associated with this view. If it's not found, use 137 // the most recent profile where accessibility events were sent, or 138 // the default profile. 139 Profile* profile = NULL; 140 views::Widget* widget = view->GetWidget(); 141 if (widget) { 142 profile = reinterpret_cast<Profile*>( 143 widget->GetNativeWindowProperty(Profile::kProfileKey)); 144 } 145 if (!profile) 146 profile = most_recent_profile_; 147 if (!profile) { 148 if (g_browser_process->profile_manager()) 149 profile = g_browser_process->profile_manager()->GetLastUsedProfile(); 150 } 151 if (!profile) { 152 LOG(WARNING) << "Accessibility notification but no profile"; 153 return; 154 } 155 156 most_recent_profile_ = profile; 157 158 if (type == ui::AX_EVENT_MENU_START || 159 type == ui::AX_EVENT_MENU_POPUP_START || 160 type == ui::AX_EVENT_MENU_END || 161 type == ui::AX_EVENT_MENU_POPUP_END) { 162 SendMenuNotification(view, type, profile); 163 return; 164 } 165 166 ui::AXViewState state; 167 view->GetAccessibleState(&state); 168 169 if (type == ui::AX_EVENT_ALERT && 170 !(state.role == ui::AX_ROLE_ALERT || 171 state.role == ui::AX_ROLE_WINDOW)) { 172 SendAlertControlNotification(view, type, profile); 173 return; 174 } 175 176 switch (state.role) { 177 case ui::AX_ROLE_ALERT: 178 case ui::AX_ROLE_DIALOG: 179 case ui::AX_ROLE_WINDOW: 180 SendWindowNotification(view, type, profile); 181 break; 182 case ui::AX_ROLE_POP_UP_BUTTON: 183 case ui::AX_ROLE_MENU_BAR: 184 case ui::AX_ROLE_MENU_LIST_POPUP: 185 SendMenuNotification(view, type, profile); 186 break; 187 case ui::AX_ROLE_BUTTON_DROP_DOWN: 188 case ui::AX_ROLE_BUTTON: 189 SendButtonNotification(view, type, profile); 190 break; 191 case ui::AX_ROLE_CHECK_BOX: 192 SendCheckboxNotification(view, type, profile); 193 break; 194 case ui::AX_ROLE_COMBO_BOX: 195 SendComboboxNotification(view, type, profile); 196 break; 197 case ui::AX_ROLE_LINK: 198 SendLinkNotification(view, type, profile); 199 break; 200 case ui::AX_ROLE_LOCATION_BAR: 201 case ui::AX_ROLE_TEXT_FIELD: 202 SendTextfieldNotification(view, type, profile); 203 break; 204 case ui::AX_ROLE_MENU_ITEM: 205 SendMenuItemNotification(view, type, profile); 206 break; 207 case ui::AX_ROLE_RADIO_BUTTON: 208 // Not used anymore? 209 case ui::AX_ROLE_SLIDER: 210 SendSliderNotification(view, type, profile); 211 break; 212 case ui::AX_ROLE_TREE: 213 SendTreeNotification(view, type, profile); 214 break; 215 case ui::AX_ROLE_TREE_ITEM: 216 SendTreeItemNotification(view, type, profile); 217 break; 218 default: 219 // Hover events can fire on literally any view, so it's safe to 220 // ignore ones we don't care about. 221 if (type == ui::AX_EVENT_HOVER) 222 break; 223 224 // If this is encountered, please file a bug with the role that wasn't 225 // caught so we can add accessibility extension API support. 226 NOTREACHED(); 227 } 228} 229 230// static 231void AccessibilityEventRouterViews::SendButtonNotification( 232 views::View* view, 233 ui::AXEvent event, 234 Profile* profile) { 235 AccessibilityButtonInfo info( 236 profile, GetViewName(view), GetViewContext(view)); 237 SendControlAccessibilityNotification(event, &info); 238} 239 240// static 241void AccessibilityEventRouterViews::SendLinkNotification( 242 views::View* view, 243 ui::AXEvent event, 244 Profile* profile) { 245 AccessibilityLinkInfo info(profile, GetViewName(view), GetViewContext(view)); 246 SendControlAccessibilityNotification(event, &info); 247} 248 249// static 250void AccessibilityEventRouterViews::SendMenuNotification( 251 views::View* view, 252 ui::AXEvent event, 253 Profile* profile) { 254 AccessibilityMenuInfo info(profile, GetViewName(view)); 255 SendMenuAccessibilityNotification(event, &info); 256} 257 258// static 259void AccessibilityEventRouterViews::SendMenuItemNotification( 260 views::View* view, 261 ui::AXEvent event, 262 Profile* profile) { 263 std::string name = GetViewName(view); 264 std::string context = GetViewContext(view); 265 266 bool has_submenu = false; 267 int index = -1; 268 int count = -1; 269 270 if (!strcmp(view->GetClassName(), views::MenuItemView::kViewClassName)) 271 has_submenu = static_cast<views::MenuItemView*>(view)->HasSubmenu(); 272 273 views::View* parent_menu = view->parent(); 274 while (parent_menu != NULL && strcmp(parent_menu->GetClassName(), 275 views::SubmenuView::kViewClassName)) { 276 parent_menu = parent_menu->parent(); 277 } 278 if (parent_menu) { 279 count = 0; 280 RecursiveGetMenuItemIndexAndCount(parent_menu, view, &index, &count); 281 } 282 283 AccessibilityMenuItemInfo info( 284 profile, name, context, has_submenu, index, count); 285 SendControlAccessibilityNotification(event, &info); 286} 287 288// static 289void AccessibilityEventRouterViews::SendTreeNotification( 290 views::View* view, 291 ui::AXEvent event, 292 Profile* profile) { 293 AccessibilityTreeInfo info(profile, GetViewName(view)); 294 SendControlAccessibilityNotification(event, &info); 295} 296 297// static 298void AccessibilityEventRouterViews::SendTreeItemNotification( 299 views::View* view, 300 ui::AXEvent event, 301 Profile* profile) { 302 std::string name = GetViewName(view); 303 std::string context = GetViewContext(view); 304 305 if (strcmp(view->GetClassName(), views::TreeView::kViewClassName) != 0) { 306 NOTREACHED(); 307 return; 308 } 309 310 views::TreeView* tree = static_cast<views::TreeView*>(view); 311 ui::TreeModelNode* selected_node = tree->GetSelectedNode(); 312 ui::TreeModel* model = tree->model(); 313 314 int siblings_count = model->GetChildCount(model->GetRoot()); 315 int children_count = -1; 316 int index = -1; 317 int depth = -1; 318 bool is_expanded = false; 319 320 if (selected_node) { 321 children_count = model->GetChildCount(selected_node); 322 is_expanded = tree->IsExpanded(selected_node); 323 ui::TreeModelNode* parent_node = model->GetParent(selected_node); 324 if (parent_node) { 325 index = model->GetIndexOf(parent_node, selected_node); 326 siblings_count = model->GetChildCount(parent_node); 327 } 328 // Get node depth. 329 depth = 0; 330 while (parent_node) { 331 depth++; 332 parent_node = model->GetParent(parent_node); 333 } 334 } 335 336 AccessibilityTreeItemInfo info( 337 profile, name, context, depth, index, siblings_count, children_count, 338 is_expanded); 339 SendControlAccessibilityNotification(event, &info); 340} 341 342// static 343void AccessibilityEventRouterViews::SendTextfieldNotification( 344 views::View* view, 345 ui::AXEvent event, 346 Profile* profile) { 347 ui::AXViewState state; 348 view->GetAccessibleState(&state); 349 std::string name = base::UTF16ToUTF8(state.name); 350 std::string context = GetViewContext(view); 351 bool password = state.HasStateFlag(ui::AX_STATE_PROTECTED); 352 AccessibilityTextBoxInfo info(profile, name, context, password); 353 std::string value = base::UTF16ToUTF8(state.value); 354 info.SetValue(value, state.selection_start, state.selection_end); 355 SendControlAccessibilityNotification(event, &info); 356} 357 358// static 359void AccessibilityEventRouterViews::SendComboboxNotification( 360 views::View* view, 361 ui::AXEvent event, 362 Profile* profile) { 363 ui::AXViewState state; 364 view->GetAccessibleState(&state); 365 std::string name = base::UTF16ToUTF8(state.name); 366 std::string value = base::UTF16ToUTF8(state.value); 367 std::string context = GetViewContext(view); 368 AccessibilityComboBoxInfo info( 369 profile, name, context, value, state.index, state.count); 370 SendControlAccessibilityNotification(event, &info); 371} 372 373// static 374void AccessibilityEventRouterViews::SendCheckboxNotification( 375 views::View* view, 376 ui::AXEvent event, 377 Profile* profile) { 378 ui::AXViewState state; 379 view->GetAccessibleState(&state); 380 std::string name = base::UTF16ToUTF8(state.name); 381 std::string context = GetViewContext(view); 382 AccessibilityCheckboxInfo info( 383 profile, 384 name, 385 context, 386 state.HasStateFlag(ui::AX_STATE_CHECKED)); 387 SendControlAccessibilityNotification(event, &info); 388} 389 390// static 391void AccessibilityEventRouterViews::SendWindowNotification( 392 views::View* view, 393 ui::AXEvent event, 394 Profile* profile) { 395 ui::AXViewState state; 396 view->GetAccessibleState(&state); 397 std::string window_text; 398 399 // If it's an alert, try to get the text from the contents of the 400 // static text, not the window title. 401 if (state.role == ui::AX_ROLE_ALERT) 402 window_text = RecursiveGetStaticText(view); 403 404 // Otherwise get it from the window's accessible name. 405 if (window_text.empty()) 406 window_text = base::UTF16ToUTF8(state.name); 407 408 AccessibilityWindowInfo info(profile, window_text); 409 SendWindowAccessibilityNotification(event, &info); 410} 411 412// static 413void AccessibilityEventRouterViews::SendSliderNotification( 414 views::View* view, 415 ui::AXEvent event, 416 Profile* profile) { 417 ui::AXViewState state; 418 view->GetAccessibleState(&state); 419 420 std::string name = base::UTF16ToUTF8(state.name); 421 std::string value = base::UTF16ToUTF8(state.value); 422 std::string context = GetViewContext(view); 423 AccessibilitySliderInfo info( 424 profile, 425 name, 426 context, 427 value); 428 SendControlAccessibilityNotification(event, &info); 429} 430 431// static 432void AccessibilityEventRouterViews::SendAlertControlNotification( 433 views::View* view, 434 ui::AXEvent event, 435 Profile* profile) { 436 ui::AXViewState state; 437 view->GetAccessibleState(&state); 438 439 std::string name = base::UTF16ToUTF8(state.name); 440 AccessibilityAlertInfo info( 441 profile, 442 name); 443 SendControlAccessibilityNotification(event, &info); 444} 445 446// static 447std::string AccessibilityEventRouterViews::GetViewName(views::View* view) { 448 ui::AXViewState state; 449 view->GetAccessibleState(&state); 450 return base::UTF16ToUTF8(state.name); 451} 452 453// static 454std::string AccessibilityEventRouterViews::GetViewContext(views::View* view) { 455 for (views::View* parent = view->parent(); 456 parent; 457 parent = parent->parent()) { 458 ui::AXViewState state; 459 parent->GetAccessibleState(&state); 460 461 // Two cases are handled right now. More could be added in the future 462 // depending on how the UI evolves. 463 464 // A control inside of alert, toolbar or dialog should use that container's 465 // accessible name. 466 if ((state.role == ui::AX_ROLE_ALERT || 467 state.role == ui::AX_ROLE_DIALOG || 468 state.role == ui::AX_ROLE_TOOLBAR) && 469 !state.name.empty()) { 470 return base::UTF16ToUTF8(state.name); 471 } 472 473 // A control inside of an alert or dialog (including an infobar) 474 // should grab the first static text descendant as the context; 475 // that's the prompt. 476 if (state.role == ui::AX_ROLE_ALERT || 477 state.role == ui::AX_ROLE_DIALOG) { 478 views::View* static_text_child = FindDescendantWithAccessibleRole( 479 parent, ui::AX_ROLE_STATIC_TEXT); 480 if (static_text_child) { 481 ui::AXViewState state; 482 static_text_child->GetAccessibleState(&state); 483 if (!state.name.empty()) 484 return base::UTF16ToUTF8(state.name); 485 } 486 return std::string(); 487 } 488 } 489 490 return std::string(); 491} 492 493// static 494views::View* AccessibilityEventRouterViews::FindDescendantWithAccessibleRole( 495 views::View* view, ui::AXRole role) { 496 ui::AXViewState state; 497 view->GetAccessibleState(&state); 498 if (state.role == role) 499 return view; 500 501 for (int i = 0; i < view->child_count(); i++) { 502 views::View* child = view->child_at(i); 503 views::View* result = FindDescendantWithAccessibleRole(child, role); 504 if (result) 505 return result; 506 } 507 508 return NULL; 509} 510 511// static 512void AccessibilityEventRouterViews::RecursiveGetMenuItemIndexAndCount( 513 views::View* menu, 514 views::View* item, 515 int* index, 516 int* count) { 517 for (int i = 0; i < menu->child_count(); ++i) { 518 views::View* child = menu->child_at(i); 519 if (!child->visible()) 520 continue; 521 522 int previous_count = *count; 523 RecursiveGetMenuItemIndexAndCount(child, item, index, count); 524 ui::AXViewState state; 525 child->GetAccessibleState(&state); 526 if (state.role == ui::AX_ROLE_MENU_ITEM && 527 *count == previous_count) { 528 if (item == child) 529 *index = *count; 530 (*count)++; 531 } else if (state.role == ui::AX_ROLE_BUTTON) { 532 if (item == child) 533 *index = *count; 534 (*count)++; 535 } 536 } 537} 538 539// static 540std::string AccessibilityEventRouterViews::RecursiveGetStaticText( 541 views::View* view) { 542 ui::AXViewState state; 543 view->GetAccessibleState(&state); 544 if (state.role == ui::AX_ROLE_STATIC_TEXT) 545 return base::UTF16ToUTF8(state.name); 546 547 for (int i = 0; i < view->child_count(); ++i) { 548 views::View* child = view->child_at(i); 549 std::string result = RecursiveGetStaticText(child); 550 if (!result.empty()) 551 return result; 552 } 553 return std::string(); 554} 555