Updating trunk VERSION from 2139.0 to 2140.0
[chromium-blink-merge.git] / ui / message_center / cocoa / tray_view_controller.mm
bloba63f3d3ccc02721f37cc127762e222053f207f21
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.
5 #import "ui/message_center/cocoa/tray_view_controller.h"
7 #include <cmath>
9 #include "base/mac/scoped_nsautorelease_pool.h"
10 #include "base/time/time.h"
11 #include "skia/ext/skia_utils_mac.h"
12 #import "ui/base/cocoa/hover_image_button.h"
13 #include "ui/base/l10n/l10n_util_mac.h"
14 #include "ui/base/resource/resource_bundle.h"
15 #import "ui/message_center/cocoa/notification_controller.h"
16 #import "ui/message_center/cocoa/opaque_views.h"
17 #import "ui/message_center/cocoa/settings_controller.h"
18 #include "ui/message_center/message_center.h"
19 #include "ui/message_center/message_center_style.h"
20 #include "ui/message_center/notifier_settings.h"
21 #include "ui/resources/grit/ui_resources.h"
22 #include "ui/strings/grit/ui_strings.h"
24 const int kBackButtonSize = 16;
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_;
32 @end
34 @implementation MCClipView
35 - (void)setFrozen:(BOOL)frozen {
36   frozen_ = frozen;
39 - (NSPoint)constrainScrollPoint:(NSPoint)proposedNewOrigin {
40   return frozen_ ? [self documentVisibleRect].origin :
41       [super constrainScrollPoint:proposedNewOrigin];
43 @end
45 @interface MCTrayViewController (Private)
46 // Creates all the views for the control area of the tray.
47 - (void)layoutControlArea;
49 // Update both tray view and window by resizing it to fit its content.
50 - (void)updateTrayViewAndWindow;
52 // Remove notifications dismissed by the user. It is done in the following
53 // 3 steps.
54 - (void)closeNotificationsByUser;
56 // Step 1: hide all notifications pending removal with fade-out animation.
57 - (void)hideNotificationsPendingRemoval;
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;
63 // Step 3: finalize the tray view and window to get rid of the empty space.
64 - (void)finalizeTrayViewAndWindow;
66 // Clear a notification by sliding it out from left to right. This occurs when
67 // "Clear All" is clicked.
68 - (void)clearOneNotification;
70 // When all visible notifications slide out, re-enable controls and remove
71 // notifications from the message center.
72 - (void)finalizeClearAll;
74 // Sets the images of the quiet mode button based on the message center state.
75 - (void)updateQuietModeButtonImage;
76 @end
78 namespace {
80 // The duration of fade-out and bounds animation.
81 const NSTimeInterval kAnimationDuration = 0.2;
83 // The delay to start animating clearing next notification since current
84 // animation starts.
85 const NSTimeInterval kAnimateClearingNextNotificationDelay = 0.04;
87 // The height of the bar at the top of the tray that contains buttons.
88 const CGFloat kControlAreaHeight = 50;
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;
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;
98 }  // namespace
100 @implementation MCTrayViewController
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;
114 - (NSString*)trayTitle {
115   return [title_ stringValue];
118 - (void)setTrayTitle:(NSString*)title {
119   [title_ setStringValue:title];
120   [title_ sizeToFit];
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];
133     for (NSViewAnimation* animation in clearAllAnimations_.get()) {
134       [animation stopAnimation];
135       [animation setDelegate:nil];
136     }
137     [clearAllAnimations_ removeAllObjects];
138     [self finalizeClearAll];
139   }
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];
155   [self layoutControlArea];
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_];
172   [self onMessageCenterTrayChanged];
175 - (void)onMessageCenterTrayChanged {
176   if (settingsController_)
177     return [self updateTrayViewAndWindow];
179   std::map<std::string, MCNotificationController*> newMap;
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];
186   CGFloat minY = message_center::kMarginBetweenItems;
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]];
207       [notifications_ addObject:controller];  // Transfer ownership.
208       messageCenter_->DisplayedNotification(
209           (*it)->id(), message_center::DISPLAY_SOURCE_MESSAGE_CENTER);
211       notification = controller.get();
212     } else {
213       notification = existing->second;
214       [notification updateNotification:*it];
215       notificationsMap_.erase(existing);
216     }
218     DCHECK(notification);
220     NSRect frame = [[notification view] frame];
221     frame.origin.x = message_center::kMarginBetweenItems;
222     frame.origin.y = minY;
223     [[notification view] setFrame:frame];
225     newMap.insert(std::make_pair((*it)->id(), notification));
227     minY = NSMaxY(frame) + message_center::kMarginBetweenItems;
228   }
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   }
236   // Copy the new map of notifications to replace the old.
237   notificationsMap_ = newMap;
239   [self updateTrayViewAndWindow];
242 - (void)toggleQuietMode:(id)sender {
243   if (messageCenter_->IsQuietMode())
244     messageCenter_->SetQuietMode(false);
245   else
246     messageCenter_->EnterQuietModeWithExpire(base::TimeDelta::FromDays(1));
248   [self updateQuietModeButtonImage];
251 - (void)clearAllNotifications:(id)sender {
252   if ([self isAnimating]) {
253     clearAllDelayed_ = YES;
254     return;
255   }
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;
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];
276   // Start sliding out the top notification.
277   clearAllAnimations_.reset([[NSMutableArray alloc] init]);
278   [self clearOneNotification];
280   clearAllInProgress_ = YES;
283 - (void)showSettings:(id)sender {
284   if (settingsController_)
285     return [self showMessages:sender];
287   message_center::NotifierSettingsProvider* provider =
288       messageCenter_->GetNotifierSettingsProvider();
289   settingsController_.reset(
290       [[MCSettingsController alloc] initWithProvider:provider
291                                   trayViewController:self]);
293   [[self view] addSubview:[settingsController_ view]];
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];
302   [scrollView_ setHidden:YES];
304   [[[self view] window] recalculateKeyViewLoop];
305   messageCenter_->SetVisibility(message_center::VISIBILITY_SETTINGS);
307   [self updateTrayViewAndWindow];
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]];
318   [self updateTrayViewAndWindow];
321 - (void)showMessages:(id)sender {
322   messageCenter_->SetVisibility(message_center::VISIBILITY_MESSAGE_CENTER);
323   [self cleanupSettings];
324   [[[self view] window] recalculateKeyViewLoop];
325   [self updateTrayViewAndWindow];
328 - (void)cleanupSettings {
329   [scrollView_ setHidden:NO];
331   [[settingsController_ view] removeFromSuperview];
332   settingsController_.reset();
334   NSRect titleFrame = [title_ frame];
335   titleFrame.origin.x = NSMinX([backButton_ frame]);
336   [title_ setFrame:titleFrame];
337   [backButton_ setHidden:YES];
338   [clearAllButton_ setEnabled:YES];
342 - (void)scrollToTop {
343   NSPoint topPoint =
344       NSMakePoint(0.0, [[scrollView_ documentView] bounds].size.height);
345   [[scrollView_ documentView] scrollPoint:topPoint];
348 - (BOOL)isAnimating {
349   return [animation_ isAnimating] || [clearAllAnimations_ count];
352 + (CGFloat)maxTrayClientHeight {
353   NSRect screenFrame = [[[NSScreen screens] objectAtIndex:0] visibleFrame];
354   return NSHeight(screenFrame) - kTrayBottomMargin - kControlAreaHeight;
357 + (CGFloat)trayWidth {
358   return message_center::kNotificationWidth +
359          2 * message_center::kMarginBetweenItems;
362 // Testing API /////////////////////////////////////////////////////////////////
364 - (NSBox*)divider {
365   return divider_.get();
368 - (NSTextField*)emptyDescription {
369   return emptyDescription_.get();
372 - (NSScrollView*)scrollView {
373   return scrollView_.get();
376 - (HoverImageButton*)pauseButton {
377   return pauseButton_.get();
380 - (HoverImageButton*)clearAllButton {
381   return clearAllButton_.get();
384 - (void)setAnimationDuration:(NSTimeInterval)duration {
385   animationDuration_ = duration;
388 - (void)setAnimateClearingNextNotificationDelay:(NSTimeInterval)delay {
389   animateClearingNextNotificationDelay_ = delay;
392 - (void)setAnimationEndedCallback:
393     (message_center::TrayAnimationEndedCallback)callback {
394   testingAnimationEndedCallback_.reset(Block_copy(callback));
397 // Private /////////////////////////////////////////////////////////////////////
399 - (void)layoutControlArea {
400   ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
401   NSView* view = [self view];
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]);
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];
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_];
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   };
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_];
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_];
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   };
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_];
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_];
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_];
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]);
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];
552   [view addSubview:emptyDescription_];
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];
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));
573     centeredFrame.origin = centeredOrigin;
574     [emptyDescription_ setFrame:centeredFrame];
575   }
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];
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];
597   // Resize the scroll view.
598   scrollViewFrame.size.height = NSHeight(frame) - kControlAreaHeight;
599   [scrollView_ setFrame:scrollViewFrame];
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;
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];
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   }
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];
637       if (clearAllDelayed_)
638         [self clearAllNotifications:nil];
639     }
640   }
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()();
648 - (void)closeNotificationsByUser {
649   // No need to close individual notification if clear-all is in progress.
650   if (clearAllInProgress_)
651     return;
653   if ([self isAnimating])
654     return;
655   [self hideNotificationsPendingRemoval];
658 - (void)hideNotificationsPendingRemoval {
659   base::scoped_nsobject<NSMutableArray> animationDataArray(
660       [[NSMutableArray alloc] init]);
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   }
674   if ([notificationsPendingRemoval_ count] == 0)
675     return;
677   for (MCNotificationController* notification in
678            notificationsPendingRemoval_.get()) {
679     [notifications_ removeObject:notification];
680   }
682   // Start the animation.
683   animation_.reset([[NSViewAnimation alloc]
684       initWithViewAnimations:animationDataArray]);
685   [animation_ setDuration:animationDuration_];
686   [animation_ setDelegate:self];
687   [animation_ startAnimation];
690 - (void)moveUpRemainingNotifications {
691   base::scoped_nsobject<NSMutableArray> animationDataArray(
692       [[NSMutableArray alloc] init]);
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   }
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   }
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];
725   // Start the animation.
726   animation_.reset([[NSViewAnimation alloc]
727       initWithViewAnimations:animationDataArray]);
728   [animation_ setDuration:animationDuration_];
729   [animation_ setDelegate:self];
730   [animation_ startAnimation];
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   }
746   [self updateTrayViewAndWindow];
748   // Check if there're more notifications pending removal.
749   [self closeNotificationsByUser];
752 - (void)clearOneNotification {
753   DCHECK(!visibleNotificationsPendingClear_.empty());
755   MCNotificationController* notification =
756       visibleNotificationsPendingClear_.back();
757   visibleNotificationsPendingClear_.pop_back();
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];
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   }
782 - (void)finalizeClearAll {
783   DCHECK(clearAllInProgress_);
784   clearAllInProgress_ = NO;
786   DCHECK(![clearAllAnimations_ count]);
787   clearAllAnimations_.reset();
789   [pauseButton_ setEnabled:YES];
790   [clearAllButton_ setEnabled:YES];
791   [settingsButton_ setEnabled:YES];
792   [clipView_ setFrozen:NO];
794   messageCenter_->RemoveAllVisibleNotifications(true);
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   }
810 @end