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