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