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/message_popup_collection.h"
      6 
      7 #include <set>
      8 
      9 #include "base/bind.h"
     10 #include "base/i18n/rtl.h"
     11 #include "base/logging.h"
     12 #include "base/memory/weak_ptr.h"
     13 #include "base/run_loop.h"
     14 #include "base/time/time.h"
     15 #include "base/timer/timer.h"
     16 #include "ui/base/accessibility/accessibility_types.h"
     17 #include "ui/base/animation/animation_delegate.h"
     18 #include "ui/base/animation/slide_animation.h"
     19 #include "ui/gfx/screen.h"
     20 #include "ui/message_center/message_center.h"
     21 #include "ui/message_center/message_center_style.h"
     22 #include "ui/message_center/notification.h"
     23 #include "ui/message_center/notification_list.h"
     24 #include "ui/message_center/views/notification_view.h"
     25 #include "ui/message_center/views/toast_contents_view.h"
     26 #include "ui/views/background.h"
     27 #include "ui/views/layout/fill_layout.h"
     28 #include "ui/views/view.h"
     29 #include "ui/views/views_delegate.h"
     30 #include "ui/views/widget/widget.h"
     31 #include "ui/views/widget/widget_delegate.h"
     32 
     33 namespace message_center {
     34 namespace {
     35 
     36 // Timeout between the last user-initiated close of the toast and the moment
     37 // when normal layout/update of the toast stack continues. If the last toast was
     38 // just closed, the timeout is shorter.
     39 const int kMouseExitedDeferTimeoutMs = 200;
     40 
     41 // The margin between messages (and between the anchor unless
     42 // first_item_has_no_margin was specified).
     43 const int kToastMarginY = kMarginBetweenItems;
     44 #if defined(OS_CHROMEOS)
     45 const int kToastMarginX = 3;
     46 #else
     47 const int kToastMarginX = kMarginBetweenItems;
     48 #endif
     49 
     50 
     51 // If there should be no margin for the first item, this value needs to be
     52 // substracted to flush the message to the shelf (the width of the border +
     53 // shadow).
     54 const int kNoToastMarginBorderAndShadowOffset = 2;
     55 
     56 }  // namespace.
     57 
     58 MessagePopupCollection::MessagePopupCollection(gfx::NativeView parent,
     59                                                MessageCenter* message_center,
     60                                                MessageCenterTray* tray,
     61                                                bool first_item_has_no_margin)
     62     : parent_(parent),
     63       message_center_(message_center),
     64       tray_(tray),
     65       defer_counter_(0),
     66       latest_toast_entered_(NULL),
     67       user_is_closing_toasts_by_clicking_(false),
     68       first_item_has_no_margin_(first_item_has_no_margin) {
     69   DCHECK(message_center_);
     70   defer_timer_.reset(new base::OneShotTimer<MessagePopupCollection>);
     71   message_center_->AddObserver(this);
     72   gfx::Screen* screen = NULL;
     73   gfx::Display display;
     74   if (!parent_) {
     75     // On Win+Aura, we don't have a parent since the popups currently show up
     76     // on the Windows desktop, not in the Aura/Ash desktop.  This code will
     77     // display the popups on the primary display.
     78     screen = gfx::Screen::GetNativeScreen();
     79     display = screen->GetPrimaryDisplay();
     80   } else {
     81     screen = gfx::Screen::GetScreenFor(parent_);
     82     display = screen->GetDisplayNearestWindow(parent_);
     83   }
     84   screen->AddObserver(this);
     85 
     86   display_id_ = display.id();
     87   work_area_ = display.work_area();
     88   ComputePopupAlignment(work_area_, display.bounds());
     89 
     90   // We should not update before work area and popup alignment are computed.
     91   DoUpdateIfPossible();
     92 }
     93 
     94 MessagePopupCollection::~MessagePopupCollection() {
     95   gfx::Screen* screen = parent_ ?
     96       gfx::Screen::GetScreenFor(parent_) : gfx::Screen::GetNativeScreen();
     97   screen->RemoveObserver(this);
     98   message_center_->RemoveObserver(this);
     99   CloseAllWidgets();
    100 }
    101 
    102 void MessagePopupCollection::RemoveToast(ToastContentsView* toast) {
    103   OnMouseExited(toast);
    104 
    105   for (Toasts::iterator iter = toasts_.begin(); iter != toasts_.end(); ++iter) {
    106     if ((*iter) == toast) {
    107       toasts_.erase(iter);
    108       break;
    109     }
    110   }
    111 }
    112 
    113 void MessagePopupCollection::UpdateWidgets() {
    114   NotificationList::PopupNotifications popups =
    115       message_center_->GetPopupNotifications();
    116 
    117   if (popups.empty()) {
    118     CloseAllWidgets();
    119     return;
    120   }
    121 
    122   bool top_down = alignment_ & POPUP_ALIGNMENT_TOP;
    123   int base = GetBaseLine(toasts_.empty() ? NULL : toasts_.back());
    124 
    125   // Iterate in the reverse order to keep the oldest toasts on screen. Newer
    126   // items may be ignored if there are no room to place them.
    127   for (NotificationList::PopupNotifications::const_reverse_iterator iter =
    128            popups.rbegin(); iter != popups.rend(); ++iter) {
    129     if (FindToast((*iter)->id()))
    130       continue;
    131 
    132     MessageView* view =
    133         NotificationView::Create(*(*iter),
    134                                  message_center_,
    135                                  tray_,
    136                                  true,  // Create expanded.
    137                                  true); // Create top-level notification.
    138     int view_height = ToastContentsView::GetToastSizeForView(view).height();
    139     int height_available = top_down ? work_area_.bottom() - base : base;
    140 
    141     if (height_available - view_height - kToastMarginY < 0) {
    142       delete view;
    143       break;
    144     }
    145 
    146     ToastContentsView* toast = new ToastContentsView(
    147         *iter, AsWeakPtr(), message_center_);
    148     toast->CreateWidget(parent_);
    149     toast->SetContents(view);
    150     toasts_.push_back(toast);
    151 
    152     gfx::Size preferred_size = toast->GetPreferredSize();
    153     gfx::Point origin(
    154         GetToastOriginX(gfx::Rect(preferred_size)) + preferred_size.width(),
    155         top_down ? base + view_height : base);
    156     toast->RevealWithAnimation(origin);
    157 
    158     // Shift the base line to be a few pixels above the last added toast or (few
    159     // pixels below last added toast if top-aligned).
    160     if (top_down)
    161       base += view_height + kToastMarginY;
    162     else
    163       base -= view_height + kToastMarginY;
    164 
    165     message_center_->DisplayedNotification((*iter)->id());
    166     if (views::ViewsDelegate::views_delegate) {
    167       views::ViewsDelegate::views_delegate->NotifyAccessibilityEvent(
    168           toast, ui::AccessibilityTypes::EVENT_ALERT);
    169     }
    170   }
    171 }
    172 
    173 void MessagePopupCollection::OnMouseEntered(ToastContentsView* toast_entered) {
    174   // Sometimes we can get two MouseEntered/MouseExited in a row when animating
    175   // toasts.  So we need to keep track of which one is the currently active one.
    176   latest_toast_entered_ = toast_entered;
    177 
    178   message_center_->PausePopupTimers();
    179 
    180   if (user_is_closing_toasts_by_clicking_)
    181     defer_timer_->Stop();
    182 }
    183 
    184 void MessagePopupCollection::OnMouseExited(ToastContentsView* toast_exited) {
    185   // If we're exiting a toast after entering a different toast, then ignore
    186   // this mouse event.
    187   if (toast_exited != latest_toast_entered_)
    188     return;
    189   latest_toast_entered_ = NULL;
    190 
    191   if (user_is_closing_toasts_by_clicking_) {
    192     defer_timer_->Start(
    193         FROM_HERE,
    194         base::TimeDelta::FromMilliseconds(kMouseExitedDeferTimeoutMs),
    195         this,
    196         &MessagePopupCollection::OnDeferTimerExpired);
    197   } else {
    198     message_center_->RestartPopupTimers();
    199   }
    200 }
    201 
    202 void MessagePopupCollection::CloseAllWidgets() {
    203   for (Toasts::iterator iter = toasts_.begin(); iter != toasts_.end();) {
    204     // the toast can be removed from toasts_ during CloseWithAnimation().
    205     Toasts::iterator curiter = iter++;
    206     (*curiter)->CloseWithAnimation(true);
    207   }
    208   DCHECK(toasts_.empty());
    209 }
    210 
    211 int MessagePopupCollection::GetToastOriginX(const gfx::Rect& toast_bounds) {
    212 #if defined(OS_CHROMEOS)
    213   // In ChromeOS, RTL UI language mirrors the whole desktop layout, so the toast
    214   // widgets should be at the bottom-left instead of bottom right.
    215   if (base::i18n::IsRTL())
    216     return work_area_.x() + kToastMarginX;
    217 #endif
    218   if (alignment_ & POPUP_ALIGNMENT_LEFT)
    219     return work_area_.x() + kToastMarginX;
    220   return work_area_.right() - kToastMarginX - toast_bounds.width();
    221 }
    222 
    223 void MessagePopupCollection::RepositionWidgets() {
    224   bool top_down = alignment_ & POPUP_ALIGNMENT_TOP;
    225   int base = GetBaseLine(NULL);  // We don't want to position relative to last
    226                                  // toast - we want re-position.
    227 
    228   for (Toasts::iterator iter = toasts_.begin(); iter != toasts_.end();) {
    229     Toasts::iterator curr = iter++;
    230     gfx::Rect bounds((*curr)->bounds());
    231     bounds.set_x(GetToastOriginX(bounds));
    232     bounds.set_y(alignment_ & POPUP_ALIGNMENT_TOP ? base
    233                                                   : base - bounds.height());
    234 
    235     // The notification may scrolls the boundary of the screen due to image
    236     // load and such notifications should disappear. Do not call
    237     // CloseWithAnimation, we don't want to show the closing animation, and we
    238     // don't want to mark such notifications as shown. See crbug.com/233424
    239     if ((top_down ? work_area_.bottom() - bounds.bottom() : bounds.y()) >= 0)
    240       (*curr)->SetBoundsWithAnimation(bounds);
    241     else
    242       (*curr)->CloseWithAnimation(false);
    243 
    244     // Shift the base line to be a few pixels above the last added toast or (few
    245     // pixels below last added toast if top-aligned).
    246     if (top_down)
    247       base += bounds.height() + kToastMarginY;
    248     else
    249       base -= bounds.height() + kToastMarginY;
    250   }
    251 }
    252 
    253 void MessagePopupCollection::RepositionWidgetsWithTarget() {
    254   if (toasts_.empty())
    255     return;
    256 
    257   bool top_down = alignment_ & POPUP_ALIGNMENT_TOP;
    258 
    259   // Nothing to do if there are no widgets above target if bottom-aligned or no
    260   // widgets below target if top-aligned.
    261   if (top_down ? toasts_.back()->origin().y() < target_top_edge_
    262                : toasts_.back()->origin().y() > target_top_edge_)
    263     return;
    264 
    265   Toasts::reverse_iterator iter = toasts_.rbegin();
    266   for (; iter != toasts_.rend(); ++iter) {
    267     // We only reposition widgets above target if bottom-aligned or widgets
    268     // below target if top-aligned.
    269     if (top_down ? (*iter)->origin().y() < target_top_edge_
    270                  : (*iter)->origin().y() > target_top_edge_)
    271       break;
    272   }
    273   --iter;
    274 
    275   // Slide length is the number of pixels the widgets should move so that their
    276   // bottom edge (top-edge if top-aligned) touches the target.
    277   int slide_length = std::abs(target_top_edge_ - (*iter)->origin().y());
    278   for (;; --iter) {
    279     gfx::Rect bounds((*iter)->bounds());
    280 
    281     // If top-aligned, shift widgets upwards by slide_length. If bottom-aligned,
    282     // shift them downwards by slide_length.
    283     if (top_down)
    284       bounds.set_y(bounds.y() - slide_length);
    285     else
    286       bounds.set_y(bounds.y() + slide_length);
    287     (*iter)->SetBoundsWithAnimation(bounds);
    288 
    289     if (iter == toasts_.rbegin())
    290       break;
    291   }
    292 }
    293 
    294 void MessagePopupCollection::ComputePopupAlignment(gfx::Rect work_area,
    295                                                    gfx::Rect screen_bounds) {
    296   // If the taskbar is at the top, render notifications top down. Some platforms
    297   // like Gnome can have taskbars at top and bottom. In this case it's more
    298   // likely that the systray is on the top one.
    299   alignment_ = work_area.y() > screen_bounds.y() ? POPUP_ALIGNMENT_TOP
    300                                                  : POPUP_ALIGNMENT_BOTTOM;
    301 
    302   // If the taskbar is on the left show the notifications on the left. Otherwise
    303   // show it on right since it's very likely that the systray is on the right if
    304   // the taskbar is on the top or bottom.
    305   // Since on some platforms like Ubuntu Unity there's also a launcher along
    306   // with a taskbar (panel), we need to check that there is really nothing at
    307   // the top before concluding that the taskbar is at the left.
    308   alignment_ = static_cast<PopupAlignment>(
    309       alignment_ |
    310       ((work_area.x() > screen_bounds.x() && work_area.y() == screen_bounds.y())
    311            ? POPUP_ALIGNMENT_LEFT
    312            : POPUP_ALIGNMENT_RIGHT));
    313 }
    314 
    315 int MessagePopupCollection::GetBaseLine(ToastContentsView* last_toast) {
    316   bool top_down = alignment_ & POPUP_ALIGNMENT_TOP;
    317   int base;
    318 
    319   if (top_down) {
    320     if (!last_toast) {
    321       base = work_area_.y();
    322       if (!first_item_has_no_margin_)
    323         base += kToastMarginY;
    324       else
    325         base -= kNoToastMarginBorderAndShadowOffset;
    326     } else {
    327       base = toasts_.back()->bounds().bottom() + kToastMarginY;
    328     }
    329   } else {
    330     if (!last_toast) {
    331       base = work_area_.bottom();
    332       if (!first_item_has_no_margin_)
    333         base -= kToastMarginY;
    334       else
    335         base += kNoToastMarginBorderAndShadowOffset;
    336     } else {
    337       base = toasts_.back()->origin().y() - kToastMarginY;
    338     }
    339   }
    340   return base;
    341 }
    342 
    343 void MessagePopupCollection::OnNotificationAdded(
    344     const std::string& notification_id) {
    345   DoUpdateIfPossible();
    346 }
    347 
    348 void MessagePopupCollection::OnNotificationRemoved(
    349     const std::string& notification_id,
    350     bool by_user) {
    351   // Find a toast.
    352   Toasts::iterator iter = toasts_.begin();
    353   for (; iter != toasts_.end(); ++iter) {
    354     if ((*iter)->id() == notification_id)
    355       break;
    356   }
    357   if (iter == toasts_.end())
    358     return;
    359 
    360   target_top_edge_ = (*iter)->bounds().y();
    361   (*iter)->CloseWithAnimation(true);
    362   if (by_user) {
    363     RepositionWidgetsWithTarget();
    364     // [Re] start a timeout after which the toasts re-position to their
    365     // normal locations after tracking the mouse pointer for easy deletion.
    366     // This provides a period of time when toasts are easy to remove because
    367     // they re-position themselves to have Close button right under the mouse
    368     // pointer. If the user continue to remove the toasts, the delay is reset.
    369     // Once user stopped removing the toasts, the toasts re-populate/rearrange
    370     // after the specified delay.
    371     if (!user_is_closing_toasts_by_clicking_) {
    372       user_is_closing_toasts_by_clicking_ = true;
    373       IncrementDeferCounter();
    374     }
    375   }
    376 }
    377 
    378 void MessagePopupCollection::OnDeferTimerExpired() {
    379   user_is_closing_toasts_by_clicking_ = false;
    380   DecrementDeferCounter();
    381 
    382   message_center_->RestartPopupTimers();
    383 }
    384 
    385 void MessagePopupCollection::OnNotificationUpdated(
    386     const std::string& notification_id) {
    387   // Find a toast.
    388   Toasts::iterator toast_iter = toasts_.begin();
    389   for (; toast_iter != toasts_.end(); ++toast_iter) {
    390     if ((*toast_iter)->id() == notification_id)
    391       break;
    392   }
    393   if (toast_iter == toasts_.end())
    394     return;
    395 
    396   NotificationList::PopupNotifications notifications =
    397       message_center_->GetPopupNotifications();
    398   bool updated = false;
    399 
    400   for (NotificationList::PopupNotifications::iterator iter =
    401            notifications.begin(); iter != notifications.end(); ++iter) {
    402     if ((*iter)->id() != notification_id)
    403       continue;
    404 
    405     MessageView* view =
    406         NotificationView::Create(*(*iter),
    407                                  message_center_,
    408                                  tray_,
    409                                  true,  // Create expanded.
    410                                  true); // Create top-level notification.
    411     (*toast_iter)->SetContents(view);
    412     updated = true;
    413   }
    414 
    415   // OnNotificationUpdated() can be called when a notification is excluded from
    416   // the popup notification list but still remains in the full notification
    417   // list. In that case the widget for the notification has to be closed here.
    418   if (!updated)
    419     (*toast_iter)->CloseWithAnimation(true);
    420 
    421   if (user_is_closing_toasts_by_clicking_)
    422     RepositionWidgetsWithTarget();
    423   else
    424     DoUpdateIfPossible();
    425 }
    426 
    427 ToastContentsView* MessagePopupCollection::FindToast(
    428     const std::string& notification_id) {
    429   for (Toasts::iterator iter = toasts_.begin(); iter != toasts_.end(); ++iter) {
    430     if ((*iter)->id() == notification_id)
    431       return *iter;
    432   }
    433   return NULL;
    434 }
    435 
    436 void MessagePopupCollection::IncrementDeferCounter() {
    437   defer_counter_++;
    438 }
    439 
    440 void MessagePopupCollection::DecrementDeferCounter() {
    441   defer_counter_--;
    442   DCHECK(defer_counter_ >= 0);
    443   DoUpdateIfPossible();
    444 }
    445 
    446 // This is the main sequencer of tasks. It does a step, then waits for
    447 // all started transitions to play out before doing the next step.
    448 // First, remove all expired toasts.
    449 // Then, reposition widgets (the reposition on close happens before all
    450 // deferred tasks are even able to run)
    451 // Then, see if there is vacant space for new toasts.
    452 void MessagePopupCollection::DoUpdateIfPossible() {
    453   if (defer_counter_ > 0)
    454     return;
    455 
    456   RepositionWidgets();
    457 
    458   if (defer_counter_ > 0)
    459     return;
    460 
    461   // Reposition could create extra space which allows additional widgets.
    462   UpdateWidgets();
    463 
    464   if (defer_counter_ > 0)
    465     return;
    466 
    467   // Test support. Quit the test run loop when no more updates are deferred,
    468   // meaining th echeck for updates did not cause anything to change so no new
    469   // transition animations were started.
    470   if (run_loop_for_test_.get())
    471     run_loop_for_test_->Quit();
    472 }
    473 
    474 void MessagePopupCollection::SetDisplayInfo(const gfx::Rect& work_area,
    475                                             const gfx::Rect& screen_bounds) {
    476   if (work_area_ == work_area)
    477     return;
    478 
    479   work_area_ = work_area;
    480   ComputePopupAlignment(work_area, screen_bounds);
    481   RepositionWidgets();
    482 }
    483 
    484 void MessagePopupCollection::OnDisplayBoundsChanged(
    485     const gfx::Display& display) {
    486   if (display.id() != display_id_)
    487     return;
    488 
    489   SetDisplayInfo(display.work_area(), display.bounds());
    490 }
    491 
    492 void MessagePopupCollection::OnDisplayAdded(const gfx::Display& new_display) {
    493 }
    494 
    495 void MessagePopupCollection::OnDisplayRemoved(const gfx::Display& old_display) {
    496 }
    497 
    498 views::Widget* MessagePopupCollection::GetWidgetForTest(const std::string& id) {
    499   for (Toasts::iterator iter = toasts_.begin(); iter != toasts_.end(); ++iter) {
    500     if ((*iter)->id() == id)
    501       return (*iter)->GetWidget();
    502   }
    503   return NULL;
    504 }
    505 
    506 void MessagePopupCollection::RunLoopForTest() {
    507   run_loop_for_test_.reset(new base::RunLoop());
    508   run_loop_for_test_->Run();
    509   run_loop_for_test_.reset();
    510 }
    511 
    512 gfx::Rect MessagePopupCollection::GetToastRectAt(size_t index) {
    513   DCHECK(defer_counter_ == 0) << "Fetching the bounds with animations active.";
    514   size_t i = 0;
    515   for (Toasts::iterator iter = toasts_.begin(); iter != toasts_.end(); ++iter) {
    516     if (i++ == index) {
    517       views::Widget* widget = (*iter)->GetWidget();
    518       if (widget)
    519         return widget->GetWindowBoundsInScreen();
    520       break;
    521     }
    522   }
    523   return gfx::Rect();
    524 }
    525 
    526 }  // namespace message_center
    527