Get foreground tab on Android
[chromium-blink-merge.git] / ui / message_center / cocoa / tray_view_controller.mm
blob68655629950c9a930653c34f70847ccced639610
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 "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"
23 const int kBackButtonSize = 16;
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_;
31 @end
33 @implementation MCClipView
34 - (void)setFrozen:(BOOL)frozen {
35   frozen_ = frozen;
38 - (NSPoint)constrainScrollPoint:(NSPoint)proposedNewOrigin {
39   return frozen_ ? [self documentVisibleRect].origin :
40       [super constrainScrollPoint:proposedNewOrigin];
42 @end
44 @interface MCTrayViewController (Private)
45 // Creates all the views for the control area of the tray.
46 - (void)layoutControlArea;
48 // Update both tray view and window by resizing it to fit its content.
49 - (void)updateTrayViewAndWindow;
51 // Remove notifications dismissed by the user. It is done in the following
52 // 3 steps.
53 - (void)closeNotificationsByUser;
55 // Step 1: hide all notifications pending removal with fade-out animation.
56 - (void)hideNotificationsPendingRemoval;
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;
62 // Step 3: finalize the tray view and window to get rid of the empty space.
63 - (void)finalizeTrayViewAndWindow;
65 // Clear a notification by sliding it out from left to right. This occurs when
66 // "Clear All" is clicked.
67 - (void)clearOneNotification;
69 // When all visible notificatons slide out, re-enable controls and remove
70 // notifications from the message center.
71 - (void)finalizeClearAll;
73 // Sets the images of the quiet mode button based on the message center state.
74 - (void)updateQuietModeButtonImage;
75 @end
77 namespace {
79 // The duration of fade-out and bounds animation.
80 const NSTimeInterval kAnimationDuration = 0.2;
82 // The delay to start animating clearing next notification since current
83 // animation starts.
84 const NSTimeInterval kAnimateClearingNextNotificationDelay = 0.04;
86 // The height of the bar at the top of the tray that contains buttons.
87 const CGFloat kControlAreaHeight = 50;
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;
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;
97 }  // namespace
99 @implementation MCTrayViewController
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;
113 - (NSString*)trayTitle {
114   return [title_ stringValue];
117 - (void)setTrayTitle:(NSString*)title {
118   [title_ setStringValue:title];
119   [title_ sizeToFit];
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];
132     for (NSViewAnimation* animation in clearAllAnimations_.get()) {
133       [animation stopAnimation];
134       [animation setDelegate:nil];
135     }
136     [clearAllAnimations_ removeAllObjects];
137     [self finalizeClearAll];
138   }
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];
154   [self layoutControlArea];
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_];
171   [self onMessageCenterTrayChanged];
174 - (void)onMessageCenterTrayChanged {
175   if (settingsController_)
176     return [self updateTrayViewAndWindow];
178   std::map<std::string, MCNotificationController*> newMap;
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];
185   CGFloat minY = message_center::kMarginBetweenItems;
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]];
206       [notifications_ addObject:controller];  // Transfer ownership.
207       messageCenter_->DisplayedNotification((*it)->id());
209       notification = controller.get();
210     } else {
211       notification = existing->second;
212       [notification updateNotification:*it];
213       notificationsMap_.erase(existing);
214     }
216     DCHECK(notification);
218     NSRect frame = [[notification view] frame];
219     frame.origin.x = message_center::kMarginBetweenItems;
220     frame.origin.y = minY;
221     [[notification view] setFrame:frame];
223     newMap.insert(std::make_pair((*it)->id(), notification));
225     minY = NSMaxY(frame) + message_center::kMarginBetweenItems;
226   }
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   }
234   // Copy the new map of notifications to replace the old.
235   notificationsMap_ = newMap;
237   [self updateTrayViewAndWindow];
240 - (void)toggleQuietMode:(id)sender {
241   if (messageCenter_->IsQuietMode())
242     messageCenter_->SetQuietMode(false);
243   else
244     messageCenter_->EnterQuietModeWithExpire(base::TimeDelta::FromDays(1));
246   [self updateQuietModeButtonImage];
249 - (void)clearAllNotifications:(id)sender {
250   if ([self isAnimating]) {
251     clearAllDelayed_ = YES;
252     return;
253   }
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;
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];
274   // Start sliding out the top notification.
275   clearAllAnimations_.reset([[NSMutableArray alloc] init]);
276   [self clearOneNotification];
278   clearAllInProgress_ = YES;
281 - (void)showSettings:(id)sender {
282   if (settingsController_)
283     return [self showMessages:sender];
285   message_center::NotifierSettingsProvider* provider =
286       messageCenter_->GetNotifierSettingsProvider();
287   settingsController_.reset(
288       [[MCSettingsController alloc] initWithProvider:provider
289                                   trayViewController:self]);
291   [[self view] addSubview:[settingsController_ view]];
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];
300   [scrollView_ setHidden:YES];
302   [[[self view] window] recalculateKeyViewLoop];
303   messageCenter_->SetVisibility(message_center::VISIBILITY_SETTINGS);
305   [self updateTrayViewAndWindow];
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]];
316   [self updateTrayViewAndWindow];
319 - (void)showMessages:(id)sender {
320   messageCenter_->SetVisibility(message_center::VISIBILITY_MESSAGE_CENTER);
321   [self cleanupSettings];
322   [[[self view] window] recalculateKeyViewLoop];
323   [self updateTrayViewAndWindow];
326 - (void)cleanupSettings {
327   [scrollView_ setHidden:NO];
329   [[settingsController_ view] removeFromSuperview];
330   settingsController_.reset();
332   NSRect titleFrame = [title_ frame];
333   titleFrame.origin.x = NSMinX([backButton_ frame]);
334   [title_ setFrame:titleFrame];
335   [backButton_ setHidden:YES];
336   [clearAllButton_ setEnabled:YES];
340 - (void)scrollToTop {
341   NSPoint topPoint =
342       NSMakePoint(0.0, [[scrollView_ documentView] bounds].size.height);
343   [[scrollView_ documentView] scrollPoint:topPoint];
346 - (BOOL)isAnimating {
347   return [animation_ isAnimating] || [clearAllAnimations_ count];
350 + (CGFloat)maxTrayClientHeight {
351   NSRect screenFrame = [[[NSScreen screens] objectAtIndex:0] visibleFrame];
352   return NSHeight(screenFrame) - kTrayBottomMargin - kControlAreaHeight;
355 + (CGFloat)trayWidth {
356   return message_center::kNotificationWidth +
357          2 * message_center::kMarginBetweenItems;
360 // Testing API /////////////////////////////////////////////////////////////////
362 - (NSScrollView*)scrollView {
363   return scrollView_.get();
366 - (HoverImageButton*)pauseButton {
367   return pauseButton_.get();
370 - (HoverImageButton*)clearAllButton {
371   return clearAllButton_.get();
374 - (void)setAnimationDuration:(NSTimeInterval)duration {
375   animationDuration_ = duration;
378 - (void)setAnimateClearingNextNotificationDelay:(NSTimeInterval)delay {
379   animateClearingNextNotificationDelay_ = delay;
382 - (void)setAnimationEndedCallback:
383     (message_center::TrayAnimationEndedCallback)callback {
384   testingAnimationEndedCallback_.reset(Block_copy(callback));
387 // Private /////////////////////////////////////////////////////////////////////
389 - (void)layoutControlArea {
390   ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
391   NSView* view = [self view];
393   // Create the "Notifications" label at the top of the tray.
394   NSFont* font = [NSFont labelFontOfSize:message_center::kTitleFontSize];
395   title_.reset([[NSTextField alloc] initWithFrame:NSZeroRect]);
396   [title_ setAutoresizingMask:NSViewMinYMargin];
397   [title_ setBezeled:NO];
398   [title_ setBordered:NO];
399   [title_ setDrawsBackground:NO];
400   [title_ setEditable:NO];
401   [title_ setFont:font];
402   [title_ setSelectable:NO];
403   [title_ setStringValue:
404       l10n_util::GetNSString(IDS_MESSAGE_CENTER_FOOTER_TITLE)];
405   [title_ setTextColor:gfx::SkColorToCalibratedNSColor(
406       message_center::kRegularTextColor)];
407   [title_ sizeToFit];
409   NSRect titleFrame = [title_ frame];
410   titleFrame.origin.x = message_center::kMarginBetweenItems;
411   titleFrame.origin.y = kControlAreaHeight/2 - NSMidY(titleFrame);
412   [title_ setFrame:titleFrame];
413   [view addSubview:title_];
415   auto configureButton = ^(HoverImageButton* button) {
416       [[button cell] setHighlightsBy:NSOnState];
417       [button setTrackingEnabled:YES];
418       [button setBordered:NO];
419       [button setAutoresizingMask:NSViewMinYMargin];
420       [button setTarget:self];
421   };
423   // Back button. On top of the "Notifications" label, hidden by default.
424   NSRect backButtonFrame =
425       NSMakeRect(NSMinX(titleFrame),
426                  (kControlAreaHeight - kBackButtonSize) / 2,
427                  kBackButtonSize,
428                  kBackButtonSize);
429   backButton_.reset([[HoverImageButton alloc] initWithFrame:backButtonFrame]);
430   [backButton_ setDefaultImage:
431       rb.GetNativeImageNamed(IDR_NOTIFICATION_ARROW).ToNSImage()];
432   [backButton_ setHoverImage:
433       rb.GetNativeImageNamed(IDR_NOTIFICATION_ARROW_HOVER).ToNSImage()];
434   [backButton_ setPressedImage:
435       rb.GetNativeImageNamed(IDR_NOTIFICATION_ARROW_PRESSED).ToNSImage()];
436   [backButton_ setAction:@selector(showMessages:)];
437   configureButton(backButton_);
438   [backButton_ setHidden:YES];
439   [backButton_ setKeyEquivalent:@"\e"];
440   [backButton_ setToolTip:l10n_util::GetNSString(
441       IDS_MESSAGE_CENTER_SETTINGS_GO_BACK_BUTTON_TOOLTIP)];
442   [[backButton_ cell]
443       accessibilitySetOverrideValue:[backButton_ toolTip]
444                        forAttribute:NSAccessibilityDescriptionAttribute];
445   [[self view] addSubview:backButton_];
447   // Create the divider line between the control area and the notifications.
448   base::scoped_nsobject<NSBox> divider(
449       [[NSBox alloc] initWithFrame:NSMakeRect(0, 0, NSWidth([view frame]), 1)]);
450   [divider setAutoresizingMask:NSViewMinYMargin];
451   [divider setBorderType:NSNoBorder];
452   [divider setBoxType:NSBoxCustom];
453   [divider setContentViewMargins:NSZeroSize];
454   [divider setFillColor:gfx::SkColorToCalibratedNSColor(
455       message_center::kFooterDelimiterColor)];
456   [divider setTitlePosition:NSNoTitle];
457   [view addSubview:divider];
459   auto getButtonFrame = ^NSRect(CGFloat maxX, NSImage* image) {
460       NSSize size = [image size];
461       return NSMakeRect(
462           maxX - size.width,
463           kControlAreaHeight/2 - size.height/2,
464           size.width,
465           size.height);
466   };
468   // Create the settings button at the far-right.
469   NSImage* defaultImage =
470       rb.GetNativeImageNamed(IDR_NOTIFICATION_SETTINGS).ToNSImage();
471   NSRect settingsButtonFrame = getButtonFrame(
472       NSWidth([view frame]) - message_center::kMarginBetweenItems,
473       defaultImage);
474   settingsButton_.reset(
475       [[HoverImageButton alloc] initWithFrame:settingsButtonFrame]);
476   [settingsButton_ setDefaultImage:defaultImage];
477   [settingsButton_ setHoverImage:
478       rb.GetNativeImageNamed(IDR_NOTIFICATION_SETTINGS_HOVER).ToNSImage()];
479   [settingsButton_ setPressedImage:
480       rb.GetNativeImageNamed(IDR_NOTIFICATION_SETTINGS_PRESSED).ToNSImage()];
481   [settingsButton_ setToolTip:
482       l10n_util::GetNSString(IDS_MESSAGE_CENTER_SETTINGS_BUTTON_LABEL)];
483   [[settingsButton_ cell]
484       accessibilitySetOverrideValue:[settingsButton_ toolTip]
485                        forAttribute:NSAccessibilityDescriptionAttribute];
486   [settingsButton_ setAction:@selector(showSettings:)];
487   configureButton(settingsButton_);
488   [view addSubview:settingsButton_];
490   // Create the clear all button.
491   defaultImage = rb.GetNativeImageNamed(IDR_NOTIFICATION_CLEAR_ALL).ToNSImage();
492   NSRect clearAllButtonFrame = getButtonFrame(
493       NSMinX(settingsButtonFrame) - kButtonXMargin,
494       defaultImage);
495   clearAllButton_.reset(
496       [[HoverImageButton alloc] initWithFrame:clearAllButtonFrame]);
497   [clearAllButton_ setDefaultImage:defaultImage];
498   [clearAllButton_ setHoverImage:
499       rb.GetNativeImageNamed(IDR_NOTIFICATION_CLEAR_ALL_HOVER).ToNSImage()];
500   [clearAllButton_ setPressedImage:
501       rb.GetNativeImageNamed(IDR_NOTIFICATION_CLEAR_ALL_PRESSED).ToNSImage()];
502   [clearAllButton_ setToolTip:
503       l10n_util::GetNSString(IDS_MESSAGE_CENTER_CLEAR_ALL)];
504   [[clearAllButton_ cell]
505       accessibilitySetOverrideValue:[clearAllButton_ toolTip]
506                        forAttribute:NSAccessibilityDescriptionAttribute];
507   [clearAllButton_ setAction:@selector(clearAllNotifications:)];
508   configureButton(clearAllButton_);
509   [view addSubview:clearAllButton_];
511   // Create the pause button.
512   NSRect pauseButtonFrame = getButtonFrame(
513       NSMinX(clearAllButtonFrame) - kButtonXMargin,
514       defaultImage);
515   pauseButton_.reset([[HoverImageButton alloc] initWithFrame:pauseButtonFrame]);
516   [self updateQuietModeButtonImage];
517   [pauseButton_ setHoverImage: rb.GetNativeImageNamed(
518       IDR_NOTIFICATION_DO_NOT_DISTURB_HOVER).ToNSImage()];
519   [pauseButton_ setToolTip:
520       l10n_util::GetNSString(IDS_MESSAGE_CENTER_QUIET_MODE_BUTTON_TOOLTIP)];
521   [[pauseButton_ cell]
522       accessibilitySetOverrideValue:[pauseButton_ toolTip]
523                        forAttribute:NSAccessibilityDescriptionAttribute];
524   [pauseButton_ setAction:@selector(toggleQuietMode:)];
525   configureButton(pauseButton_);
526   [view addSubview:pauseButton_];
529 - (void)updateTrayViewAndWindow {
530   CGFloat scrollContentHeight = 0;
531   if ([notifications_ count]) {
532     scrollContentHeight = NSMaxY([[[notifications_ lastObject] view] frame]) +
533         message_center::kMarginBetweenItems;;
534   }
536   // Resize the scroll view's content.
537   NSRect scrollViewFrame = [scrollView_ frame];
538   NSRect documentFrame = [[scrollView_ documentView] frame];
539   documentFrame.size.width = NSWidth(scrollViewFrame);
540   documentFrame.size.height = scrollContentHeight;
541   [[scrollView_ documentView] setFrame:documentFrame];
543   // Resize the container view.
544   NSRect frame = [[self view] frame];
545   CGFloat oldHeight = NSHeight(frame);
546   if (settingsController_) {
547     frame.size.height = NSHeight([[settingsController_ view] frame]);
548   } else {
549     frame.size.height = std::min([MCTrayViewController maxTrayClientHeight],
550                                  scrollContentHeight);
551   }
552   frame.size.height += kControlAreaHeight;
553   CGFloat newHeight = NSHeight(frame);
554   [[self view] setFrame:frame];
556   // Resize the scroll view.
557   scrollViewFrame.size.height = NSHeight(frame) - kControlAreaHeight;
558   [scrollView_ setFrame:scrollViewFrame];
560   // Resize the window.
561   NSRect windowFrame = [[[self view] window] frame];
562   CGFloat delta = newHeight - oldHeight;
563   windowFrame.origin.y -= delta;
564   windowFrame.size.height += delta;
566   [[[self view] window] setFrame:windowFrame display:YES];
567   // Hide the clear-all button if there are no notifications. Simply swap the
568   // X position of it and the pause button in that case.
569   BOOL hidden = ![notifications_ count];
570   if ([clearAllButton_ isHidden] != hidden) {
571     [clearAllButton_ setHidden:hidden];
573     NSRect pauseButtonFrame = [pauseButton_ frame];
574     NSRect clearAllButtonFrame = [clearAllButton_ frame];
575     std::swap(clearAllButtonFrame.origin.x, pauseButtonFrame.origin.x);
576     [pauseButton_ setFrame:pauseButtonFrame];
577     [clearAllButton_ setFrame:clearAllButtonFrame];
578   }
581 - (void)animationDidEnd:(NSAnimation*)animation {
582   if (clearAllInProgress_) {
583     // For clear-all animation.
584     [clearAllAnimations_ removeObject:animation];
585     if (![clearAllAnimations_ count] &&
586         visibleNotificationsPendingClear_.empty()) {
587       [self finalizeClearAll];
588     }
589   } else {
590     // For notification removal and reposition animation.
591     if ([notificationsPendingRemoval_ count]) {
592       [self moveUpRemainingNotifications];
593     } else {
594       [self finalizeTrayViewAndWindow];
596       if (clearAllDelayed_)
597         [self clearAllNotifications:nil];
598     }
599   }
601   // Give the testing code a chance to do something, i.e. quitting the test
602   // run loop.
603   if (![self isAnimating] && testingAnimationEndedCallback_)
604     testingAnimationEndedCallback_.get()();
607 - (void)closeNotificationsByUser {
608   // No need to close individual notification if clear-all is in progress.
609   if (clearAllInProgress_)
610     return;
612   if ([self isAnimating])
613     return;
614   [self hideNotificationsPendingRemoval];
617 - (void)hideNotificationsPendingRemoval {
618   base::scoped_nsobject<NSMutableArray> animationDataArray(
619       [[NSMutableArray alloc] init]);
621   // Fade-out those notifications pending removal.
622   for (MCNotificationController* notification in notifications_.get()) {
623     if (messageCenter_->HasNotification([notification notificationID]))
624       continue;
625     [notificationsPendingRemoval_ addObject:notification];
626     [animationDataArray addObject:@{
627         NSViewAnimationTargetKey : [notification view],
628         NSViewAnimationEffectKey : NSViewAnimationFadeOutEffect
629     }];
630   }
632   if ([notificationsPendingRemoval_ count] == 0)
633     return;
635   for (MCNotificationController* notification in
636            notificationsPendingRemoval_.get()) {
637     [notifications_ removeObject:notification];
638   }
640   // Start the animation.
641   animation_.reset([[NSViewAnimation alloc]
642       initWithViewAnimations:animationDataArray]);
643   [animation_ setDuration:animationDuration_];
644   [animation_ setDelegate:self];
645   [animation_ startAnimation];
648 - (void)moveUpRemainingNotifications {
649   base::scoped_nsobject<NSMutableArray> animationDataArray(
650       [[NSMutableArray alloc] init]);
652   // Compute the position where the remaining notifications should start.
653   CGFloat minY = message_center::kMarginBetweenItems;
654   for (MCNotificationController* notification in
655            notificationsPendingRemoval_.get()) {
656     NSView* view = [notification view];
657     minY += NSHeight([view frame]) + message_center::kMarginBetweenItems;
658   }
660   // Reposition the remaining notifications starting at the computed position.
661   for (MCNotificationController* notification in notifications_.get()) {
662     NSView* view = [notification view];
663     NSRect frame = [view frame];
664     NSRect oldFrame = frame;
665     frame.origin.y = minY;
666     if (!NSEqualRects(oldFrame, frame)) {
667       [animationDataArray addObject:@{
668           NSViewAnimationTargetKey : view,
669           NSViewAnimationEndFrameKey : [NSValue valueWithRect:frame]
670       }];
671     }
672     minY = NSMaxY(frame) + message_center::kMarginBetweenItems;
673   }
675   // Now remove notifications pending removal.
676   for (MCNotificationController* notification in
677            notificationsPendingRemoval_.get()) {
678     [[notification view] removeFromSuperview];
679     notificationsMap_.erase([notification notificationID]);
680   }
681   [notificationsPendingRemoval_ removeAllObjects];
683   // Start the animation.
684   animation_.reset([[NSViewAnimation alloc]
685       initWithViewAnimations:animationDataArray]);
686   [animation_ setDuration:animationDuration_];
687   [animation_ setDelegate:self];
688   [animation_ startAnimation];
691 - (void)finalizeTrayViewAndWindow {
692   // Reposition the remaining notifications starting at the bottom.
693   CGFloat minY = message_center::kMarginBetweenItems;
694   for (MCNotificationController* notification in notifications_.get()) {
695     NSView* view = [notification view];
696     NSRect frame = [view frame];
697     NSRect oldFrame = frame;
698     frame.origin.y = minY;
699     if (!NSEqualRects(oldFrame, frame))
700       [view setFrame:frame];
701     minY = NSMaxY(frame) + message_center::kMarginBetweenItems;
702   }
704   [self updateTrayViewAndWindow];
706   // Check if there're more notifications pending removal.
707   [self closeNotificationsByUser];
710 - (void)clearOneNotification {
711   DCHECK(!visibleNotificationsPendingClear_.empty());
713   MCNotificationController* notification =
714       visibleNotificationsPendingClear_.back();
715   visibleNotificationsPendingClear_.pop_back();
717   // Slide out the notification from left to right with fade-out simultaneously.
718   NSRect newFrame = [[notification view] frame];
719   newFrame.origin.x = NSMaxX(newFrame) + message_center::kMarginBetweenItems;
720   NSDictionary* animationDict = @{
721     NSViewAnimationTargetKey : [notification view],
722     NSViewAnimationEndFrameKey : [NSValue valueWithRect:newFrame],
723     NSViewAnimationEffectKey : NSViewAnimationFadeOutEffect
724   };
725   base::scoped_nsobject<NSViewAnimation> animation([[NSViewAnimation alloc]
726       initWithViewAnimations:[NSArray arrayWithObject:animationDict]]);
727   [animation setDuration:animationDuration_];
728   [animation setDelegate:self];
729   [animation startAnimation];
730   [clearAllAnimations_ addObject:animation];
732   // Schedule to start sliding out next notification after a short delay.
733   if (!visibleNotificationsPendingClear_.empty()) {
734     [self performSelector:@selector(clearOneNotification)
735                withObject:nil
736                afterDelay:animateClearingNextNotificationDelay_];
737   }
740 - (void)finalizeClearAll {
741   DCHECK(clearAllInProgress_);
742   clearAllInProgress_ = NO;
744   DCHECK(![clearAllAnimations_ count]);
745   clearAllAnimations_.reset();
747   [pauseButton_ setEnabled:YES];
748   [clearAllButton_ setEnabled:YES];
749   [settingsButton_ setEnabled:YES];
750   [clipView_ setFrozen:NO];
752   messageCenter_->RemoveAllNotifications(true);
755 - (void)updateQuietModeButtonImage {
756   ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
757   if (messageCenter_->IsQuietMode()) {
758     [pauseButton_ setTrackingEnabled:NO];
759     [pauseButton_ setDefaultImage: rb.GetNativeImageNamed(
760         IDR_NOTIFICATION_DO_NOT_DISTURB_PRESSED).ToNSImage()];
761   } else {
762     [pauseButton_ setTrackingEnabled:YES];
763     [pauseButton_ setDefaultImage:
764         rb.GetNativeImageNamed(IDR_NOTIFICATION_DO_NOT_DISTURB).ToNSImage()];
765   }
768 @end