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
AccessibilityEventRouterViews()28 AccessibilityEventRouterViews::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
~AccessibilityEventRouterViews()36 AccessibilityEventRouterViews::~AccessibilityEventRouterViews() {
37 }
38
39 // static
GetInstance()40 AccessibilityEventRouterViews* AccessibilityEventRouterViews::GetInstance() {
41 return Singleton<AccessibilityEventRouterViews>::get();
42 }
43
HandleAccessibilityEvent(views::View * view,ui::AXEvent event_type)44 void 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
HandleMenuItemFocused(const base::string16 & menu_name,const base::string16 & menu_item_name,int item_index,int item_count,bool has_submenu)82 void 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
Observe(int type,const content::NotificationSource & source,const content::NotificationDetails & details)106 void 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
DispatchEventOnViewStorageId(int view_storage_id,ui::AXEvent type)120 void 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
DispatchAccessibilityEvent(views::View * view,ui::AXEvent type)134 void 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
SendButtonNotification(views::View * view,ui::AXEvent event,Profile * profile)231 void 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
SendLinkNotification(views::View * view,ui::AXEvent event,Profile * profile)241 void 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
SendMenuNotification(views::View * view,ui::AXEvent event,Profile * profile)250 void 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
SendMenuItemNotification(views::View * view,ui::AXEvent event,Profile * profile)259 void 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
SendTreeNotification(views::View * view,ui::AXEvent event,Profile * profile)289 void 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
SendTreeItemNotification(views::View * view,ui::AXEvent event,Profile * profile)298 void 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
SendTextfieldNotification(views::View * view,ui::AXEvent event,Profile * profile)343 void 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
SendComboboxNotification(views::View * view,ui::AXEvent event,Profile * profile)359 void 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
SendCheckboxNotification(views::View * view,ui::AXEvent event,Profile * profile)374 void 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
SendWindowNotification(views::View * view,ui::AXEvent event,Profile * profile)391 void 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
SendSliderNotification(views::View * view,ui::AXEvent event,Profile * profile)413 void 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
SendAlertControlNotification(views::View * view,ui::AXEvent event,Profile * profile)432 void 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
GetViewName(views::View * view)447 std::string AccessibilityEventRouterViews::GetViewName(views::View* view) {
448 ui::AXViewState state;
449 view->GetAccessibleState(&state);
450 return base::UTF16ToUTF8(state.name);
451 }
452
453 // static
GetViewContext(views::View * view)454 std::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
FindDescendantWithAccessibleRole(views::View * view,ui::AXRole role)494 views::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
RecursiveGetMenuItemIndexAndCount(views::View * menu,views::View * item,int * index,int * count)512 void 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
RecursiveGetStaticText(views::View * view)540 std::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