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 "ash/system/tray_accessibility.h" 6 7 #include "ash/accessibility_delegate.h" 8 #include "ash/metrics/user_metrics_recorder.h" 9 #include "ash/shell.h" 10 #include "ash/system/tray/hover_highlight_view.h" 11 #include "ash/system/tray/system_tray.h" 12 #include "ash/system/tray/system_tray_delegate.h" 13 #include "ash/system/tray/system_tray_notifier.h" 14 #include "ash/system/tray/tray_constants.h" 15 #include "ash/system/tray/tray_details_view.h" 16 #include "ash/system/tray/tray_item_more.h" 17 #include "ash/system/tray/tray_popup_label_button.h" 18 #include "base/strings/utf_string_conversions.h" 19 #include "grit/ash_resources.h" 20 #include "grit/ash_strings.h" 21 #include "ui/base/l10n/l10n_util.h" 22 #include "ui/base/resource/resource_bundle.h" 23 #include "ui/gfx/image/image.h" 24 #include "ui/views/controls/image_view.h" 25 #include "ui/views/controls/label.h" 26 #include "ui/views/layout/box_layout.h" 27 #include "ui/views/widget/widget.h" 28 29 namespace ash { 30 namespace { 31 32 enum AccessibilityState { 33 A11Y_NONE = 0, 34 A11Y_SPOKEN_FEEDBACK = 1 << 0, 35 A11Y_HIGH_CONTRAST = 1 << 1, 36 A11Y_SCREEN_MAGNIFIER = 1 << 2, 37 A11Y_LARGE_CURSOR = 1 << 3, 38 A11Y_AUTOCLICK = 1 << 4, 39 A11Y_VIRTUAL_KEYBOARD = 1 << 5, 40 A11Y_BRAILLE_DISPLAY_CONNECTED = 1 << 6, 41 }; 42 43 uint32 GetAccessibilityState() { 44 AccessibilityDelegate* delegate = 45 Shell::GetInstance()->accessibility_delegate(); 46 uint32 state = A11Y_NONE; 47 if (delegate->IsSpokenFeedbackEnabled()) 48 state |= A11Y_SPOKEN_FEEDBACK; 49 if (delegate->IsHighContrastEnabled()) 50 state |= A11Y_HIGH_CONTRAST; 51 if (delegate->IsMagnifierEnabled()) 52 state |= A11Y_SCREEN_MAGNIFIER; 53 if (delegate->IsLargeCursorEnabled()) 54 state |= A11Y_LARGE_CURSOR; 55 if (delegate->IsAutoclickEnabled()) 56 state |= A11Y_AUTOCLICK; 57 if (delegate->IsVirtualKeyboardEnabled()) 58 state |= A11Y_VIRTUAL_KEYBOARD; 59 if (delegate->IsBrailleDisplayConnected()) 60 state |= A11Y_BRAILLE_DISPLAY_CONNECTED; 61 return state; 62 } 63 64 user::LoginStatus GetCurrentLoginStatus() { 65 return Shell::GetInstance()->system_tray_delegate()->GetUserLoginStatus(); 66 } 67 68 } // namespace 69 70 namespace tray { 71 72 class DefaultAccessibilityView : public TrayItemMore { 73 public: 74 explicit DefaultAccessibilityView(SystemTrayItem* owner) 75 : TrayItemMore(owner, true) { 76 ui::ResourceBundle& bundle = ui::ResourceBundle::GetSharedInstance(); 77 SetImage(bundle.GetImageNamed(IDR_AURA_UBER_TRAY_ACCESSIBILITY_DARK). 78 ToImageSkia()); 79 base::string16 label = bundle.GetLocalizedString( 80 IDS_ASH_STATUS_TRAY_ACCESSIBILITY); 81 SetLabel(label); 82 SetAccessibleName(label); 83 set_id(test::kAccessibilityTrayItemViewId); 84 } 85 86 virtual ~DefaultAccessibilityView() { 87 } 88 89 private: 90 DISALLOW_COPY_AND_ASSIGN(DefaultAccessibilityView); 91 }; 92 93 //////////////////////////////////////////////////////////////////////////////// 94 // ash::tray::AccessibilityPopupView 95 96 AccessibilityPopupView::AccessibilityPopupView(SystemTrayItem* owner, 97 uint32 enabled_state_bits) 98 : TrayNotificationView(owner, IDR_AURA_UBER_TRAY_ACCESSIBILITY_DARK), 99 label_(CreateLabel(enabled_state_bits)) { 100 InitView(label_); 101 } 102 103 views::Label* AccessibilityPopupView::CreateLabel(uint32 enabled_state_bits) { 104 DCHECK((enabled_state_bits & 105 (A11Y_SPOKEN_FEEDBACK | A11Y_BRAILLE_DISPLAY_CONNECTED)) != 0); 106 base::string16 text; 107 if (enabled_state_bits & A11Y_BRAILLE_DISPLAY_CONNECTED) { 108 text.append(l10n_util::GetStringUTF16( 109 IDS_ASH_STATUS_TRAY_BRAILLE_DISPLAY_CONNECTED_BUBBLE)); 110 } 111 if (enabled_state_bits & A11Y_SPOKEN_FEEDBACK) { 112 if (!text.empty()) 113 text.append(base::ASCIIToUTF16(" ")); 114 text.append(l10n_util::GetStringUTF16( 115 IDS_ASH_STATUS_TRAY_SPOKEN_FEEDBACK_ENABLED_BUBBLE)); 116 } 117 views::Label* label = new views::Label(text); 118 label->SetMultiLine(true); 119 label->SetHorizontalAlignment(gfx::ALIGN_LEFT); 120 return label; 121 } 122 123 //////////////////////////////////////////////////////////////////////////////// 124 // ash::tray::AccessibilityDetailedView 125 126 AccessibilityDetailedView::AccessibilityDetailedView( 127 SystemTrayItem* owner, user::LoginStatus login) : 128 TrayDetailsView(owner), 129 spoken_feedback_view_(NULL), 130 high_contrast_view_(NULL), 131 screen_magnifier_view_(NULL), 132 large_cursor_view_(NULL), 133 help_view_(NULL), 134 settings_view_(NULL), 135 autoclick_view_(NULL), 136 virtual_keyboard_view_(NULL), 137 spoken_feedback_enabled_(false), 138 high_contrast_enabled_(false), 139 screen_magnifier_enabled_(false), 140 large_cursor_enabled_(false), 141 autoclick_enabled_(false), 142 virtual_keyboard_enabled_(false), 143 login_(login) { 144 145 Reset(); 146 147 AppendAccessibilityList(); 148 AppendHelpEntries(); 149 CreateSpecialRow(IDS_ASH_STATUS_TRAY_ACCESSIBILITY_TITLE, this); 150 151 Layout(); 152 } 153 154 void AccessibilityDetailedView::AppendAccessibilityList() { 155 CreateScrollableList(); 156 ui::ResourceBundle& bundle = ui::ResourceBundle::GetSharedInstance(); 157 158 AccessibilityDelegate* delegate = 159 Shell::GetInstance()->accessibility_delegate(); 160 spoken_feedback_enabled_ = delegate->IsSpokenFeedbackEnabled(); 161 spoken_feedback_view_ = AddScrollListItem( 162 bundle.GetLocalizedString( 163 IDS_ASH_STATUS_TRAY_ACCESSIBILITY_SPOKEN_FEEDBACK), 164 spoken_feedback_enabled_ ? gfx::Font::BOLD : gfx::Font::NORMAL, 165 spoken_feedback_enabled_); 166 167 // Large Cursor item is shown only in Login screen. 168 if (login_ == user::LOGGED_IN_NONE) { 169 large_cursor_enabled_ = delegate->IsLargeCursorEnabled(); 170 large_cursor_view_ = AddScrollListItem( 171 bundle.GetLocalizedString( 172 IDS_ASH_STATUS_TRAY_ACCESSIBILITY_LARGE_CURSOR), 173 large_cursor_enabled_ ? gfx::Font::BOLD : gfx::Font::NORMAL, 174 large_cursor_enabled_); 175 } 176 177 high_contrast_enabled_ = delegate->IsHighContrastEnabled(); 178 high_contrast_view_ = AddScrollListItem( 179 bundle.GetLocalizedString( 180 IDS_ASH_STATUS_TRAY_ACCESSIBILITY_HIGH_CONTRAST_MODE), 181 high_contrast_enabled_ ? gfx::Font::BOLD : gfx::Font::NORMAL, 182 high_contrast_enabled_); 183 screen_magnifier_enabled_ = delegate->IsMagnifierEnabled(); 184 screen_magnifier_view_ = AddScrollListItem( 185 bundle.GetLocalizedString( 186 IDS_ASH_STATUS_TRAY_ACCESSIBILITY_SCREEN_MAGNIFIER), 187 screen_magnifier_enabled_ ? gfx::Font::BOLD : gfx::Font::NORMAL, 188 screen_magnifier_enabled_); 189 190 // Don't show autoclick option at login screen. 191 if (login_ != user::LOGGED_IN_NONE) { 192 autoclick_enabled_ = delegate->IsAutoclickEnabled(); 193 autoclick_view_ = AddScrollListItem( 194 bundle.GetLocalizedString( 195 IDS_ASH_STATUS_TRAY_ACCESSIBILITY_AUTOCLICK), 196 autoclick_enabled_ ? gfx::Font::BOLD : gfx::Font::NORMAL, 197 autoclick_enabled_); 198 } 199 200 virtual_keyboard_enabled_ = delegate->IsVirtualKeyboardEnabled(); 201 virtual_keyboard_view_ = AddScrollListItem( 202 bundle.GetLocalizedString( 203 IDS_ASH_STATUS_TRAY_ACCESSIBILITY_VIRTUAL_KEYBOARD), 204 virtual_keyboard_enabled_ ? gfx::Font::BOLD : gfx::Font::NORMAL, 205 virtual_keyboard_enabled_); 206 } 207 208 void AccessibilityDetailedView::AppendHelpEntries() { 209 // Currently the help page requires a browser window. 210 // TODO(yoshiki): show this even on login/lock screen. crbug.com/158286 211 if (login_ == user::LOGGED_IN_NONE || 212 login_ == user::LOGGED_IN_LOCKED) 213 return; 214 215 views::View* bottom_row = new View(); 216 views::BoxLayout* layout = new 217 views::BoxLayout(views::BoxLayout::kHorizontal, 218 kTrayMenuBottomRowPadding, 219 kTrayMenuBottomRowPadding, 220 kTrayMenuBottomRowPaddingBetweenItems); 221 layout->set_main_axis_alignment(views::BoxLayout::MAIN_AXIS_ALIGNMENT_FILL); 222 bottom_row->SetLayoutManager(layout); 223 224 ui::ResourceBundle& bundle = ui::ResourceBundle::GetSharedInstance(); 225 226 TrayPopupLabelButton* help = new TrayPopupLabelButton( 227 this, 228 bundle.GetLocalizedString( 229 IDS_ASH_STATUS_TRAY_ACCESSIBILITY_LEARN_MORE)); 230 bottom_row->AddChildView(help); 231 help_view_ = help; 232 233 TrayPopupLabelButton* settings = new TrayPopupLabelButton( 234 this, 235 bundle.GetLocalizedString( 236 IDS_ASH_STATUS_TRAY_ACCESSIBILITY_SETTINGS)); 237 bottom_row->AddChildView(settings); 238 settings_view_ = settings; 239 240 AddChildView(bottom_row); 241 } 242 243 HoverHighlightView* AccessibilityDetailedView::AddScrollListItem( 244 const base::string16& text, 245 gfx::Font::FontStyle style, 246 bool checked) { 247 HoverHighlightView* container = new HoverHighlightView(this); 248 container->AddCheckableLabel(text, style, checked); 249 scroll_content()->AddChildView(container); 250 return container; 251 } 252 253 void AccessibilityDetailedView::OnViewClicked(views::View* sender) { 254 AccessibilityDelegate* delegate = 255 Shell::GetInstance()->accessibility_delegate(); 256 if (sender == footer()->content()) { 257 TransitionToDefaultView(); 258 } else if (sender == spoken_feedback_view_) { 259 Shell::GetInstance()->metrics()->RecordUserMetricsAction( 260 delegate->IsSpokenFeedbackEnabled() ? 261 ash::UMA_STATUS_AREA_DISABLE_SPOKEN_FEEDBACK : 262 ash::UMA_STATUS_AREA_ENABLE_SPOKEN_FEEDBACK); 263 delegate->ToggleSpokenFeedback(ash::A11Y_NOTIFICATION_NONE); 264 } else if (sender == high_contrast_view_) { 265 Shell::GetInstance()->metrics()->RecordUserMetricsAction( 266 delegate->IsHighContrastEnabled() ? 267 ash::UMA_STATUS_AREA_DISABLE_HIGH_CONTRAST : 268 ash::UMA_STATUS_AREA_ENABLE_HIGH_CONTRAST); 269 delegate->ToggleHighContrast(); 270 } else if (sender == screen_magnifier_view_) { 271 Shell::GetInstance()->metrics()->RecordUserMetricsAction( 272 delegate->IsMagnifierEnabled() ? 273 ash::UMA_STATUS_AREA_DISABLE_MAGNIFIER : 274 ash::UMA_STATUS_AREA_ENABLE_MAGNIFIER); 275 delegate->SetMagnifierEnabled(!delegate->IsMagnifierEnabled()); 276 } else if (large_cursor_view_ && sender == large_cursor_view_) { 277 Shell::GetInstance()->metrics()->RecordUserMetricsAction( 278 delegate->IsLargeCursorEnabled() ? 279 ash::UMA_STATUS_AREA_DISABLE_LARGE_CURSOR : 280 ash::UMA_STATUS_AREA_ENABLE_LARGE_CURSOR); 281 delegate->SetLargeCursorEnabled(!delegate->IsLargeCursorEnabled()); 282 } else if (autoclick_view_ && sender == autoclick_view_) { 283 Shell::GetInstance()->metrics()->RecordUserMetricsAction( 284 delegate->IsAutoclickEnabled() ? 285 ash::UMA_STATUS_AREA_DISABLE_AUTO_CLICK : 286 ash::UMA_STATUS_AREA_ENABLE_AUTO_CLICK); 287 delegate->SetAutoclickEnabled(!delegate->IsAutoclickEnabled()); 288 } else if (virtual_keyboard_view_ && sender == virtual_keyboard_view_) { 289 Shell::GetInstance()->metrics()->RecordUserMetricsAction( 290 delegate->IsVirtualKeyboardEnabled() ? 291 ash::UMA_STATUS_AREA_DISABLE_VIRTUAL_KEYBOARD : 292 ash::UMA_STATUS_AREA_ENABLE_VIRTUAL_KEYBOARD); 293 delegate->SetVirtualKeyboardEnabled(!delegate->IsVirtualKeyboardEnabled()); 294 } 295 } 296 297 void AccessibilityDetailedView::ButtonPressed(views::Button* sender, 298 const ui::Event& event) { 299 SystemTrayDelegate* tray_delegate = 300 Shell::GetInstance()->system_tray_delegate(); 301 if (sender == help_view_) 302 tray_delegate->ShowAccessibilityHelp(); 303 else if (sender == settings_view_) 304 tray_delegate->ShowAccessibilitySettings(); 305 } 306 307 } // namespace tray 308 309 //////////////////////////////////////////////////////////////////////////////// 310 // ash::TrayAccessibility 311 312 TrayAccessibility::TrayAccessibility(SystemTray* system_tray) 313 : TrayImageItem(system_tray, IDR_AURA_UBER_TRAY_ACCESSIBILITY), 314 default_(NULL), 315 detailed_popup_(NULL), 316 detailed_menu_(NULL), 317 request_popup_view_state_(A11Y_NONE), 318 tray_icon_visible_(false), 319 login_(GetCurrentLoginStatus()), 320 previous_accessibility_state_(GetAccessibilityState()), 321 show_a11y_menu_on_lock_screen_(true) { 322 DCHECK(Shell::GetInstance()->delegate()); 323 DCHECK(system_tray); 324 Shell::GetInstance()->system_tray_notifier()->AddAccessibilityObserver(this); 325 } 326 327 TrayAccessibility::~TrayAccessibility() { 328 Shell::GetInstance()->system_tray_notifier()-> 329 RemoveAccessibilityObserver(this); 330 } 331 332 void TrayAccessibility::SetTrayIconVisible(bool visible) { 333 if (tray_view()) 334 tray_view()->SetVisible(visible); 335 tray_icon_visible_ = visible; 336 } 337 338 tray::AccessibilityDetailedView* TrayAccessibility::CreateDetailedMenu() { 339 return new tray::AccessibilityDetailedView(this, login_); 340 } 341 342 bool TrayAccessibility::GetInitialVisibility() { 343 // Shows accessibility icon if any accessibility feature is enabled. 344 // Otherwise, doen't show it. 345 return GetAccessibilityState() != A11Y_NONE; 346 } 347 348 views::View* TrayAccessibility::CreateDefaultView(user::LoginStatus status) { 349 CHECK(default_ == NULL); 350 351 // Shows accessibility menu if: 352 // - on login screen (not logged in); 353 // - "Enable accessibility menu" on chrome://settings is checked; 354 // - or any of accessibility features is enabled 355 // Otherwise, not shows it. 356 AccessibilityDelegate* delegate = 357 Shell::GetInstance()->accessibility_delegate(); 358 if (login_ != user::LOGGED_IN_NONE && 359 !delegate->ShouldShowAccessibilityMenu() && 360 // On login screen, keeps the initial visibility of the menu. 361 (status != user::LOGGED_IN_LOCKED || !show_a11y_menu_on_lock_screen_)) 362 return NULL; 363 364 CHECK(default_ == NULL); 365 default_ = new tray::DefaultAccessibilityView(this); 366 367 return default_; 368 } 369 370 views::View* TrayAccessibility::CreateDetailedView(user::LoginStatus status) { 371 CHECK(detailed_popup_ == NULL); 372 CHECK(detailed_menu_ == NULL); 373 374 if (request_popup_view_state_) { 375 detailed_popup_ = 376 new tray::AccessibilityPopupView(this, request_popup_view_state_); 377 request_popup_view_state_ = A11Y_NONE; 378 return detailed_popup_; 379 } else { 380 Shell::GetInstance()->metrics()->RecordUserMetricsAction( 381 ash::UMA_STATUS_AREA_DETAILED_ACCESSABILITY); 382 detailed_menu_ = CreateDetailedMenu(); 383 return detailed_menu_; 384 } 385 } 386 387 void TrayAccessibility::DestroyDefaultView() { 388 default_ = NULL; 389 } 390 391 void TrayAccessibility::DestroyDetailedView() { 392 detailed_popup_ = NULL; 393 detailed_menu_ = NULL; 394 } 395 396 void TrayAccessibility::UpdateAfterLoginStatusChange(user::LoginStatus status) { 397 // Stores the a11y feature status on just entering the lock screen. 398 if (login_ != user::LOGGED_IN_LOCKED && status == user::LOGGED_IN_LOCKED) 399 show_a11y_menu_on_lock_screen_ = (GetAccessibilityState() != A11Y_NONE); 400 401 login_ = status; 402 SetTrayIconVisible(GetInitialVisibility()); 403 } 404 405 void TrayAccessibility::OnAccessibilityModeChanged( 406 AccessibilityNotificationVisibility notify) { 407 SetTrayIconVisible(GetInitialVisibility()); 408 409 uint32 accessibility_state = GetAccessibilityState(); 410 // We'll get an extra notification if a braille display is connected when 411 // spoken feedback wasn't already enabled. This is because the braille 412 // connection state is already updated when spoken feedback is enabled so 413 // that the notifications can be consolidated into one. Therefore, we 414 // return early if there's no change in the state that we keep track of. 415 if (accessibility_state == previous_accessibility_state_) 416 return; 417 // Contains bits for spoken feedback and braille display connected currently 418 // being enabled. 419 uint32 being_enabled = 420 (accessibility_state & ~previous_accessibility_state_) & 421 (A11Y_SPOKEN_FEEDBACK | A11Y_BRAILLE_DISPLAY_CONNECTED); 422 if ((notify == ash::A11Y_NOTIFICATION_SHOW) && being_enabled != A11Y_NONE) { 423 // Shows popup if |notify| is true and the spoken feedback is being enabled. 424 request_popup_view_state_ = being_enabled; 425 PopupDetailedView(kTrayPopupAutoCloseDelayForTextInSeconds, false); 426 } else { 427 if (detailed_popup_) 428 detailed_popup_->GetWidget()->Close(); 429 if (detailed_menu_) 430 detailed_menu_->GetWidget()->Close(); 431 } 432 433 previous_accessibility_state_ = accessibility_state; 434 } 435 436 } // namespace ash 437