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