| // 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 <cmath> |
| |
| #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<NSBox> 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<NSView> 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<std::string, MCNotificationController*> newMap; |
| |
| base::scoped_nsobject<NSShadow> 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<MCNotificationController> 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<NSMutableArray> 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<NSMutableArray> 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<NSViewAnimation> 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 |