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