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.
5 #import "ui/message_center/cocoa/popup_collection.h"
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"
13 const float kAnimationDuration = 0.2;
15 @interface MCPopupCollection (Private)
16 // Returns the primary screen's visible frame rectangle.
17 - (NSRect)screenFrame;
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;
23 // Updates the contents of the notification with the given ID.
24 - (void)updateNotification:(const std::string&)notificationID;
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;
30 // Closes all the popups.
31 - (void)removeAllNotifications;
33 // Returns the index of the popup showing the notification with the given ID.
34 - (NSUInteger)indexOfPopupWithNotificationID:(const std::string&)notificationID;
36 // Repositions all popup notifications if needed.
37 - (void)layoutNotifications;
39 // Fits as many new notifications as possible on screen.
40 - (void)layoutNewNotifications;
42 // Process notifications pending to remove when no animation is being played.
43 - (void)processPendingRemoveNotifications;
45 // Process notifications pending to update when no animation is being played.
46 - (void)processPendingUpdateNotifications;
51 class PopupCollectionObserver : public message_center::MessageCenterObserver {
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);
60 virtual ~PopupCollectionObserver() {
61 message_center_->RemoveObserver(this);
64 virtual void OnNotificationAdded(
65 const std::string& notification_id) OVERRIDE {
66 [popup_collection_ layoutNewNotifications];
69 virtual void OnNotificationRemoved(const std::string& notification_id,
70 bool user_id) OVERRIDE {
71 [popup_collection_ removeNotification:notification_id];
74 virtual void OnNotificationUpdated(
75 const std::string& notification_id) OVERRIDE {
76 [popup_collection_ updateNotification:notification_id];
80 message_center::MessageCenter* message_center_; // Weak, global.
82 MCPopupCollection* popup_collection_; // Weak, owns this.
87 @implementation MCPopupCollection
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;
101 [popupsBeingRemoved_ makeObjectsPerformSelector:
102 @selector(markPopupCollectionGone)];
103 [self removeAllNotifications];
107 - (BOOL)isAnimating {
108 return !animatingNotificationIDs_.empty();
111 - (NSTimeInterval)popupAnimationDuration {
112 return popupAnimationDuration_;
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;
120 if (index != NSNotFound)
121 [popupsBeingRemoved_ removeObjectAtIndex:index];
123 animatingNotificationIDs_.erase(notificationID);
124 if (![self isAnimating])
125 [self layoutNotifications];
127 // Give the testing code a chance to do something, i.e. quitting the test
129 if (![self isAnimating] && testingAnimationEndedCallback_)
130 testingAnimationEndedCallback_.get()();
133 // Testing API /////////////////////////////////////////////////////////////////
136 return popups_.get();
139 - (void)setScreenFrame:(NSRect)frame {
140 testingScreenFrame_ = frame;
143 - (void)setAnimationDuration:(NSTimeInterval)duration {
144 popupAnimationDuration_ = duration;
147 - (void)setAnimationEndedCallback:
148 (message_center::AnimationEndedCallback)callback {
149 testingAnimationEndedCallback_.reset(Block_copy(callback));
152 // Private /////////////////////////////////////////////////////////////////////
154 - (NSRect)screenFrame {
155 if (!NSIsEmptyRect(testingScreenFrame_))
156 return testingScreenFrame_;
157 return [[[NSScreen screens] objectAtIndex:0] visibleFrame];
160 - (BOOL)addNotification:(const message_center::Notification*)notification {
161 // Wait till all existing animations end.
162 if ([self isAnimating])
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];
171 NSRect screenFrame = [self screenFrame];
172 NSRect popupFrame = [popup bounds];
174 CGFloat x = NSMaxX(screenFrame) - message_center::kMarginBetweenItems -
178 MCPopupController* bottomPopup = [popups_ lastObject];
180 y = NSMaxY(screenFrame);
182 y = NSMinY([bottomPopup bounds]);
185 y -= message_center::kMarginBetweenItems + NSHeight(popupFrame);
187 if (y > NSMinY(screenFrame)) {
188 animatingNotificationIDs_.insert(notification->id());
189 NSRect bounds = [popup bounds];
192 [popup showWithAnimation:bounds];
193 [popups_ addObject:popup];
194 messageCenter_->DisplayedNotification(
195 notification->id(), message_center::DISPLAY_SOURCE_POPUP);
199 // The popup cannot fit on screen, so it has to be released now.
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];
211 // Don't bother with the update if the notification is going to be removed.
212 if (pendingRemoveNotificationIDs_.find(notificationID) !=
213 pendingRemoveNotificationIDs_.end()) {
217 pendingUpdateNotificationIDs_.insert(notificationID);
218 [self processPendingUpdateNotifications];
221 - (void)removeNotification:(const std::string&)notificationID {
222 // The notification may not be on screen.
223 if ([self indexOfPopupWithNotificationID:notificationID] == NSNotFound)
226 // Don't bother with the update if the notification is going to be removed.
227 pendingUpdateNotificationIDs_.erase(notificationID);
229 pendingRemoveNotificationIDs_.insert(notificationID);
230 [self processPendingRemoveNotifications];
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)];
241 [popups_ makeObjectsPerformSelector:@selector(closeWithAnimation)];
242 [popups_ makeObjectsPerformSelector:@selector(markPopupCollectionGone)];
243 [popups_ removeAllObjects];
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;
254 - (void)layoutNotifications {
255 // Wait till all existing animations end.
256 if ([self isAnimating])
259 NSRect screenFrame = [self screenFrame];
261 // The popup starts at top-right corner.
262 CGFloat maxY = NSMaxY(screenFrame);
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 -
274 // If this popup does not fit on screen, stop repositioning and close this
275 // and subsequent popups.
276 if (NSMinY(frame) < NSMinY(screenFrame)) {
281 if (!NSEqualRects(frame, oldFrame)) {
282 [popup setBounds:frame];
283 animatingNotificationIDs_.insert([popup notificationID]);
286 // Set the new maximum Y to be the bottom of this notification.
287 maxY = NSMinY(frame);
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];
297 [self layoutNewNotifications];
300 [self processPendingRemoveNotifications];
301 [self processPendingUpdateNotifications];
304 - (void)layoutNewNotifications {
305 // Wait till all existing animations end.
306 if ([self isAnimating])
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
316 if (![self addNotification:*it])
322 - (void)processPendingRemoveNotifications {
323 // Wait till all existing animations end.
324 if ([self isAnimating])
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);
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];
340 pendingRemoveNotificationIDs_.clear();
343 - (void)processPendingUpdateNotifications {
344 // Wait till all existing animations end.
345 if ([self isAnimating])
348 if (pendingUpdateNotificationIDs_.empty())
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();
357 // Does the notification need to be updated?
358 std::set<std::string>::iterator pendingUpdateIter =
359 pendingUpdateNotificationIDs_.find(notificationID);
360 if (pendingUpdateIter == pendingUpdateNotificationIDs_.end())
362 pendingUpdateNotificationIDs_.erase(pendingUpdateIter);
364 // Is the notification still on screen?
365 NSUInteger index = [self indexOfPopupWithNotificationID:notificationID];
366 if (index == NSNotFound)
369 MCPopupController* popup = [popups_ objectAtIndex:index];
372 NSHeight([[[popup notificationController] view] frame]);
373 CGFloat newHeight = NSHeight(
374 [[popup notificationController] updateNotification:*iter]);
376 // The notification has changed height. This requires updating the popup
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]);
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);
395 pendingUpdateNotificationIDs_.clear();
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];