// Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #import "ui/message_center/cocoa/tray_view_controller.h" #include #include "base/mac/scoped_nsautorelease_pool.h" #include "base/time/time.h" #include "skia/ext/skia_utils_mac.h" #import "ui/base/cocoa/hover_image_button.h" #include "ui/base/l10n/l10n_util_mac.h" #include "ui/base/resource/resource_bundle.h" #import "ui/message_center/cocoa/notification_controller.h" #import "ui/message_center/cocoa/opaque_views.h" #import "ui/message_center/cocoa/settings_controller.h" #include "ui/message_center/message_center.h" #include "ui/message_center/message_center_style.h" #include "ui/message_center/notifier_settings.h" #include "ui/resources/grit/ui_resources.h" #include "ui/strings/grit/ui_strings.h" const int kBackButtonSize = 16; // NSClipView subclass. @interface MCClipView : NSClipView { // If this is set, the visible document area will remain intact no matter how // the user scrolls or drags the thumb. BOOL frozen_; } @end @implementation MCClipView - (void)setFrozen:(BOOL)frozen { frozen_ = frozen; } - (NSPoint)constrainScrollPoint:(NSPoint)proposedNewOrigin { return frozen_ ? [self documentVisibleRect].origin : [super constrainScrollPoint:proposedNewOrigin]; } @end @interface MCTrayViewController (Private) // Creates all the views for the control area of the tray. - (void)layoutControlArea; // Update both tray view and window by resizing it to fit its content. - (void)updateTrayViewAndWindow; // Remove notifications dismissed by the user. It is done in the following // 3 steps. - (void)closeNotificationsByUser; // Step 1: hide all notifications pending removal with fade-out animation. - (void)hideNotificationsPendingRemoval; // Step 2: move up all remaining notifications to take over the available space // due to hiding notifications. The scroll view and the window remain unchanged. - (void)moveUpRemainingNotifications; // Step 3: finalize the tray view and window to get rid of the empty space. - (void)finalizeTrayViewAndWindow; // Clear a notification by sliding it out from left to right. This occurs when // "Clear All" is clicked. - (void)clearOneNotification; // When all visible notifications slide out, re-enable controls and remove // notifications from the message center. - (void)finalizeClearAll; // Sets the images of the quiet mode button based on the message center state. - (void)updateQuietModeButtonImage; @end namespace { // The duration of fade-out and bounds animation. const NSTimeInterval kAnimationDuration = 0.2; // The delay to start animating clearing next notification since current // animation starts. const NSTimeInterval kAnimateClearingNextNotificationDelay = 0.04; // The height of the bar at the top of the tray that contains buttons. const CGFloat kControlAreaHeight = 50; // Amount of spacing between control buttons. There is kMarginBetweenItems // between a button and the edge of the tray, though. const CGFloat kButtonXMargin = 20; // Amount of padding to leave between the bottom of the screen and the bottom // of the message center tray. const CGFloat kTrayBottomMargin = 75; } // namespace @implementation MCTrayViewController - (id)initWithMessageCenter:(message_center::MessageCenter*)messageCenter { if ((self = [super initWithNibName:nil bundle:nil])) { messageCenter_ = messageCenter; animationDuration_ = kAnimationDuration; animateClearingNextNotificationDelay_ = kAnimateClearingNextNotificationDelay; notifications_.reset([[NSMutableArray alloc] init]); notificationsPendingRemoval_.reset([[NSMutableArray alloc] init]); } return self; } - (NSString*)trayTitle { return [title_ stringValue]; } - (void)setTrayTitle:(NSString*)title { [title_ setStringValue:title]; [title_ sizeToFit]; } - (void)onWindowClosing { if (animation_) { [animation_ stopAnimation]; [animation_ setDelegate:nil]; animation_.reset(); } if (clearAllInProgress_) { // To stop chain of clearOneNotification calls to start new animations. [NSObject cancelPreviousPerformRequestsWithTarget:self]; for (NSViewAnimation* animation in clearAllAnimations_.get()) { [animation stopAnimation]; [animation setDelegate:nil]; } [clearAllAnimations_ removeAllObjects]; [self finalizeClearAll]; } } - (void)loadView { // Configure the root view as a background-colored box. base::scoped_nsobject view([[NSBox alloc] initWithFrame:NSMakeRect( 0, 0, [MCTrayViewController trayWidth], kControlAreaHeight)]); [view setBorderType:NSNoBorder]; [view setBoxType:NSBoxCustom]; [view setContentViewMargins:NSZeroSize]; [view setFillColor:gfx::SkColorToCalibratedNSColor( message_center::kMessageCenterBackgroundColor)]; [view setTitlePosition:NSNoTitle]; [view setWantsLayer:YES]; // Needed for notification view shadows. [self setView:view]; [self layoutControlArea]; // Configure the scroll view in which all the notifications go. base::scoped_nsobject documentView( [[NSView alloc] initWithFrame:NSZeroRect]); scrollView_.reset([[NSScrollView alloc] initWithFrame:[view frame]]); clipView_.reset( [[MCClipView alloc] initWithFrame:[[scrollView_ contentView] frame]]); [scrollView_ setContentView:clipView_]; [scrollView_ setAutohidesScrollers:YES]; [scrollView_ setAutoresizingMask:NSViewHeightSizable | NSViewMaxYMargin]; [scrollView_ setDocumentView:documentView]; [scrollView_ setDrawsBackground:NO]; [scrollView_ setHasHorizontalScroller:NO]; [scrollView_ setHasVerticalScroller:YES]; [view addSubview:scrollView_]; [self onMessageCenterTrayChanged]; } - (void)onMessageCenterTrayChanged { if (settingsController_) return [self updateTrayViewAndWindow]; std::map newMap; base::scoped_nsobject shadow([[NSShadow alloc] init]); [shadow setShadowColor:[NSColor colorWithDeviceWhite:0 alpha:0.55]]; [shadow setShadowOffset:NSMakeSize(0, -1)]; [shadow setShadowBlurRadius:2.0]; CGFloat minY = message_center::kMarginBetweenItems; // Iterate over the notifications in reverse, since the Cocoa coordinate // origin is in the lower-left. Remove from |notificationsMap_| all the // ones still in the updated model, so that those that should be removed // will remain in the map. const auto& modelNotifications = messageCenter_->GetVisibleNotifications(); for (auto it = modelNotifications.rbegin(); it != modelNotifications.rend(); ++it) { // Check if this notification is already in the tray. const auto& existing = notificationsMap_.find((*it)->id()); MCNotificationController* notification = nil; if (existing == notificationsMap_.end()) { base::scoped_nsobject controller( [[MCNotificationController alloc] initWithNotification:*it messageCenter:messageCenter_]); [[controller view] setShadow:shadow]; [[scrollView_ documentView] addSubview:[controller view]]; [notifications_ addObject:controller]; // Transfer ownership. messageCenter_->DisplayedNotification( (*it)->id(), message_center::DISPLAY_SOURCE_MESSAGE_CENTER); notification = controller.get(); } else { notification = existing->second; [notification updateNotification:*it]; notificationsMap_.erase(existing); } DCHECK(notification); NSRect frame = [[notification view] frame]; frame.origin.x = message_center::kMarginBetweenItems; frame.origin.y = minY; [[notification view] setFrame:frame]; newMap.insert(std::make_pair((*it)->id(), notification)); minY = NSMaxY(frame) + message_center::kMarginBetweenItems; } // Remove any notifications that are no longer in the model. for (const auto& pair : notificationsMap_) { [[pair.second view] removeFromSuperview]; [notifications_ removeObject:pair.second]; } // Copy the new map of notifications to replace the old. notificationsMap_ = newMap; [self updateTrayViewAndWindow]; } - (void)toggleQuietMode:(id)sender { if (messageCenter_->IsQuietMode()) messageCenter_->SetQuietMode(false); else messageCenter_->EnterQuietModeWithExpire(base::TimeDelta::FromDays(1)); [self updateQuietModeButtonImage]; } - (void)clearAllNotifications:(id)sender { if ([self isAnimating]) { clearAllDelayed_ = YES; return; } // Build a list for all notifications within the visible scroll range // in preparation to slide them out one by one. NSRect visibleScrollRect = [scrollView_ documentVisibleRect]; for (MCNotificationController* notification in notifications_.get()) { NSRect rect = [[notification view] frame]; if (!NSIsEmptyRect(NSIntersectionRect(visibleScrollRect, rect))) { visibleNotificationsPendingClear_.push_back(notification); } } if (visibleNotificationsPendingClear_.empty()) return; // Disbale buttons and freeze scroll bar to prevent the user from clicking on // them accidentally. [pauseButton_ setEnabled:NO]; [clearAllButton_ setEnabled:NO]; [settingsButton_ setEnabled:NO]; [clipView_ setFrozen:YES]; // Start sliding out the top notification. clearAllAnimations_.reset([[NSMutableArray alloc] init]); [self clearOneNotification]; clearAllInProgress_ = YES; } - (void)showSettings:(id)sender { if (settingsController_) return [self showMessages:sender]; message_center::NotifierSettingsProvider* provider = messageCenter_->GetNotifierSettingsProvider(); settingsController_.reset( [[MCSettingsController alloc] initWithProvider:provider trayViewController:self]); [[self view] addSubview:[settingsController_ view]]; NSRect titleFrame = [title_ frame]; titleFrame.origin.x = NSMaxX([backButton_ frame]) + message_center::kMarginBetweenItems / 2; [title_ setFrame:titleFrame]; [backButton_ setHidden:NO]; [clearAllButton_ setEnabled:NO]; [scrollView_ setHidden:YES]; [[[self view] window] recalculateKeyViewLoop]; messageCenter_->SetVisibility(message_center::VISIBILITY_SETTINGS); [self updateTrayViewAndWindow]; } - (void)updateSettings { // TODO(jianli): This class should not be calling -loadView, but instead // should just observe a resize notification. // (http://crbug.com/270251) [[settingsController_ view] removeFromSuperview]; [settingsController_ loadView]; [[self view] addSubview:[settingsController_ view]]; [self updateTrayViewAndWindow]; } - (void)showMessages:(id)sender { messageCenter_->SetVisibility(message_center::VISIBILITY_MESSAGE_CENTER); [self cleanupSettings]; [[[self view] window] recalculateKeyViewLoop]; [self updateTrayViewAndWindow]; } - (void)cleanupSettings { [scrollView_ setHidden:NO]; [[settingsController_ view] removeFromSuperview]; settingsController_.reset(); NSRect titleFrame = [title_ frame]; titleFrame.origin.x = NSMinX([backButton_ frame]); [title_ setFrame:titleFrame]; [backButton_ setHidden:YES]; [clearAllButton_ setEnabled:YES]; } - (void)scrollToTop { NSPoint topPoint = NSMakePoint(0.0, [[scrollView_ documentView] bounds].size.height); [[scrollView_ documentView] scrollPoint:topPoint]; } - (BOOL)isAnimating { return [animation_ isAnimating] || [clearAllAnimations_ count]; } + (CGFloat)maxTrayClientHeight { NSRect screenFrame = [[[NSScreen screens] objectAtIndex:0] visibleFrame]; return NSHeight(screenFrame) - kTrayBottomMargin - kControlAreaHeight; } + (CGFloat)trayWidth { return message_center::kNotificationWidth + 2 * message_center::kMarginBetweenItems; } // Testing API ///////////////////////////////////////////////////////////////// - (NSBox*)divider { return divider_.get(); } - (NSTextField*)emptyDescription { return emptyDescription_.get(); } - (NSScrollView*)scrollView { return scrollView_.get(); } - (HoverImageButton*)pauseButton { return pauseButton_.get(); } - (HoverImageButton*)clearAllButton { return clearAllButton_.get(); } - (void)setAnimationDuration:(NSTimeInterval)duration { animationDuration_ = duration; } - (void)setAnimateClearingNextNotificationDelay:(NSTimeInterval)delay { animateClearingNextNotificationDelay_ = delay; } - (void)setAnimationEndedCallback: (message_center::TrayAnimationEndedCallback)callback { testingAnimationEndedCallback_.reset(Block_copy(callback)); } // Private ///////////////////////////////////////////////////////////////////// - (void)layoutControlArea { ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); NSView* view = [self view]; // Create the "Notifications" label at the top of the tray. NSFont* font = [NSFont labelFontOfSize:message_center::kTitleFontSize]; NSColor* color = gfx::SkColorToCalibratedNSColor( message_center::kMessageCenterBackgroundColor); title_.reset( [[MCTextField alloc] initWithFrame:NSZeroRect backgroundColor:color]); [title_ setFont:font]; [title_ setStringValue: l10n_util::GetNSString(IDS_MESSAGE_CENTER_FOOTER_TITLE)]; [title_ setTextColor:gfx::SkColorToCalibratedNSColor( message_center::kRegularTextColor)]; [title_ sizeToFit]; NSRect titleFrame = [title_ frame]; titleFrame.origin.x = message_center::kMarginBetweenItems; titleFrame.origin.y = kControlAreaHeight/2 - NSMidY(titleFrame); [title_ setFrame:titleFrame]; [view addSubview:title_]; auto configureButton = ^(HoverImageButton* button) { [[button cell] setHighlightsBy:NSOnState]; [button setTrackingEnabled:YES]; [button setBordered:NO]; [button setAutoresizingMask:NSViewMinYMargin]; [button setTarget:self]; }; // Back button. On top of the "Notifications" label, hidden by default. NSRect backButtonFrame = NSMakeRect(NSMinX(titleFrame), (kControlAreaHeight - kBackButtonSize) / 2, kBackButtonSize, kBackButtonSize); backButton_.reset([[HoverImageButton alloc] initWithFrame:backButtonFrame]); [backButton_ setDefaultImage: rb.GetNativeImageNamed(IDR_NOTIFICATION_ARROW).ToNSImage()]; [backButton_ setHoverImage: rb.GetNativeImageNamed(IDR_NOTIFICATION_ARROW_HOVER).ToNSImage()]; [backButton_ setPressedImage: rb.GetNativeImageNamed(IDR_NOTIFICATION_ARROW_PRESSED).ToNSImage()]; [backButton_ setAction:@selector(showMessages:)]; configureButton(backButton_); [backButton_ setHidden:YES]; [backButton_ setKeyEquivalent:@"\e"]; [backButton_ setToolTip:l10n_util::GetNSString( IDS_MESSAGE_CENTER_SETTINGS_GO_BACK_BUTTON_TOOLTIP)]; [[backButton_ cell] accessibilitySetOverrideValue:[backButton_ toolTip] forAttribute:NSAccessibilityDescriptionAttribute]; [[self view] addSubview:backButton_]; // Create the divider line between the control area and the notifications. divider_.reset( [[NSBox alloc] initWithFrame:NSMakeRect(0, 0, NSWidth([view frame]), 1)]); [divider_ setAutoresizingMask:NSViewMinYMargin]; [divider_ setBorderType:NSNoBorder]; [divider_ setBoxType:NSBoxCustom]; [divider_ setContentViewMargins:NSZeroSize]; [divider_ setFillColor:gfx::SkColorToCalibratedNSColor( message_center::kFooterDelimiterColor)]; [divider_ setTitlePosition:NSNoTitle]; [view addSubview:divider_]; auto getButtonFrame = ^NSRect(CGFloat maxX, NSImage* image) { NSSize size = [image size]; return NSMakeRect( maxX - size.width, kControlAreaHeight/2 - size.height/2, size.width, size.height); }; // Create the settings button at the far-right. NSImage* defaultImage = rb.GetNativeImageNamed(IDR_NOTIFICATION_SETTINGS).ToNSImage(); NSRect settingsButtonFrame = getButtonFrame( NSWidth([view frame]) - message_center::kMarginBetweenItems, defaultImage); settingsButton_.reset( [[HoverImageButton alloc] initWithFrame:settingsButtonFrame]); [settingsButton_ setDefaultImage:defaultImage]; [settingsButton_ setHoverImage: rb.GetNativeImageNamed(IDR_NOTIFICATION_SETTINGS_HOVER).ToNSImage()]; [settingsButton_ setPressedImage: rb.GetNativeImageNamed(IDR_NOTIFICATION_SETTINGS_PRESSED).ToNSImage()]; [settingsButton_ setToolTip: l10n_util::GetNSString(IDS_MESSAGE_CENTER_SETTINGS_BUTTON_LABEL)]; [[settingsButton_ cell] accessibilitySetOverrideValue:[settingsButton_ toolTip] forAttribute:NSAccessibilityDescriptionAttribute]; [settingsButton_ setAction:@selector(showSettings:)]; configureButton(settingsButton_); [view addSubview:settingsButton_]; // Create the clear all button. defaultImage = rb.GetNativeImageNamed(IDR_NOTIFICATION_CLEAR_ALL).ToNSImage(); NSRect clearAllButtonFrame = getButtonFrame( NSMinX(settingsButtonFrame) - kButtonXMargin, defaultImage); clearAllButton_.reset( [[HoverImageButton alloc] initWithFrame:clearAllButtonFrame]); [clearAllButton_ setDefaultImage:defaultImage]; [clearAllButton_ setHoverImage: rb.GetNativeImageNamed(IDR_NOTIFICATION_CLEAR_ALL_HOVER).ToNSImage()]; [clearAllButton_ setPressedImage: rb.GetNativeImageNamed(IDR_NOTIFICATION_CLEAR_ALL_PRESSED).ToNSImage()]; [clearAllButton_ setToolTip: l10n_util::GetNSString(IDS_MESSAGE_CENTER_CLEAR_ALL)]; [[clearAllButton_ cell] accessibilitySetOverrideValue:[clearAllButton_ toolTip] forAttribute:NSAccessibilityDescriptionAttribute]; [clearAllButton_ setAction:@selector(clearAllNotifications:)]; configureButton(clearAllButton_); [view addSubview:clearAllButton_]; // Create the pause button. NSRect pauseButtonFrame = getButtonFrame( NSMinX(clearAllButtonFrame) - kButtonXMargin, defaultImage); pauseButton_.reset([[HoverImageButton alloc] initWithFrame:pauseButtonFrame]); [self updateQuietModeButtonImage]; [pauseButton_ setHoverImage: rb.GetNativeImageNamed( IDR_NOTIFICATION_DO_NOT_DISTURB_HOVER).ToNSImage()]; [pauseButton_ setToolTip: l10n_util::GetNSString(IDS_MESSAGE_CENTER_QUIET_MODE_BUTTON_TOOLTIP)]; [[pauseButton_ cell] accessibilitySetOverrideValue:[pauseButton_ toolTip] forAttribute:NSAccessibilityDescriptionAttribute]; [pauseButton_ setAction:@selector(toggleQuietMode:)]; configureButton(pauseButton_); [view addSubview:pauseButton_]; // Create the description field for the empty message center. Initially it is // invisible. emptyDescription_.reset( [[MCTextField alloc] initWithFrame:NSZeroRect backgroundColor:color]); NSFont* smallFont = [NSFont labelFontOfSize:message_center::kEmptyCenterFontSize]; [emptyDescription_ setFont:smallFont]; [emptyDescription_ setStringValue: l10n_util::GetNSString(IDS_MESSAGE_CENTER_NO_MESSAGES)]; [emptyDescription_ setTextColor:gfx::SkColorToCalibratedNSColor( message_center::kDimTextColor)]; [emptyDescription_ sizeToFit]; [emptyDescription_ setHidden:YES]; [view addSubview:emptyDescription_]; } - (void)updateTrayViewAndWindow { CGFloat scrollContentHeight = message_center::kMinScrollViewHeight; if ([notifications_ count]) { [emptyDescription_ setHidden:YES]; [scrollView_ setHidden:NO]; [divider_ setHidden:NO]; scrollContentHeight = NSMaxY([[[notifications_ lastObject] view] frame]) + message_center::kMarginBetweenItems;; } else { [emptyDescription_ setHidden:NO]; [scrollView_ setHidden:YES]; [divider_ setHidden:YES]; NSRect centeredFrame = [emptyDescription_ frame]; NSPoint centeredOrigin = NSMakePoint( floor((NSWidth([[self view] frame]) - NSWidth(centeredFrame))/2 + 0.5), floor((scrollContentHeight - NSHeight(centeredFrame))/2 + 0.5)); centeredFrame.origin = centeredOrigin; [emptyDescription_ setFrame:centeredFrame]; } // Resize the scroll view's content. NSRect scrollViewFrame = [scrollView_ frame]; NSRect documentFrame = [[scrollView_ documentView] frame]; documentFrame.size.width = NSWidth(scrollViewFrame); documentFrame.size.height = scrollContentHeight; [[scrollView_ documentView] setFrame:documentFrame]; // Resize the container view. NSRect frame = [[self view] frame]; CGFloat oldHeight = NSHeight(frame); if (settingsController_) { frame.size.height = NSHeight([[settingsController_ view] frame]); } else { frame.size.height = std::min([MCTrayViewController maxTrayClientHeight], scrollContentHeight); } frame.size.height += kControlAreaHeight; CGFloat newHeight = NSHeight(frame); [[self view] setFrame:frame]; // Resize the scroll view. scrollViewFrame.size.height = NSHeight(frame) - kControlAreaHeight; [scrollView_ setFrame:scrollViewFrame]; // Resize the window. NSRect windowFrame = [[[self view] window] frame]; CGFloat delta = newHeight - oldHeight; windowFrame.origin.y -= delta; windowFrame.size.height += delta; [[[self view] window] setFrame:windowFrame display:YES]; // Hide the clear-all button if there are no notifications. Simply swap the // X position of it and the pause button in that case. BOOL hidden = ![notifications_ count]; if ([clearAllButton_ isHidden] != hidden) { [clearAllButton_ setHidden:hidden]; NSRect pauseButtonFrame = [pauseButton_ frame]; NSRect clearAllButtonFrame = [clearAllButton_ frame]; std::swap(clearAllButtonFrame.origin.x, pauseButtonFrame.origin.x); [pauseButton_ setFrame:pauseButtonFrame]; [clearAllButton_ setFrame:clearAllButtonFrame]; } } - (void)animationDidEnd:(NSAnimation*)animation { if (clearAllInProgress_) { // For clear-all animation. [clearAllAnimations_ removeObject:animation]; if (![clearAllAnimations_ count] && visibleNotificationsPendingClear_.empty()) { [self finalizeClearAll]; } } else { // For notification removal and reposition animation. if ([notificationsPendingRemoval_ count]) { [self moveUpRemainingNotifications]; } else { [self finalizeTrayViewAndWindow]; if (clearAllDelayed_) [self clearAllNotifications:nil]; } } // Give the testing code a chance to do something, i.e. quitting the test // run loop. if (![self isAnimating] && testingAnimationEndedCallback_) testingAnimationEndedCallback_.get()(); } - (void)closeNotificationsByUser { // No need to close individual notification if clear-all is in progress. if (clearAllInProgress_) return; if ([self isAnimating]) return; [self hideNotificationsPendingRemoval]; } - (void)hideNotificationsPendingRemoval { base::scoped_nsobject animationDataArray( [[NSMutableArray alloc] init]); // Fade-out those notifications pending removal. for (MCNotificationController* notification in notifications_.get()) { if (messageCenter_->FindVisibleNotificationById( [notification notificationID])) continue; [notificationsPendingRemoval_ addObject:notification]; [animationDataArray addObject:@{ NSViewAnimationTargetKey : [notification view], NSViewAnimationEffectKey : NSViewAnimationFadeOutEffect }]; } if ([notificationsPendingRemoval_ count] == 0) return; for (MCNotificationController* notification in notificationsPendingRemoval_.get()) { [notifications_ removeObject:notification]; } // Start the animation. animation_.reset([[NSViewAnimation alloc] initWithViewAnimations:animationDataArray]); [animation_ setDuration:animationDuration_]; [animation_ setDelegate:self]; [animation_ startAnimation]; } - (void)moveUpRemainingNotifications { base::scoped_nsobject animationDataArray( [[NSMutableArray alloc] init]); // Compute the position where the remaining notifications should start. CGFloat minY = message_center::kMarginBetweenItems; for (MCNotificationController* notification in notificationsPendingRemoval_.get()) { NSView* view = [notification view]; minY += NSHeight([view frame]) + message_center::kMarginBetweenItems; } // Reposition the remaining notifications starting at the computed position. for (MCNotificationController* notification in notifications_.get()) { NSView* view = [notification view]; NSRect frame = [view frame]; NSRect oldFrame = frame; frame.origin.y = minY; if (!NSEqualRects(oldFrame, frame)) { [animationDataArray addObject:@{ NSViewAnimationTargetKey : view, NSViewAnimationEndFrameKey : [NSValue valueWithRect:frame] }]; } minY = NSMaxY(frame) + message_center::kMarginBetweenItems; } // Now remove notifications pending removal. for (MCNotificationController* notification in notificationsPendingRemoval_.get()) { [[notification view] removeFromSuperview]; notificationsMap_.erase([notification notificationID]); } [notificationsPendingRemoval_ removeAllObjects]; // Start the animation. animation_.reset([[NSViewAnimation alloc] initWithViewAnimations:animationDataArray]); [animation_ setDuration:animationDuration_]; [animation_ setDelegate:self]; [animation_ startAnimation]; } - (void)finalizeTrayViewAndWindow { // Reposition the remaining notifications starting at the bottom. CGFloat minY = message_center::kMarginBetweenItems; for (MCNotificationController* notification in notifications_.get()) { NSView* view = [notification view]; NSRect frame = [view frame]; NSRect oldFrame = frame; frame.origin.y = minY; if (!NSEqualRects(oldFrame, frame)) [view setFrame:frame]; minY = NSMaxY(frame) + message_center::kMarginBetweenItems; } [self updateTrayViewAndWindow]; // Check if there're more notifications pending removal. [self closeNotificationsByUser]; } - (void)clearOneNotification { DCHECK(!visibleNotificationsPendingClear_.empty()); MCNotificationController* notification = visibleNotificationsPendingClear_.back(); visibleNotificationsPendingClear_.pop_back(); // Slide out the notification from left to right with fade-out simultaneously. NSRect newFrame = [[notification view] frame]; newFrame.origin.x = NSMaxX(newFrame) + message_center::kMarginBetweenItems; NSDictionary* animationDict = @{ NSViewAnimationTargetKey : [notification view], NSViewAnimationEndFrameKey : [NSValue valueWithRect:newFrame], NSViewAnimationEffectKey : NSViewAnimationFadeOutEffect }; base::scoped_nsobject animation([[NSViewAnimation alloc] initWithViewAnimations:[NSArray arrayWithObject:animationDict]]); [animation setDuration:animationDuration_]; [animation setDelegate:self]; [animation startAnimation]; [clearAllAnimations_ addObject:animation]; // Schedule to start sliding out next notification after a short delay. if (!visibleNotificationsPendingClear_.empty()) { [self performSelector:@selector(clearOneNotification) withObject:nil afterDelay:animateClearingNextNotificationDelay_]; } } - (void)finalizeClearAll { DCHECK(clearAllInProgress_); clearAllInProgress_ = NO; DCHECK(![clearAllAnimations_ count]); clearAllAnimations_.reset(); [pauseButton_ setEnabled:YES]; [clearAllButton_ setEnabled:YES]; [settingsButton_ setEnabled:YES]; [clipView_ setFrozen:NO]; messageCenter_->RemoveAllVisibleNotifications(true); } - (void)updateQuietModeButtonImage { ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); if (messageCenter_->IsQuietMode()) { [pauseButton_ setTrackingEnabled:NO]; [pauseButton_ setDefaultImage: rb.GetNativeImageNamed( IDR_NOTIFICATION_DO_NOT_DISTURB_PRESSED).ToNSImage()]; } else { [pauseButton_ setTrackingEnabled:YES]; [pauseButton_ setDefaultImage: rb.GetNativeImageNamed(IDR_NOTIFICATION_DO_NOT_DISTURB).ToNSImage()]; } } @end