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