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