Home | History | Annotate | Download | only in views
      1 // Copyright (c) 2013 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 "ui/message_center/views/toast_contents_view.h"
      6 
      7 #include "base/bind.h"
      8 #include "base/compiler_specific.h"
      9 #include "base/memory/scoped_ptr.h"
     10 #include "base/memory/weak_ptr.h"
     11 #include "base/time/time.h"
     12 #include "base/timer/timer.h"
     13 #include "ui/base/accessibility/accessible_view_state.h"
     14 #include "ui/base/animation/animation_delegate.h"
     15 #include "ui/base/animation/slide_animation.h"
     16 #include "ui/gfx/display.h"
     17 #include "ui/gfx/screen.h"
     18 #include "ui/message_center/message_center.h"
     19 #include "ui/message_center/message_center_style.h"
     20 #include "ui/message_center/notification.h"
     21 #include "ui/message_center/views/message_popup_collection.h"
     22 #include "ui/message_center/views/message_view.h"
     23 #include "ui/views/view.h"
     24 #include "ui/views/widget/widget.h"
     25 #include "ui/views/widget/widget_delegate.h"
     26 
     27 namespace message_center {
     28 namespace {
     29 
     30 // The width of a toast before animated reveal and after closing.
     31 const int kClosedToastWidth = 5;
     32 
     33 // FadeIn/Out look a bit better if they are slightly longer then default slide.
     34 const int kFadeInOutDuration = 200;
     35 
     36 }  // namespace.
     37 
     38 // static
     39 gfx::Size ToastContentsView::GetToastSizeForView(views::View* view) {
     40   int width = kNotificationWidth + view->GetInsets().width();
     41   return gfx::Size(width, view->GetHeightForWidth(width));
     42 }
     43 
     44 ToastContentsView::ToastContentsView(
     45   const Notification* notification,
     46   base::WeakPtr<MessagePopupCollection> collection,
     47   MessageCenter* message_center)
     48     : collection_(collection),
     49       message_center_(message_center),
     50       id_(notification->id()),
     51       is_animating_bounds_(false),
     52       is_closing_(false),
     53       closing_animation_(NULL) {
     54   DCHECK(collection_);
     55 
     56   set_notify_enter_exit_on_child(true);
     57   // Sets the transparent background. Then, when the message view is slid out,
     58   // the whole toast seems to slide although the actual bound of the widget
     59   // remains. This is hacky but easier to keep the consistency.
     60   set_background(views::Background::CreateSolidBackground(0, 0, 0, 0));
     61 
     62   fade_animation_.reset(new ui::SlideAnimation(this));
     63   fade_animation_->SetSlideDuration(kFadeInOutDuration);
     64 }
     65 
     66 // This is destroyed when the toast window closes.
     67 ToastContentsView::~ToastContentsView() {
     68 }
     69 
     70 views::Widget* ToastContentsView::CreateWidget(gfx::NativeView parent) {
     71   views::Widget::InitParams params(
     72       views::Widget::InitParams::TYPE_POPUP);
     73   params.keep_on_top = true;
     74   if (parent)
     75     params.parent = parent;
     76   else
     77     params.top_level = true;
     78   params.opacity = views::Widget::InitParams::TRANSLUCENT_WINDOW;
     79   params.delegate = this;
     80   views::Widget* widget = new views::Widget();
     81   widget->set_focus_on_creation(false);
     82   widget->Init(params);
     83   return widget;
     84 }
     85 
     86 void ToastContentsView::SetContents(MessageView* view) {
     87   bool already_has_contents = child_count() > 0;
     88   RemoveAllChildViews(true);
     89   AddChildView(view);
     90   preferred_size_ = GetToastSizeForView(view);
     91   Layout();
     92   // If it has the contents already, this invocation means an update of the
     93   // popup toast, and the new contents should be read through a11y feature.
     94   if (already_has_contents)
     95     NotifyAccessibilityEvent(ui::AccessibilityTypes::EVENT_FOCUS, false);
     96 }
     97 
     98 void ToastContentsView::RevealWithAnimation(gfx::Point origin) {
     99   // Place/move the toast widgets. Currently it stacks the widgets from the
    100   // right-bottom of the work area.
    101   // TODO(mukai): allow to specify the placement policy from outside of this
    102   // class. The policy should be specified from preference on Windows, or
    103   // the launcher alignment on ChromeOS.
    104   origin_ = gfx::Point(origin.x() - preferred_size_.width(),
    105                        origin.y() - preferred_size_.height());
    106 
    107   gfx::Rect stable_bounds(origin_, preferred_size_);
    108 
    109   SetBoundsInstantly(GetClosedToastBounds(stable_bounds));
    110   StartFadeIn();
    111   SetBoundsWithAnimation(stable_bounds);
    112 }
    113 
    114 void ToastContentsView::CloseWithAnimation(bool mark_as_shown) {
    115   if (is_closing_)
    116     return;
    117   is_closing_ = true;
    118   if (collection_)
    119     collection_->RemoveToast(this);
    120   if (mark_as_shown)
    121     message_center_->MarkSinglePopupAsShown(id(), false);
    122   StartFadeOut();
    123 }
    124 
    125 void ToastContentsView::SetBoundsInstantly(gfx::Rect new_bounds) {
    126   if (new_bounds == bounds())
    127     return;
    128 
    129   origin_ = new_bounds.origin();
    130   if (!GetWidget())
    131     return;
    132   GetWidget()->SetBounds(new_bounds);
    133 }
    134 
    135 void ToastContentsView::SetBoundsWithAnimation(gfx::Rect new_bounds) {
    136   if (new_bounds == bounds())
    137     return;
    138 
    139   origin_ = new_bounds.origin();
    140   if (!GetWidget())
    141     return;
    142 
    143   // This picks up the current bounds, so if there was a previous animation
    144   // half-done, the next one will pick up from the current location.
    145   // This is the only place that should query current location of the Widget
    146   // on screen, the rest should refer to the bounds_.
    147   animated_bounds_start_ = GetWidget()->GetWindowBoundsInScreen();
    148   animated_bounds_end_ = new_bounds;
    149 
    150   if (collection_)
    151     collection_->IncrementDeferCounter();
    152 
    153   if (bounds_animation_.get())
    154     bounds_animation_->Stop();
    155 
    156   bounds_animation_.reset(new ui::SlideAnimation(this));
    157   bounds_animation_->Show();
    158 }
    159 
    160 void ToastContentsView::StartFadeIn() {
    161   // The decrement is done in OnBoundsAnimationEndedOrCancelled callback.
    162   if (collection_)
    163     collection_->IncrementDeferCounter();
    164   fade_animation_->Stop();
    165 
    166   GetWidget()->SetOpacity(0);
    167   GetWidget()->Show();
    168   fade_animation_->Reset(0);
    169   fade_animation_->Show();
    170 }
    171 
    172 void ToastContentsView::StartFadeOut() {
    173   // The decrement is done in OnBoundsAnimationEndedOrCancelled callback.
    174   if (collection_)
    175     collection_->IncrementDeferCounter();
    176   fade_animation_->Stop();
    177 
    178   closing_animation_ = (is_closing_ ? fade_animation_.get() : NULL);
    179   fade_animation_->Reset(1);
    180   fade_animation_->Hide();
    181 }
    182 
    183 void ToastContentsView::OnBoundsAnimationEndedOrCancelled(
    184     const ui::Animation* animation) {
    185   if (is_closing_ && closing_animation_ == animation && GetWidget()) {
    186     views::Widget* widget = GetWidget();
    187 #if defined(USE_AURA)
    188     // TODO(dewittj): This is a workaround to prevent a nasty bug where
    189     // closing a transparent widget doesn't actually remove the window,
    190     // causing entire areas of the screen to become unresponsive to clicks.
    191     // See crbug.com/243469
    192     widget->Hide();
    193 # if defined(OS_WIN)
    194     widget->SetOpacity(0xFF);
    195 # endif
    196 #endif
    197     widget->Close();
    198   }
    199 
    200   // This cannot be called before GetWidget()->Close(). Decrementing defer count
    201   // will invoke update, which may invoke another close animation with
    202   // incrementing defer counter. Close() after such process will cause a
    203   // mismatch between increment/decrement. See crbug.com/238477
    204   if (collection_)
    205     collection_->DecrementDeferCounter();
    206 }
    207 
    208 // ui::AnimationDelegate
    209 void ToastContentsView::AnimationProgressed(const ui::Animation* animation) {
    210   if (animation == bounds_animation_.get()) {
    211     gfx::Rect current(animation->CurrentValueBetween(
    212         animated_bounds_start_, animated_bounds_end_));
    213     GetWidget()->SetBounds(current);
    214   } else if (animation == fade_animation_.get()) {
    215     unsigned char opacity =
    216         static_cast<unsigned char>(fade_animation_->GetCurrentValue() * 255);
    217     GetWidget()->SetOpacity(opacity);
    218   }
    219 }
    220 
    221 void ToastContentsView::AnimationEnded(const ui::Animation* animation) {
    222   OnBoundsAnimationEndedOrCancelled(animation);
    223 }
    224 
    225 void ToastContentsView::AnimationCanceled(
    226     const ui::Animation* animation) {
    227   OnBoundsAnimationEndedOrCancelled(animation);
    228 }
    229 
    230 // views::WidgetDelegate
    231 views::View* ToastContentsView::GetContentsView() {
    232   return this;
    233 }
    234 
    235 void ToastContentsView::WindowClosing() {
    236   if (!is_closing_ && collection_)
    237     collection_->RemoveToast(this);
    238 }
    239 
    240 bool ToastContentsView::CanActivate() const {
    241 #if defined(OS_WIN) && defined(USE_AURA)
    242   return true;
    243 #else
    244   return false;
    245 #endif
    246 }
    247 
    248 void ToastContentsView::OnDisplayChanged() {
    249   views::Widget* widget = GetWidget();
    250   if (!widget)
    251     return;
    252 
    253   gfx::NativeView native_view = widget->GetNativeView();
    254   if (!native_view || !collection_)
    255     return;
    256 
    257   collection_->OnDisplayBoundsChanged(gfx::Screen::GetScreenFor(
    258       native_view)->GetDisplayNearestWindow(native_view));
    259 }
    260 
    261 void ToastContentsView::OnWorkAreaChanged() {
    262   views::Widget* widget = GetWidget();
    263   if (!widget)
    264     return;
    265 
    266   gfx::NativeView native_view = widget->GetNativeView();
    267   if (!native_view || !collection_)
    268     return;
    269 
    270   collection_->OnDisplayBoundsChanged(gfx::Screen::GetScreenFor(
    271       native_view)->GetDisplayNearestWindow(native_view));
    272 }
    273 
    274 // views::View
    275 void ToastContentsView::OnMouseEntered(const ui::MouseEvent& event) {
    276   if (collection_)
    277     collection_->OnMouseEntered(this);
    278 }
    279 
    280 void ToastContentsView::OnMouseExited(const ui::MouseEvent& event) {
    281   if (collection_)
    282     collection_->OnMouseExited(this);
    283 }
    284 
    285 void ToastContentsView::Layout() {
    286   if (child_count() > 0) {
    287     child_at(0)->SetBounds(
    288         0, 0, preferred_size_.width(), preferred_size_.height());
    289   }
    290 }
    291 
    292 gfx::Size ToastContentsView::GetPreferredSize() {
    293   return child_count() ? GetToastSizeForView(child_at(0)) : gfx::Size();
    294 }
    295 
    296 void ToastContentsView::GetAccessibleState(ui::AccessibleViewState* state) {
    297   if (child_count() > 0)
    298     child_at(0)->GetAccessibleState(state);
    299   state->role = ui::AccessibilityTypes::ROLE_WINDOW;
    300 }
    301 
    302 gfx::Rect ToastContentsView::GetClosedToastBounds(gfx::Rect bounds) {
    303   return gfx::Rect(bounds.x() + bounds.width() - kClosedToastWidth,
    304                    bounds.y(),
    305                    kClosedToastWidth,
    306                    bounds.height());
    307 }
    308 
    309 }  // namespace message_center
    310