Add ICU message format support
[chromium-blink-merge.git] / ui / message_center / cocoa / popup_collection.mm
blob5961e61409b7eb244259e8a598784dd0ecc815f2
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   ~PopupCollectionObserver() override { message_center_->RemoveObserver(this); }
62   void OnNotificationAdded(const std::string& notification_id) override {
63     [popup_collection_ layoutNewNotifications];
64   }
66   void OnNotificationRemoved(const std::string& notification_id,
67                              bool user_id) override {
68     [popup_collection_ removeNotification:notification_id];
69   }
71   void OnNotificationUpdated(const std::string& notification_id) override {
72     [popup_collection_ updateNotification:notification_id];
73   }
75  private:
76   message_center::MessageCenter* message_center_;  // Weak, global.
78   MCPopupCollection* popup_collection_;  // Weak, owns this.
81 }  // namespace
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;
92   }
93   return self;
96 - (void)dealloc {
97   [popupsBeingRemoved_ makeObjectsPerformSelector:
98       @selector(markPopupCollectionGone)];
99   [self removeAllNotifications];
100   [super dealloc];
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;
115       }];
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
124   // run loop.
125   if (![self isAnimating] && testingAnimationEndedCallback_)
126     testingAnimationEndedCallback_.get()();
129 // Testing API /////////////////////////////////////////////////////////////////
131 - (NSArray*)popups {
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])
159     return NO;
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 -
171       NSWidth(popupFrame);
172   CGFloat y = 0;
174   MCPopupController* bottomPopup = [popups_ lastObject];
175   if (!bottomPopup) {
176     y = NSMaxY(screenFrame);
177   } else {
178     y = NSMinY([bottomPopup bounds]);
179   }
181   y -= message_center::kMarginBetweenItems + NSHeight(popupFrame);
183   if (y > NSMinY(screenFrame)) {
184     animatingNotificationIDs_.insert(notification->id());
185     NSRect bounds = [popup bounds];
186     bounds.origin.x = x;
187     bounds.origin.y = y;
188     [popup showWithAnimation:bounds];
189     [popups_ addObject:popup];
190     messageCenter_->DisplayedNotification(
191         notification->id(), message_center::DISPLAY_SOURCE_POPUP);
192     return YES;
193   }
195   // The popup cannot fit on screen, so it has to be closed now.
196   [popup close];
197   return NO;
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];
204     return;
205   }
207   // Don't bother with the update if the notification is going to be removed.
208   if (pendingRemoveNotificationIDs_.find(notificationID) !=
209           pendingRemoveNotificationIDs_.end()) {
210     return;
211   }
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)
220     return;
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)];
236   else
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;
247       }];
250 - (void)layoutNotifications {
251   // Wait till all existing animations end.
252   if ([self isAnimating])
253     return;
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 -
268                      NSHeight(frame);
270     // If this popup does not fit on screen, stop repositioning and close this
271     // and subsequent popups.
272     if (NSMinY(frame) < NSMinY(screenFrame)) {
273       removeAt = i;
274       break;
275     }
277     if (!NSEqualRects(frame, oldFrame)) {
278       [popup setBounds:frame];
279       animatingNotificationIDs_.insert([popup notificationID]);
280     }
282     // Set the new maximum Y to be the bottom of this notification.
283     maxY = NSMinY(frame);
284   }
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];
291     }
292   } else {
293     [self layoutNewNotifications];
294   }
296   [self processPendingRemoveNotifications];
297   [self processPendingUpdateNotifications];
300 - (void)layoutNewNotifications {
301   // Wait till all existing animations end.
302   if ([self isAnimating])
303     return;
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
311       // trying.
312       if (![self addNotification:*it])
313         break;
314     }
315   }
318 - (void)processPendingRemoveNotifications {
319   // Wait till all existing animations end.
320   if ([self isAnimating])
321     return;
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];
334     }
335   }
336   pendingRemoveNotificationIDs_.clear();
339 - (void)processPendingUpdateNotifications {
340   // Wait till all existing animations end.
341   if ([self isAnimating])
342     return;
344   if (pendingUpdateNotificationIDs_.empty())
345     return;
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())
357       continue;
358     pendingUpdateNotificationIDs_.erase(pendingUpdateIter);
360     // Is the notification still on screen?
361     NSUInteger index = [self indexOfPopupWithNotificationID:notificationID];
362     if (index == NSNotFound)
363       continue;
365     MCPopupController* popup = [popups_ objectAtIndex:index];
367     CGFloat oldHeight =
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
373     // window.
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]);
380     }
381   }
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);
389   }
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];
398 @end