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/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 
     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 
     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 
     90 void MessagePopupCollection::ClickOnNotification(
     91     const std::string& notification_id) {
     92   message_center_->ClickOnNotification(notification_id);
     93 }
     94 
     95 void MessagePopupCollection::RemoveNotification(
     96     const std::string& notification_id,
     97     bool by_user) {
     98   message_center_->RemoveNotification(notification_id, by_user);
     99 }
    100 
    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 
    107 bool MessagePopupCollection::HasClickedListener(
    108     const std::string& notification_id) {
    109   return message_center_->HasClickedListener(notification_id);
    110 }
    111 
    112 void MessagePopupCollection::ClickOnNotificationButton(
    113     const std::string& notification_id,
    114     int button_index) {
    115   message_center_->ClickOnNotificationButton(notification_id, button_index);
    116 }
    117 
    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 
    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 
    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 
    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 
    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 
    241 void MessagePopupCollection::ForgetToast(ToastContentsView* toast) {
    242   toasts_.remove(toast);
    243   OnMouseExited(toast);
    244 }
    245 
    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 
    256 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 
    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 
    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 
    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 
    361 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 
    389 void MessagePopupCollection::OnNotificationAdded(
    390     const std::string& notification_id) {
    391   DoUpdateIfPossible();
    392 }
    393 
    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 
    429 void MessagePopupCollection::OnDeferTimerExpired() {
    430   user_is_closing_toasts_by_clicking_ = false;
    431   DecrementDeferCounter();
    432 
    433   message_center_->RestartPopupTimers();
    434 }
    435 
    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 
    484 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 
    494 void MessagePopupCollection::IncrementDeferCounter() {
    495   defer_counter_++;
    496 }
    497 
    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.
    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 
    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 
    564 void MessagePopupCollection::OnDisplayAdded(const gfx::Display& new_display) {
    565 }
    566 
    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 
    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 
    584 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 
    594 void MessagePopupCollection::CreateRunLoopForTest() {
    595   run_loop_for_test_.reset(new base::RunLoop());
    596 }
    597 
    598 void MessagePopupCollection::WaitForTest() {
    599   run_loop_for_test_->Run();
    600   run_loop_for_test_.reset();
    601 }
    602 
    603 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