1// Copyright 2014 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/chromeos/ui/idle_app_name_notification_view.h"
6
7#include <string>
8
9#include "ash/shell.h"
10#include "ash/shell_delegate.h"
11#include "ash/shell_window_ids.h"
12#include "ash/wm/window_animations.h"
13#include "base/strings/string_util.h"
14#include "base/strings/utf_string_conversions.h"
15#include "base/time/time.h"
16#include "base/timer/timer.h"
17#include "chrome/grit/generated_resources.h"
18#include "extensions/common/extension.h"
19#include "ui/accessibility/ax_view_state.h"
20#include "ui/aura/window.h"
21#include "ui/base/l10n/l10n_util.h"
22#include "ui/base/resource/resource_bundle.h"
23#include "ui/compositor/layer_animation_observer.h"
24#include "ui/compositor/scoped_layer_animation_settings.h"
25#include "ui/gfx/canvas.h"
26#include "ui/gfx/font_list.h"
27#include "ui/gfx/text_utils.h"
28#include "ui/views/controls/label.h"
29#include "ui/views/layout/box_layout.h"
30#include "ui/views/layout/fill_layout.h"
31#include "ui/views/view.h"
32#include "ui/views/widget/widget.h"
33#include "ui/views/widget/widget_delegate.h"
34
35namespace ui {
36class LayerAnimationSequence;
37}
38
39namespace chromeos {
40namespace {
41
42// Color of the text of the warning message.
43const SkColor kTextColor = SK_ColorBLACK;
44
45// Color of the text of the warning message.
46const SkColor kErrorTextColor = SK_ColorRED;
47
48// Color of the window background.
49const SkColor kWindowBackgroundColor = SK_ColorWHITE;
50
51// Radius of the rounded corners of the window.
52const int kWindowCornerRadius = 4;
53
54// Creates and shows the message widget for |view| with |animation_time_ms|.
55void CreateAndShowWidgetWithContent(views::WidgetDelegate* delegate,
56                                    views::View* view,
57                                    int animation_time_ms) {
58  aura::Window* root_window = ash::Shell::GetTargetRootWindow();
59  gfx::Size rs = root_window->bounds().size();
60  gfx::Size ps = view->GetPreferredSize();
61  gfx::Rect bounds((rs.width() - ps.width()) / 2,
62                   -ps.height(),
63                   ps.width(),
64                   ps.height());
65  views::Widget::InitParams params;
66  params.type = views::Widget::InitParams::TYPE_POPUP;
67  params.opacity = views::Widget::InitParams::TRANSLUCENT_WINDOW;
68  params.ownership = views::Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET;
69  params.accept_events = false;
70  params.keep_on_top = true;
71  params.remove_standard_frame = true;
72  params.delegate = delegate;
73  params.bounds = bounds;
74  params.parent = ash::Shell::GetContainer(
75      root_window, ash::kShellWindowId_SettingBubbleContainer);
76  views::Widget* widget = new views::Widget;
77  widget->Init(params);
78  widget->SetContentsView(view);
79  gfx::NativeView native_view = widget->GetNativeView();
80  native_view->SetName("KioskIdleAppNameNotification");
81
82  // Note: We cannot use the Window show/hide animations since they are disabled
83  // for kiosk by command line.
84  ui::LayerAnimator* animator = new ui::LayerAnimator(
85          base::TimeDelta::FromMilliseconds(animation_time_ms));
86  native_view->layer()->SetAnimator(animator);
87  widget->Show();
88
89  // We don't care about the show animation since it is off screen, so stop the
90  // started animation and move the message into view.
91  animator->StopAnimating();
92  bounds.set_y((rs.height() - ps.height()) / 20);
93  widget->SetBounds(bounds);
94
95  // Allow to use the message for spoken feedback.
96  view->NotifyAccessibilityEvent(ui::AX_EVENT_ALERT, true);
97}
98
99}  // namespace
100
101// The class which implements the content view for the message.
102class IdleAppNameNotificationDelegateView
103    : public views::WidgetDelegateView,
104      public ui::ImplicitAnimationObserver {
105 public:
106  // An idle message which will get shown from the caller and hides itself after
107  // a time, calling |owner->CloseMessage| to inform the owner that it got
108  // destroyed. The |app_name| is a string which gets used as message and
109  // |error| is true if something is not correct.
110  // |message_visibility_time_in_ms| ms's after creation the message will start
111  // to remove itself from the screen.
112  IdleAppNameNotificationDelegateView(IdleAppNameNotificationView *owner,
113                                      const base::string16& app_name,
114                                      bool error,
115                                      int message_visibility_time_in_ms)
116      : owner_(owner),
117        widget_closed_(false) {
118    ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
119    // Add the application name label to the message.
120    AddLabel(app_name,
121             rb.GetFontList(ui::ResourceBundle::BoldFont),
122             error ? kErrorTextColor : kTextColor);
123    spoken_text_ = app_name;
124    SetLayoutManager(new views::FillLayout);
125
126    // Set a timer which will trigger to remove the message after the given
127    // time.
128    hide_timer_.Start(
129        FROM_HERE,
130        base::TimeDelta::FromMilliseconds(message_visibility_time_in_ms),
131        this,
132        &IdleAppNameNotificationDelegateView::RemoveMessage);
133  }
134
135  virtual ~IdleAppNameNotificationDelegateView() {
136    // The widget is already closing, but the other cleanup items need to be
137    // performed.
138    widget_closed_ = true;
139    Close();
140  }
141
142  // Close the widget immediately. This can be called from the owner or from
143  // this class.
144  void Close() {
145    // Stop the timer (if it was running).
146    hide_timer_.Stop();
147    // Inform our owner that we are going away.
148    if (owner_) {
149      IdleAppNameNotificationView* owner = owner_;
150      owner_ = NULL;
151      owner->CloseMessage();
152    }
153    // Close the owning widget - if required.
154    if (!widget_closed_) {
155      widget_closed_ = true;
156      GetWidget()->Close();
157    }
158  }
159
160  // Animate the window away (and close once done).
161  void RemoveMessage() {
162    aura::Window* widget_view = GetWidget()->GetNativeView();
163    ui::Layer* layer = widget_view->layer();
164    ui::ScopedLayerAnimationSettings settings(layer->GetAnimator());
165    settings.AddObserver(this);
166    gfx::Rect rect = widget_view->bounds();
167    rect.set_y(-GetPreferredSize().height());
168    layer->SetBounds(rect);
169  }
170
171  virtual void OnPaint(gfx::Canvas* canvas) OVERRIDE {
172    SkPaint paint;
173    paint.setStyle(SkPaint::kFill_Style);
174    paint.setColor(kWindowBackgroundColor);
175    canvas->DrawRoundRect(GetLocalBounds(), kWindowCornerRadius, paint);
176    views::WidgetDelegateView::OnPaint(canvas);
177  }
178
179  virtual void GetAccessibleState(ui::AXViewState* state) OVERRIDE {
180    state->name = spoken_text_;
181    state->role = ui::AX_ROLE_ALERT;
182  }
183
184  // ImplicitAnimationObserver overrides
185  virtual void OnImplicitAnimationsCompleted() OVERRIDE {
186    Close();
187  }
188
189 private:
190  // Adds the label to the view, using |text| with a |font| and a |text_color|.
191  void AddLabel(const base::string16& text,
192                const gfx::FontList& font,
193                SkColor text_color) {
194    views::Label* label = new views::Label;
195    label->SetText(text);
196    label->SetHorizontalAlignment(gfx::ALIGN_CENTER);
197    label->SetFontList(font);
198    label->SetEnabledColor(text_color);
199    label->SetDisabledColor(text_color);
200    label->SetAutoColorReadabilityEnabled(false);
201    AddChildView(label);
202  }
203
204  // A timer which calls us to remove the message from the screen.
205  base::OneShotTimer<IdleAppNameNotificationDelegateView> hide_timer_;
206
207  // The owner of this message which needs to get notified when the message
208  // closes.
209  IdleAppNameNotificationView* owner_;
210
211  // The spoken text.
212  base::string16 spoken_text_;
213
214  // True if the widget got already closed.
215  bool widget_closed_;
216
217  DISALLOW_COPY_AND_ASSIGN(IdleAppNameNotificationDelegateView);
218};
219
220IdleAppNameNotificationView::IdleAppNameNotificationView(
221    int message_visibility_time_in_ms,
222    int animation_time_ms,
223    const extensions::Extension* extension)
224    : view_(NULL) {
225  ShowMessage(message_visibility_time_in_ms, animation_time_ms, extension);
226}
227
228IdleAppNameNotificationView::~IdleAppNameNotificationView() {
229  CloseMessage();
230}
231
232void IdleAppNameNotificationView::CloseMessage() {
233  if (view_) {
234    IdleAppNameNotificationDelegateView* view = view_;
235    view_ = NULL;
236    view->Close();
237  }
238}
239
240bool IdleAppNameNotificationView::IsVisible() {
241  return view_ != NULL;
242}
243
244base::string16 IdleAppNameNotificationView::GetShownTextForTest() {
245  ui::AXViewState state;
246  DCHECK(view_);
247  view_->GetAccessibleState(&state);
248  return state.name;
249}
250
251void IdleAppNameNotificationView::ShowMessage(
252    int message_visibility_time_in_ms,
253    int animation_time_ms,
254    const extensions::Extension* extension) {
255  DCHECK(!view_);
256
257  base::string16 app_name;
258  bool error = false;
259  if (extension &&
260      !base::ContainsOnlyChars(extension->name(), base::kWhitespaceASCII)) {
261    app_name = base::UTF8ToUTF16(extension->name());
262  } else {
263    error = true;
264    app_name = l10n_util::GetStringUTF16(
265        IDS_IDLE_APP_NAME_UNKNOWN_APPLICATION_NOTIFICATION);
266  }
267
268  view_ = new IdleAppNameNotificationDelegateView(
269      this,
270      app_name,
271      error,
272      message_visibility_time_in_ms + animation_time_ms);
273  CreateAndShowWidgetWithContent(view_, view_, animation_time_ms);
274}
275
276}  // namespace chromeos
277