Upstream version 5.34.104.0
[platform/framework/web/crosswalk.git] / src / ui / message_center / cocoa / tray_view_controller.mm
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 - (NSBox*)divider {
363   return divider_.get();
364 }
365
366 - (NSTextField*)emptyDescription {
367   return emptyDescription_.get();
368 }
369
370 - (NSScrollView*)scrollView {
371   return scrollView_.get();
372 }
373
374 - (HoverImageButton*)pauseButton {
375   return pauseButton_.get();
376 }
377
378 - (HoverImageButton*)clearAllButton {
379   return clearAllButton_.get();
380 }
381
382 - (void)setAnimationDuration:(NSTimeInterval)duration {
383   animationDuration_ = duration;
384 }
385
386 - (void)setAnimateClearingNextNotificationDelay:(NSTimeInterval)delay {
387   animateClearingNextNotificationDelay_ = delay;
388 }
389
390 - (void)setAnimationEndedCallback:
391     (message_center::TrayAnimationEndedCallback)callback {
392   testingAnimationEndedCallback_.reset(Block_copy(callback));
393 }
394
395 // Private /////////////////////////////////////////////////////////////////////
396
397 - (void)layoutControlArea {
398   ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
399   NSView* view = [self view];
400
401   auto configureLabel = ^(NSTextField* textField) {
402       [textField setAutoresizingMask:NSViewMinYMargin];
403       [textField setBezeled:NO];
404       [textField setBordered:NO];
405       [textField setDrawsBackground:NO];
406       [textField setEditable:NO];
407       [textField setSelectable:NO];
408   };
409
410   // Create the "Notifications" label at the top of the tray.
411   NSFont* font = [NSFont labelFontOfSize:message_center::kTitleFontSize];
412   title_.reset([[NSTextField alloc] initWithFrame:NSZeroRect]);
413   configureLabel(title_);
414
415   [title_ setFont:font];
416   [title_ setStringValue:
417       l10n_util::GetNSString(IDS_MESSAGE_CENTER_FOOTER_TITLE)];
418   [title_ setTextColor:gfx::SkColorToCalibratedNSColor(
419       message_center::kRegularTextColor)];
420   [title_ sizeToFit];
421
422   NSRect titleFrame = [title_ frame];
423   titleFrame.origin.x = message_center::kMarginBetweenItems;
424   titleFrame.origin.y = kControlAreaHeight/2 - NSMidY(titleFrame);
425   [title_ setFrame:titleFrame];
426   [view addSubview:title_];
427
428   auto configureButton = ^(HoverImageButton* button) {
429       [[button cell] setHighlightsBy:NSOnState];
430       [button setTrackingEnabled:YES];
431       [button setBordered:NO];
432       [button setAutoresizingMask:NSViewMinYMargin];
433       [button setTarget:self];
434   };
435
436   // Back button. On top of the "Notifications" label, hidden by default.
437   NSRect backButtonFrame =
438       NSMakeRect(NSMinX(titleFrame),
439                  (kControlAreaHeight - kBackButtonSize) / 2,
440                  kBackButtonSize,
441                  kBackButtonSize);
442   backButton_.reset([[HoverImageButton alloc] initWithFrame:backButtonFrame]);
443   [backButton_ setDefaultImage:
444       rb.GetNativeImageNamed(IDR_NOTIFICATION_ARROW).ToNSImage()];
445   [backButton_ setHoverImage:
446       rb.GetNativeImageNamed(IDR_NOTIFICATION_ARROW_HOVER).ToNSImage()];
447   [backButton_ setPressedImage:
448       rb.GetNativeImageNamed(IDR_NOTIFICATION_ARROW_PRESSED).ToNSImage()];
449   [backButton_ setAction:@selector(showMessages:)];
450   configureButton(backButton_);
451   [backButton_ setHidden:YES];
452   [backButton_ setKeyEquivalent:@"\e"];
453   [backButton_ setToolTip:l10n_util::GetNSString(
454       IDS_MESSAGE_CENTER_SETTINGS_GO_BACK_BUTTON_TOOLTIP)];
455   [[backButton_ cell]
456       accessibilitySetOverrideValue:[backButton_ toolTip]
457                        forAttribute:NSAccessibilityDescriptionAttribute];
458   [[self view] addSubview:backButton_];
459
460   // Create the divider line between the control area and the notifications.
461   divider_.reset(
462       [[NSBox alloc] initWithFrame:NSMakeRect(0, 0, NSWidth([view frame]), 1)]);
463   [divider_ setAutoresizingMask:NSViewMinYMargin];
464   [divider_ setBorderType:NSNoBorder];
465   [divider_ setBoxType:NSBoxCustom];
466   [divider_ setContentViewMargins:NSZeroSize];
467   [divider_ setFillColor:gfx::SkColorToCalibratedNSColor(
468       message_center::kFooterDelimiterColor)];
469   [divider_ setTitlePosition:NSNoTitle];
470   [view addSubview:divider_];
471
472
473   auto getButtonFrame = ^NSRect(CGFloat maxX, NSImage* image) {
474       NSSize size = [image size];
475       return NSMakeRect(
476           maxX - size.width,
477           kControlAreaHeight/2 - size.height/2,
478           size.width,
479           size.height);
480   };
481
482   // Create the settings button at the far-right.
483   NSImage* defaultImage =
484       rb.GetNativeImageNamed(IDR_NOTIFICATION_SETTINGS).ToNSImage();
485   NSRect settingsButtonFrame = getButtonFrame(
486       NSWidth([view frame]) - message_center::kMarginBetweenItems,
487       defaultImage);
488   settingsButton_.reset(
489       [[HoverImageButton alloc] initWithFrame:settingsButtonFrame]);
490   [settingsButton_ setDefaultImage:defaultImage];
491   [settingsButton_ setHoverImage:
492       rb.GetNativeImageNamed(IDR_NOTIFICATION_SETTINGS_HOVER).ToNSImage()];
493   [settingsButton_ setPressedImage:
494       rb.GetNativeImageNamed(IDR_NOTIFICATION_SETTINGS_PRESSED).ToNSImage()];
495   [settingsButton_ setToolTip:
496       l10n_util::GetNSString(IDS_MESSAGE_CENTER_SETTINGS_BUTTON_LABEL)];
497   [[settingsButton_ cell]
498       accessibilitySetOverrideValue:[settingsButton_ toolTip]
499                        forAttribute:NSAccessibilityDescriptionAttribute];
500   [settingsButton_ setAction:@selector(showSettings:)];
501   configureButton(settingsButton_);
502   [view addSubview:settingsButton_];
503
504   // Create the clear all button.
505   defaultImage = rb.GetNativeImageNamed(IDR_NOTIFICATION_CLEAR_ALL).ToNSImage();
506   NSRect clearAllButtonFrame = getButtonFrame(
507       NSMinX(settingsButtonFrame) - kButtonXMargin,
508       defaultImage);
509   clearAllButton_.reset(
510       [[HoverImageButton alloc] initWithFrame:clearAllButtonFrame]);
511   [clearAllButton_ setDefaultImage:defaultImage];
512   [clearAllButton_ setHoverImage:
513       rb.GetNativeImageNamed(IDR_NOTIFICATION_CLEAR_ALL_HOVER).ToNSImage()];
514   [clearAllButton_ setPressedImage:
515       rb.GetNativeImageNamed(IDR_NOTIFICATION_CLEAR_ALL_PRESSED).ToNSImage()];
516   [clearAllButton_ setToolTip:
517       l10n_util::GetNSString(IDS_MESSAGE_CENTER_CLEAR_ALL)];
518   [[clearAllButton_ cell]
519       accessibilitySetOverrideValue:[clearAllButton_ toolTip]
520                        forAttribute:NSAccessibilityDescriptionAttribute];
521   [clearAllButton_ setAction:@selector(clearAllNotifications:)];
522   configureButton(clearAllButton_);
523   [view addSubview:clearAllButton_];
524
525   // Create the pause button.
526   NSRect pauseButtonFrame = getButtonFrame(
527       NSMinX(clearAllButtonFrame) - kButtonXMargin,
528       defaultImage);
529   pauseButton_.reset([[HoverImageButton alloc] initWithFrame:pauseButtonFrame]);
530   [self updateQuietModeButtonImage];
531   [pauseButton_ setHoverImage: rb.GetNativeImageNamed(
532       IDR_NOTIFICATION_DO_NOT_DISTURB_HOVER).ToNSImage()];
533   [pauseButton_ setToolTip:
534       l10n_util::GetNSString(IDS_MESSAGE_CENTER_QUIET_MODE_BUTTON_TOOLTIP)];
535   [[pauseButton_ cell]
536       accessibilitySetOverrideValue:[pauseButton_ toolTip]
537                        forAttribute:NSAccessibilityDescriptionAttribute];
538   [pauseButton_ setAction:@selector(toggleQuietMode:)];
539   configureButton(pauseButton_);
540   [view addSubview:pauseButton_];
541
542   // Create the description field for the empty message center.  Initially it is
543   // invisible.
544   emptyDescription_.reset([[NSTextField alloc] initWithFrame:NSZeroRect]);
545   configureLabel(emptyDescription_);
546
547   NSFont* smallFont =
548       [NSFont labelFontOfSize:message_center::kEmptyCenterFontSize];
549   [emptyDescription_ setFont:smallFont];
550   [emptyDescription_ setStringValue:
551       l10n_util::GetNSString(IDS_MESSAGE_CENTER_NO_MESSAGES)];
552   [emptyDescription_ setTextColor:gfx::SkColorToCalibratedNSColor(
553       message_center::kDimTextColor)];
554   [emptyDescription_ sizeToFit];
555   [emptyDescription_ setHidden:YES];
556
557   [view addSubview:emptyDescription_];
558 }
559
560 - (void)updateTrayViewAndWindow {
561   CGFloat scrollContentHeight = message_center::kMinScrollViewHeight;
562   if ([notifications_ count]) {
563     [emptyDescription_ setHidden:YES];
564     [scrollView_ setHidden:NO];
565     [divider_ setHidden:NO];
566     scrollContentHeight = NSMaxY([[[notifications_ lastObject] view] frame]) +
567         message_center::kMarginBetweenItems;;
568   } else {
569     [emptyDescription_ setHidden:NO];
570     [scrollView_ setHidden:YES];
571     [divider_ setHidden:YES];
572
573     NSRect centeredFrame = [emptyDescription_ frame];
574     NSPoint centeredOrigin = NSMakePoint(
575       floor((NSWidth([[self view] frame]) - NSWidth(centeredFrame))/2 + 0.5),
576       floor((scrollContentHeight - NSHeight(centeredFrame))/2 + 0.5));
577
578     centeredFrame.origin = centeredOrigin;
579     [emptyDescription_ setFrame:centeredFrame];
580   }
581
582   // Resize the scroll view's content.
583   NSRect scrollViewFrame = [scrollView_ frame];
584   NSRect documentFrame = [[scrollView_ documentView] frame];
585   documentFrame.size.width = NSWidth(scrollViewFrame);
586   documentFrame.size.height = scrollContentHeight;
587   [[scrollView_ documentView] setFrame:documentFrame];
588
589   // Resize the container view.
590   NSRect frame = [[self view] frame];
591   CGFloat oldHeight = NSHeight(frame);
592   if (settingsController_) {
593     frame.size.height = NSHeight([[settingsController_ view] frame]);
594   } else {
595     frame.size.height = std::min([MCTrayViewController maxTrayClientHeight],
596                                  scrollContentHeight);
597   }
598   frame.size.height += kControlAreaHeight;
599   CGFloat newHeight = NSHeight(frame);
600   [[self view] setFrame:frame];
601
602   // Resize the scroll view.
603   scrollViewFrame.size.height = NSHeight(frame) - kControlAreaHeight;
604   [scrollView_ setFrame:scrollViewFrame];
605
606   // Resize the window.
607   NSRect windowFrame = [[[self view] window] frame];
608   CGFloat delta = newHeight - oldHeight;
609   windowFrame.origin.y -= delta;
610   windowFrame.size.height += delta;
611
612   [[[self view] window] setFrame:windowFrame display:YES];
613   // Hide the clear-all button if there are no notifications. Simply swap the
614   // X position of it and the pause button in that case.
615   BOOL hidden = ![notifications_ count];
616   if ([clearAllButton_ isHidden] != hidden) {
617     [clearAllButton_ setHidden:hidden];
618
619     NSRect pauseButtonFrame = [pauseButton_ frame];
620     NSRect clearAllButtonFrame = [clearAllButton_ frame];
621     std::swap(clearAllButtonFrame.origin.x, pauseButtonFrame.origin.x);
622     [pauseButton_ setFrame:pauseButtonFrame];
623     [clearAllButton_ setFrame:clearAllButtonFrame];
624   }
625 }
626
627 - (void)animationDidEnd:(NSAnimation*)animation {
628   if (clearAllInProgress_) {
629     // For clear-all animation.
630     [clearAllAnimations_ removeObject:animation];
631     if (![clearAllAnimations_ count] &&
632         visibleNotificationsPendingClear_.empty()) {
633       [self finalizeClearAll];
634     }
635   } else {
636     // For notification removal and reposition animation.
637     if ([notificationsPendingRemoval_ count]) {
638       [self moveUpRemainingNotifications];
639     } else {
640       [self finalizeTrayViewAndWindow];
641
642       if (clearAllDelayed_)
643         [self clearAllNotifications:nil];
644     }
645   }
646
647   // Give the testing code a chance to do something, i.e. quitting the test
648   // run loop.
649   if (![self isAnimating] && testingAnimationEndedCallback_)
650     testingAnimationEndedCallback_.get()();
651 }
652
653 - (void)closeNotificationsByUser {
654   // No need to close individual notification if clear-all is in progress.
655   if (clearAllInProgress_)
656     return;
657
658   if ([self isAnimating])
659     return;
660   [self hideNotificationsPendingRemoval];
661 }
662
663 - (void)hideNotificationsPendingRemoval {
664   base::scoped_nsobject<NSMutableArray> animationDataArray(
665       [[NSMutableArray alloc] init]);
666
667   // Fade-out those notifications pending removal.
668   for (MCNotificationController* notification in notifications_.get()) {
669     if (messageCenter_->HasNotification([notification notificationID]))
670       continue;
671     [notificationsPendingRemoval_ addObject:notification];
672     [animationDataArray addObject:@{
673         NSViewAnimationTargetKey : [notification view],
674         NSViewAnimationEffectKey : NSViewAnimationFadeOutEffect
675     }];
676   }
677
678   if ([notificationsPendingRemoval_ count] == 0)
679     return;
680
681   for (MCNotificationController* notification in
682            notificationsPendingRemoval_.get()) {
683     [notifications_ removeObject:notification];
684   }
685
686   // Start the animation.
687   animation_.reset([[NSViewAnimation alloc]
688       initWithViewAnimations:animationDataArray]);
689   [animation_ setDuration:animationDuration_];
690   [animation_ setDelegate:self];
691   [animation_ startAnimation];
692 }
693
694 - (void)moveUpRemainingNotifications {
695   base::scoped_nsobject<NSMutableArray> animationDataArray(
696       [[NSMutableArray alloc] init]);
697
698   // Compute the position where the remaining notifications should start.
699   CGFloat minY = message_center::kMarginBetweenItems;
700   for (MCNotificationController* notification in
701            notificationsPendingRemoval_.get()) {
702     NSView* view = [notification view];
703     minY += NSHeight([view frame]) + message_center::kMarginBetweenItems;
704   }
705
706   // Reposition the remaining notifications starting at the computed position.
707   for (MCNotificationController* notification in notifications_.get()) {
708     NSView* view = [notification view];
709     NSRect frame = [view frame];
710     NSRect oldFrame = frame;
711     frame.origin.y = minY;
712     if (!NSEqualRects(oldFrame, frame)) {
713       [animationDataArray addObject:@{
714           NSViewAnimationTargetKey : view,
715           NSViewAnimationEndFrameKey : [NSValue valueWithRect:frame]
716       }];
717     }
718     minY = NSMaxY(frame) + message_center::kMarginBetweenItems;
719   }
720
721   // Now remove notifications pending removal.
722   for (MCNotificationController* notification in
723            notificationsPendingRemoval_.get()) {
724     [[notification view] removeFromSuperview];
725     notificationsMap_.erase([notification notificationID]);
726   }
727   [notificationsPendingRemoval_ removeAllObjects];
728
729   // Start the animation.
730   animation_.reset([[NSViewAnimation alloc]
731       initWithViewAnimations:animationDataArray]);
732   [animation_ setDuration:animationDuration_];
733   [animation_ setDelegate:self];
734   [animation_ startAnimation];
735 }
736
737 - (void)finalizeTrayViewAndWindow {
738   // Reposition the remaining notifications starting at the bottom.
739   CGFloat minY = message_center::kMarginBetweenItems;
740   for (MCNotificationController* notification in notifications_.get()) {
741     NSView* view = [notification view];
742     NSRect frame = [view frame];
743     NSRect oldFrame = frame;
744     frame.origin.y = minY;
745     if (!NSEqualRects(oldFrame, frame))
746       [view setFrame:frame];
747     minY = NSMaxY(frame) + message_center::kMarginBetweenItems;
748   }
749
750   [self updateTrayViewAndWindow];
751
752   // Check if there're more notifications pending removal.
753   [self closeNotificationsByUser];
754 }
755
756 - (void)clearOneNotification {
757   DCHECK(!visibleNotificationsPendingClear_.empty());
758
759   MCNotificationController* notification =
760       visibleNotificationsPendingClear_.back();
761   visibleNotificationsPendingClear_.pop_back();
762
763   // Slide out the notification from left to right with fade-out simultaneously.
764   NSRect newFrame = [[notification view] frame];
765   newFrame.origin.x = NSMaxX(newFrame) + message_center::kMarginBetweenItems;
766   NSDictionary* animationDict = @{
767     NSViewAnimationTargetKey : [notification view],
768     NSViewAnimationEndFrameKey : [NSValue valueWithRect:newFrame],
769     NSViewAnimationEffectKey : NSViewAnimationFadeOutEffect
770   };
771   base::scoped_nsobject<NSViewAnimation> animation([[NSViewAnimation alloc]
772       initWithViewAnimations:[NSArray arrayWithObject:animationDict]]);
773   [animation setDuration:animationDuration_];
774   [animation setDelegate:self];
775   [animation startAnimation];
776   [clearAllAnimations_ addObject:animation];
777
778   // Schedule to start sliding out next notification after a short delay.
779   if (!visibleNotificationsPendingClear_.empty()) {
780     [self performSelector:@selector(clearOneNotification)
781                withObject:nil
782                afterDelay:animateClearingNextNotificationDelay_];
783   }
784 }
785
786 - (void)finalizeClearAll {
787   DCHECK(clearAllInProgress_);
788   clearAllInProgress_ = NO;
789
790   DCHECK(![clearAllAnimations_ count]);
791   clearAllAnimations_.reset();
792
793   [pauseButton_ setEnabled:YES];
794   [clearAllButton_ setEnabled:YES];
795   [settingsButton_ setEnabled:YES];
796   [clipView_ setFrozen:NO];
797
798   messageCenter_->RemoveAllVisibleNotifications(true);
799 }
800
801 - (void)updateQuietModeButtonImage {
802   ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
803   if (messageCenter_->IsQuietMode()) {
804     [pauseButton_ setTrackingEnabled:NO];
805     [pauseButton_ setDefaultImage: rb.GetNativeImageNamed(
806         IDR_NOTIFICATION_DO_NOT_DISTURB_PRESSED).ToNSImage()];
807   } else {
808     [pauseButton_ setTrackingEnabled:YES];
809     [pauseButton_ setDefaultImage:
810         rb.GetNativeImageNamed(IDR_NOTIFICATION_DO_NOT_DISTURB).ToNSImage()];
811   }
812 }
813
814 @end