1// Copyright (c) 2013 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#import "ui/message_center/cocoa/tray_view_controller.h" 6 7#include <cmath> 8 9#include "base/mac/scoped_nsautorelease_pool.h" 10#include "base/time/time.h" 11#include "grit/ui_resources.h" 12#include "grit/ui_strings.h" 13#include "skia/ext/skia_utils_mac.h" 14#import "ui/base/cocoa/hover_image_button.h" 15#include "ui/base/l10n/l10n_util_mac.h" 16#include "ui/base/resource/resource_bundle.h" 17#import "ui/message_center/cocoa/notification_controller.h" 18#import "ui/message_center/cocoa/settings_controller.h" 19#include "ui/message_center/message_center.h" 20#include "ui/message_center/message_center_style.h" 21#include "ui/message_center/notifier_settings.h" 22 23const int kBackButtonSize = 16; 24 25// NSClipView subclass. 26@interface MCClipView : NSClipView { 27 // If this is set, the visible document area will remain intact no matter how 28 // the user scrolls or drags the thumb. 29 BOOL frozen_; 30} 31@end 32 33@implementation MCClipView 34- (void)setFrozen:(BOOL)frozen { 35 frozen_ = frozen; 36} 37 38- (NSPoint)constrainScrollPoint:(NSPoint)proposedNewOrigin { 39 return frozen_ ? [self documentVisibleRect].origin : 40 [super constrainScrollPoint:proposedNewOrigin]; 41} 42@end 43 44@interface MCTrayViewController (Private) 45// Creates all the views for the control area of the tray. 46- (void)layoutControlArea; 47 48// Update both tray view and window by resizing it to fit its content. 49- (void)updateTrayViewAndWindow; 50 51// Remove notifications dismissed by the user. It is done in the following 52// 3 steps. 53- (void)closeNotificationsByUser; 54 55// Step 1: hide all notifications pending removal with fade-out animation. 56- (void)hideNotificationsPendingRemoval; 57 58// Step 2: move up all remaining notfications to take over the available space 59// due to hiding notifications. The scroll view and the window remain unchanged. 60- (void)moveUpRemainingNotifications; 61 62// Step 3: finalize the tray view and window to get rid of the empty space. 63- (void)finalizeTrayViewAndWindow; 64 65// Clear a notification by sliding it out from left to right. This occurs when 66// "Clear All" is clicked. 67- (void)clearOneNotification; 68 69// When all visible notificatons slide out, re-enable controls and remove 70// notifications from the message center. 71- (void)finalizeClearAll; 72 73// Sets the images of the quiet mode button based on the message center state. 74- (void)updateQuietModeButtonImage; 75@end 76 77namespace { 78 79// The duration of fade-out and bounds animation. 80const NSTimeInterval kAnimationDuration = 0.2; 81 82// The delay to start animating clearing next notification since current 83// animation starts. 84const NSTimeInterval kAnimateClearingNextNotificationDelay = 0.04; 85 86// The height of the bar at the top of the tray that contains buttons. 87const CGFloat kControlAreaHeight = 50; 88 89// Amount of spacing between control buttons. There is kMarginBetweenItems 90// between a button and the edge of the tray, though. 91const CGFloat kButtonXMargin = 20; 92 93// Amount of padding to leave between the bottom of the screen and the bottom 94// of the message center tray. 95const CGFloat kTrayBottomMargin = 75; 96 97} // namespace 98 99@implementation MCTrayViewController 100 101- (id)initWithMessageCenter:(message_center::MessageCenter*)messageCenter { 102 if ((self = [super initWithNibName:nil bundle:nil])) { 103 messageCenter_ = messageCenter; 104 animationDuration_ = kAnimationDuration; 105 animateClearingNextNotificationDelay_ = 106 kAnimateClearingNextNotificationDelay; 107 notifications_.reset([[NSMutableArray alloc] init]); 108 notificationsPendingRemoval_.reset([[NSMutableArray alloc] init]); 109 } 110 return self; 111} 112 113- (NSString*)trayTitle { 114 return [title_ stringValue]; 115} 116 117- (void)setTrayTitle:(NSString*)title { 118 [title_ setStringValue:title]; 119 [title_ sizeToFit]; 120} 121 122- (void)onWindowClosing { 123 if (animation_) { 124 [animation_ stopAnimation]; 125 [animation_ setDelegate:nil]; 126 animation_.reset(); 127 } 128 if (clearAllInProgress_) { 129 // To stop chain of clearOneNotification calls to start new animations. 130 [NSObject cancelPreviousPerformRequestsWithTarget:self]; 131 132 for (NSViewAnimation* animation in clearAllAnimations_.get()) { 133 [animation stopAnimation]; 134 [animation setDelegate:nil]; 135 } 136 [clearAllAnimations_ removeAllObjects]; 137 [self finalizeClearAll]; 138 } 139} 140 141- (void)loadView { 142 // Configure the root view as a background-colored box. 143 base::scoped_nsobject<NSBox> view([[NSBox alloc] initWithFrame:NSMakeRect( 144 0, 0, [MCTrayViewController trayWidth], kControlAreaHeight)]); 145 [view setBorderType:NSNoBorder]; 146 [view setBoxType:NSBoxCustom]; 147 [view setContentViewMargins:NSZeroSize]; 148 [view setFillColor:gfx::SkColorToCalibratedNSColor( 149 message_center::kMessageCenterBackgroundColor)]; 150 [view setTitlePosition:NSNoTitle]; 151 [view setWantsLayer:YES]; // Needed for notification view shadows. 152 [self setView:view]; 153 154 [self layoutControlArea]; 155 156 // Configure the scroll view in which all the notifications go. 157 base::scoped_nsobject<NSView> documentView( 158 [[NSView alloc] initWithFrame:NSZeroRect]); 159 scrollView_.reset([[NSScrollView alloc] initWithFrame:[view frame]]); 160 clipView_.reset( 161 [[MCClipView alloc] initWithFrame:[[scrollView_ contentView] frame]]); 162 [scrollView_ setContentView:clipView_]; 163 [scrollView_ setAutohidesScrollers:YES]; 164 [scrollView_ setAutoresizingMask:NSViewHeightSizable | NSViewMaxYMargin]; 165 [scrollView_ setDocumentView:documentView]; 166 [scrollView_ setDrawsBackground:NO]; 167 [scrollView_ setHasHorizontalScroller:NO]; 168 [scrollView_ setHasVerticalScroller:YES]; 169 [view addSubview:scrollView_]; 170 171 [self onMessageCenterTrayChanged]; 172} 173 174- (void)onMessageCenterTrayChanged { 175 if (settingsController_) 176 return [self updateTrayViewAndWindow]; 177 178 std::map<std::string, MCNotificationController*> newMap; 179 180 base::scoped_nsobject<NSShadow> shadow([[NSShadow alloc] init]); 181 [shadow setShadowColor:[NSColor colorWithDeviceWhite:0 alpha:0.55]]; 182 [shadow setShadowOffset:NSMakeSize(0, -1)]; 183 [shadow setShadowBlurRadius:2.0]; 184 185 CGFloat minY = message_center::kMarginBetweenItems; 186 187 // Iterate over the notifications in reverse, since the Cocoa coordinate 188 // origin is in the lower-left. Remove from |notificationsMap_| all the 189 // ones still in the updated model, so that those that should be removed 190 // will remain in the map. 191 const auto& modelNotifications = messageCenter_->GetVisibleNotifications(); 192 for (auto it = modelNotifications.rbegin(); 193 it != modelNotifications.rend(); 194 ++it) { 195 // Check if this notification is already in the tray. 196 const auto& existing = notificationsMap_.find((*it)->id()); 197 MCNotificationController* notification = nil; 198 if (existing == notificationsMap_.end()) { 199 base::scoped_nsobject<MCNotificationController> controller( 200 [[MCNotificationController alloc] 201 initWithNotification:*it 202 messageCenter:messageCenter_]); 203 [[controller view] setShadow:shadow]; 204 [[scrollView_ documentView] addSubview:[controller view]]; 205 206 [notifications_ addObject:controller]; // Transfer ownership. 207 messageCenter_->DisplayedNotification((*it)->id()); 208 209 notification = controller.get(); 210 } else { 211 notification = existing->second; 212 [notification updateNotification:*it]; 213 notificationsMap_.erase(existing); 214 } 215 216 DCHECK(notification); 217 218 NSRect frame = [[notification view] frame]; 219 frame.origin.x = message_center::kMarginBetweenItems; 220 frame.origin.y = minY; 221 [[notification view] setFrame:frame]; 222 223 newMap.insert(std::make_pair((*it)->id(), notification)); 224 225 minY = NSMaxY(frame) + message_center::kMarginBetweenItems; 226 } 227 228 // Remove any notifications that are no longer in the model. 229 for (const auto& pair : notificationsMap_) { 230 [[pair.second view] removeFromSuperview]; 231 [notifications_ removeObject:pair.second]; 232 } 233 234 // Copy the new map of notifications to replace the old. 235 notificationsMap_ = newMap; 236 237 [self updateTrayViewAndWindow]; 238} 239 240- (void)toggleQuietMode:(id)sender { 241 if (messageCenter_->IsQuietMode()) 242 messageCenter_->SetQuietMode(false); 243 else 244 messageCenter_->EnterQuietModeWithExpire(base::TimeDelta::FromDays(1)); 245 246 [self updateQuietModeButtonImage]; 247} 248 249- (void)clearAllNotifications:(id)sender { 250 if ([self isAnimating]) { 251 clearAllDelayed_ = YES; 252 return; 253 } 254 255 // Build a list for all notifications within the visible scroll range 256 // in preparation to slide them out one by one. 257 NSRect visibleScrollRect = [scrollView_ documentVisibleRect]; 258 for (MCNotificationController* notification in notifications_.get()) { 259 NSRect rect = [[notification view] frame]; 260 if (!NSIsEmptyRect(NSIntersectionRect(visibleScrollRect, rect))) { 261 visibleNotificationsPendingClear_.push_back(notification); 262 } 263 } 264 if (visibleNotificationsPendingClear_.empty()) 265 return; 266 267 // Disbale buttons and freeze scroll bar to prevent the user from clicking on 268 // them accidentally. 269 [pauseButton_ setEnabled:NO]; 270 [clearAllButton_ setEnabled:NO]; 271 [settingsButton_ setEnabled:NO]; 272 [clipView_ setFrozen:YES]; 273 274 // Start sliding out the top notification. 275 clearAllAnimations_.reset([[NSMutableArray alloc] init]); 276 [self clearOneNotification]; 277 278 clearAllInProgress_ = YES; 279} 280 281- (void)showSettings:(id)sender { 282 if (settingsController_) 283 return [self showMessages:sender]; 284 285 message_center::NotifierSettingsProvider* provider = 286 messageCenter_->GetNotifierSettingsProvider(); 287 settingsController_.reset( 288 [[MCSettingsController alloc] initWithProvider:provider 289 trayViewController:self]); 290 291 [[self view] addSubview:[settingsController_ view]]; 292 293 NSRect titleFrame = [title_ frame]; 294 titleFrame.origin.x = 295 NSMaxX([backButton_ frame]) + message_center::kMarginBetweenItems / 2; 296 [title_ setFrame:titleFrame]; 297 [backButton_ setHidden:NO]; 298 [clearAllButton_ setEnabled:NO]; 299 300 [scrollView_ setHidden:YES]; 301 302 [[[self view] window] recalculateKeyViewLoop]; 303 messageCenter_->SetVisibility(message_center::VISIBILITY_SETTINGS); 304 305 [self updateTrayViewAndWindow]; 306} 307 308- (void)updateSettings { 309 // TODO(jianli): This class should not be calling -loadView, but instead 310 // should just observe a resize notification. 311 // (http://crbug.com/270251) 312 [[settingsController_ view] removeFromSuperview]; 313 [settingsController_ loadView]; 314 [[self view] addSubview:[settingsController_ view]]; 315 316 [self updateTrayViewAndWindow]; 317} 318 319- (void)showMessages:(id)sender { 320 messageCenter_->SetVisibility(message_center::VISIBILITY_MESSAGE_CENTER); 321 [self cleanupSettings]; 322 [[[self view] window] recalculateKeyViewLoop]; 323 [self updateTrayViewAndWindow]; 324} 325 326- (void)cleanupSettings { 327 [scrollView_ setHidden:NO]; 328 329 [[settingsController_ view] removeFromSuperview]; 330 settingsController_.reset(); 331 332 NSRect titleFrame = [title_ frame]; 333 titleFrame.origin.x = NSMinX([backButton_ frame]); 334 [title_ setFrame:titleFrame]; 335 [backButton_ setHidden:YES]; 336 [clearAllButton_ setEnabled:YES]; 337 338} 339 340- (void)scrollToTop { 341 NSPoint topPoint = 342 NSMakePoint(0.0, [[scrollView_ documentView] bounds].size.height); 343 [[scrollView_ documentView] scrollPoint:topPoint]; 344} 345 346- (BOOL)isAnimating { 347 return [animation_ isAnimating] || [clearAllAnimations_ count]; 348} 349 350+ (CGFloat)maxTrayClientHeight { 351 NSRect screenFrame = [[[NSScreen screens] objectAtIndex:0] visibleFrame]; 352 return NSHeight(screenFrame) - kTrayBottomMargin - kControlAreaHeight; 353} 354 355+ (CGFloat)trayWidth { 356 return message_center::kNotificationWidth + 357 2 * message_center::kMarginBetweenItems; 358} 359 360// Testing API ///////////////////////////////////////////////////////////////// 361 362- (NSScrollView*)scrollView { 363 return scrollView_.get(); 364} 365 366- (HoverImageButton*)pauseButton { 367 return pauseButton_.get(); 368} 369 370- (HoverImageButton*)clearAllButton { 371 return clearAllButton_.get(); 372} 373 374- (void)setAnimationDuration:(NSTimeInterval)duration { 375 animationDuration_ = duration; 376} 377 378- (void)setAnimateClearingNextNotificationDelay:(NSTimeInterval)delay { 379 animateClearingNextNotificationDelay_ = delay; 380} 381 382- (void)setAnimationEndedCallback: 383 (message_center::TrayAnimationEndedCallback)callback { 384 testingAnimationEndedCallback_.reset(Block_copy(callback)); 385} 386 387// Private ///////////////////////////////////////////////////////////////////// 388 389- (void)layoutControlArea { 390 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 391 NSView* view = [self view]; 392 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]; 408 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_]; 414 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 }; 422 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_]; 446 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]; 458 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 }; 467 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_]; 489 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_]; 510 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_]; 527} 528 529- (void)updateTrayViewAndWindow { 530 CGFloat scrollContentHeight = 0; 531 if ([notifications_ count]) { 532 scrollContentHeight = NSMaxY([[[notifications_ lastObject] view] frame]) + 533 message_center::kMarginBetweenItems;; 534 } 535 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]; 542 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]; 555 556 // Resize the scroll view. 557 scrollViewFrame.size.height = NSHeight(frame) - kControlAreaHeight; 558 [scrollView_ setFrame:scrollViewFrame]; 559 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; 565 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]; 572 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 } 579} 580 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]; 595 596 if (clearAllDelayed_) 597 [self clearAllNotifications:nil]; 598 } 599 } 600 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()(); 605} 606 607- (void)closeNotificationsByUser { 608 // No need to close individual notification if clear-all is in progress. 609 if (clearAllInProgress_) 610 return; 611 612 if ([self isAnimating]) 613 return; 614 [self hideNotificationsPendingRemoval]; 615} 616 617- (void)hideNotificationsPendingRemoval { 618 base::scoped_nsobject<NSMutableArray> animationDataArray( 619 [[NSMutableArray alloc] init]); 620 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 } 631 632 if ([notificationsPendingRemoval_ count] == 0) 633 return; 634 635 for (MCNotificationController* notification in 636 notificationsPendingRemoval_.get()) { 637 [notifications_ removeObject:notification]; 638 } 639 640 // Start the animation. 641 animation_.reset([[NSViewAnimation alloc] 642 initWithViewAnimations:animationDataArray]); 643 [animation_ setDuration:animationDuration_]; 644 [animation_ setDelegate:self]; 645 [animation_ startAnimation]; 646} 647 648- (void)moveUpRemainingNotifications { 649 base::scoped_nsobject<NSMutableArray> animationDataArray( 650 [[NSMutableArray alloc] init]); 651 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 } 659 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 } 674 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]; 682 683 // Start the animation. 684 animation_.reset([[NSViewAnimation alloc] 685 initWithViewAnimations:animationDataArray]); 686 [animation_ setDuration:animationDuration_]; 687 [animation_ setDelegate:self]; 688 [animation_ startAnimation]; 689} 690 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 } 703 704 [self updateTrayViewAndWindow]; 705 706 // Check if there're more notifications pending removal. 707 [self closeNotificationsByUser]; 708} 709 710- (void)clearOneNotification { 711 DCHECK(!visibleNotificationsPendingClear_.empty()); 712 713 MCNotificationController* notification = 714 visibleNotificationsPendingClear_.back(); 715 visibleNotificationsPendingClear_.pop_back(); 716 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]; 731 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 } 738} 739 740- (void)finalizeClearAll { 741 DCHECK(clearAllInProgress_); 742 clearAllInProgress_ = NO; 743 744 DCHECK(![clearAllAnimations_ count]); 745 clearAllAnimations_.reset(); 746 747 [pauseButton_ setEnabled:YES]; 748 [clearAllButton_ setEnabled:YES]; 749 [settingsButton_ setEnabled:YES]; 750 [clipView_ setFrozen:NO]; 751 752 messageCenter_->RemoveAllVisibleNotifications(true); 753} 754 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 } 766} 767 768@end 769