Home | History | Annotate | Download | only in notifications
      1 // Copyright (c) 2011 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 "chrome/browser/notifications/balloon_collection_impl.h"
      6 
      7 #include "base/logging.h"
      8 #include "base/stl_util-inl.h"
      9 #include "chrome/browser/notifications/balloon.h"
     10 #include "chrome/browser/notifications/balloon_host.h"
     11 #include "chrome/browser/notifications/notification.h"
     12 #include "chrome/browser/ui/window_sizer.h"
     13 #include "ui/gfx/rect.h"
     14 #include "ui/gfx/size.h"
     15 
     16 namespace {
     17 
     18 // Portion of the screen allotted for notifications. When notification balloons
     19 // extend over this, no new notifications are shown until some are closed.
     20 const double kPercentBalloonFillFactor = 0.7;
     21 
     22 // Allow at least this number of balloons on the screen.
     23 const int kMinAllowedBalloonCount = 2;
     24 
     25 // Delay from the mouse leaving the balloon collection before
     26 // there is a relayout, in milliseconds.
     27 const int kRepositionDelay = 300;
     28 
     29 }  // namespace
     30 
     31 BalloonCollectionImpl::BalloonCollectionImpl()
     32 #if USE_OFFSETS
     33     : ALLOW_THIS_IN_INITIALIZER_LIST(reposition_factory_(this)),
     34       added_as_message_loop_observer_(false)
     35 #endif
     36 {
     37 
     38   SetPositionPreference(BalloonCollection::DEFAULT_POSITION);
     39 }
     40 
     41 BalloonCollectionImpl::~BalloonCollectionImpl() {
     42 }
     43 
     44 void BalloonCollectionImpl::Add(const Notification& notification,
     45                                 Profile* profile) {
     46   Balloon* new_balloon = MakeBalloon(notification, profile);
     47   // The +1 on width is necessary because width is fixed on notifications,
     48   // so since we always have the max size, we would always hit the scrollbar
     49   // condition.  We are only interested in comparing height to maximum.
     50   new_balloon->set_min_scrollbar_size(gfx::Size(1 + layout_.max_balloon_width(),
     51                                                 layout_.max_balloon_height()));
     52   new_balloon->SetPosition(layout_.OffScreenLocation(), false);
     53   new_balloon->Show();
     54 #if USE_OFFSETS
     55   int count = base_.count();
     56   if (count > 0 && layout_.RequiresOffsets())
     57     new_balloon->set_offset(base_.balloons()[count - 1]->offset());
     58 #endif
     59   base_.Add(new_balloon);
     60   PositionBalloons(false);
     61 
     62   // There may be no listener in a unit test.
     63   if (space_change_listener_)
     64     space_change_listener_->OnBalloonSpaceChanged();
     65 
     66   // This is used only for testing.
     67   if (on_collection_changed_callback_.get())
     68     on_collection_changed_callback_->Run();
     69 }
     70 
     71 bool BalloonCollectionImpl::RemoveById(const std::string& id) {
     72   return base_.CloseById(id);
     73 }
     74 
     75 bool BalloonCollectionImpl::RemoveBySourceOrigin(const GURL& origin) {
     76   return base_.CloseAllBySourceOrigin(origin);
     77 }
     78 
     79 void BalloonCollectionImpl::RemoveAll() {
     80   base_.CloseAll();
     81 }
     82 
     83 bool BalloonCollectionImpl::HasSpace() const {
     84   int count = base_.count();
     85   if (count < kMinAllowedBalloonCount)
     86     return true;
     87 
     88   int max_balloon_size = 0;
     89   int total_size = 0;
     90   layout_.GetMaxLinearSize(&max_balloon_size, &total_size);
     91 
     92   int current_max_size = max_balloon_size * count;
     93   int max_allowed_size = static_cast<int>(total_size *
     94                                           kPercentBalloonFillFactor);
     95   return current_max_size < max_allowed_size - max_balloon_size;
     96 }
     97 
     98 void BalloonCollectionImpl::ResizeBalloon(Balloon* balloon,
     99                                           const gfx::Size& size) {
    100   balloon->set_content_size(Layout::ConstrainToSizeLimits(size));
    101   PositionBalloons(true);
    102 }
    103 
    104 void BalloonCollectionImpl::DisplayChanged() {
    105   layout_.RefreshSystemMetrics();
    106   PositionBalloons(true);
    107 }
    108 
    109 void BalloonCollectionImpl::OnBalloonClosed(Balloon* source) {
    110   // We want to free the balloon when finished.
    111   const Balloons& balloons = base_.balloons();
    112   Balloons::const_iterator it = balloons.begin();
    113 
    114 #if USE_OFFSETS
    115   if (layout_.RequiresOffsets()) {
    116     gfx::Point offset;
    117     bool apply_offset = false;
    118     while (it != balloons.end()) {
    119       if (*it == source) {
    120         ++it;
    121         if (it != balloons.end()) {
    122           apply_offset = true;
    123           offset.set_y((source)->offset().y() - (*it)->offset().y() +
    124               (*it)->content_size().height() - source->content_size().height());
    125         }
    126       } else {
    127         if (apply_offset)
    128           (*it)->add_offset(offset);
    129         ++it;
    130       }
    131     }
    132     // Start listening for UI events so we cancel the offset when the mouse
    133     // leaves the balloon area.
    134     if (apply_offset)
    135       AddMessageLoopObserver();
    136   }
    137 #endif
    138 
    139   base_.Remove(source);
    140   PositionBalloons(true);
    141 
    142   // There may be no listener in a unit test.
    143   if (space_change_listener_)
    144     space_change_listener_->OnBalloonSpaceChanged();
    145 
    146   // This is used only for testing.
    147   if (on_collection_changed_callback_.get())
    148     on_collection_changed_callback_->Run();
    149 }
    150 
    151 const BalloonCollection::Balloons& BalloonCollectionImpl::GetActiveBalloons() {
    152   return base_.balloons();
    153 }
    154 
    155 void BalloonCollectionImpl::PositionBalloonsInternal(bool reposition) {
    156   const Balloons& balloons = base_.balloons();
    157 
    158   layout_.RefreshSystemMetrics();
    159   gfx::Point origin = layout_.GetLayoutOrigin();
    160   for (Balloons::const_iterator it = balloons.begin();
    161        it != balloons.end();
    162        ++it) {
    163     gfx::Point upper_left = layout_.NextPosition((*it)->GetViewSize(), &origin);
    164     (*it)->SetPosition(upper_left, reposition);
    165   }
    166 }
    167 
    168 gfx::Rect BalloonCollectionImpl::GetBalloonsBoundingBox() const {
    169   // Start from the layout origin.
    170   gfx::Rect bounds = gfx::Rect(layout_.GetLayoutOrigin(), gfx::Size(0, 0));
    171 
    172   // For each balloon, extend the rectangle.  This approach is indifferent to
    173   // the orientation of the balloons.
    174   const Balloons& balloons = base_.balloons();
    175   Balloons::const_iterator iter;
    176   for (iter = balloons.begin(); iter != balloons.end(); ++iter) {
    177     gfx::Rect balloon_box = gfx::Rect((*iter)->GetPosition(),
    178                                       (*iter)->GetViewSize());
    179     bounds = bounds.Union(balloon_box);
    180   }
    181 
    182   return bounds;
    183 }
    184 
    185 #if USE_OFFSETS
    186 void BalloonCollectionImpl::AddMessageLoopObserver() {
    187   if (!added_as_message_loop_observer_) {
    188     MessageLoopForUI::current()->AddObserver(this);
    189     added_as_message_loop_observer_ = true;
    190   }
    191 }
    192 
    193 void BalloonCollectionImpl::RemoveMessageLoopObserver() {
    194   if (added_as_message_loop_observer_) {
    195     MessageLoopForUI::current()->RemoveObserver(this);
    196     added_as_message_loop_observer_ = false;
    197   }
    198 }
    199 
    200 void BalloonCollectionImpl::CancelOffsets() {
    201   reposition_factory_.RevokeAll();
    202 
    203   // Unhook from listening to all UI events.
    204   RemoveMessageLoopObserver();
    205 
    206   const Balloons& balloons = base_.balloons();
    207   for (Balloons::const_iterator it = balloons.begin();
    208        it != balloons.end();
    209        ++it)
    210     (*it)->set_offset(gfx::Point(0, 0));
    211 
    212   PositionBalloons(true);
    213 }
    214 
    215 void BalloonCollectionImpl::HandleMouseMoveEvent() {
    216   if (!IsCursorInBalloonCollection()) {
    217     // Mouse has left the region.  Schedule a reposition after
    218     // a short delay.
    219     if (reposition_factory_.empty()) {
    220       MessageLoop::current()->PostDelayedTask(
    221           FROM_HERE,
    222           reposition_factory_.NewRunnableMethod(
    223               &BalloonCollectionImpl::CancelOffsets),
    224           kRepositionDelay);
    225     }
    226   } else {
    227     // Mouse moved back into the region.  Cancel the reposition.
    228     reposition_factory_.RevokeAll();
    229   }
    230 }
    231 #endif
    232 
    233 BalloonCollectionImpl::Layout::Layout() : placement_(INVALID) {
    234   RefreshSystemMetrics();
    235 }
    236 
    237 void BalloonCollectionImpl::Layout::GetMaxLinearSize(int* max_balloon_size,
    238                                                      int* total_size) const {
    239   DCHECK(max_balloon_size && total_size);
    240 
    241   // All placement schemes are vertical, so we only care about height.
    242   *total_size = work_area_.height();
    243   *max_balloon_size = max_balloon_height();
    244 }
    245 
    246 gfx::Point BalloonCollectionImpl::Layout::GetLayoutOrigin() const {
    247   int x = 0;
    248   int y = 0;
    249   switch (placement_) {
    250     case VERTICALLY_FROM_TOP_LEFT:
    251       x = work_area_.x() + HorizontalEdgeMargin();
    252       y = work_area_.y() + VerticalEdgeMargin();
    253       break;
    254     case VERTICALLY_FROM_TOP_RIGHT:
    255       x = work_area_.right() - HorizontalEdgeMargin();
    256       y = work_area_.y() + VerticalEdgeMargin();
    257       break;
    258     case VERTICALLY_FROM_BOTTOM_LEFT:
    259       x = work_area_.x() + HorizontalEdgeMargin();
    260       y = work_area_.bottom() - VerticalEdgeMargin();
    261       break;
    262     case VERTICALLY_FROM_BOTTOM_RIGHT:
    263       x = work_area_.right() - HorizontalEdgeMargin();
    264       y = work_area_.bottom() - VerticalEdgeMargin();
    265       break;
    266     default:
    267       NOTREACHED();
    268       break;
    269   }
    270   return gfx::Point(x, y);
    271 }
    272 
    273 gfx::Point BalloonCollectionImpl::Layout::NextPosition(
    274     const gfx::Size& balloon_size,
    275     gfx::Point* position_iterator) const {
    276   DCHECK(position_iterator);
    277 
    278   int x = 0;
    279   int y = 0;
    280   switch (placement_) {
    281     case VERTICALLY_FROM_TOP_LEFT:
    282       x = position_iterator->x();
    283       y = position_iterator->y();
    284       position_iterator->set_y(position_iterator->y() + balloon_size.height() +
    285                                InterBalloonMargin());
    286       break;
    287     case VERTICALLY_FROM_TOP_RIGHT:
    288       x = position_iterator->x() - balloon_size.width();
    289       y = position_iterator->y();
    290       position_iterator->set_y(position_iterator->y() + balloon_size.height() +
    291                                InterBalloonMargin());
    292       break;
    293     case VERTICALLY_FROM_BOTTOM_LEFT:
    294       position_iterator->set_y(position_iterator->y() - balloon_size.height() -
    295                                InterBalloonMargin());
    296       x = position_iterator->x();
    297       y = position_iterator->y();
    298       break;
    299     case VERTICALLY_FROM_BOTTOM_RIGHT:
    300       position_iterator->set_y(position_iterator->y() - balloon_size.height() -
    301                                InterBalloonMargin());
    302       x = position_iterator->x() - balloon_size.width();
    303       y = position_iterator->y();
    304       break;
    305     default:
    306       NOTREACHED();
    307       break;
    308   }
    309   return gfx::Point(x, y);
    310 }
    311 
    312 gfx::Point BalloonCollectionImpl::Layout::OffScreenLocation() const {
    313   int x = 0;
    314   int y = 0;
    315   switch (placement_) {
    316     case VERTICALLY_FROM_TOP_LEFT:
    317       x = work_area_.x() + HorizontalEdgeMargin();
    318       y = work_area_.y() + kBalloonMaxHeight + VerticalEdgeMargin();
    319       break;
    320     case VERTICALLY_FROM_TOP_RIGHT:
    321       x = work_area_.right() - kBalloonMaxWidth - HorizontalEdgeMargin();
    322       y = work_area_.y() + kBalloonMaxHeight + VerticalEdgeMargin();
    323       break;
    324     case VERTICALLY_FROM_BOTTOM_LEFT:
    325       x = work_area_.x() + HorizontalEdgeMargin();
    326       y = work_area_.bottom() + kBalloonMaxHeight + VerticalEdgeMargin();
    327       break;
    328     case VERTICALLY_FROM_BOTTOM_RIGHT:
    329       x = work_area_.right() - kBalloonMaxWidth - HorizontalEdgeMargin();
    330       y = work_area_.bottom() + kBalloonMaxHeight + VerticalEdgeMargin();
    331       break;
    332     default:
    333       NOTREACHED();
    334       break;
    335   }
    336   return gfx::Point(x, y);
    337 }
    338 
    339 bool BalloonCollectionImpl::Layout::RequiresOffsets() const {
    340   // Layout schemes that grow up from the bottom require offsets;
    341   // schemes that grow down do not require offsets.
    342   bool offsets = (placement_ == VERTICALLY_FROM_BOTTOM_LEFT ||
    343                   placement_ == VERTICALLY_FROM_BOTTOM_RIGHT);
    344 
    345 #if defined(OS_MACOSX)
    346   // These schemes are in screen-coordinates, and top and bottom
    347   // are inverted on Mac.
    348   offsets = !offsets;
    349 #endif
    350 
    351   return offsets;
    352 }
    353 
    354 // static
    355 gfx::Size BalloonCollectionImpl::Layout::ConstrainToSizeLimits(
    356     const gfx::Size& size) {
    357   // restrict to the min & max sizes
    358   return gfx::Size(
    359       std::max(min_balloon_width(),
    360                std::min(max_balloon_width(), size.width())),
    361       std::max(min_balloon_height(),
    362                std::min(max_balloon_height(), size.height())));
    363 }
    364 
    365 bool BalloonCollectionImpl::Layout::RefreshSystemMetrics() {
    366   bool changed = false;
    367 
    368 #if defined(OS_MACOSX)
    369   gfx::Rect new_work_area = GetMacWorkArea();
    370 #else
    371   scoped_ptr<WindowSizer::MonitorInfoProvider> info_provider(
    372       WindowSizer::CreateDefaultMonitorInfoProvider());
    373   gfx::Rect new_work_area = info_provider->GetPrimaryMonitorWorkArea();
    374 #endif
    375   if (!work_area_.Equals(new_work_area)) {
    376     work_area_.SetRect(new_work_area.x(), new_work_area.y(),
    377                        new_work_area.width(), new_work_area.height());
    378     changed = true;
    379   }
    380 
    381   return changed;
    382 }
    383