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