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/system_tray_bubble.h"
6
7#include "ash/shell.h"
8#include "ash/system/tray/system_tray.h"
9#include "ash/system/tray/system_tray_delegate.h"
10#include "ash/system/tray/system_tray_item.h"
11#include "ash/system/tray/tray_bubble_wrapper.h"
12#include "ash/system/tray/tray_constants.h"
13#include "base/message_loop/message_loop.h"
14#include "ui/aura/window.h"
15#include "ui/compositor/layer.h"
16#include "ui/compositor/layer_animation_observer.h"
17#include "ui/compositor/scoped_layer_animation_settings.h"
18#include "ui/gfx/canvas.h"
19#include "ui/views/layout/box_layout.h"
20#include "ui/views/view.h"
21#include "ui/views/widget/widget.h"
22
23using views::TrayBubbleView;
24
25namespace ash {
26
27namespace {
28
29// Normally a detailed view is the same size as the default view. However,
30// when showing a detailed view directly (e.g. clicking on a notification),
31// we may not know the height of the default view, or the default view may
32// be too short, so we use this as a default and minimum height for any
33// detailed view.
34const int kDetailedBubbleMaxHeight = kTrayPopupItemHeight * 5;
35
36// Duration of swipe animation used when transitioning from a default to
37// detailed view or vice versa.
38const int kSwipeDelayMS = 150;
39
40// A view with some special behaviour for tray items in the popup:
41// - optionally changes background color on hover.
42class TrayPopupItemContainer : public views::View {
43 public:
44  TrayPopupItemContainer(views::View* view,
45                         bool change_background,
46                         bool draw_border)
47      : hover_(false),
48        change_background_(change_background) {
49    set_notify_enter_exit_on_child(true);
50    if (draw_border) {
51      SetBorder(
52          views::Border::CreateSolidSidedBorder(0, 0, 1, 0, kBorderLightColor));
53    }
54    views::BoxLayout* layout = new views::BoxLayout(
55        views::BoxLayout::kVertical, 0, 0, 0);
56    layout->set_main_axis_alignment(views::BoxLayout::MAIN_AXIS_ALIGNMENT_FILL);
57    SetLayoutManager(layout);
58    SetPaintToLayer(view->layer() != NULL);
59    if (view->layer())
60      SetFillsBoundsOpaquely(view->layer()->fills_bounds_opaquely());
61    AddChildView(view);
62    SetVisible(view->visible());
63  }
64
65  virtual ~TrayPopupItemContainer() {}
66
67 private:
68  // Overridden from views::View.
69  virtual void ChildVisibilityChanged(View* child) OVERRIDE {
70    if (visible() == child->visible())
71      return;
72    SetVisible(child->visible());
73    PreferredSizeChanged();
74  }
75
76  virtual void ChildPreferredSizeChanged(View* child) OVERRIDE {
77    PreferredSizeChanged();
78  }
79
80  virtual void OnMouseEntered(const ui::MouseEvent& event) OVERRIDE {
81    hover_ = true;
82    SchedulePaint();
83  }
84
85  virtual void OnMouseExited(const ui::MouseEvent& event) OVERRIDE {
86    hover_ = false;
87    SchedulePaint();
88  }
89
90  virtual void OnPaintBackground(gfx::Canvas* canvas) OVERRIDE {
91    if (child_count() == 0)
92      return;
93
94    views::View* view = child_at(0);
95    if (!view->background()) {
96      canvas->FillRect(gfx::Rect(size()), (hover_ && change_background_) ?
97          kHoverBackgroundColor : kBackgroundColor);
98    }
99  }
100
101  bool hover_;
102  bool change_background_;
103
104  DISALLOW_COPY_AND_ASSIGN(TrayPopupItemContainer);
105};
106
107// Implicit animation observer that deletes itself and the layer at the end of
108// the animation.
109class AnimationObserverDeleteLayer : public ui::ImplicitAnimationObserver {
110 public:
111  explicit AnimationObserverDeleteLayer(ui::Layer* layer)
112      : layer_(layer) {
113  }
114
115  virtual ~AnimationObserverDeleteLayer() {
116  }
117
118  virtual void OnImplicitAnimationsCompleted() OVERRIDE {
119    base::MessageLoopForUI::current()->DeleteSoon(FROM_HERE, this);
120  }
121
122 private:
123  scoped_ptr<ui::Layer> layer_;
124
125  DISALLOW_COPY_AND_ASSIGN(AnimationObserverDeleteLayer);
126};
127
128}  // namespace
129
130// SystemTrayBubble
131
132SystemTrayBubble::SystemTrayBubble(
133    ash::SystemTray* tray,
134    const std::vector<ash::SystemTrayItem*>& items,
135    BubbleType bubble_type)
136    : tray_(tray),
137      bubble_view_(NULL),
138      items_(items),
139      bubble_type_(bubble_type),
140      autoclose_delay_(0) {
141}
142
143SystemTrayBubble::~SystemTrayBubble() {
144  DestroyItemViews();
145  // Reset the host pointer in bubble_view_ in case its destruction is deferred.
146  if (bubble_view_)
147    bubble_view_->reset_delegate();
148}
149
150void SystemTrayBubble::UpdateView(
151    const std::vector<ash::SystemTrayItem*>& items,
152    BubbleType bubble_type) {
153  DCHECK(bubble_type != BUBBLE_TYPE_NOTIFICATION);
154
155  scoped_ptr<ui::Layer> scoped_layer;
156  if (bubble_type != bubble_type_) {
157    base::TimeDelta swipe_duration =
158        base::TimeDelta::FromMilliseconds(kSwipeDelayMS);
159    scoped_layer = bubble_view_->RecreateLayer();
160    // Keep the reference to layer as we need it after releasing it.
161    ui::Layer* layer = scoped_layer.get();
162    DCHECK(layer);
163    layer->SuppressPaint();
164
165    // When transitioning from detailed view to default view, animate the
166    // existing view (slide out towards the right).
167    if (bubble_type == BUBBLE_TYPE_DEFAULT) {
168      ui::ScopedLayerAnimationSettings settings(layer->GetAnimator());
169      settings.AddObserver(
170          new AnimationObserverDeleteLayer(scoped_layer.release()));
171      settings.SetTransitionDuration(swipe_duration);
172      settings.SetTweenType(gfx::Tween::EASE_OUT);
173      gfx::Transform transform;
174      transform.Translate(layer->bounds().width(), 0.0);
175      layer->SetTransform(transform);
176    }
177
178    {
179      // Add a shadow layer to make the old layer darker as the animation
180      // progresses.
181      ui::Layer* shadow = new ui::Layer(ui::LAYER_SOLID_COLOR);
182      shadow->SetColor(SK_ColorBLACK);
183      shadow->SetOpacity(0.01f);
184      shadow->SetBounds(layer->bounds());
185      layer->Add(shadow);
186      layer->StackAtTop(shadow);
187      {
188        // Animate the darkening effect a little longer than the swipe-in. This
189        // is to make sure the darkening animation does not end up finishing
190        // early, because the dark layer goes away at the end of the animation,
191        // and there is a brief moment when the old view is still visible, but
192        // it does not have the shadow layer on top.
193        ui::ScopedLayerAnimationSettings settings(shadow->GetAnimator());
194        settings.AddObserver(new AnimationObserverDeleteLayer(shadow));
195        settings.SetTransitionDuration(swipe_duration +
196                                       base::TimeDelta::FromMilliseconds(150));
197        settings.SetTweenType(gfx::Tween::LINEAR);
198        shadow->SetOpacity(0.15f);
199      }
200    }
201  }
202
203  DestroyItemViews();
204  bubble_view_->RemoveAllChildViews(true);
205
206  items_ = items;
207  bubble_type_ = bubble_type;
208  CreateItemViews(
209      Shell::GetInstance()->system_tray_delegate()->GetUserLoginStatus());
210
211  // Close bubble view if we failed to create the item view.
212  if (!bubble_view_->has_children()) {
213    Close();
214    return;
215  }
216
217  bubble_view_->GetWidget()->GetContentsView()->Layout();
218  // Make sure that the bubble is large enough for the default view.
219  if (bubble_type_ == BUBBLE_TYPE_DEFAULT) {
220    bubble_view_->SetMaxHeight(0);  // Clear max height limit.
221  }
222
223  if (scoped_layer) {
224    // When transitioning from default view to detailed view, animate the new
225    // view (slide in from the right).
226    if (bubble_type == BUBBLE_TYPE_DETAILED) {
227      ui::Layer* new_layer = bubble_view_->layer();
228
229      // Make sure the new layer is stacked above the old layer during the
230      // animation.
231      new_layer->parent()->StackAbove(new_layer, scoped_layer.get());
232
233      gfx::Rect bounds = new_layer->bounds();
234      gfx::Transform transform;
235      transform.Translate(bounds.width(), 0.0);
236      new_layer->SetTransform(transform);
237      {
238        ui::ScopedLayerAnimationSettings settings(new_layer->GetAnimator());
239        settings.AddObserver(
240            new AnimationObserverDeleteLayer(scoped_layer.release()));
241        settings.SetTransitionDuration(
242            base::TimeDelta::FromMilliseconds(kSwipeDelayMS));
243        settings.SetTweenType(gfx::Tween::EASE_OUT);
244        new_layer->SetTransform(gfx::Transform());
245      }
246    }
247  }
248}
249
250void SystemTrayBubble::InitView(views::View* anchor,
251                                user::LoginStatus login_status,
252                                TrayBubbleView::InitParams* init_params) {
253  DCHECK(bubble_view_ == NULL);
254
255  if (bubble_type_ == BUBBLE_TYPE_DETAILED &&
256      init_params->max_height < kDetailedBubbleMaxHeight) {
257    init_params->max_height = kDetailedBubbleMaxHeight;
258  } else if (bubble_type_ == BUBBLE_TYPE_NOTIFICATION) {
259    init_params->close_on_deactivate = false;
260  }
261  bubble_view_ = TrayBubbleView::Create(
262      tray_->GetBubbleWindowContainer(), anchor, tray_, init_params);
263  bubble_view_->set_adjust_if_offscreen(false);
264  CreateItemViews(login_status);
265
266  if (bubble_view_->CanActivate()) {
267    bubble_view_->NotifyAccessibilityEvent(
268        ui::AX_EVENT_ALERT, true);
269  }
270}
271
272void SystemTrayBubble::FocusDefaultIfNeeded() {
273  views::FocusManager* manager = bubble_view_->GetFocusManager();
274  if (!manager || manager->GetFocusedView())
275    return;
276
277  views::View* view = manager->GetNextFocusableView(NULL, NULL, false, false);
278  if (view)
279    view->RequestFocus();
280}
281
282void SystemTrayBubble::DestroyItemViews() {
283  for (std::vector<ash::SystemTrayItem*>::iterator it = items_.begin();
284       it != items_.end();
285       ++it) {
286    switch (bubble_type_) {
287      case BUBBLE_TYPE_DEFAULT:
288        (*it)->DestroyDefaultView();
289        break;
290      case BUBBLE_TYPE_DETAILED:
291        (*it)->DestroyDetailedView();
292        break;
293      case BUBBLE_TYPE_NOTIFICATION:
294        (*it)->DestroyNotificationView();
295        break;
296    }
297  }
298}
299
300void SystemTrayBubble::BubbleViewDestroyed() {
301  bubble_view_ = NULL;
302}
303
304void SystemTrayBubble::StartAutoCloseTimer(int seconds) {
305  autoclose_.Stop();
306  autoclose_delay_ = seconds;
307  if (autoclose_delay_) {
308    autoclose_.Start(FROM_HERE,
309                     base::TimeDelta::FromSeconds(autoclose_delay_),
310                     this, &SystemTrayBubble::Close);
311  }
312}
313
314void SystemTrayBubble::StopAutoCloseTimer() {
315  autoclose_.Stop();
316}
317
318void SystemTrayBubble::RestartAutoCloseTimer() {
319  if (autoclose_delay_)
320    StartAutoCloseTimer(autoclose_delay_);
321}
322
323void SystemTrayBubble::Close() {
324  tray_->HideBubbleWithView(bubble_view());
325}
326
327void SystemTrayBubble::SetVisible(bool is_visible) {
328  if (!bubble_view_)
329    return;
330  views::Widget* bubble_widget = bubble_view_->GetWidget();
331  if (is_visible)
332    bubble_widget->Show();
333  else
334    bubble_widget->Hide();
335}
336
337bool SystemTrayBubble::IsVisible() {
338  return bubble_view() && bubble_view()->GetWidget()->IsVisible();
339}
340
341bool SystemTrayBubble::ShouldShowShelf() const {
342  for (std::vector<ash::SystemTrayItem*>::const_iterator it = items_.begin();
343       it != items_.end();
344       ++it) {
345    if ((*it)->ShouldShowShelf())
346      return true;
347  }
348  return false;
349}
350
351void SystemTrayBubble::CreateItemViews(user::LoginStatus login_status) {
352  std::vector<views::View*> item_views;
353  // If a system modal dialog is present, create the same tray as
354  // in locked state.
355  if (Shell::GetInstance()->IsSystemModalWindowOpen() &&
356      login_status != user::LOGGED_IN_NONE) {
357    login_status = user::LOGGED_IN_LOCKED;
358  }
359
360  views::View* focus_view = NULL;
361  for (size_t i = 0; i < items_.size(); ++i) {
362    views::View* view = NULL;
363    switch (bubble_type_) {
364      case BUBBLE_TYPE_DEFAULT:
365        view = items_[i]->CreateDefaultView(login_status);
366        if (items_[i]->restore_focus())
367          focus_view = view;
368        break;
369      case BUBBLE_TYPE_DETAILED:
370        view = items_[i]->CreateDetailedView(login_status);
371        break;
372      case BUBBLE_TYPE_NOTIFICATION:
373        view = items_[i]->CreateNotificationView(login_status);
374        break;
375    }
376    if (view)
377      item_views.push_back(view);
378  }
379
380  bool is_default_bubble = bubble_type_ == BUBBLE_TYPE_DEFAULT;
381  for (size_t i = 0; i < item_views.size(); ++i) {
382    // For default view, draw bottom border for each item, except the last
383    // 2 items, which are the bottom header row and the one just above it.
384    bubble_view_->AddChildView(new TrayPopupItemContainer(
385        item_views[i], is_default_bubble,
386        is_default_bubble && (i < item_views.size() - 2)));
387  }
388  if (focus_view)
389    focus_view->RequestFocus();
390}
391
392}  // namespace ash
393