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