Home | History | Annotate | Download | only in notifications
      1 // Copyright (c) 2012 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/bind.h"
      8 #include "base/logging.h"
      9 #include "base/stl_util.h"
     10 #include "chrome/browser/chrome_notification_types.h"
     11 #include "chrome/browser/notifications/balloon.h"
     12 #include "chrome/browser/notifications/balloon_host.h"
     13 #include "chrome/browser/notifications/notification.h"
     14 #include "chrome/browser/ui/browser.h"
     15 #include "chrome/browser/ui/panels/docked_panel_collection.h"
     16 #include "chrome/browser/ui/panels/panel.h"
     17 #include "chrome/browser/ui/panels/panel_manager.h"
     18 #include "content/public/browser/notification_registrar.h"
     19 #include "content/public/browser/notification_service.h"
     20 #include "ui/gfx/rect.h"
     21 #include "ui/gfx/screen.h"
     22 #include "ui/gfx/size.h"
     23 
     24 namespace {
     25 
     26 // Portion of the screen allotted for notifications. When notification balloons
     27 // extend over this, no new notifications are shown until some are closed.
     28 const double kPercentBalloonFillFactor = 0.7;
     29 
     30 // Allow at least this number of balloons on the screen.
     31 const int kMinAllowedBalloonCount = 2;
     32 
     33 // Delay from the mouse leaving the balloon collection before
     34 // there is a relayout, in milliseconds.
     35 const int kRepositionDelayMs = 300;
     36 
     37 // The spacing between the balloon and the panel.
     38 const int kVerticalSpacingBetweenBalloonAndPanel = 5;
     39 
     40 }  // namespace
     41 
     42 BalloonCollectionImpl::BalloonCollectionImpl()
     43 #if USE_OFFSETS
     44     : reposition_factory_(this),
     45       added_as_message_loop_observer_(false)
     46 #endif
     47 {
     48   registrar_.Add(this, chrome::NOTIFICATION_PANEL_COLLECTION_UPDATED,
     49                  content::NotificationService::AllSources());
     50   registrar_.Add(this, chrome::NOTIFICATION_PANEL_CHANGED_EXPANSION_STATE,
     51                  content::NotificationService::AllSources());
     52 
     53   SetPositionPreference(BalloonCollection::DEFAULT_POSITION);
     54 }
     55 
     56 BalloonCollectionImpl::~BalloonCollectionImpl() {
     57 #if USE_OFFSETS
     58   RemoveMessageLoopObserver();
     59 #endif
     60 }
     61 
     62 void BalloonCollectionImpl::AddImpl(const Notification& notification,
     63                                     Profile* profile,
     64                                     bool add_to_front) {
     65   Balloon* new_balloon = MakeBalloon(notification, profile);
     66   // The +1 on width is necessary because width is fixed on notifications,
     67   // so since we always have the max size, we would always hit the scrollbar
     68   // condition.  We are only interested in comparing height to maximum.
     69   new_balloon->set_min_scrollbar_size(gfx::Size(1 + layout_.max_balloon_width(),
     70                                                 layout_.max_balloon_height()));
     71   new_balloon->SetPosition(layout_.OffScreenLocation(), false);
     72   new_balloon->Show();
     73 #if USE_OFFSETS
     74   int count = base_.count();
     75   if (count > 0 && layout_.RequiresOffsets())
     76     new_balloon->set_offset(base_.balloons()[count - 1]->offset());
     77 #endif
     78   base_.Add(new_balloon, add_to_front);
     79   PositionBalloons(false);
     80 
     81   // There may be no listener in a unit test.
     82   if (space_change_listener_)
     83     space_change_listener_->OnBalloonSpaceChanged();
     84 
     85   // This is used only for testing.
     86   if (!on_collection_changed_callback_.is_null())
     87     on_collection_changed_callback_.Run();
     88 }
     89 
     90 void BalloonCollectionImpl::Add(const Notification& notification,
     91                                 Profile* profile) {
     92   AddImpl(notification, profile, false);
     93 }
     94 
     95 const Notification* BalloonCollectionImpl::FindById(
     96     const std::string& id) const {
     97   return base_.FindById(id);
     98 }
     99 
    100 bool BalloonCollectionImpl::RemoveById(const std::string& id) {
    101   return base_.CloseById(id);
    102 }
    103 
    104 bool BalloonCollectionImpl::RemoveBySourceOrigin(const GURL& origin) {
    105   return base_.CloseAllBySourceOrigin(origin);
    106 }
    107 
    108 bool BalloonCollectionImpl::RemoveByProfile(Profile* profile) {
    109   return base_.CloseAllByProfile(profile);
    110 }
    111 
    112 void BalloonCollectionImpl::RemoveAll() {
    113   base_.CloseAll();
    114 }
    115 
    116 bool BalloonCollectionImpl::HasSpace() const {
    117   int count = base_.count();
    118   if (count < kMinAllowedBalloonCount)
    119     return true;
    120 
    121   int max_balloon_size = 0;
    122   int total_size = 0;
    123   layout_.GetMaxLinearSize(&max_balloon_size, &total_size);
    124 
    125   int current_max_size = max_balloon_size * count;
    126   int max_allowed_size = static_cast<int>(total_size *
    127                                           kPercentBalloonFillFactor);
    128   return current_max_size < max_allowed_size - max_balloon_size;
    129 }
    130 
    131 void BalloonCollectionImpl::ResizeBalloon(Balloon* balloon,
    132                                           const gfx::Size& size) {
    133   balloon->set_content_size(Layout::ConstrainToSizeLimits(size));
    134   PositionBalloons(true);
    135 }
    136 
    137 void BalloonCollectionImpl::DisplayChanged() {
    138   layout_.RefreshSystemMetrics();
    139   PositionBalloons(true);
    140 }
    141 
    142 void BalloonCollectionImpl::OnBalloonClosed(Balloon* source) {
    143 #if USE_OFFSETS
    144   // We want to free the balloon when finished.
    145   const Balloons& balloons = base_.balloons();
    146 
    147   Balloons::const_iterator it = balloons.begin();
    148   if (layout_.RequiresOffsets()) {
    149     gfx::Vector2d offset;
    150     bool apply_offset = false;
    151     while (it != balloons.end()) {
    152       if (*it == source) {
    153         ++it;
    154         if (it != balloons.end()) {
    155           apply_offset = true;
    156           offset.set_y((source)->offset().y() - (*it)->offset().y() +
    157               (*it)->content_size().height() - source->content_size().height());
    158         }
    159       } else {
    160         if (apply_offset)
    161           (*it)->add_offset(offset);
    162         ++it;
    163       }
    164     }
    165     // Start listening for UI events so we cancel the offset when the mouse
    166     // leaves the balloon area.
    167     if (apply_offset)
    168       AddMessageLoopObserver();
    169   }
    170 #endif
    171 
    172   base_.Remove(source);
    173   PositionBalloons(true);
    174 
    175   // There may be no listener in a unit test.
    176   if (space_change_listener_)
    177     space_change_listener_->OnBalloonSpaceChanged();
    178 
    179   // This is used only for testing.
    180   if (!on_collection_changed_callback_.is_null())
    181     on_collection_changed_callback_.Run();
    182 }
    183 
    184 const BalloonCollection::Balloons& BalloonCollectionImpl::GetActiveBalloons() {
    185   return base_.balloons();
    186 }
    187 
    188 void BalloonCollectionImpl::Observe(
    189     int type,
    190     const content::NotificationSource& source,
    191     const content::NotificationDetails& details) {
    192   gfx::Rect bounds;
    193   switch (type) {
    194     case chrome::NOTIFICATION_PANEL_COLLECTION_UPDATED:
    195     case chrome::NOTIFICATION_PANEL_CHANGED_EXPANSION_STATE:
    196       layout_.enable_computing_panel_offset();
    197       if (layout_.ComputeOffsetToMoveAbovePanels())
    198         PositionBalloons(true);
    199       break;
    200     default:
    201       NOTREACHED();
    202       break;
    203   }
    204 }
    205 
    206 void BalloonCollectionImpl::PositionBalloonsInternal(bool reposition) {
    207   const Balloons& balloons = base_.balloons();
    208 
    209   layout_.RefreshSystemMetrics();
    210   gfx::Point origin = layout_.GetLayoutOrigin();
    211   for (Balloons::const_iterator it = balloons.begin();
    212        it != balloons.end();
    213        ++it) {
    214     gfx::Point upper_left = layout_.NextPosition((*it)->GetViewSize(), &origin);
    215     (*it)->SetPosition(upper_left, reposition);
    216   }
    217 }
    218 
    219 gfx::Rect BalloonCollectionImpl::GetBalloonsBoundingBox() const {
    220   // Start from the layout origin.
    221   gfx::Rect bounds = gfx::Rect(layout_.GetLayoutOrigin(), gfx::Size(0, 0));
    222 
    223   // For each balloon, extend the rectangle.  This approach is indifferent to
    224   // the orientation of the balloons.
    225   const Balloons& balloons = base_.balloons();
    226   Balloons::const_iterator iter;
    227   for (iter = balloons.begin(); iter != balloons.end(); ++iter) {
    228     gfx::Rect balloon_box = gfx::Rect((*iter)->GetPosition(),
    229                                       (*iter)->GetViewSize());
    230     bounds.Union(balloon_box);
    231   }
    232 
    233   return bounds;
    234 }
    235 
    236 #if USE_OFFSETS
    237 void BalloonCollectionImpl::AddMessageLoopObserver() {
    238   if (!added_as_message_loop_observer_) {
    239     base::MessageLoopForUI::current()->AddObserver(this);
    240     added_as_message_loop_observer_ = true;
    241   }
    242 }
    243 
    244 void BalloonCollectionImpl::RemoveMessageLoopObserver() {
    245   if (added_as_message_loop_observer_) {
    246     base::MessageLoopForUI::current()->RemoveObserver(this);
    247     added_as_message_loop_observer_ = false;
    248   }
    249 }
    250 
    251 void BalloonCollectionImpl::CancelOffsets() {
    252   reposition_factory_.InvalidateWeakPtrs();
    253 
    254   // Unhook from listening to all UI events.
    255   RemoveMessageLoopObserver();
    256 
    257   const Balloons& balloons = base_.balloons();
    258   for (Balloons::const_iterator it = balloons.begin();
    259        it != balloons.end();
    260        ++it)
    261     (*it)->set_offset(gfx::Vector2d());
    262 
    263   PositionBalloons(true);
    264 }
    265 
    266 void BalloonCollectionImpl::HandleMouseMoveEvent() {
    267   if (!IsCursorInBalloonCollection()) {
    268     // Mouse has left the region.  Schedule a reposition after
    269     // a short delay.
    270     if (!reposition_factory_.HasWeakPtrs()) {
    271       base::MessageLoop::current()->PostDelayedTask(
    272           FROM_HERE,
    273           base::Bind(&BalloonCollectionImpl::CancelOffsets,
    274                      reposition_factory_.GetWeakPtr()),
    275           base::TimeDelta::FromMilliseconds(kRepositionDelayMs));
    276     }
    277   } else {
    278     // Mouse moved back into the region.  Cancel the reposition.
    279     reposition_factory_.InvalidateWeakPtrs();
    280   }
    281 }
    282 #endif
    283 
    284 BalloonCollectionImpl::Layout::Layout()
    285     : placement_(INVALID),
    286       need_to_compute_panel_offset_(false),
    287       offset_to_move_above_panels_(0) {
    288   RefreshSystemMetrics();
    289 }
    290 
    291 void BalloonCollectionImpl::Layout::GetMaxLinearSize(int* max_balloon_size,
    292                                                      int* total_size) const {
    293   DCHECK(max_balloon_size && total_size);
    294 
    295   // All placement schemes are vertical, so we only care about height.
    296   *total_size = work_area_.height();
    297   *max_balloon_size = max_balloon_height();
    298 }
    299 
    300 gfx::Point BalloonCollectionImpl::Layout::GetLayoutOrigin() const {
    301   // For lower-left and lower-right positioning, we need to add an offset
    302   // to ensure balloons to stay on top of panels to avoid overlapping.
    303   int x = 0;
    304   int y = 0;
    305   switch (placement_) {
    306     case VERTICALLY_FROM_TOP_LEFT: {
    307       x = work_area_.x() + HorizontalEdgeMargin();
    308       y = work_area_.y() + VerticalEdgeMargin() + offset_to_move_above_panels_;
    309       break;
    310     }
    311     case VERTICALLY_FROM_TOP_RIGHT: {
    312       x = work_area_.right() - HorizontalEdgeMargin();
    313       y = work_area_.y() + VerticalEdgeMargin() + offset_to_move_above_panels_;
    314       break;
    315     }
    316     case VERTICALLY_FROM_BOTTOM_LEFT:
    317       x = work_area_.x() + HorizontalEdgeMargin();
    318       y = work_area_.bottom() - VerticalEdgeMargin() -
    319           offset_to_move_above_panels_;
    320       break;
    321     case VERTICALLY_FROM_BOTTOM_RIGHT:
    322       x = work_area_.right() - HorizontalEdgeMargin();
    323       y = work_area_.bottom() - VerticalEdgeMargin() -
    324           offset_to_move_above_panels_;
    325       break;
    326     default:
    327       NOTREACHED();
    328       break;
    329   }
    330   return gfx::Point(x, y);
    331 }
    332 
    333 gfx::Point BalloonCollectionImpl::Layout::NextPosition(
    334     const gfx::Size& balloon_size,
    335     gfx::Point* position_iterator) const {
    336   DCHECK(position_iterator);
    337 
    338   int x = 0;
    339   int y = 0;
    340   switch (placement_) {
    341     case VERTICALLY_FROM_TOP_LEFT:
    342       x = position_iterator->x();
    343       y = position_iterator->y();
    344       position_iterator->set_y(position_iterator->y() + balloon_size.height() +
    345                                InterBalloonMargin());
    346       break;
    347     case VERTICALLY_FROM_TOP_RIGHT:
    348       x = position_iterator->x() - balloon_size.width();
    349       y = position_iterator->y();
    350       position_iterator->set_y(position_iterator->y() + balloon_size.height() +
    351                                InterBalloonMargin());
    352       break;
    353     case VERTICALLY_FROM_BOTTOM_LEFT:
    354       position_iterator->set_y(position_iterator->y() - balloon_size.height() -
    355                                InterBalloonMargin());
    356       x = position_iterator->x();
    357       y = position_iterator->y();
    358       break;
    359     case VERTICALLY_FROM_BOTTOM_RIGHT:
    360       position_iterator->set_y(position_iterator->y() - balloon_size.height() -
    361                                InterBalloonMargin());
    362       x = position_iterator->x() - balloon_size.width();
    363       y = position_iterator->y();
    364       break;
    365     default:
    366       NOTREACHED();
    367       break;
    368   }
    369   return gfx::Point(x, y);
    370 }
    371 
    372 gfx::Point BalloonCollectionImpl::Layout::OffScreenLocation() const {
    373   gfx::Point location = GetLayoutOrigin();
    374   switch (placement_) {
    375     case VERTICALLY_FROM_TOP_LEFT:
    376     case VERTICALLY_FROM_BOTTOM_LEFT:
    377       location.Offset(0, kBalloonMaxHeight);
    378       break;
    379     case VERTICALLY_FROM_TOP_RIGHT:
    380     case VERTICALLY_FROM_BOTTOM_RIGHT:
    381       location.Offset(-kBalloonMaxWidth - BalloonView::GetHorizontalMargin(),
    382                       kBalloonMaxHeight);
    383       break;
    384     default:
    385       NOTREACHED();
    386       break;
    387   }
    388   return location;
    389 }
    390 
    391 bool BalloonCollectionImpl::Layout::RequiresOffsets() const {
    392   // Layout schemes that grow up from the bottom require offsets;
    393   // schemes that grow down do not require offsets.
    394   bool offsets = (placement_ == VERTICALLY_FROM_BOTTOM_LEFT ||
    395                   placement_ == VERTICALLY_FROM_BOTTOM_RIGHT);
    396 
    397 #if defined(OS_MACOSX)
    398   // These schemes are in screen-coordinates, and top and bottom
    399   // are inverted on Mac.
    400   offsets = !offsets;
    401 #endif
    402 
    403   return offsets;
    404 }
    405 
    406 // static
    407 gfx::Size BalloonCollectionImpl::Layout::ConstrainToSizeLimits(
    408     const gfx::Size& size) {
    409   // restrict to the min & max sizes
    410   return gfx::Size(
    411       std::max(min_balloon_width(),
    412                std::min(max_balloon_width(), size.width())),
    413       std::max(min_balloon_height(),
    414                std::min(max_balloon_height(), size.height())));
    415 }
    416 
    417 bool BalloonCollectionImpl::Layout::ComputeOffsetToMoveAbovePanels() {
    418   // If the offset is not enabled due to that we have not received a
    419   // notification about panel, don't proceed because we don't want to call
    420   // PanelManager::GetInstance() to create an instance when panel is not
    421   // present.
    422   if (!need_to_compute_panel_offset_)
    423     return false;
    424 
    425   const DockedPanelCollection::Panels& panels =
    426       PanelManager::GetInstance()->docked_collection()->panels();
    427   int offset_to_move_above_panels = 0;
    428 
    429   // The offset is the maximum height of panels that could overlap with the
    430   // balloons.
    431   if (NeedToMoveAboveLeftSidePanels()) {
    432     for (DockedPanelCollection::Panels::const_reverse_iterator iter =
    433              panels.rbegin();
    434          iter != panels.rend(); ++iter) {
    435       // No need to check panels beyond the area occupied by the balloons.
    436       if ((*iter)->GetBounds().x() >= work_area_.x() + max_balloon_width())
    437         break;
    438 
    439       int current_height = (*iter)->GetBounds().height();
    440       if (current_height > offset_to_move_above_panels)
    441         offset_to_move_above_panels = current_height;
    442     }
    443   } else if (NeedToMoveAboveRightSidePanels()) {
    444     for (DockedPanelCollection::Panels::const_iterator iter = panels.begin();
    445          iter != panels.end(); ++iter) {
    446       // No need to check panels beyond the area occupied by the balloons.
    447       if ((*iter)->GetBounds().right() <=
    448           work_area_.right() - max_balloon_width())
    449         break;
    450 
    451       int current_height = (*iter)->GetBounds().height();
    452       if (current_height > offset_to_move_above_panels)
    453         offset_to_move_above_panels = current_height;
    454     }
    455   }
    456 
    457   // Ensure that we have some sort of margin between the 1st balloon and the
    458   // panel beneath it even the vertical edge margin is 0 as on Mac.
    459   if (offset_to_move_above_panels && !VerticalEdgeMargin())
    460     offset_to_move_above_panels += kVerticalSpacingBetweenBalloonAndPanel;
    461 
    462   // If no change is detected, return false to indicate that we do not need to
    463   // reposition balloons.
    464   if (offset_to_move_above_panels_ == offset_to_move_above_panels)
    465     return false;
    466 
    467   offset_to_move_above_panels_ = offset_to_move_above_panels;
    468   return true;
    469 }
    470 
    471 bool BalloonCollectionImpl::Layout::RefreshSystemMetrics() {
    472   bool changed = false;
    473 
    474 #if defined(OS_MACOSX)
    475   gfx::Rect new_work_area = GetMacWorkArea();
    476 #else
    477   // TODO(scottmg): NativeScreen is wrong. http://crbug.com/133312
    478   gfx::Rect new_work_area =
    479       gfx::Screen::GetNativeScreen()->GetPrimaryDisplay().work_area();
    480 #endif
    481   if (work_area_ != new_work_area) {
    482     work_area_.SetRect(new_work_area.x(), new_work_area.y(),
    483                        new_work_area.width(), new_work_area.height());
    484     changed = true;
    485   }
    486 
    487   return changed;
    488 }
    489