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