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 ~PopupCollectionObserver() override { message_center_->RemoveObserver(this); }
62 void OnNotificationAdded(const std::string& notification_id) override {
63 [popup_collection_ layoutNewNotifications];
66 void OnNotificationRemoved(const std::string& notification_id,
67 bool user_id) override {
68 [popup_collection_ removeNotification:notification_id];
71 void OnNotificationUpdated(const std::string& notification_id) override {
72 [popup_collection_ updateNotification:notification_id];
76 message_center::MessageCenter* message_center_; // Weak, global.
78 MCPopupCollection* popup_collection_; // Weak, owns this.
83 @implementation MCPopupCollection
85 - (id)initWithMessageCenter:(message_center::MessageCenter*)messageCenter {
86 if ((self = [super init])) {
87 messageCenter_ = messageCenter;
88 observer_.reset(new PopupCollectionObserver(messageCenter_, self));
89 popups_.reset([[NSMutableArray alloc] init]);
90 popupsBeingRemoved_.reset([[NSMutableArray alloc] init]);
91 popupAnimationDuration_ = kAnimationDuration;
97 [popupsBeingRemoved_ makeObjectsPerformSelector:
98 @selector(markPopupCollectionGone)];
99 [self removeAllNotifications];
103 - (BOOL)isAnimating {
104 return !animatingNotificationIDs_.empty();
107 - (NSTimeInterval)popupAnimationDuration {
108 return popupAnimationDuration_;
111 - (void)onPopupAnimationEnded:(const std::string&)notificationID {
112 NSUInteger index = [popupsBeingRemoved_ indexOfObjectPassingTest:
113 ^BOOL(id popup, NSUInteger index, BOOL* stop) {
114 return [popup notificationID] == notificationID;
116 if (index != NSNotFound)
117 [popupsBeingRemoved_ removeObjectAtIndex:index];
119 animatingNotificationIDs_.erase(notificationID);
120 if (![self isAnimating])
121 [self layoutNotifications];
123 // Give the testing code a chance to do something, i.e. quitting the test
125 if (![self isAnimating] && testingAnimationEndedCallback_)
126 testingAnimationEndedCallback_.get()();
129 // Testing API /////////////////////////////////////////////////////////////////
132 return popups_.get();
135 - (void)setScreenFrame:(NSRect)frame {
136 testingScreenFrame_ = frame;
139 - (void)setAnimationDuration:(NSTimeInterval)duration {
140 popupAnimationDuration_ = duration;
143 - (void)setAnimationEndedCallback:
144 (message_center::AnimationEndedCallback)callback {
145 testingAnimationEndedCallback_.reset(Block_copy(callback));
148 // Private /////////////////////////////////////////////////////////////////////
150 - (NSRect)screenFrame {
151 if (!NSIsEmptyRect(testingScreenFrame_))
152 return testingScreenFrame_;
153 return [[[NSScreen screens] objectAtIndex:0] visibleFrame];
156 - (BOOL)addNotification:(const message_center::Notification*)notification {
157 // Wait till all existing animations end.
158 if ([self isAnimating])
161 // The popup is owned by itself. It will be released at close.
162 MCPopupController* popup =
163 [[MCPopupController alloc] initWithNotification:notification
164 messageCenter:messageCenter_
165 popupCollection:self];
167 NSRect screenFrame = [self screenFrame];
168 NSRect popupFrame = [popup bounds];
170 CGFloat x = NSMaxX(screenFrame) - message_center::kMarginBetweenItems -
174 MCPopupController* bottomPopup = [popups_ lastObject];
176 y = NSMaxY(screenFrame);
178 y = NSMinY([bottomPopup bounds]);
181 y -= message_center::kMarginBetweenItems + NSHeight(popupFrame);
183 if (y > NSMinY(screenFrame)) {
184 animatingNotificationIDs_.insert(notification->id());
185 NSRect bounds = [popup bounds];
188 [popup showWithAnimation:bounds];
189 [popups_ addObject:popup];
190 messageCenter_->DisplayedNotification(
191 notification->id(), message_center::DISPLAY_SOURCE_POPUP);
195 // The popup cannot fit on screen, so it has to be released now.
200 - (void)updateNotification:(const std::string&)notificationID {
201 // The notification may not be on screen. Create it if needed.
202 if ([self indexOfPopupWithNotificationID:notificationID] == NSNotFound) {
203 [self layoutNewNotifications];
207 // Don't bother with the update if the notification is going to be removed.
208 if (pendingRemoveNotificationIDs_.find(notificationID) !=
209 pendingRemoveNotificationIDs_.end()) {
213 pendingUpdateNotificationIDs_.insert(notificationID);
214 [self processPendingUpdateNotifications];
217 - (void)removeNotification:(const std::string&)notificationID {
218 // The notification may not be on screen.
219 if ([self indexOfPopupWithNotificationID:notificationID] == NSNotFound)
222 // Don't bother with the update if the notification is going to be removed.
223 pendingUpdateNotificationIDs_.erase(notificationID);
225 pendingRemoveNotificationIDs_.insert(notificationID);
226 [self processPendingRemoveNotifications];
229 - (void)removeAllNotifications {
230 // In rare cases, the popup collection would be gone while an animation is
231 // still playing. For exmaple, the test code could show a new notification
232 // and dispose the collection immediately. Close the popup without animation
233 // when this is the case.
234 if ([self isAnimating])
235 [popups_ makeObjectsPerformSelector:@selector(close)];
237 [popups_ makeObjectsPerformSelector:@selector(closeWithAnimation)];
238 [popups_ makeObjectsPerformSelector:@selector(markPopupCollectionGone)];
239 [popups_ removeAllObjects];
242 - (NSUInteger)indexOfPopupWithNotificationID:
243 (const std::string&)notificationID {
244 return [popups_ indexOfObjectPassingTest:
245 ^BOOL(id popup, NSUInteger index, BOOL* stop) {
246 return [popup notificationID] == notificationID;
250 - (void)layoutNotifications {
251 // Wait till all existing animations end.
252 if ([self isAnimating])
255 NSRect screenFrame = [self screenFrame];
257 // The popup starts at top-right corner.
258 CGFloat maxY = NSMaxY(screenFrame);
260 // Iterate all notifications and reposition each if needed. If one does not
261 // fit on screen, close it and any other on-screen popups that come after it.
262 NSUInteger removeAt = NSNotFound;
263 for (NSUInteger i = 0; i < [popups_ count]; ++i) {
264 MCPopupController* popup = [popups_ objectAtIndex:i];
265 NSRect oldFrame = [popup bounds];
266 NSRect frame = oldFrame;
267 frame.origin.y = maxY - message_center::kMarginBetweenItems -
270 // If this popup does not fit on screen, stop repositioning and close this
271 // and subsequent popups.
272 if (NSMinY(frame) < NSMinY(screenFrame)) {
277 if (!NSEqualRects(frame, oldFrame)) {
278 [popup setBounds:frame];
279 animatingNotificationIDs_.insert([popup notificationID]);
282 // Set the new maximum Y to be the bottom of this notification.
283 maxY = NSMinY(frame);
286 if (removeAt != NSNotFound) {
287 // Remove any popups that are on screen but no longer fit.
288 while ([popups_ count] >= removeAt && [popups_ count]) {
289 [[popups_ lastObject] close];
290 [popups_ removeLastObject];
293 [self layoutNewNotifications];
296 [self processPendingRemoveNotifications];
297 [self processPendingUpdateNotifications];
300 - (void)layoutNewNotifications {
301 // Wait till all existing animations end.
302 if ([self isAnimating])
305 // Display any new popups that can now fit on screen, starting from the
306 // oldest notification that has not been shown up.
307 const auto& allPopups = messageCenter_->GetPopupNotifications();
308 for (auto it = allPopups.rbegin(); it != allPopups.rend(); ++it) {
309 if ([self indexOfPopupWithNotificationID:(*it)->id()] == NSNotFound) {
310 // If there's no room left on screen to display notifications, stop
312 if (![self addNotification:*it])
318 - (void)processPendingRemoveNotifications {
319 // Wait till all existing animations end.
320 if ([self isAnimating])
323 for (const auto& notificationID : pendingRemoveNotificationIDs_) {
324 NSUInteger index = [self indexOfPopupWithNotificationID:notificationID];
325 if (index != NSNotFound) {
326 [[popups_ objectAtIndex:index] closeWithAnimation];
327 animatingNotificationIDs_.insert(notificationID);
329 // Still need to track popup object and only remove it after the animation
330 // ends. We need to notify these objects that the collection is gone
331 // in the collection destructor.
332 [popupsBeingRemoved_ addObject:[popups_ objectAtIndex:index]];
333 [popups_ removeObjectAtIndex:index];
336 pendingRemoveNotificationIDs_.clear();
339 - (void)processPendingUpdateNotifications {
340 // Wait till all existing animations end.
341 if ([self isAnimating])
344 if (pendingUpdateNotificationIDs_.empty())
347 // Go through all model objects in the message center. If there is a replaced
348 // notification, the controller's current model object may be stale.
349 const auto& modelPopups = messageCenter_->GetPopupNotifications();
350 for (auto iter = modelPopups.begin(); iter != modelPopups.end(); ++iter) {
351 const std::string& notificationID = (*iter)->id();
353 // Does the notification need to be updated?
354 std::set<std::string>::iterator pendingUpdateIter =
355 pendingUpdateNotificationIDs_.find(notificationID);
356 if (pendingUpdateIter == pendingUpdateNotificationIDs_.end())
358 pendingUpdateNotificationIDs_.erase(pendingUpdateIter);
360 // Is the notification still on screen?
361 NSUInteger index = [self indexOfPopupWithNotificationID:notificationID];
362 if (index == NSNotFound)
365 MCPopupController* popup = [popups_ objectAtIndex:index];
368 NSHeight([[[popup notificationController] view] frame]);
369 CGFloat newHeight = NSHeight(
370 [[popup notificationController] updateNotification:*iter]);
372 // The notification has changed height. This requires updating the popup
374 if (oldHeight != newHeight) {
375 NSRect popupFrame = [popup bounds];
376 popupFrame.origin.y -= newHeight - oldHeight;
377 popupFrame.size.height += newHeight - oldHeight;
378 [popup setBounds:popupFrame];
379 animatingNotificationIDs_.insert([popup notificationID]);
383 // Notification update could be received when a notification is excluded from
384 // the popup notification list but still remains in the full notification
385 // list, as in clicking the popup. In that case, the popup should be closed.
386 for (auto iter = pendingUpdateNotificationIDs_.begin();
387 iter != pendingUpdateNotificationIDs_.end(); ++iter) {
388 pendingRemoveNotificationIDs_.insert(*iter);
391 pendingUpdateNotificationIDs_.clear();
393 // Start re-layout of all notifications, so that it readjusts the Y origin of
394 // all updated popups and any popups that come below them.
395 [self layoutNotifications];