• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
13const 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
49namespace {
50
51class 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(
195        notification->id(), message_center::DISPLAY_SOURCE_POPUP);
196    return YES;
197  }
198
199  // The popup cannot fit on screen, so it has to be released now.
200  [popup release];
201  return NO;
202}
203
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  }
210
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  }
216
217  pendingUpdateNotificationIDs_.insert(notificationID);
218  [self processPendingUpdateNotifications];
219}
220
221- (void)removeNotification:(const std::string&)notificationID {
222  // The notification may not be on screen.
223  if ([self indexOfPopupWithNotificationID:notificationID] == NSNotFound)
224    return;
225
226  // Don't bother with the update if the notification is going to be removed.
227  pendingUpdateNotificationIDs_.erase(notificationID);
228
229  pendingRemoveNotificationIDs_.insert(notificationID);
230  [self processPendingRemoveNotifications];
231}
232
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];
244}
245
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      }];
252}
253
254- (void)layoutNotifications {
255  // Wait till all existing animations end.
256  if ([self isAnimating])
257    return;
258
259  NSRect screenFrame = [self screenFrame];
260
261  // The popup starts at top-right corner.
262  CGFloat maxY = NSMaxY(screenFrame);
263
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);
273
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    }
280
281    if (!NSEqualRects(frame, oldFrame)) {
282      [popup setBounds:frame];
283      animatingNotificationIDs_.insert([popup notificationID]);
284    }
285
286    // Set the new maximum Y to be the bottom of this notification.
287    maxY = NSMinY(frame);
288  }
289
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  }
299
300  [self processPendingRemoveNotifications];
301  [self processPendingUpdateNotifications];
302}
303
304- (void)layoutNewNotifications {
305  // Wait till all existing animations end.
306  if ([self isAnimating])
307    return;
308
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  }
320}
321
322- (void)processPendingRemoveNotifications {
323  // Wait till all existing animations end.
324  if ([self isAnimating])
325    return;
326
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);
332
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();
341}
342
343- (void)processPendingUpdateNotifications {
344  // Wait till all existing animations end.
345  if ([self isAnimating])
346    return;
347
348  if (pendingUpdateNotificationIDs_.empty())
349    return;
350
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();
356
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);
363
364    // Is the notification still on screen?
365    NSUInteger index = [self indexOfPopupWithNotificationID:notificationID];
366    if (index == NSNotFound)
367      continue;
368
369    MCPopupController* popup = [popups_ objectAtIndex:index];
370
371    CGFloat oldHeight =
372        NSHeight([[[popup notificationController] view] frame]);
373    CGFloat newHeight = NSHeight(
374        [[popup notificationController] updateNotification:*iter]);
375
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  }
386
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  }
394
395  pendingUpdateNotificationIDs_.clear();
396
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];
400}
401
402@end
403