accessibility_event_router_views.cc revision effb81e5f8246d0db0270817048dc992db66e9fb
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 // If this is encountered, please file a bug with the role that wasn't 220 // caught so we can add accessibility extension API support. 221 NOTREACHED(); 222 } 223} 224 225// static 226void AccessibilityEventRouterViews::SendButtonNotification( 227 views::View* view, 228 ui::AXEvent event, 229 Profile* profile) { 230 AccessibilityButtonInfo info( 231 profile, GetViewName(view), GetViewContext(view)); 232 SendControlAccessibilityNotification(event, &info); 233} 234 235// static 236void AccessibilityEventRouterViews::SendLinkNotification( 237 views::View* view, 238 ui::AXEvent event, 239 Profile* profile) { 240 AccessibilityLinkInfo info(profile, GetViewName(view), GetViewContext(view)); 241 SendControlAccessibilityNotification(event, &info); 242} 243 244// static 245void AccessibilityEventRouterViews::SendMenuNotification( 246 views::View* view, 247 ui::AXEvent event, 248 Profile* profile) { 249 AccessibilityMenuInfo info(profile, GetViewName(view)); 250 SendMenuAccessibilityNotification(event, &info); 251} 252 253// static 254void AccessibilityEventRouterViews::SendMenuItemNotification( 255 views::View* view, 256 ui::AXEvent event, 257 Profile* profile) { 258 std::string name = GetViewName(view); 259 std::string context = GetViewContext(view); 260 261 bool has_submenu = false; 262 int index = -1; 263 int count = -1; 264 265 if (!strcmp(view->GetClassName(), views::MenuItemView::kViewClassName)) 266 has_submenu = static_cast<views::MenuItemView*>(view)->HasSubmenu(); 267 268 views::View* parent_menu = view->parent(); 269 while (parent_menu != NULL && strcmp(parent_menu->GetClassName(), 270 views::SubmenuView::kViewClassName)) { 271 parent_menu = parent_menu->parent(); 272 } 273 if (parent_menu) { 274 count = 0; 275 RecursiveGetMenuItemIndexAndCount(parent_menu, view, &index, &count); 276 } 277 278 AccessibilityMenuItemInfo info( 279 profile, name, context, has_submenu, index, count); 280 SendControlAccessibilityNotification(event, &info); 281} 282 283// static 284void AccessibilityEventRouterViews::SendTreeNotification( 285 views::View* view, 286 ui::AXEvent event, 287 Profile* profile) { 288 AccessibilityTreeInfo info(profile, GetViewName(view)); 289 SendControlAccessibilityNotification(event, &info); 290} 291 292// static 293void AccessibilityEventRouterViews::SendTreeItemNotification( 294 views::View* view, 295 ui::AXEvent event, 296 Profile* profile) { 297 std::string name = GetViewName(view); 298 std::string context = GetViewContext(view); 299 300 if (strcmp(view->GetClassName(), views::TreeView::kViewClassName) != 0) { 301 NOTREACHED(); 302 return; 303 } 304 305 views::TreeView* tree = static_cast<views::TreeView*>(view); 306 ui::TreeModelNode* selected_node = tree->GetSelectedNode(); 307 ui::TreeModel* model = tree->model(); 308 309 int siblings_count = model->GetChildCount(model->GetRoot()); 310 int children_count = -1; 311 int index = -1; 312 int depth = -1; 313 bool is_expanded = false; 314 315 if (selected_node) { 316 children_count = model->GetChildCount(selected_node); 317 is_expanded = tree->IsExpanded(selected_node); 318 ui::TreeModelNode* parent_node = model->GetParent(selected_node); 319 if (parent_node) { 320 index = model->GetIndexOf(parent_node, selected_node); 321 siblings_count = model->GetChildCount(parent_node); 322 } 323 // Get node depth. 324 depth = 0; 325 while (parent_node) { 326 depth++; 327 parent_node = model->GetParent(parent_node); 328 } 329 } 330 331 AccessibilityTreeItemInfo info( 332 profile, name, context, depth, index, siblings_count, children_count, 333 is_expanded); 334 SendControlAccessibilityNotification(event, &info); 335} 336 337// static 338void AccessibilityEventRouterViews::SendTextfieldNotification( 339 views::View* view, 340 ui::AXEvent event, 341 Profile* profile) { 342 ui::AXViewState state; 343 view->GetAccessibleState(&state); 344 std::string name = base::UTF16ToUTF8(state.name); 345 std::string context = GetViewContext(view); 346 bool password = state.HasStateFlag(ui::AX_STATE_PROTECTED); 347 AccessibilityTextBoxInfo info(profile, name, context, password); 348 std::string value = base::UTF16ToUTF8(state.value); 349 info.SetValue(value, state.selection_start, state.selection_end); 350 SendControlAccessibilityNotification(event, &info); 351} 352 353// static 354void AccessibilityEventRouterViews::SendComboboxNotification( 355 views::View* view, 356 ui::AXEvent event, 357 Profile* profile) { 358 ui::AXViewState state; 359 view->GetAccessibleState(&state); 360 std::string name = base::UTF16ToUTF8(state.name); 361 std::string value = base::UTF16ToUTF8(state.value); 362 std::string context = GetViewContext(view); 363 AccessibilityComboBoxInfo info( 364 profile, name, context, value, state.index, state.count); 365 SendControlAccessibilityNotification(event, &info); 366} 367 368// static 369void AccessibilityEventRouterViews::SendCheckboxNotification( 370 views::View* view, 371 ui::AXEvent event, 372 Profile* profile) { 373 ui::AXViewState state; 374 view->GetAccessibleState(&state); 375 std::string name = base::UTF16ToUTF8(state.name); 376 std::string context = GetViewContext(view); 377 AccessibilityCheckboxInfo info( 378 profile, 379 name, 380 context, 381 state.HasStateFlag(ui::AX_STATE_CHECKED)); 382 SendControlAccessibilityNotification(event, &info); 383} 384 385// static 386void AccessibilityEventRouterViews::SendWindowNotification( 387 views::View* view, 388 ui::AXEvent event, 389 Profile* profile) { 390 ui::AXViewState state; 391 view->GetAccessibleState(&state); 392 std::string window_text; 393 394 // If it's an alert, try to get the text from the contents of the 395 // static text, not the window title. 396 if (state.role == ui::AX_ROLE_ALERT) 397 window_text = RecursiveGetStaticText(view); 398 399 // Otherwise get it from the window's accessible name. 400 if (window_text.empty()) 401 window_text = base::UTF16ToUTF8(state.name); 402 403 AccessibilityWindowInfo info(profile, window_text); 404 SendWindowAccessibilityNotification(event, &info); 405} 406 407// static 408void AccessibilityEventRouterViews::SendSliderNotification( 409 views::View* view, 410 ui::AXEvent event, 411 Profile* profile) { 412 ui::AXViewState state; 413 view->GetAccessibleState(&state); 414 415 std::string name = base::UTF16ToUTF8(state.name); 416 std::string value = base::UTF16ToUTF8(state.value); 417 std::string context = GetViewContext(view); 418 AccessibilitySliderInfo info( 419 profile, 420 name, 421 context, 422 value); 423 SendControlAccessibilityNotification(event, &info); 424} 425 426// static 427void AccessibilityEventRouterViews::SendAlertControlNotification( 428 views::View* view, 429 ui::AXEvent event, 430 Profile* profile) { 431 ui::AXViewState state; 432 view->GetAccessibleState(&state); 433 434 std::string name = base::UTF16ToUTF8(state.name); 435 AccessibilityAlertInfo info( 436 profile, 437 name); 438 SendControlAccessibilityNotification(event, &info); 439} 440 441// static 442std::string AccessibilityEventRouterViews::GetViewName(views::View* view) { 443 ui::AXViewState state; 444 view->GetAccessibleState(&state); 445 return base::UTF16ToUTF8(state.name); 446} 447 448// static 449std::string AccessibilityEventRouterViews::GetViewContext(views::View* view) { 450 for (views::View* parent = view->parent(); 451 parent; 452 parent = parent->parent()) { 453 ui::AXViewState state; 454 parent->GetAccessibleState(&state); 455 456 // Two cases are handled right now. More could be added in the future 457 // depending on how the UI evolves. 458 459 // A control inside of alert, toolbar or dialog should use that container's 460 // accessible name. 461 if ((state.role == ui::AX_ROLE_ALERT || 462 state.role == ui::AX_ROLE_DIALOG || 463 state.role == ui::AX_ROLE_TOOLBAR) && 464 !state.name.empty()) { 465 return base::UTF16ToUTF8(state.name); 466 } 467 468 // A control inside of an alert or dialog (including an infobar) 469 // should grab the first static text descendant as the context; 470 // that's the prompt. 471 if (state.role == ui::AX_ROLE_ALERT || 472 state.role == ui::AX_ROLE_DIALOG) { 473 views::View* static_text_child = FindDescendantWithAccessibleRole( 474 parent, ui::AX_ROLE_STATIC_TEXT); 475 if (static_text_child) { 476 ui::AXViewState state; 477 static_text_child->GetAccessibleState(&state); 478 if (!state.name.empty()) 479 return base::UTF16ToUTF8(state.name); 480 } 481 return std::string(); 482 } 483 } 484 485 return std::string(); 486} 487 488// static 489views::View* AccessibilityEventRouterViews::FindDescendantWithAccessibleRole( 490 views::View* view, ui::AXRole role) { 491 ui::AXViewState state; 492 view->GetAccessibleState(&state); 493 if (state.role == role) 494 return view; 495 496 for (int i = 0; i < view->child_count(); i++) { 497 views::View* child = view->child_at(i); 498 views::View* result = FindDescendantWithAccessibleRole(child, role); 499 if (result) 500 return result; 501 } 502 503 return NULL; 504} 505 506// static 507void AccessibilityEventRouterViews::RecursiveGetMenuItemIndexAndCount( 508 views::View* menu, 509 views::View* item, 510 int* index, 511 int* count) { 512 for (int i = 0; i < menu->child_count(); ++i) { 513 views::View* child = menu->child_at(i); 514 if (!child->visible()) 515 continue; 516 517 int previous_count = *count; 518 RecursiveGetMenuItemIndexAndCount(child, item, index, count); 519 ui::AXViewState state; 520 child->GetAccessibleState(&state); 521 if (state.role == ui::AX_ROLE_MENU_ITEM && 522 *count == previous_count) { 523 if (item == child) 524 *index = *count; 525 (*count)++; 526 } else if (state.role == ui::AX_ROLE_BUTTON) { 527 if (item == child) 528 *index = *count; 529 (*count)++; 530 } 531 } 532} 533 534// static 535std::string AccessibilityEventRouterViews::RecursiveGetStaticText( 536 views::View* view) { 537 ui::AXViewState state; 538 view->GetAccessibleState(&state); 539 if (state.role == ui::AX_ROLE_STATIC_TEXT) 540 return base::UTF16ToUTF8(state.name); 541 542 for (int i = 0; i < view->child_count(); ++i) { 543 views::View* child = view->child_at(i); 544 std::string result = RecursiveGetStaticText(child); 545 if (!result.empty()) 546 return result; 547 } 548 return std::string(); 549} 550