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(
    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