Update path of checkdeps to buildtools checkout
[chromium-blink-merge.git] / ui / message_center / cocoa / popup_collection.mm
blobadc4f6216d6a7e020870ac4ff2ead79db224d62a
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;
47 @end
49 namespace {
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   }
60   virtual ~PopupCollectionObserver() {
61     message_center_->RemoveObserver(this);
62   }
64   virtual void OnNotificationAdded(
65       const std::string& notification_id) OVERRIDE {
66     [popup_collection_ layoutNewNotifications];
67   }
69   virtual void OnNotificationRemoved(const std::string& notification_id,
70                                      bool user_id) OVERRIDE {
71     [popup_collection_ removeNotification:notification_id];
72   }
74   virtual void OnNotificationUpdated(
75       const std::string& notification_id) OVERRIDE {
76     [popup_collection_ updateNotification:notification_id];
77   }
79  private:
80   message_center::MessageCenter* message_center_;  // Weak, global.
82   MCPopupCollection* popup_collection_;  // Weak, owns this.
85 }  // namespace
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;
96   }
97   return self;
100 - (void)dealloc {
101   [popupsBeingRemoved_ makeObjectsPerformSelector:
102       @selector(markPopupCollectionGone)];
103   [self removeAllNotifications];
104   [super dealloc];
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;
119       }];
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
128   // run loop.
129   if (![self isAnimating] && testingAnimationEndedCallback_)
130     testingAnimationEndedCallback_.get()();
133 // Testing API /////////////////////////////////////////////////////////////////
135 - (NSArray*)popups {
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])
163     return NO;
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 -
175       NSWidth(popupFrame);
176   CGFloat y = 0;
178   MCPopupController* bottomPopup = [popups_ lastObject];
179   if (!bottomPopup) {
180     y = NSMaxY(screenFrame);
181   } else {
182     y = NSMinY([bottomPopup bounds]);
183   }
185   y -= message_center::kMarginBetweenItems + NSHeight(popupFrame);
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   }
199   // The popup cannot fit on screen, so it has to be released now.
200   [popup release];
201   return NO;
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   }
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   }
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)
224     return;
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)];
240   else
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;
251       }];
254 - (void)layoutNotifications {
255   // Wait till all existing animations end.
256   if ([self isAnimating])
257     return;
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 -
272                      NSHeight(frame);
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     }
281     if (!NSEqualRects(frame, oldFrame)) {
282       [popup setBounds:frame];
283       animatingNotificationIDs_.insert([popup notificationID]);
284     }
286     // Set the new maximum Y to be the bottom of this notification.
287     maxY = NSMinY(frame);
288   }
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   }
300   [self processPendingRemoveNotifications];
301   [self processPendingUpdateNotifications];
304 - (void)layoutNewNotifications {
305   // Wait till all existing animations end.
306   if ([self isAnimating])
307     return;
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   }
322 - (void)processPendingRemoveNotifications {
323   // Wait till all existing animations end.
324   if ([self isAnimating])
325     return;
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];
338     }
339   }
340   pendingRemoveNotificationIDs_.clear();
343 - (void)processPendingUpdateNotifications {
344   // Wait till all existing animations end.
345   if ([self isAnimating])
346     return;
348   if (pendingUpdateNotificationIDs_.empty())
349     return;
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())
361       continue;
362     pendingUpdateNotificationIDs_.erase(pendingUpdateIter);
364     // Is the notification still on screen?
365     NSUInteger index = [self indexOfPopupWithNotificationID:notificationID];
366     if (index == NSNotFound)
367       continue;
369     MCPopupController* popup = [popups_ objectAtIndex:index];
371     CGFloat oldHeight =
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
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   }
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   }
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];
402 @end