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