Home | History | Annotate | Download | only in cocoa
      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 
     13 const 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 
     49 namespace {
     50 
     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   }
     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(notification->id());
    195     return YES;
    196   }
    197 
    198   // The popup cannot fit on screen, so it has to be released now.
    199   [popup release];
    200   return NO;
    201 }
    202 
    203 - (void)updateNotification:(const std::string&)notificationID {
    204   // The notification may not be on screen.
    205   if ([self indexOfPopupWithNotificationID:notificationID] == NSNotFound)
    206     return;
    207 
    208   // Don't bother with the update if the notification is going to be removed.
    209   if (pendingRemoveNotificationIDs_.find(notificationID) !=
    210           pendingRemoveNotificationIDs_.end()) {
    211     return;
    212   }
    213 
    214   pendingUpdateNotificationIDs_.insert(notificationID);
    215   [self processPendingUpdateNotifications];
    216 }
    217 
    218 - (void)removeNotification:(const std::string&)notificationID {
    219   // The notification may not be on screen.
    220   if ([self indexOfPopupWithNotificationID:notificationID] == NSNotFound)
    221     return;
    222 
    223   // Don't bother with the update if the notification is going to be removed.
    224   pendingUpdateNotificationIDs_.erase(notificationID);
    225 
    226   pendingRemoveNotificationIDs_.insert(notificationID);
    227   [self processPendingRemoveNotifications];
    228 }
    229 
    230 - (void)removeAllNotifications {
    231   // In rare cases, the popup collection would be gone while an animation is
    232   // still playing. For exmaple, the test code could show a new notification
    233   // and dispose the collection immediately. Close the popup without animation
    234   // when this is the case.
    235   if ([self isAnimating])
    236     [popups_ makeObjectsPerformSelector:@selector(close)];
    237   else
    238     [popups_ makeObjectsPerformSelector:@selector(closeWithAnimation)];
    239   [popups_ makeObjectsPerformSelector:@selector(markPopupCollectionGone)];
    240   [popups_ removeAllObjects];
    241 }
    242 
    243 - (NSUInteger)indexOfPopupWithNotificationID:
    244     (const std::string&)notificationID {
    245   return [popups_ indexOfObjectPassingTest:
    246       ^BOOL(id popup, NSUInteger index, BOOL* stop) {
    247           return [popup notificationID] == notificationID;
    248       }];
    249 }
    250 
    251 - (void)layoutNotifications {
    252   // Wait till all existing animations end.
    253   if ([self isAnimating])
    254     return;
    255 
    256   NSRect screenFrame = [self screenFrame];
    257 
    258   // The popup starts at top-right corner.
    259   CGFloat maxY = NSMaxY(screenFrame);
    260 
    261   // Iterate all notifications and reposition each if needed. If one does not
    262   // fit on screen, close it and any other on-screen popups that come after it.
    263   NSUInteger removeAt = NSNotFound;
    264   for (NSUInteger i = 0; i < [popups_ count]; ++i) {
    265     MCPopupController* popup = [popups_ objectAtIndex:i];
    266     NSRect oldFrame = [popup bounds];
    267     NSRect frame = oldFrame;
    268     frame.origin.y = maxY - message_center::kMarginBetweenItems -
    269                      NSHeight(frame);
    270 
    271     // If this popup does not fit on screen, stop repositioning and close this
    272     // and subsequent popups.
    273     if (NSMinY(frame) < NSMinY(screenFrame)) {
    274       removeAt = i;
    275       break;
    276     }
    277 
    278     if (!NSEqualRects(frame, oldFrame)) {
    279       [popup setBounds:frame];
    280       animatingNotificationIDs_.insert([popup notificationID]);
    281     }
    282 
    283     // Set the new maximum Y to be the bottom of this notification.
    284     maxY = NSMinY(frame);
    285   }
    286 
    287   if (removeAt != NSNotFound) {
    288     // Remove any popups that are on screen but no longer fit.
    289     while ([popups_ count] >= removeAt && [popups_ count]) {
    290       [[popups_ lastObject] close];
    291       [popups_ removeLastObject];
    292     }
    293   } else {
    294     [self layoutNewNotifications];
    295   }
    296 
    297   [self processPendingRemoveNotifications];
    298   [self processPendingUpdateNotifications];
    299 }
    300 
    301 - (void)layoutNewNotifications {
    302   // Wait till all existing animations end.
    303   if ([self isAnimating])
    304     return;
    305 
    306   // Display any new popups that can now fit on screen, starting from the
    307   // oldest notification that has not been shown up.
    308   const auto& allPopups = messageCenter_->GetPopupNotifications();
    309   for (auto it = allPopups.rbegin(); it != allPopups.rend(); ++it) {
    310     if ([self indexOfPopupWithNotificationID:(*it)->id()] == NSNotFound) {
    311       // If there's no room left on screen to display notifications, stop
    312       // trying.
    313       if (![self addNotification:*it])
    314         break;
    315     }
    316   }
    317 }
    318 
    319 - (void)processPendingRemoveNotifications {
    320   // Wait till all existing animations end.
    321   if ([self isAnimating])
    322     return;
    323 
    324   for (const auto& notificationID : pendingRemoveNotificationIDs_) {
    325     NSUInteger index = [self indexOfPopupWithNotificationID:notificationID];
    326     if (index != NSNotFound) {
    327       [[popups_ objectAtIndex:index] closeWithAnimation];
    328       animatingNotificationIDs_.insert(notificationID);
    329 
    330       // Still need to track popup object and only remove it after the animation
    331       // ends. We need to notify these objects that the collection is gone
    332       // in the collection destructor.
    333       [popupsBeingRemoved_ addObject:[popups_ objectAtIndex:index]];
    334       [popups_ removeObjectAtIndex:index];
    335     }
    336   }
    337   pendingRemoveNotificationIDs_.clear();
    338 }
    339 
    340 - (void)processPendingUpdateNotifications {
    341   // Wait till all existing animations end.
    342   if ([self isAnimating])
    343     return;
    344 
    345   if (pendingUpdateNotificationIDs_.empty())
    346     return;
    347 
    348   // Go through all model objects in the message center. If there is a replaced
    349   // notification, the controller's current model object may be stale.
    350   const auto& modelPopups = messageCenter_->GetPopupNotifications();
    351   for (auto iter = modelPopups.begin(); iter != modelPopups.end(); ++iter) {
    352     const std::string& notificationID = (*iter)->id();
    353 
    354     // Does the notification need to be updated?
    355     std::set<std::string>::iterator pendingUpdateIter =
    356         pendingUpdateNotificationIDs_.find(notificationID);
    357     if (pendingUpdateIter == pendingUpdateNotificationIDs_.end())
    358       continue;
    359     pendingUpdateNotificationIDs_.erase(pendingUpdateIter);
    360 
    361     // Is the notification still on screen?
    362     NSUInteger index = [self indexOfPopupWithNotificationID:notificationID];
    363     if (index == NSNotFound)
    364       continue;
    365 
    366     MCPopupController* popup = [popups_ objectAtIndex:index];
    367 
    368     CGFloat oldHeight =
    369         NSHeight([[[popup notificationController] view] frame]);
    370     CGFloat newHeight = NSHeight(
    371         [[popup notificationController] updateNotification:*iter]);
    372 
    373     // The notification has changed height. This requires updating the popup
    374     // window.
    375     if (oldHeight != newHeight) {
    376       NSRect popupFrame = [popup bounds];
    377       popupFrame.origin.y -= newHeight - oldHeight;
    378       popupFrame.size.height += newHeight - oldHeight;
    379       [popup setBounds:popupFrame];
    380       animatingNotificationIDs_.insert([popup notificationID]);
    381     }
    382   }
    383 
    384   // Notification update could be received when a notification is excluded from
    385   // the popup notification list but still remains in the full notification
    386   // list, as in clicking the popup. In that case, the popup should be closed.
    387   for (auto iter = pendingUpdateNotificationIDs_.begin();
    388        iter != pendingUpdateNotificationIDs_.end(); ++iter) {
    389     pendingRemoveNotificationIDs_.insert(*iter);
    390   }
    391 
    392   pendingUpdateNotificationIDs_.clear();
    393 
    394   // Start re-layout of all notifications, so that it readjusts the Y origin of
    395   // all updated popups and any popups that come below them.
    396   [self layoutNotifications];
    397 }
    398 
    399 @end
    400