accessibility_event_router_views.cc revision f2477e01787aa58f445919b809d89e252beef54f
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/base/accessibility/accessible_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/focus/view_storage.h" 22#include "ui/views/view.h" 23#include "ui/views/widget/widget.h" 24 25using views::FocusManager; 26 27AccessibilityEventRouterViews::AccessibilityEventRouterViews() 28 : most_recent_profile_(NULL) { 29 // Register for notification when profile is destroyed to ensure that all 30 // observers are detatched at that time. 31 registrar_.Add(this, chrome::NOTIFICATION_PROFILE_DESTROYED, 32 content::NotificationService::AllSources()); 33} 34 35AccessibilityEventRouterViews::~AccessibilityEventRouterViews() { 36} 37 38// static 39AccessibilityEventRouterViews* AccessibilityEventRouterViews::GetInstance() { 40 return Singleton<AccessibilityEventRouterViews>::get(); 41} 42 43void AccessibilityEventRouterViews::HandleAccessibilityEvent( 44 views::View* view, ui::AccessibilityTypes::Event event_type) { 45 if (!ExtensionAccessibilityEventRouter::GetInstance()-> 46 IsAccessibilityEnabled()) { 47 return; 48 } 49 50 if (event_type == ui::AccessibilityTypes::EVENT_TEXT_CHANGED || 51 event_type == ui::AccessibilityTypes::EVENT_SELECTION_CHANGED) { 52 // These two events should only be sent for views that have focus. This 53 // enforces the invariant that we fire events triggered by user action and 54 // not by programmatic logic. For example, the location bar can be updated 55 // by javascript while the user focus is within some other part of the 56 // user interface. In contrast, the other supported events here do not 57 // depend on focus. For example, a menu within a menubar can open or close 58 // while focus is within the location bar or anywhere else as a result of 59 // user action. Note that the below logic can at some point be removed if 60 // we pass more information along to the listener such as focused state. 61 if (!view->GetFocusManager() || 62 view->GetFocusManager()->GetFocusedView() != view) 63 return; 64 } 65 66 // Don't dispatch the accessibility event until the next time through the 67 // event loop, to handle cases where the view's state changes after 68 // the call to post the event. It's safe to use base::Unretained(this) 69 // because AccessibilityEventRouterViews is a singleton. 70 views::ViewStorage* view_storage = views::ViewStorage::GetInstance(); 71 int view_storage_id = view_storage->CreateStorageID(); 72 view_storage->StoreView(view_storage_id, view); 73 base::MessageLoop::current()->PostTask( 74 FROM_HERE, 75 base::Bind( 76 &AccessibilityEventRouterViews::DispatchEventOnViewStorageId, 77 view_storage_id, 78 event_type)); 79} 80 81void AccessibilityEventRouterViews::HandleMenuItemFocused( 82 const string16& menu_name, 83 const string16& menu_item_name, 84 int item_index, 85 int item_count, 86 bool has_submenu) { 87 if (!ExtensionAccessibilityEventRouter::GetInstance()-> 88 IsAccessibilityEnabled()) { 89 return; 90 } 91 92 if (!most_recent_profile_) 93 return; 94 95 AccessibilityMenuItemInfo info(most_recent_profile_, 96 UTF16ToUTF8(menu_item_name), 97 UTF16ToUTF8(menu_name), 98 has_submenu, 99 item_index, 100 item_count); 101 SendControlAccessibilityNotification( 102 ui::AccessibilityTypes::EVENT_FOCUS, &info); 103} 104 105void AccessibilityEventRouterViews::Observe( 106 int type, 107 const content::NotificationSource& source, 108 const content::NotificationDetails& details) { 109 DCHECK_EQ(type, chrome::NOTIFICATION_PROFILE_DESTROYED); 110 Profile* profile = content::Source<Profile>(source).ptr(); 111 if (profile == most_recent_profile_) 112 most_recent_profile_ = NULL; 113} 114 115// 116// Private methods 117// 118 119void AccessibilityEventRouterViews::DispatchEventOnViewStorageId( 120 int view_storage_id, 121 ui::AccessibilityTypes::Event type) { 122 views::ViewStorage* view_storage = views::ViewStorage::GetInstance(); 123 views::View* view = view_storage->RetrieveView(view_storage_id); 124 view_storage->RemoveView(view_storage_id); 125 if (!view) 126 return; 127 128 AccessibilityEventRouterViews* instance = 129 AccessibilityEventRouterViews::GetInstance(); 130 instance->DispatchAccessibilityEvent(view, type); 131} 132 133void AccessibilityEventRouterViews::DispatchAccessibilityEvent( 134 views::View* view, ui::AccessibilityTypes::Event type) { 135 // Get the profile associated with this view. If it's not found, use 136 // the most recent profile where accessibility events were sent, or 137 // the default profile. 138 Profile* profile = NULL; 139 views::Widget* widget = view->GetWidget(); 140 if (widget) { 141 profile = reinterpret_cast<Profile*>( 142 widget->GetNativeWindowProperty(Profile::kProfileKey)); 143 } 144 if (!profile) 145 profile = most_recent_profile_; 146 if (!profile) { 147 if (g_browser_process->profile_manager()) 148 profile = g_browser_process->profile_manager()->GetLastUsedProfile(); 149 } 150 if (!profile) { 151 LOG(WARNING) << "Accessibility notification but no profile"; 152 return; 153 } 154 155 most_recent_profile_ = profile; 156 157 if (type == ui::AccessibilityTypes::EVENT_MENUSTART || 158 type == ui::AccessibilityTypes::EVENT_MENUPOPUPSTART || 159 type == ui::AccessibilityTypes::EVENT_MENUEND || 160 type == ui::AccessibilityTypes::EVENT_MENUPOPUPEND) { 161 SendMenuNotification(view, type, profile); 162 return; 163 } 164 165 ui::AccessibleViewState state; 166 view->GetAccessibleState(&state); 167 168 if (type == ui::AccessibilityTypes::EVENT_ALERT && 169 !(state.role == ui::AccessibilityTypes::ROLE_ALERT || 170 state.role == ui::AccessibilityTypes::ROLE_WINDOW)) { 171 SendAlertControlNotification(view, type, profile); 172 return; 173 } 174 175 switch (state.role) { 176 case ui::AccessibilityTypes::ROLE_ALERT: 177 case ui::AccessibilityTypes::ROLE_DIALOG: 178 case ui::AccessibilityTypes::ROLE_WINDOW: 179 SendWindowNotification(view, type, profile); 180 break; 181 case ui::AccessibilityTypes::ROLE_BUTTONMENU: 182 case ui::AccessibilityTypes::ROLE_MENUBAR: 183 case ui::AccessibilityTypes::ROLE_MENUPOPUP: 184 SendMenuNotification(view, type, profile); 185 break; 186 case ui::AccessibilityTypes::ROLE_BUTTONDROPDOWN: 187 case ui::AccessibilityTypes::ROLE_PUSHBUTTON: 188 SendButtonNotification(view, type, profile); 189 break; 190 case ui::AccessibilityTypes::ROLE_CHECKBUTTON: 191 SendCheckboxNotification(view, type, profile); 192 break; 193 case ui::AccessibilityTypes::ROLE_COMBOBOX: 194 SendComboboxNotification(view, type, profile); 195 break; 196 case ui::AccessibilityTypes::ROLE_LINK: 197 SendLinkNotification(view, type, profile); 198 break; 199 case ui::AccessibilityTypes::ROLE_LOCATION_BAR: 200 case ui::AccessibilityTypes::ROLE_TEXT: 201 SendTextfieldNotification(view, type, profile); 202 break; 203 case ui::AccessibilityTypes::ROLE_MENUITEM: 204 SendMenuItemNotification(view, type, profile); 205 break; 206 case ui::AccessibilityTypes::ROLE_RADIOBUTTON: 207 // Not used anymore? 208 case ui::AccessibilityTypes::ROLE_SLIDER: 209 SendSliderNotification(view, type, profile); 210 break; 211 default: 212 // If this is encountered, please file a bug with the role that wasn't 213 // caught so we can add accessibility extension API support. 214 NOTREACHED(); 215 } 216} 217 218// static 219void AccessibilityEventRouterViews::SendButtonNotification( 220 views::View* view, 221 ui::AccessibilityTypes::Event event, 222 Profile* profile) { 223 AccessibilityButtonInfo info( 224 profile, GetViewName(view), GetViewContext(view)); 225 SendControlAccessibilityNotification(event, &info); 226} 227 228// static 229void AccessibilityEventRouterViews::SendLinkNotification( 230 views::View* view, 231 ui::AccessibilityTypes::Event event, 232 Profile* profile) { 233 AccessibilityLinkInfo info(profile, GetViewName(view), GetViewContext(view)); 234 SendControlAccessibilityNotification(event, &info); 235} 236 237// static 238void AccessibilityEventRouterViews::SendMenuNotification( 239 views::View* view, 240 ui::AccessibilityTypes::Event event, 241 Profile* profile) { 242 AccessibilityMenuInfo info(profile, GetViewName(view)); 243 SendMenuAccessibilityNotification(event, &info); 244} 245 246// static 247void AccessibilityEventRouterViews::SendMenuItemNotification( 248 views::View* view, 249 ui::AccessibilityTypes::Event event, 250 Profile* profile) { 251 std::string name = GetViewName(view); 252 std::string context = GetViewContext(view); 253 254 bool has_submenu = false; 255 int index = -1; 256 int count = -1; 257 258 if (!strcmp(view->GetClassName(), views::MenuItemView::kViewClassName)) 259 has_submenu = static_cast<views::MenuItemView*>(view)->HasSubmenu(); 260 261 views::View* parent_menu = view->parent(); 262 while (parent_menu != NULL && strcmp(parent_menu->GetClassName(), 263 views::SubmenuView::kViewClassName)) { 264 parent_menu = parent_menu->parent(); 265 } 266 if (parent_menu) { 267 count = 0; 268 RecursiveGetMenuItemIndexAndCount(parent_menu, view, &index, &count); 269 } 270 271 AccessibilityMenuItemInfo info( 272 profile, name, context, has_submenu, index, count); 273 SendControlAccessibilityNotification(event, &info); 274} 275 276// static 277void AccessibilityEventRouterViews::SendTextfieldNotification( 278 views::View* view, 279 ui::AccessibilityTypes::Event event, 280 Profile* profile) { 281 ui::AccessibleViewState state; 282 view->GetAccessibleState(&state); 283 std::string name = UTF16ToUTF8(state.name); 284 std::string context = GetViewContext(view); 285 bool password = 286 (state.state & ui::AccessibilityTypes::STATE_PROTECTED) != 0; 287 AccessibilityTextBoxInfo info(profile, name, context, password); 288 std::string value = UTF16ToUTF8(state.value); 289 info.SetValue(value, state.selection_start, state.selection_end); 290 SendControlAccessibilityNotification(event, &info); 291} 292 293// static 294void AccessibilityEventRouterViews::SendComboboxNotification( 295 views::View* view, 296 ui::AccessibilityTypes::Event event, 297 Profile* profile) { 298 ui::AccessibleViewState state; 299 view->GetAccessibleState(&state); 300 std::string name = UTF16ToUTF8(state.name); 301 std::string value = UTF16ToUTF8(state.value); 302 std::string context = GetViewContext(view); 303 AccessibilityComboBoxInfo info( 304 profile, name, context, value, state.index, state.count); 305 SendControlAccessibilityNotification(event, &info); 306} 307 308// static 309void AccessibilityEventRouterViews::SendCheckboxNotification( 310 views::View* view, 311 ui::AccessibilityTypes::Event event, 312 Profile* profile) { 313 ui::AccessibleViewState state; 314 view->GetAccessibleState(&state); 315 std::string name = UTF16ToUTF8(state.name); 316 std::string context = GetViewContext(view); 317 AccessibilityCheckboxInfo info( 318 profile, 319 name, 320 context, 321 state.state == ui::AccessibilityTypes::STATE_CHECKED); 322 SendControlAccessibilityNotification(event, &info); 323} 324 325// static 326void AccessibilityEventRouterViews::SendWindowNotification( 327 views::View* view, 328 ui::AccessibilityTypes::Event event, 329 Profile* profile) { 330 ui::AccessibleViewState state; 331 view->GetAccessibleState(&state); 332 std::string window_text; 333 334 // If it's an alert, try to get the text from the contents of the 335 // static text, not the window title. 336 if (state.role == ui::AccessibilityTypes::ROLE_ALERT) 337 window_text = RecursiveGetStaticText(view); 338 339 // Otherwise get it from the window's accessible name. 340 if (window_text.empty()) 341 window_text = UTF16ToUTF8(state.name); 342 343 AccessibilityWindowInfo info(profile, window_text); 344 SendWindowAccessibilityNotification(event, &info); 345} 346 347// static 348void AccessibilityEventRouterViews::SendSliderNotification( 349 views::View* view, 350 ui::AccessibilityTypes::Event event, 351 Profile* profile) { 352 ui::AccessibleViewState state; 353 view->GetAccessibleState(&state); 354 355 std::string name = UTF16ToUTF8(state.name); 356 std::string value = UTF16ToUTF8(state.value); 357 std::string context = GetViewContext(view); 358 AccessibilitySliderInfo info( 359 profile, 360 name, 361 context, 362 value); 363 SendControlAccessibilityNotification(event, &info); 364} 365 366// static 367void AccessibilityEventRouterViews::SendAlertControlNotification( 368 views::View* view, 369 ui::AccessibilityTypes::Event event, 370 Profile* profile) { 371 ui::AccessibleViewState state; 372 view->GetAccessibleState(&state); 373 374 std::string name = UTF16ToUTF8(state.name); 375 AccessibilityAlertInfo info( 376 profile, 377 name); 378 SendControlAccessibilityNotification(event, &info); 379} 380 381// static 382std::string AccessibilityEventRouterViews::GetViewName(views::View* view) { 383 ui::AccessibleViewState state; 384 view->GetAccessibleState(&state); 385 return UTF16ToUTF8(state.name); 386} 387 388// static 389std::string AccessibilityEventRouterViews::GetViewContext(views::View* view) { 390 for (views::View* parent = view->parent(); 391 parent; 392 parent = parent->parent()) { 393 ui::AccessibleViewState state; 394 parent->GetAccessibleState(&state); 395 396 // Two cases are handled right now. More could be added in the future 397 // depending on how the UI evolves. 398 399 // A control inside of alert, toolbar or dialog should use that container's 400 // accessible name. 401 if ((state.role == ui::AccessibilityTypes::ROLE_ALERT || 402 state.role == ui::AccessibilityTypes::ROLE_DIALOG || 403 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 429views::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 447void AccessibilityEventRouterViews::RecursiveGetMenuItemIndexAndCount( 448 views::View* menu, 449 views::View* item, 450 int* index, 451 int* count) { 452 for (int i = 0; i < menu->child_count(); ++i) { 453 views::View* child = menu->child_at(i); 454 if (!child->visible()) 455 continue; 456 457 int previous_count = *count; 458 RecursiveGetMenuItemIndexAndCount(child, item, index, count); 459 ui::AccessibleViewState state; 460 child->GetAccessibleState(&state); 461 if (state.role == ui::AccessibilityTypes::ROLE_MENUITEM && 462 *count == previous_count) { 463 if (item == child) 464 *index = *count; 465 (*count)++; 466 } else if (state.role == ui::AccessibilityTypes::ROLE_PUSHBUTTON) { 467 if (item == child) 468 *index = *count; 469 (*count)++; 470 } 471 } 472} 473 474// static 475std::string AccessibilityEventRouterViews::RecursiveGetStaticText( 476 views::View* view) { 477 ui::AccessibleViewState state; 478 view->GetAccessibleState(&state); 479 if (state.role == ui::AccessibilityTypes::ROLE_STATICTEXT) 480 return UTF16ToUTF8(state.name); 481 482 for (int i = 0; i < view->child_count(); ++i) { 483 views::View* child = view->child_at(i); 484 std::string result = RecursiveGetStaticText(child); 485 if (!result.empty()) 486 return result; 487 } 488 return std::string(); 489} 490