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 #import "ui/message_center/cocoa/popup_collection.h" 6 7 #import "ui/message_center/cocoa/notification_controller.h" 8 #import "ui/message_center/cocoa/popup_controller.h" 9 #include "ui/message_center/message_center.h" 10 #include "ui/message_center/message_center_observer.h" 11 #include "ui/message_center/message_center_style.h" 12 13 const float kAnimationDuration = 0.2; 14 15 @interface MCPopupCollection (Private) 16 // Returns the primary screen's visible frame rectangle. 17 - (NSRect)screenFrame; 18 19 // Shows a popup, if there is room on-screen, for the given notification. 20 // Returns YES if the notification was actually displayed. 21 - (BOOL)addNotification:(const message_center::Notification*)notification; 22 23 // Updates the contents of the notification with the given ID. 24 - (void)updateNotification:(const std::string&)notificationID; 25 26 // Removes a popup from the screen and lays out new notifications that can 27 // now potentially fit on the screen. 28 - (void)removeNotification:(const std::string&)notificationID; 29 30 // Closes all the popups. 31 - (void)removeAllNotifications; 32 33 // Returns the index of the popup showing the notification with the given ID. 34 - (NSUInteger)indexOfPopupWithNotificationID:(const std::string&)notificationID; 35 36 // Repositions all popup notifications if needed. 37 - (void)layoutNotifications; 38 39 // Fits as many new notifications as possible on screen. 40 - (void)layoutNewNotifications; 41 42 // Process notifications pending to remove when no animation is being played. 43 - (void)processPendingRemoveNotifications; 44 45 // Process notifications pending to update when no animation is being played. 46 - (void)processPendingUpdateNotifications; 47 @end 48 49 namespace { 50 51 class PopupCollectionObserver : public message_center::MessageCenterObserver { 52 public: 53 PopupCollectionObserver(message_center::MessageCenter* message_center, 54 MCPopupCollection* popup_collection) 55 : message_center_(message_center), 56 popup_collection_(popup_collection) { 57 message_center_->AddObserver(this); 58 } 59 60 virtual ~PopupCollectionObserver() { 61 message_center_->RemoveObserver(this); 62 } 63 64 virtual void OnNotificationAdded( 65 const std::string& notification_id) OVERRIDE { 66 [popup_collection_ layoutNewNotifications]; 67 } 68 69 virtual void OnNotificationRemoved(const std::string& notification_id, 70 bool user_id) OVERRIDE { 71 [popup_collection_ removeNotification:notification_id]; 72 } 73 74 virtual void OnNotificationUpdated( 75 const std::string& notification_id) OVERRIDE { 76 [popup_collection_ updateNotification:notification_id]; 77 } 78 79 private: 80 message_center::MessageCenter* message_center_; // Weak, global. 81 82 MCPopupCollection* popup_collection_; // Weak, owns this. 83 }; 84 85 } // namespace 86 87 @implementation MCPopupCollection 88 89 - (id)initWithMessageCenter:(message_center::MessageCenter*)messageCenter { 90 if ((self = [super init])) { 91 messageCenter_ = messageCenter; 92 observer_.reset(new PopupCollectionObserver(messageCenter_, self)); 93 popups_.reset([[NSMutableArray alloc] init]); 94 popupsBeingRemoved_.reset([[NSMutableArray alloc] init]); 95 popupAnimationDuration_ = kAnimationDuration; 96 } 97 return self; 98 } 99 100 - (void)dealloc { 101 [popupsBeingRemoved_ makeObjectsPerformSelector: 102 @selector(markPopupCollectionGone)]; 103 [self removeAllNotifications]; 104 [super dealloc]; 105 } 106 107 - (BOOL)isAnimating { 108 return !animatingNotificationIDs_.empty(); 109 } 110 111 - (NSTimeInterval)popupAnimationDuration { 112 return popupAnimationDuration_; 113 } 114 115 - (void)onPopupAnimationEnded:(const std::string&)notificationID { 116 NSUInteger index = [popupsBeingRemoved_ indexOfObjectPassingTest: 117 ^BOOL(id popup, NSUInteger index, BOOL* stop) { 118 return [popup notificationID] == notificationID; 119 }]; 120 if (index != NSNotFound) 121 [popupsBeingRemoved_ removeObjectAtIndex:index]; 122 123 animatingNotificationIDs_.erase(notificationID); 124 if (![self isAnimating]) 125 [self layoutNotifications]; 126 127 // Give the testing code a chance to do something, i.e. quitting the test 128 // run loop. 129 if (![self isAnimating] && testingAnimationEndedCallback_) 130 testingAnimationEndedCallback_.get()(); 131 } 132 133 // Testing API ///////////////////////////////////////////////////////////////// 134 135 - (NSArray*)popups { 136 return popups_.get(); 137 } 138 139 - (void)setScreenFrame:(NSRect)frame { 140 testingScreenFrame_ = frame; 141 } 142 143 - (void)setAnimationDuration:(NSTimeInterval)duration { 144 popupAnimationDuration_ = duration; 145 } 146 147 - (void)setAnimationEndedCallback: 148 (message_center::AnimationEndedCallback)callback { 149 testingAnimationEndedCallback_.reset(Block_copy(callback)); 150 } 151 152 // Private ///////////////////////////////////////////////////////////////////// 153 154 - (NSRect)screenFrame { 155 if (!NSIsEmptyRect(testingScreenFrame_)) 156 return testingScreenFrame_; 157 return [[[NSScreen screens] objectAtIndex:0] visibleFrame]; 158 } 159 160 - (BOOL)addNotification:(const message_center::Notification*)notification { 161 // Wait till all existing animations end. 162 if ([self isAnimating]) 163 return NO; 164 165 // The popup is owned by itself. It will be released at close. 166 MCPopupController* popup = 167 [[MCPopupController alloc] initWithNotification:notification 168 messageCenter:messageCenter_ 169 popupCollection:self]; 170 171 NSRect screenFrame = [self screenFrame]; 172 NSRect popupFrame = [popup bounds]; 173 174 CGFloat x = NSMaxX(screenFrame) - message_center::kMarginBetweenItems - 175 NSWidth(popupFrame); 176 CGFloat y = 0; 177 178 MCPopupController* bottomPopup = [popups_ lastObject]; 179 if (!bottomPopup) { 180 y = NSMaxY(screenFrame); 181 } else { 182 y = NSMinY([bottomPopup bounds]); 183 } 184 185 y -= message_center::kMarginBetweenItems + NSHeight(popupFrame); 186 187 if (y > NSMinY(screenFrame)) { 188 animatingNotificationIDs_.insert(notification->id()); 189 NSRect bounds = [popup bounds]; 190 bounds.origin.x = x; 191 bounds.origin.y = y; 192 [popup showWithAnimation:bounds]; 193 [popups_ addObject:popup]; 194 messageCenter_->DisplayedNotification( 195 notification->id(), message_center::DISPLAY_SOURCE_POPUP); 196 return YES; 197 } 198 199 // The popup cannot fit on screen, so it has to be released now. 200 [popup release]; 201 return NO; 202 } 203 204 - (void)updateNotification:(const std::string&)notificationID { 205 // The notification may not be on screen. Create it if needed. 206 if ([self indexOfPopupWithNotificationID:notificationID] == NSNotFound) { 207 [self layoutNewNotifications]; 208 return; 209 } 210 211 // Don't bother with the update if the notification is going to be removed. 212 if (pendingRemoveNotificationIDs_.find(notificationID) != 213 pendingRemoveNotificationIDs_.end()) { 214 return; 215 } 216 217 pendingUpdateNotificationIDs_.insert(notificationID); 218 [self processPendingUpdateNotifications]; 219 } 220 221 - (void)removeNotification:(const std::string&)notificationID { 222 // The notification may not be on screen. 223 if ([self indexOfPopupWithNotificationID:notificationID] == NSNotFound) 224 return; 225 226 // Don't bother with the update if the notification is going to be removed. 227 pendingUpdateNotificationIDs_.erase(notificationID); 228 229 pendingRemoveNotificationIDs_.insert(notificationID); 230 [self processPendingRemoveNotifications]; 231 } 232 233 - (void)removeAllNotifications { 234 // In rare cases, the popup collection would be gone while an animation is 235 // still playing. For exmaple, the test code could show a new notification 236 // and dispose the collection immediately. Close the popup without animation 237 // when this is the case. 238 if ([self isAnimating]) 239 [popups_ makeObjectsPerformSelector:@selector(close)]; 240 else 241 [popups_ makeObjectsPerformSelector:@selector(closeWithAnimation)]; 242 [popups_ makeObjectsPerformSelector:@selector(markPopupCollectionGone)]; 243 [popups_ removeAllObjects]; 244 } 245 246 - (NSUInteger)indexOfPopupWithNotificationID: 247 (const std::string&)notificationID { 248 return [popups_ indexOfObjectPassingTest: 249 ^BOOL(id popup, NSUInteger index, BOOL* stop) { 250 return [popup notificationID] == notificationID; 251 }]; 252 } 253 254 - (void)layoutNotifications { 255 // Wait till all existing animations end. 256 if ([self isAnimating]) 257 return; 258 259 NSRect screenFrame = [self screenFrame]; 260 261 // The popup starts at top-right corner. 262 CGFloat maxY = NSMaxY(screenFrame); 263 264 // Iterate all notifications and reposition each if needed. If one does not 265 // fit on screen, close it and any other on-screen popups that come after it. 266 NSUInteger removeAt = NSNotFound; 267 for (NSUInteger i = 0; i < [popups_ count]; ++i) { 268 MCPopupController* popup = [popups_ objectAtIndex:i]; 269 NSRect oldFrame = [popup bounds]; 270 NSRect frame = oldFrame; 271 frame.origin.y = maxY - message_center::kMarginBetweenItems - 272 NSHeight(frame); 273 274 // If this popup does not fit on screen, stop repositioning and close this 275 // and subsequent popups. 276 if (NSMinY(frame) < NSMinY(screenFrame)) { 277 removeAt = i; 278 break; 279 } 280 281 if (!NSEqualRects(frame, oldFrame)) { 282 [popup setBounds:frame]; 283 animatingNotificationIDs_.insert([popup notificationID]); 284 } 285 286 // Set the new maximum Y to be the bottom of this notification. 287 maxY = NSMinY(frame); 288 } 289 290 if (removeAt != NSNotFound) { 291 // Remove any popups that are on screen but no longer fit. 292 while ([popups_ count] >= removeAt && [popups_ count]) { 293 [[popups_ lastObject] close]; 294 [popups_ removeLastObject]; 295 } 296 } else { 297 [self layoutNewNotifications]; 298 } 299 300 [self processPendingRemoveNotifications]; 301 [self processPendingUpdateNotifications]; 302 } 303 304 - (void)layoutNewNotifications { 305 // Wait till all existing animations end. 306 if ([self isAnimating]) 307 return; 308 309 // Display any new popups that can now fit on screen, starting from the 310 // oldest notification that has not been shown up. 311 const auto& allPopups = messageCenter_->GetPopupNotifications(); 312 for (auto it = allPopups.rbegin(); it != allPopups.rend(); ++it) { 313 if ([self indexOfPopupWithNotificationID:(*it)->id()] == NSNotFound) { 314 // If there's no room left on screen to display notifications, stop 315 // trying. 316 if (![self addNotification:*it]) 317 break; 318 } 319 } 320 } 321 322 - (void)processPendingRemoveNotifications { 323 // Wait till all existing animations end. 324 if ([self isAnimating]) 325 return; 326 327 for (const auto& notificationID : pendingRemoveNotificationIDs_) { 328 NSUInteger index = [self indexOfPopupWithNotificationID:notificationID]; 329 if (index != NSNotFound) { 330 [[popups_ objectAtIndex:index] closeWithAnimation]; 331 animatingNotificationIDs_.insert(notificationID); 332 333 // Still need to track popup object and only remove it after the animation 334 // ends. We need to notify these objects that the collection is gone 335 // in the collection destructor. 336 [popupsBeingRemoved_ addObject:[popups_ objectAtIndex:index]]; 337 [popups_ removeObjectAtIndex:index]; 338 } 339 } 340 pendingRemoveNotificationIDs_.clear(); 341 } 342 343 - (void)processPendingUpdateNotifications { 344 // Wait till all existing animations end. 345 if ([self isAnimating]) 346 return; 347 348 if (pendingUpdateNotificationIDs_.empty()) 349 return; 350 351 // Go through all model objects in the message center. If there is a replaced 352 // notification, the controller's current model object may be stale. 353 const auto& modelPopups = messageCenter_->GetPopupNotifications(); 354 for (auto iter = modelPopups.begin(); iter != modelPopups.end(); ++iter) { 355 const std::string& notificationID = (*iter)->id(); 356 357 // Does the notification need to be updated? 358 std::set<std::string>::iterator pendingUpdateIter = 359 pendingUpdateNotificationIDs_.find(notificationID); 360 if (pendingUpdateIter == pendingUpdateNotificationIDs_.end()) 361 continue; 362 pendingUpdateNotificationIDs_.erase(pendingUpdateIter); 363 364 // Is the notification still on screen? 365 NSUInteger index = [self indexOfPopupWithNotificationID:notificationID]; 366 if (index == NSNotFound) 367 continue; 368 369 MCPopupController* popup = [popups_ objectAtIndex:index]; 370 371 CGFloat oldHeight = 372 NSHeight([[[popup notificationController] view] frame]); 373 CGFloat newHeight = NSHeight( 374 [[popup notificationController] updateNotification:*iter]); 375 376 // The notification has changed height. This requires updating the popup 377 // window. 378 if (oldHeight != newHeight) { 379 NSRect popupFrame = [popup bounds]; 380 popupFrame.origin.y -= newHeight - oldHeight; 381 popupFrame.size.height += newHeight - oldHeight; 382 [popup setBounds:popupFrame]; 383 animatingNotificationIDs_.insert([popup notificationID]); 384 } 385 } 386 387 // Notification update could be received when a notification is excluded from 388 // the popup notification list but still remains in the full notification 389 // list, as in clicking the popup. In that case, the popup should be closed. 390 for (auto iter = pendingUpdateNotificationIDs_.begin(); 391 iter != pendingUpdateNotificationIDs_.end(); ++iter) { 392 pendingRemoveNotificationIDs_.insert(*iter); 393 } 394 395 pendingUpdateNotificationIDs_.clear(); 396 397 // Start re-layout of all notifications, so that it readjusts the Y origin of 398 // all updated popups and any popups that come below them. 399 [self layoutNotifications]; 400 } 401 402 @end 403