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/tray_view_controller.h"
      6 
      7 #include <cmath>
      8 
      9 #include "base/mac/scoped_nsautorelease_pool.h"
     10 #include "base/time/time.h"
     11 #include "grit/ui_resources.h"
     12 #include "grit/ui_strings.h"
     13 #include "skia/ext/skia_utils_mac.h"
     14 #import "ui/base/cocoa/hover_image_button.h"
     15 #include "ui/base/l10n/l10n_util_mac.h"
     16 #include "ui/base/resource/resource_bundle.h"
     17 #import "ui/message_center/cocoa/opaque_views.h"
     18 #import "ui/message_center/cocoa/notification_controller.h"
     19 #import "ui/message_center/cocoa/settings_controller.h"
     20 #include "ui/message_center/message_center.h"
     21 #include "ui/message_center/message_center_style.h"
     22 #include "ui/message_center/notifier_settings.h"
     23 
     24 const int kBackButtonSize = 16;
     25 
     26 // NSClipView subclass.
     27 @interface MCClipView : NSClipView {
     28   // If this is set, the visible document area will remain intact no matter how
     29   // the user scrolls or drags the thumb.
     30   BOOL frozen_;
     31 }
     32 @end
     33 
     34 @implementation MCClipView
     35 - (void)setFrozen:(BOOL)frozen {
     36   frozen_ = frozen;
     37 }
     38 
     39 - (NSPoint)constrainScrollPoint:(NSPoint)proposedNewOrigin {
     40   return frozen_ ? [self documentVisibleRect].origin :
     41       [super constrainScrollPoint:proposedNewOrigin];
     42 }
     43 @end
     44 
     45 @interface MCTrayViewController (Private)
     46 // Creates all the views for the control area of the tray.
     47 - (void)layoutControlArea;
     48 
     49 // Update both tray view and window by resizing it to fit its content.
     50 - (void)updateTrayViewAndWindow;
     51 
     52 // Remove notifications dismissed by the user. It is done in the following
     53 // 3 steps.
     54 - (void)closeNotificationsByUser;
     55 
     56 // Step 1: hide all notifications pending removal with fade-out animation.
     57 - (void)hideNotificationsPendingRemoval;
     58 
     59 // Step 2: move up all remaining notifications to take over the available space
     60 // due to hiding notifications. The scroll view and the window remain unchanged.
     61 - (void)moveUpRemainingNotifications;
     62 
     63 // Step 3: finalize the tray view and window to get rid of the empty space.
     64 - (void)finalizeTrayViewAndWindow;
     65 
     66 // Clear a notification by sliding it out from left to right. This occurs when
     67 // "Clear All" is clicked.
     68 - (void)clearOneNotification;
     69 
     70 // When all visible notifications slide out, re-enable controls and remove
     71 // notifications from the message center.
     72 - (void)finalizeClearAll;
     73 
     74 // Sets the images of the quiet mode button based on the message center state.
     75 - (void)updateQuietModeButtonImage;
     76 @end
     77 
     78 namespace {
     79 
     80 // The duration of fade-out and bounds animation.
     81 const NSTimeInterval kAnimationDuration = 0.2;
     82 
     83 // The delay to start animating clearing next notification since current
     84 // animation starts.
     85 const NSTimeInterval kAnimateClearingNextNotificationDelay = 0.04;
     86 
     87 // The height of the bar at the top of the tray that contains buttons.
     88 const CGFloat kControlAreaHeight = 50;
     89 
     90 // Amount of spacing between control buttons. There is kMarginBetweenItems
     91 // between a button and the edge of the tray, though.
     92 const CGFloat kButtonXMargin = 20;
     93 
     94 // Amount of padding to leave between the bottom of the screen and the bottom
     95 // of the message center tray.
     96 const CGFloat kTrayBottomMargin = 75;
     97 
     98 }  // namespace
     99 
    100 @implementation MCTrayViewController
    101 
    102 - (id)initWithMessageCenter:(message_center::MessageCenter*)messageCenter {
    103   if ((self = [super initWithNibName:nil bundle:nil])) {
    104     messageCenter_ = messageCenter;
    105     animationDuration_ = kAnimationDuration;
    106     animateClearingNextNotificationDelay_ =
    107         kAnimateClearingNextNotificationDelay;
    108     notifications_.reset([[NSMutableArray alloc] init]);
    109     notificationsPendingRemoval_.reset([[NSMutableArray alloc] init]);
    110   }
    111   return self;
    112 }
    113 
    114 - (NSString*)trayTitle {
    115   return [title_ stringValue];
    116 }
    117 
    118 - (void)setTrayTitle:(NSString*)title {
    119   [title_ setStringValue:title];
    120   [title_ sizeToFit];
    121 }
    122 
    123 - (void)onWindowClosing {
    124   if (animation_) {
    125     [animation_ stopAnimation];
    126     [animation_ setDelegate:nil];
    127     animation_.reset();
    128   }
    129   if (clearAllInProgress_) {
    130     // To stop chain of clearOneNotification calls to start new animations.
    131     [NSObject cancelPreviousPerformRequestsWithTarget:self];
    132 
    133     for (NSViewAnimation* animation in clearAllAnimations_.get()) {
    134       [animation stopAnimation];
    135       [animation setDelegate:nil];
    136     }
    137     [clearAllAnimations_ removeAllObjects];
    138     [self finalizeClearAll];
    139   }
    140 }
    141 
    142 - (void)loadView {
    143   // Configure the root view as a background-colored box.
    144   base::scoped_nsobject<NSBox> view([[NSBox alloc] initWithFrame:NSMakeRect(
    145       0, 0, [MCTrayViewController trayWidth], kControlAreaHeight)]);
    146   [view setBorderType:NSNoBorder];
    147   [view setBoxType:NSBoxCustom];
    148   [view setContentViewMargins:NSZeroSize];
    149   [view setFillColor:gfx::SkColorToCalibratedNSColor(
    150       message_center::kMessageCenterBackgroundColor)];
    151   [view setTitlePosition:NSNoTitle];
    152   [view setWantsLayer:YES];  // Needed for notification view shadows.
    153   [self setView:view];
    154 
    155   [self layoutControlArea];
    156 
    157   // Configure the scroll view in which all the notifications go.
    158   base::scoped_nsobject<NSView> documentView(
    159       [[NSView alloc] initWithFrame:NSZeroRect]);
    160   scrollView_.reset([[NSScrollView alloc] initWithFrame:[view frame]]);
    161   clipView_.reset(
    162       [[MCClipView alloc] initWithFrame:[[scrollView_ contentView] frame]]);
    163   [scrollView_ setContentView:clipView_];
    164   [scrollView_ setAutohidesScrollers:YES];
    165   [scrollView_ setAutoresizingMask:NSViewHeightSizable | NSViewMaxYMargin];
    166   [scrollView_ setDocumentView:documentView];
    167   [scrollView_ setDrawsBackground:NO];
    168   [scrollView_ setHasHorizontalScroller:NO];
    169   [scrollView_ setHasVerticalScroller:YES];
    170   [view addSubview:scrollView_];
    171 
    172   [self onMessageCenterTrayChanged];
    173 }
    174 
    175 - (void)onMessageCenterTrayChanged {
    176   if (settingsController_)
    177     return [self updateTrayViewAndWindow];
    178 
    179   std::map<std::string, MCNotificationController*> newMap;
    180 
    181   base::scoped_nsobject<NSShadow> shadow([[NSShadow alloc] init]);
    182   [shadow setShadowColor:[NSColor colorWithDeviceWhite:0 alpha:0.55]];
    183   [shadow setShadowOffset:NSMakeSize(0, -1)];
    184   [shadow setShadowBlurRadius:2.0];
    185 
    186   CGFloat minY = message_center::kMarginBetweenItems;
    187 
    188   // Iterate over the notifications in reverse, since the Cocoa coordinate
    189   // origin is in the lower-left. Remove from |notificationsMap_| all the
    190   // ones still in the updated model, so that those that should be removed
    191   // will remain in the map.
    192   const auto& modelNotifications = messageCenter_->GetVisibleNotifications();
    193   for (auto it = modelNotifications.rbegin();
    194        it != modelNotifications.rend();
    195        ++it) {
    196     // Check if this notification is already in the tray.
    197     const auto& existing = notificationsMap_.find((*it)->id());
    198     MCNotificationController* notification = nil;
    199     if (existing == notificationsMap_.end()) {
    200       base::scoped_nsobject<MCNotificationController> controller(
    201           [[MCNotificationController alloc]
    202               initWithNotification:*it
    203                      messageCenter:messageCenter_]);
    204       [[controller view] setShadow:shadow];
    205       [[scrollView_ documentView] addSubview:[controller view]];
    206 
    207       [notifications_ addObject:controller];  // Transfer ownership.
    208       messageCenter_->DisplayedNotification(
    209           (*it)->id(), message_center::DISPLAY_SOURCE_MESSAGE_CENTER);
    210 
    211       notification = controller.get();
    212     } else {
    213       notification = existing->second;
    214       [notification updateNotification:*it];
    215       notificationsMap_.erase(existing);
    216     }
    217 
    218     DCHECK(notification);
    219 
    220     NSRect frame = [[notification view] frame];
    221     frame.origin.x = message_center::kMarginBetweenItems;
    222     frame.origin.y = minY;
    223     [[notification view] setFrame:frame];
    224 
    225     newMap.insert(std::make_pair((*it)->id(), notification));
    226 
    227     minY = NSMaxY(frame) + message_center::kMarginBetweenItems;
    228   }
    229 
    230   // Remove any notifications that are no longer in the model.
    231   for (const auto& pair : notificationsMap_) {
    232     [[pair.second view] removeFromSuperview];
    233     [notifications_ removeObject:pair.second];
    234   }
    235 
    236   // Copy the new map of notifications to replace the old.
    237   notificationsMap_ = newMap;
    238 
    239   [self updateTrayViewAndWindow];
    240 }
    241 
    242 - (void)toggleQuietMode:(id)sender {
    243   if (messageCenter_->IsQuietMode())
    244     messageCenter_->SetQuietMode(false);
    245   else
    246     messageCenter_->EnterQuietModeWithExpire(base::TimeDelta::FromDays(1));
    247 
    248   [self updateQuietModeButtonImage];
    249 }
    250 
    251 - (void)clearAllNotifications:(id)sender {
    252   if ([self isAnimating]) {
    253     clearAllDelayed_ = YES;
    254     return;
    255   }
    256 
    257   // Build a list for all notifications within the visible scroll range
    258   // in preparation to slide them out one by one.
    259   NSRect visibleScrollRect = [scrollView_ documentVisibleRect];
    260   for (MCNotificationController* notification in notifications_.get()) {
    261     NSRect rect = [[notification view] frame];
    262     if (!NSIsEmptyRect(NSIntersectionRect(visibleScrollRect, rect))) {
    263       visibleNotificationsPendingClear_.push_back(notification);
    264     }
    265   }
    266   if (visibleNotificationsPendingClear_.empty())
    267     return;
    268 
    269   // Disbale buttons and freeze scroll bar to prevent the user from clicking on
    270   // them accidentally.
    271   [pauseButton_ setEnabled:NO];
    272   [clearAllButton_ setEnabled:NO];
    273   [settingsButton_ setEnabled:NO];
    274   [clipView_ setFrozen:YES];
    275 
    276   // Start sliding out the top notification.
    277   clearAllAnimations_.reset([[NSMutableArray alloc] init]);
    278   [self clearOneNotification];
    279 
    280   clearAllInProgress_ = YES;
    281 }
    282 
    283 - (void)showSettings:(id)sender {
    284   if (settingsController_)
    285     return [self showMessages:sender];
    286 
    287   message_center::NotifierSettingsProvider* provider =
    288       messageCenter_->GetNotifierSettingsProvider();
    289   settingsController_.reset(
    290       [[MCSettingsController alloc] initWithProvider:provider
    291                                   trayViewController:self]);
    292 
    293   [[self view] addSubview:[settingsController_ view]];
    294 
    295   NSRect titleFrame = [title_ frame];
    296   titleFrame.origin.x =
    297       NSMaxX([backButton_ frame]) + message_center::kMarginBetweenItems / 2;
    298   [title_ setFrame:titleFrame];
    299   [backButton_ setHidden:NO];
    300   [clearAllButton_ setEnabled:NO];
    301 
    302   [scrollView_ setHidden:YES];
    303 
    304   [[[self view] window] recalculateKeyViewLoop];
    305   messageCenter_->SetVisibility(message_center::VISIBILITY_SETTINGS);
    306 
    307   [self updateTrayViewAndWindow];
    308 }
    309 
    310 - (void)updateSettings {
    311   // TODO(jianli): This class should not be calling -loadView, but instead
    312   // should just observe a resize notification.
    313   // (http://crbug.com/270251)
    314   [[settingsController_ view] removeFromSuperview];
    315   [settingsController_ loadView];
    316   [[self view] addSubview:[settingsController_ view]];
    317 
    318   [self updateTrayViewAndWindow];
    319 }
    320 
    321 - (void)showMessages:(id)sender {
    322   messageCenter_->SetVisibility(message_center::VISIBILITY_MESSAGE_CENTER);
    323   [self cleanupSettings];
    324   [[[self view] window] recalculateKeyViewLoop];
    325   [self updateTrayViewAndWindow];
    326 }
    327 
    328 - (void)cleanupSettings {
    329   [scrollView_ setHidden:NO];
    330 
    331   [[settingsController_ view] removeFromSuperview];
    332   settingsController_.reset();
    333 
    334   NSRect titleFrame = [title_ frame];
    335   titleFrame.origin.x = NSMinX([backButton_ frame]);
    336   [title_ setFrame:titleFrame];
    337   [backButton_ setHidden:YES];
    338   [clearAllButton_ setEnabled:YES];
    339 
    340 }
    341 
    342 - (void)scrollToTop {
    343   NSPoint topPoint =
    344       NSMakePoint(0.0, [[scrollView_ documentView] bounds].size.height);
    345   [[scrollView_ documentView] scrollPoint:topPoint];
    346 }
    347 
    348 - (BOOL)isAnimating {
    349   return [animation_ isAnimating] || [clearAllAnimations_ count];
    350 }
    351 
    352 + (CGFloat)maxTrayClientHeight {
    353   NSRect screenFrame = [[[NSScreen screens] objectAtIndex:0] visibleFrame];
    354   return NSHeight(screenFrame) - kTrayBottomMargin - kControlAreaHeight;
    355 }
    356 
    357 + (CGFloat)trayWidth {
    358   return message_center::kNotificationWidth +
    359          2 * message_center::kMarginBetweenItems;
    360 }
    361 
    362 // Testing API /////////////////////////////////////////////////////////////////
    363 
    364 - (NSBox*)divider {
    365   return divider_.get();
    366 }
    367 
    368 - (NSTextField*)emptyDescription {
    369   return emptyDescription_.get();
    370 }
    371 
    372 - (NSScrollView*)scrollView {
    373   return scrollView_.get();
    374 }
    375 
    376 - (HoverImageButton*)pauseButton {
    377   return pauseButton_.get();
    378 }
    379 
    380 - (HoverImageButton*)clearAllButton {
    381   return clearAllButton_.get();
    382 }
    383 
    384 - (void)setAnimationDuration:(NSTimeInterval)duration {
    385   animationDuration_ = duration;
    386 }
    387 
    388 - (void)setAnimateClearingNextNotificationDelay:(NSTimeInterval)delay {
    389   animateClearingNextNotificationDelay_ = delay;
    390 }
    391 
    392 - (void)setAnimationEndedCallback:
    393     (message_center::TrayAnimationEndedCallback)callback {
    394   testingAnimationEndedCallback_.reset(Block_copy(callback));
    395 }
    396 
    397 // Private /////////////////////////////////////////////////////////////////////
    398 
    399 - (void)layoutControlArea {
    400   ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
    401   NSView* view = [self view];
    402 
    403   // Create the "Notifications" label at the top of the tray.
    404   NSFont* font = [NSFont labelFontOfSize:message_center::kTitleFontSize];
    405   NSColor* color = gfx::SkColorToCalibratedNSColor(
    406       message_center::kMessageCenterBackgroundColor);
    407   title_.reset(
    408       [[MCTextField alloc] initWithFrame:NSZeroRect backgroundColor:color]);
    409 
    410   [title_ setFont:font];
    411   [title_ setStringValue:
    412       l10n_util::GetNSString(IDS_MESSAGE_CENTER_FOOTER_TITLE)];
    413   [title_ setTextColor:gfx::SkColorToCalibratedNSColor(
    414       message_center::kRegularTextColor)];
    415   [title_ sizeToFit];
    416 
    417   NSRect titleFrame = [title_ frame];
    418   titleFrame.origin.x = message_center::kMarginBetweenItems;
    419   titleFrame.origin.y = kControlAreaHeight/2 - NSMidY(titleFrame);
    420   [title_ setFrame:titleFrame];
    421   [view addSubview:title_];
    422 
    423   auto configureButton = ^(HoverImageButton* button) {
    424       [[button cell] setHighlightsBy:NSOnState];
    425       [button setTrackingEnabled:YES];
    426       [button setBordered:NO];
    427       [button setAutoresizingMask:NSViewMinYMargin];
    428       [button setTarget:self];
    429   };
    430 
    431   // Back button. On top of the "Notifications" label, hidden by default.
    432   NSRect backButtonFrame =
    433       NSMakeRect(NSMinX(titleFrame),
    434                  (kControlAreaHeight - kBackButtonSize) / 2,
    435                  kBackButtonSize,
    436                  kBackButtonSize);
    437   backButton_.reset([[HoverImageButton alloc] initWithFrame:backButtonFrame]);
    438   [backButton_ setDefaultImage:
    439       rb.GetNativeImageNamed(IDR_NOTIFICATION_ARROW).ToNSImage()];
    440   [backButton_ setHoverImage:
    441       rb.GetNativeImageNamed(IDR_NOTIFICATION_ARROW_HOVER).ToNSImage()];
    442   [backButton_ setPressedImage:
    443       rb.GetNativeImageNamed(IDR_NOTIFICATION_ARROW_PRESSED).ToNSImage()];
    444   [backButton_ setAction:@selector(showMessages:)];
    445   configureButton(backButton_);
    446   [backButton_ setHidden:YES];
    447   [backButton_ setKeyEquivalent:@"\e"];
    448   [backButton_ setToolTip:l10n_util::GetNSString(
    449       IDS_MESSAGE_CENTER_SETTINGS_GO_BACK_BUTTON_TOOLTIP)];
    450   [[backButton_ cell]
    451       accessibilitySetOverrideValue:[backButton_ toolTip]
    452                        forAttribute:NSAccessibilityDescriptionAttribute];
    453   [[self view] addSubview:backButton_];
    454 
    455   // Create the divider line between the control area and the notifications.
    456   divider_.reset(
    457       [[NSBox alloc] initWithFrame:NSMakeRect(0, 0, NSWidth([view frame]), 1)]);
    458   [divider_ setAutoresizingMask:NSViewMinYMargin];
    459   [divider_ setBorderType:NSNoBorder];
    460   [divider_ setBoxType:NSBoxCustom];
    461   [divider_ setContentViewMargins:NSZeroSize];
    462   [divider_ setFillColor:gfx::SkColorToCalibratedNSColor(
    463       message_center::kFooterDelimiterColor)];
    464   [divider_ setTitlePosition:NSNoTitle];
    465   [view addSubview:divider_];
    466 
    467 
    468   auto getButtonFrame = ^NSRect(CGFloat maxX, NSImage* image) {
    469       NSSize size = [image size];
    470       return NSMakeRect(
    471           maxX - size.width,
    472           kControlAreaHeight/2 - size.height/2,
    473           size.width,
    474           size.height);
    475   };
    476 
    477   // Create the settings button at the far-right.
    478   NSImage* defaultImage =
    479       rb.GetNativeImageNamed(IDR_NOTIFICATION_SETTINGS).ToNSImage();
    480   NSRect settingsButtonFrame = getButtonFrame(
    481       NSWidth([view frame]) - message_center::kMarginBetweenItems,
    482       defaultImage);
    483   settingsButton_.reset(
    484       [[HoverImageButton alloc] initWithFrame:settingsButtonFrame]);
    485   [settingsButton_ setDefaultImage:defaultImage];
    486   [settingsButton_ setHoverImage:
    487       rb.GetNativeImageNamed(IDR_NOTIFICATION_SETTINGS_HOVER).ToNSImage()];
    488   [settingsButton_ setPressedImage:
    489       rb.GetNativeImageNamed(IDR_NOTIFICATION_SETTINGS_PRESSED).ToNSImage()];
    490   [settingsButton_ setToolTip:
    491       l10n_util::GetNSString(IDS_MESSAGE_CENTER_SETTINGS_BUTTON_LABEL)];
    492   [[settingsButton_ cell]
    493       accessibilitySetOverrideValue:[settingsButton_ toolTip]
    494                        forAttribute:NSAccessibilityDescriptionAttribute];
    495   [settingsButton_ setAction:@selector(showSettings:)];
    496   configureButton(settingsButton_);
    497   [view addSubview:settingsButton_];
    498 
    499   // Create the clear all button.
    500   defaultImage = rb.GetNativeImageNamed(IDR_NOTIFICATION_CLEAR_ALL).ToNSImage();
    501   NSRect clearAllButtonFrame = getButtonFrame(
    502       NSMinX(settingsButtonFrame) - kButtonXMargin,
    503       defaultImage);
    504   clearAllButton_.reset(
    505       [[HoverImageButton alloc] initWithFrame:clearAllButtonFrame]);
    506   [clearAllButton_ setDefaultImage:defaultImage];
    507   [clearAllButton_ setHoverImage:
    508       rb.GetNativeImageNamed(IDR_NOTIFICATION_CLEAR_ALL_HOVER).ToNSImage()];
    509   [clearAllButton_ setPressedImage:
    510       rb.GetNativeImageNamed(IDR_NOTIFICATION_CLEAR_ALL_PRESSED).ToNSImage()];
    511   [clearAllButton_ setToolTip:
    512       l10n_util::GetNSString(IDS_MESSAGE_CENTER_CLEAR_ALL)];
    513   [[clearAllButton_ cell]
    514       accessibilitySetOverrideValue:[clearAllButton_ toolTip]
    515                        forAttribute:NSAccessibilityDescriptionAttribute];
    516   [clearAllButton_ setAction:@selector(clearAllNotifications:)];
    517   configureButton(clearAllButton_);
    518   [view addSubview:clearAllButton_];
    519 
    520   // Create the pause button.
    521   NSRect pauseButtonFrame = getButtonFrame(
    522       NSMinX(clearAllButtonFrame) - kButtonXMargin,
    523       defaultImage);
    524   pauseButton_.reset([[HoverImageButton alloc] initWithFrame:pauseButtonFrame]);
    525   [self updateQuietModeButtonImage];
    526   [pauseButton_ setHoverImage: rb.GetNativeImageNamed(
    527       IDR_NOTIFICATION_DO_NOT_DISTURB_HOVER).ToNSImage()];
    528   [pauseButton_ setToolTip:
    529       l10n_util::GetNSString(IDS_MESSAGE_CENTER_QUIET_MODE_BUTTON_TOOLTIP)];
    530   [[pauseButton_ cell]
    531       accessibilitySetOverrideValue:[pauseButton_ toolTip]
    532                        forAttribute:NSAccessibilityDescriptionAttribute];
    533   [pauseButton_ setAction:@selector(toggleQuietMode:)];
    534   configureButton(pauseButton_);
    535   [view addSubview:pauseButton_];
    536 
    537   // Create the description field for the empty message center.  Initially it is
    538   // invisible.
    539   emptyDescription_.reset(
    540       [[MCTextField alloc] initWithFrame:NSZeroRect backgroundColor:color]);
    541 
    542   NSFont* smallFont =
    543       [NSFont labelFontOfSize:message_center::kEmptyCenterFontSize];
    544   [emptyDescription_ setFont:smallFont];
    545   [emptyDescription_ setStringValue:
    546       l10n_util::GetNSString(IDS_MESSAGE_CENTER_NO_MESSAGES)];
    547   [emptyDescription_ setTextColor:gfx::SkColorToCalibratedNSColor(
    548       message_center::kDimTextColor)];
    549   [emptyDescription_ sizeToFit];
    550   [emptyDescription_ setHidden:YES];
    551 
    552   [view addSubview:emptyDescription_];
    553 }
    554 
    555 - (void)updateTrayViewAndWindow {
    556   CGFloat scrollContentHeight = message_center::kMinScrollViewHeight;
    557   if ([notifications_ count]) {
    558     [emptyDescription_ setHidden:YES];
    559     [scrollView_ setHidden:NO];
    560     [divider_ setHidden:NO];
    561     scrollContentHeight = NSMaxY([[[notifications_ lastObject] view] frame]) +
    562         message_center::kMarginBetweenItems;;
    563   } else {
    564     [emptyDescription_ setHidden:NO];
    565     [scrollView_ setHidden:YES];
    566     [divider_ setHidden:YES];
    567 
    568     NSRect centeredFrame = [emptyDescription_ frame];
    569     NSPoint centeredOrigin = NSMakePoint(
    570       floor((NSWidth([[self view] frame]) - NSWidth(centeredFrame))/2 + 0.5),
    571       floor((scrollContentHeight - NSHeight(centeredFrame))/2 + 0.5));
    572 
    573     centeredFrame.origin = centeredOrigin;
    574     [emptyDescription_ setFrame:centeredFrame];
    575   }
    576 
    577   // Resize the scroll view's content.
    578   NSRect scrollViewFrame = [scrollView_ frame];
    579   NSRect documentFrame = [[scrollView_ documentView] frame];
    580   documentFrame.size.width = NSWidth(scrollViewFrame);
    581   documentFrame.size.height = scrollContentHeight;
    582   [[scrollView_ documentView] setFrame:documentFrame];
    583 
    584   // Resize the container view.
    585   NSRect frame = [[self view] frame];
    586   CGFloat oldHeight = NSHeight(frame);
    587   if (settingsController_) {
    588     frame.size.height = NSHeight([[settingsController_ view] frame]);
    589   } else {
    590     frame.size.height = std::min([MCTrayViewController maxTrayClientHeight],
    591                                  scrollContentHeight);
    592   }
    593   frame.size.height += kControlAreaHeight;
    594   CGFloat newHeight = NSHeight(frame);
    595   [[self view] setFrame:frame];
    596 
    597   // Resize the scroll view.
    598   scrollViewFrame.size.height = NSHeight(frame) - kControlAreaHeight;
    599   [scrollView_ setFrame:scrollViewFrame];
    600 
    601   // Resize the window.
    602   NSRect windowFrame = [[[self view] window] frame];
    603   CGFloat delta = newHeight - oldHeight;
    604   windowFrame.origin.y -= delta;
    605   windowFrame.size.height += delta;
    606 
    607   [[[self view] window] setFrame:windowFrame display:YES];
    608   // Hide the clear-all button if there are no notifications. Simply swap the
    609   // X position of it and the pause button in that case.
    610   BOOL hidden = ![notifications_ count];
    611   if ([clearAllButton_ isHidden] != hidden) {
    612     [clearAllButton_ setHidden:hidden];
    613 
    614     NSRect pauseButtonFrame = [pauseButton_ frame];
    615     NSRect clearAllButtonFrame = [clearAllButton_ frame];
    616     std::swap(clearAllButtonFrame.origin.x, pauseButtonFrame.origin.x);
    617     [pauseButton_ setFrame:pauseButtonFrame];
    618     [clearAllButton_ setFrame:clearAllButtonFrame];
    619   }
    620 }
    621 
    622 - (void)animationDidEnd:(NSAnimation*)animation {
    623   if (clearAllInProgress_) {
    624     // For clear-all animation.
    625     [clearAllAnimations_ removeObject:animation];
    626     if (![clearAllAnimations_ count] &&
    627         visibleNotificationsPendingClear_.empty()) {
    628       [self finalizeClearAll];
    629     }
    630   } else {
    631     // For notification removal and reposition animation.
    632     if ([notificationsPendingRemoval_ count]) {
    633       [self moveUpRemainingNotifications];
    634     } else {
    635       [self finalizeTrayViewAndWindow];
    636 
    637       if (clearAllDelayed_)
    638         [self clearAllNotifications:nil];
    639     }
    640   }
    641 
    642   // Give the testing code a chance to do something, i.e. quitting the test
    643   // run loop.
    644   if (![self isAnimating] && testingAnimationEndedCallback_)
    645     testingAnimationEndedCallback_.get()();
    646 }
    647 
    648 - (void)closeNotificationsByUser {
    649   // No need to close individual notification if clear-all is in progress.
    650   if (clearAllInProgress_)
    651     return;
    652 
    653   if ([self isAnimating])
    654     return;
    655   [self hideNotificationsPendingRemoval];
    656 }
    657 
    658 - (void)hideNotificationsPendingRemoval {
    659   base::scoped_nsobject<NSMutableArray> animationDataArray(
    660       [[NSMutableArray alloc] init]);
    661 
    662   // Fade-out those notifications pending removal.
    663   for (MCNotificationController* notification in notifications_.get()) {
    664     if (messageCenter_->FindVisibleNotificationById(
    665         [notification notificationID]))
    666       continue;
    667     [notificationsPendingRemoval_ addObject:notification];
    668     [animationDataArray addObject:@{
    669         NSViewAnimationTargetKey : [notification view],
    670         NSViewAnimationEffectKey : NSViewAnimationFadeOutEffect
    671     }];
    672   }
    673 
    674   if ([notificationsPendingRemoval_ count] == 0)
    675     return;
    676 
    677   for (MCNotificationController* notification in
    678            notificationsPendingRemoval_.get()) {
    679     [notifications_ removeObject:notification];
    680   }
    681 
    682   // Start the animation.
    683   animation_.reset([[NSViewAnimation alloc]
    684       initWithViewAnimations:animationDataArray]);
    685   [animation_ setDuration:animationDuration_];
    686   [animation_ setDelegate:self];
    687   [animation_ startAnimation];
    688 }
    689 
    690 - (void)moveUpRemainingNotifications {
    691   base::scoped_nsobject<NSMutableArray> animationDataArray(
    692       [[NSMutableArray alloc] init]);
    693 
    694   // Compute the position where the remaining notifications should start.
    695   CGFloat minY = message_center::kMarginBetweenItems;
    696   for (MCNotificationController* notification in
    697            notificationsPendingRemoval_.get()) {
    698     NSView* view = [notification view];
    699     minY += NSHeight([view frame]) + message_center::kMarginBetweenItems;
    700   }
    701 
    702   // Reposition the remaining notifications starting at the computed position.
    703   for (MCNotificationController* notification in notifications_.get()) {
    704     NSView* view = [notification view];
    705     NSRect frame = [view frame];
    706     NSRect oldFrame = frame;
    707     frame.origin.y = minY;
    708     if (!NSEqualRects(oldFrame, frame)) {
    709       [animationDataArray addObject:@{
    710           NSViewAnimationTargetKey : view,
    711           NSViewAnimationEndFrameKey : [NSValue valueWithRect:frame]
    712       }];
    713     }
    714     minY = NSMaxY(frame) + message_center::kMarginBetweenItems;
    715   }
    716 
    717   // Now remove notifications pending removal.
    718   for (MCNotificationController* notification in
    719            notificationsPendingRemoval_.get()) {
    720     [[notification view] removeFromSuperview];
    721     notificationsMap_.erase([notification notificationID]);
    722   }
    723   [notificationsPendingRemoval_ removeAllObjects];
    724 
    725   // Start the animation.
    726   animation_.reset([[NSViewAnimation alloc]
    727       initWithViewAnimations:animationDataArray]);
    728   [animation_ setDuration:animationDuration_];
    729   [animation_ setDelegate:self];
    730   [animation_ startAnimation];
    731 }
    732 
    733 - (void)finalizeTrayViewAndWindow {
    734   // Reposition the remaining notifications starting at the bottom.
    735   CGFloat minY = message_center::kMarginBetweenItems;
    736   for (MCNotificationController* notification in notifications_.get()) {
    737     NSView* view = [notification view];
    738     NSRect frame = [view frame];
    739     NSRect oldFrame = frame;
    740     frame.origin.y = minY;
    741     if (!NSEqualRects(oldFrame, frame))
    742       [view setFrame:frame];
    743     minY = NSMaxY(frame) + message_center::kMarginBetweenItems;
    744   }
    745 
    746   [self updateTrayViewAndWindow];
    747 
    748   // Check if there're more notifications pending removal.
    749   [self closeNotificationsByUser];
    750 }
    751 
    752 - (void)clearOneNotification {
    753   DCHECK(!visibleNotificationsPendingClear_.empty());
    754 
    755   MCNotificationController* notification =
    756       visibleNotificationsPendingClear_.back();
    757   visibleNotificationsPendingClear_.pop_back();
    758 
    759   // Slide out the notification from left to right with fade-out simultaneously.
    760   NSRect newFrame = [[notification view] frame];
    761   newFrame.origin.x = NSMaxX(newFrame) + message_center::kMarginBetweenItems;
    762   NSDictionary* animationDict = @{
    763     NSViewAnimationTargetKey : [notification view],
    764     NSViewAnimationEndFrameKey : [NSValue valueWithRect:newFrame],
    765     NSViewAnimationEffectKey : NSViewAnimationFadeOutEffect
    766   };
    767   base::scoped_nsobject<NSViewAnimation> animation([[NSViewAnimation alloc]
    768       initWithViewAnimations:[NSArray arrayWithObject:animationDict]]);
    769   [animation setDuration:animationDuration_];
    770   [animation setDelegate:self];
    771   [animation startAnimation];
    772   [clearAllAnimations_ addObject:animation];
    773 
    774   // Schedule to start sliding out next notification after a short delay.
    775   if (!visibleNotificationsPendingClear_.empty()) {
    776     [self performSelector:@selector(clearOneNotification)
    777                withObject:nil
    778                afterDelay:animateClearingNextNotificationDelay_];
    779   }
    780 }
    781 
    782 - (void)finalizeClearAll {
    783   DCHECK(clearAllInProgress_);
    784   clearAllInProgress_ = NO;
    785 
    786   DCHECK(![clearAllAnimations_ count]);
    787   clearAllAnimations_.reset();
    788 
    789   [pauseButton_ setEnabled:YES];
    790   [clearAllButton_ setEnabled:YES];
    791   [settingsButton_ setEnabled:YES];
    792   [clipView_ setFrozen:NO];
    793 
    794   messageCenter_->RemoveAllVisibleNotifications(true);
    795 }
    796 
    797 - (void)updateQuietModeButtonImage {
    798   ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
    799   if (messageCenter_->IsQuietMode()) {
    800     [pauseButton_ setTrackingEnabled:NO];
    801     [pauseButton_ setDefaultImage: rb.GetNativeImageNamed(
    802         IDR_NOTIFICATION_DO_NOT_DISTURB_PRESSED).ToNSImage()];
    803   } else {
    804     [pauseButton_ setTrackingEnabled:YES];
    805     [pauseButton_ setDefaultImage:
    806         rb.GetNativeImageNamed(IDR_NOTIFICATION_DO_NOT_DISTURB).ToNSImage()];
    807   }
    808 }
    809 
    810 @end
    811