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