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(notification->id()); 195 return YES; 196 } 197 198 // The popup cannot fit on screen, so it has to be released now. 199 [popup release]; 200 return NO; 201 } 202 203 - (void)updateNotification:(const std::string&)notificationID { 204 // The notification may not be on screen. 205 if ([self indexOfPopupWithNotificationID:notificationID] == NSNotFound) 206 return; 207 208 // Don't bother with the update if the notification is going to be removed. 209 if (pendingRemoveNotificationIDs_.find(notificationID) != 210 pendingRemoveNotificationIDs_.end()) { 211 return; 212 } 213 214 pendingUpdateNotificationIDs_.insert(notificationID); 215 [self processPendingUpdateNotifications]; 216 } 217 218 - (void)removeNotification:(const std::string&)notificationID { 219 // The notification may not be on screen. 220 if ([self indexOfPopupWithNotificationID:notificationID] == NSNotFound) 221 return; 222 223 // Don't bother with the update if the notification is going to be removed. 224 pendingUpdateNotificationIDs_.erase(notificationID); 225 226 pendingRemoveNotificationIDs_.insert(notificationID); 227 [self processPendingRemoveNotifications]; 228 } 229 230 - (void)removeAllNotifications { 231 // In rare cases, the popup collection would be gone while an animation is 232 // still playing. For exmaple, the test code could show a new notification 233 // and dispose the collection immediately. Close the popup without animation 234 // when this is the case. 235 if ([self isAnimating]) 236 [popups_ makeObjectsPerformSelector:@selector(close)]; 237 else 238 [popups_ makeObjectsPerformSelector:@selector(closeWithAnimation)]; 239 [popups_ makeObjectsPerformSelector:@selector(markPopupCollectionGone)]; 240 [popups_ removeAllObjects]; 241 } 242 243 - (NSUInteger)indexOfPopupWithNotificationID: 244 (const std::string&)notificationID { 245 return [popups_ indexOfObjectPassingTest: 246 ^BOOL(id popup, NSUInteger index, BOOL* stop) { 247 return [popup notificationID] == notificationID; 248 }]; 249 } 250 251 - (void)layoutNotifications { 252 // Wait till all existing animations end. 253 if ([self isAnimating]) 254 return; 255 256 NSRect screenFrame = [self screenFrame]; 257 258 // The popup starts at top-right corner. 259 CGFloat maxY = NSMaxY(screenFrame); 260 261 // Iterate all notifications and reposition each if needed. If one does not 262 // fit on screen, close it and any other on-screen popups that come after it. 263 NSUInteger removeAt = NSNotFound; 264 for (NSUInteger i = 0; i < [popups_ count]; ++i) { 265 MCPopupController* popup = [popups_ objectAtIndex:i]; 266 NSRect oldFrame = [popup bounds]; 267 NSRect frame = oldFrame; 268 frame.origin.y = maxY - message_center::kMarginBetweenItems - 269 NSHeight(frame); 270 271 // If this popup does not fit on screen, stop repositioning and close this 272 // and subsequent popups. 273 if (NSMinY(frame) < NSMinY(screenFrame)) { 274 removeAt = i; 275 break; 276 } 277 278 if (!NSEqualRects(frame, oldFrame)) { 279 [popup setBounds:frame]; 280 animatingNotificationIDs_.insert([popup notificationID]); 281 } 282 283 // Set the new maximum Y to be the bottom of this notification. 284 maxY = NSMinY(frame); 285 } 286 287 if (removeAt != NSNotFound) { 288 // Remove any popups that are on screen but no longer fit. 289 while ([popups_ count] >= removeAt && [popups_ count]) { 290 [[popups_ lastObject] close]; 291 [popups_ removeLastObject]; 292 } 293 } else { 294 [self layoutNewNotifications]; 295 } 296 297 [self processPendingRemoveNotifications]; 298 [self processPendingUpdateNotifications]; 299 } 300 301 - (void)layoutNewNotifications { 302 // Wait till all existing animations end. 303 if ([self isAnimating]) 304 return; 305 306 // Display any new popups that can now fit on screen, starting from the 307 // oldest notification that has not been shown up. 308 const auto& allPopups = messageCenter_->GetPopupNotifications(); 309 for (auto it = allPopups.rbegin(); it != allPopups.rend(); ++it) { 310 if ([self indexOfPopupWithNotificationID:(*it)->id()] == NSNotFound) { 311 // If there's no room left on screen to display notifications, stop 312 // trying. 313 if (![self addNotification:*it]) 314 break; 315 } 316 } 317 } 318 319 - (void)processPendingRemoveNotifications { 320 // Wait till all existing animations end. 321 if ([self isAnimating]) 322 return; 323 324 for (const auto& notificationID : pendingRemoveNotificationIDs_) { 325 NSUInteger index = [self indexOfPopupWithNotificationID:notificationID]; 326 if (index != NSNotFound) { 327 [[popups_ objectAtIndex:index] closeWithAnimation]; 328 animatingNotificationIDs_.insert(notificationID); 329 330 // Still need to track popup object and only remove it after the animation 331 // ends. We need to notify these objects that the collection is gone 332 // in the collection destructor. 333 [popupsBeingRemoved_ addObject:[popups_ objectAtIndex:index]]; 334 [popups_ removeObjectAtIndex:index]; 335 } 336 } 337 pendingRemoveNotificationIDs_.clear(); 338 } 339 340 - (void)processPendingUpdateNotifications { 341 // Wait till all existing animations end. 342 if ([self isAnimating]) 343 return; 344 345 if (pendingUpdateNotificationIDs_.empty()) 346 return; 347 348 // Go through all model objects in the message center. If there is a replaced 349 // notification, the controller's current model object may be stale. 350 const auto& modelPopups = messageCenter_->GetPopupNotifications(); 351 for (auto iter = modelPopups.begin(); iter != modelPopups.end(); ++iter) { 352 const std::string& notificationID = (*iter)->id(); 353 354 // Does the notification need to be updated? 355 std::set<std::string>::iterator pendingUpdateIter = 356 pendingUpdateNotificationIDs_.find(notificationID); 357 if (pendingUpdateIter == pendingUpdateNotificationIDs_.end()) 358 continue; 359 pendingUpdateNotificationIDs_.erase(pendingUpdateIter); 360 361 // Is the notification still on screen? 362 NSUInteger index = [self indexOfPopupWithNotificationID:notificationID]; 363 if (index == NSNotFound) 364 continue; 365 366 MCPopupController* popup = [popups_ objectAtIndex:index]; 367 368 CGFloat oldHeight = 369 NSHeight([[[popup notificationController] view] frame]); 370 CGFloat newHeight = NSHeight( 371 [[popup notificationController] updateNotification:*iter]); 372 373 // The notification has changed height. This requires updating the popup 374 // window. 375 if (oldHeight != newHeight) { 376 NSRect popupFrame = [popup bounds]; 377 popupFrame.origin.y -= newHeight - oldHeight; 378 popupFrame.size.height += newHeight - oldHeight; 379 [popup setBounds:popupFrame]; 380 animatingNotificationIDs_.insert([popup notificationID]); 381 } 382 } 383 384 // Notification update could be received when a notification is excluded from 385 // the popup notification list but still remains in the full notification 386 // list, as in clicking the popup. In that case, the popup should be closed. 387 for (auto iter = pendingUpdateNotificationIDs_.begin(); 388 iter != pendingUpdateNotificationIDs_.end(); ++iter) { 389 pendingRemoveNotificationIDs_.insert(*iter); 390 } 391 392 pendingUpdateNotificationIDs_.clear(); 393 394 // Start re-layout of all notifications, so that it readjusts the Y origin of 395 // all updated popups and any popups that come below them. 396 [self layoutNotifications]; 397 } 398 399 @end 400