1 // Copyright (c) 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #import "ui/message_center/cocoa/tray_view_controller.h"
9 #include "base/mac/scoped_nsautorelease_pool.h"
10 #include "base/time/time.h"
11 #include "grit/ui_resources.h"
12 #include "grit/ui_strings.h"
13 #include "skia/ext/skia_utils_mac.h"
14 #import "ui/base/cocoa/hover_image_button.h"
15 #include "ui/base/l10n/l10n_util_mac.h"
16 #include "ui/base/resource/resource_bundle.h"
17 #import "ui/message_center/cocoa/notification_controller.h"
18 #import "ui/message_center/cocoa/settings_controller.h"
19 #include "ui/message_center/message_center.h"
20 #include "ui/message_center/message_center_style.h"
21 #include "ui/message_center/notifier_settings.h"
23 const int kBackButtonSize = 16;
25 // NSClipView subclass.
26 @interface MCClipView : NSClipView {
27 // If this is set, the visible document area will remain intact no matter how
28 // the user scrolls or drags the thumb.
33 @implementation MCClipView
34 - (void)setFrozen:(BOOL)frozen {
38 - (NSPoint)constrainScrollPoint:(NSPoint)proposedNewOrigin {
39 return frozen_ ? [self documentVisibleRect].origin :
40 [super constrainScrollPoint:proposedNewOrigin];
44 @interface MCTrayViewController (Private)
45 // Creates all the views for the control area of the tray.
46 - (void)layoutControlArea;
48 // Update both tray view and window by resizing it to fit its content.
49 - (void)updateTrayViewAndWindow;
51 // Remove notifications dismissed by the user. It is done in the following
53 - (void)closeNotificationsByUser;
55 // Step 1: hide all notifications pending removal with fade-out animation.
56 - (void)hideNotificationsPendingRemoval;
58 // Step 2: move up all remaining notfications to take over the available space
59 // due to hiding notifications. The scroll view and the window remain unchanged.
60 - (void)moveUpRemainingNotifications;
62 // Step 3: finalize the tray view and window to get rid of the empty space.
63 - (void)finalizeTrayViewAndWindow;
65 // Clear a notification by sliding it out from left to right. This occurs when
66 // "Clear All" is clicked.
67 - (void)clearOneNotification;
69 // When all visible notificatons slide out, re-enable controls and remove
70 // notifications from the message center.
71 - (void)finalizeClearAll;
73 // Sets the images of the quiet mode button based on the message center state.
74 - (void)updateQuietModeButtonImage;
79 // The duration of fade-out and bounds animation.
80 const NSTimeInterval kAnimationDuration = 0.2;
82 // The delay to start animating clearing next notification since current
84 const NSTimeInterval kAnimateClearingNextNotificationDelay = 0.04;
86 // The height of the bar at the top of the tray that contains buttons.
87 const CGFloat kControlAreaHeight = 50;
89 // Amount of spacing between control buttons. There is kMarginBetweenItems
90 // between a button and the edge of the tray, though.
91 const CGFloat kButtonXMargin = 20;
93 // Amount of padding to leave between the bottom of the screen and the bottom
94 // of the message center tray.
95 const CGFloat kTrayBottomMargin = 75;
99 @implementation MCTrayViewController
101 - (id)initWithMessageCenter:(message_center::MessageCenter*)messageCenter {
102 if ((self = [super initWithNibName:nil bundle:nil])) {
103 messageCenter_ = messageCenter;
104 animationDuration_ = kAnimationDuration;
105 animateClearingNextNotificationDelay_ =
106 kAnimateClearingNextNotificationDelay;
107 notifications_.reset([[NSMutableArray alloc] init]);
108 notificationsPendingRemoval_.reset([[NSMutableArray alloc] init]);
113 - (NSString*)trayTitle {
114 return [title_ stringValue];
117 - (void)setTrayTitle:(NSString*)title {
118 [title_ setStringValue:title];
122 - (void)onWindowClosing {
124 [animation_ stopAnimation];
125 [animation_ setDelegate:nil];
128 if (clearAllInProgress_) {
129 // To stop chain of clearOneNotification calls to start new animations.
130 [NSObject cancelPreviousPerformRequestsWithTarget:self];
132 for (NSViewAnimation* animation in clearAllAnimations_.get()) {
133 [animation stopAnimation];
134 [animation setDelegate:nil];
136 [clearAllAnimations_ removeAllObjects];
137 [self finalizeClearAll];
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.
154 [self layoutControlArea];
156 // Configure the scroll view in which all the notifications go.
157 base::scoped_nsobject<NSView> documentView(
158 [[NSView alloc] initWithFrame:NSZeroRect]);
159 scrollView_.reset([[NSScrollView alloc] initWithFrame:[view frame]]);
161 [[MCClipView alloc] initWithFrame:[[scrollView_ contentView] frame]]);
162 [scrollView_ setContentView:clipView_];
163 [scrollView_ setAutohidesScrollers:YES];
164 [scrollView_ setAutoresizingMask:NSViewHeightSizable | NSViewMaxYMargin];
165 [scrollView_ setDocumentView:documentView];
166 [scrollView_ setDrawsBackground:NO];
167 [scrollView_ setHasHorizontalScroller:NO];
168 [scrollView_ setHasVerticalScroller:YES];
169 [view addSubview:scrollView_];
171 [self onMessageCenterTrayChanged];
174 - (void)onMessageCenterTrayChanged {
175 if (settingsController_)
176 return [self updateTrayViewAndWindow];
178 std::map<std::string, MCNotificationController*> newMap;
180 base::scoped_nsobject<NSShadow> shadow([[NSShadow alloc] init]);
181 [shadow setShadowColor:[NSColor colorWithDeviceWhite:0 alpha:0.55]];
182 [shadow setShadowOffset:NSMakeSize(0, -1)];
183 [shadow setShadowBlurRadius:2.0];
185 CGFloat minY = message_center::kMarginBetweenItems;
187 // Iterate over the notifications in reverse, since the Cocoa coordinate
188 // origin is in the lower-left. Remove from |notificationsMap_| all the
189 // ones still in the updated model, so that those that should be removed
190 // will remain in the map.
191 const auto& modelNotifications = messageCenter_->GetVisibleNotifications();
192 for (auto it = modelNotifications.rbegin();
193 it != modelNotifications.rend();
195 // Check if this notification is already in the tray.
196 const auto& existing = notificationsMap_.find((*it)->id());
197 MCNotificationController* notification = nil;
198 if (existing == notificationsMap_.end()) {
199 base::scoped_nsobject<MCNotificationController> controller(
200 [[MCNotificationController alloc]
201 initWithNotification:*it
202 messageCenter:messageCenter_]);
203 [[controller view] setShadow:shadow];
204 [[scrollView_ documentView] addSubview:[controller view]];
206 [notifications_ addObject:controller]; // Transfer ownership.
207 messageCenter_->DisplayedNotification((*it)->id());
209 notification = controller.get();
211 notification = existing->second;
212 [notification updateNotification:*it];
213 notificationsMap_.erase(existing);
216 DCHECK(notification);
218 NSRect frame = [[notification view] frame];
219 frame.origin.x = message_center::kMarginBetweenItems;
220 frame.origin.y = minY;
221 [[notification view] setFrame:frame];
223 newMap.insert(std::make_pair((*it)->id(), notification));
225 minY = NSMaxY(frame) + message_center::kMarginBetweenItems;
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];
234 // Copy the new map of notifications to replace the old.
235 notificationsMap_ = newMap;
237 [self updateTrayViewAndWindow];
240 - (void)toggleQuietMode:(id)sender {
241 if (messageCenter_->IsQuietMode())
242 messageCenter_->SetQuietMode(false);
244 messageCenter_->EnterQuietModeWithExpire(base::TimeDelta::FromDays(1));
246 [self updateQuietModeButtonImage];
249 - (void)clearAllNotifications:(id)sender {
250 if ([self isAnimating]) {
251 clearAllDelayed_ = YES;
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);
264 if (visibleNotificationsPendingClear_.empty())
267 // Disbale buttons and freeze scroll bar to prevent the user from clicking on
268 // them accidentally.
269 [pauseButton_ setEnabled:NO];
270 [clearAllButton_ setEnabled:NO];
271 [settingsButton_ setEnabled:NO];
272 [clipView_ setFrozen:YES];
274 // Start sliding out the top notification.
275 clearAllAnimations_.reset([[NSMutableArray alloc] init]);
276 [self clearOneNotification];
278 clearAllInProgress_ = YES;
281 - (void)showSettings:(id)sender {
282 if (settingsController_)
283 return [self showMessages:sender];
285 message_center::NotifierSettingsProvider* provider =
286 messageCenter_->GetNotifierSettingsProvider();
287 settingsController_.reset(
288 [[MCSettingsController alloc] initWithProvider:provider
289 trayViewController:self]);
291 [[self view] addSubview:[settingsController_ view]];
293 NSRect titleFrame = [title_ frame];
294 titleFrame.origin.x =
295 NSMaxX([backButton_ frame]) + message_center::kMarginBetweenItems / 2;
296 [title_ setFrame:titleFrame];
297 [backButton_ setHidden:NO];
298 [clearAllButton_ setEnabled:NO];
300 [scrollView_ setHidden:YES];
302 [[[self view] window] recalculateKeyViewLoop];
303 messageCenter_->SetVisibility(message_center::VISIBILITY_SETTINGS);
305 [self updateTrayViewAndWindow];
308 - (void)updateSettings {
309 // TODO(jianli): This class should not be calling -loadView, but instead
310 // should just observe a resize notification.
311 // (http://crbug.com/270251)
312 [[settingsController_ view] removeFromSuperview];
313 [settingsController_ loadView];
314 [[self view] addSubview:[settingsController_ view]];
316 [self updateTrayViewAndWindow];
319 - (void)showMessages:(id)sender {
320 messageCenter_->SetVisibility(message_center::VISIBILITY_MESSAGE_CENTER);
321 [self cleanupSettings];
322 [[[self view] window] recalculateKeyViewLoop];
323 [self updateTrayViewAndWindow];
326 - (void)cleanupSettings {
327 [scrollView_ setHidden:NO];
329 [[settingsController_ view] removeFromSuperview];
330 settingsController_.reset();
332 NSRect titleFrame = [title_ frame];
333 titleFrame.origin.x = NSMinX([backButton_ frame]);
334 [title_ setFrame:titleFrame];
335 [backButton_ setHidden:YES];
336 [clearAllButton_ setEnabled:YES];
340 - (void)scrollToTop {
342 NSMakePoint(0.0, [[scrollView_ documentView] bounds].size.height);
343 [[scrollView_ documentView] scrollPoint:topPoint];
346 - (BOOL)isAnimating {
347 return [animation_ isAnimating] || [clearAllAnimations_ count];
350 + (CGFloat)maxTrayClientHeight {
351 NSRect screenFrame = [[[NSScreen screens] objectAtIndex:0] visibleFrame];
352 return NSHeight(screenFrame) - kTrayBottomMargin - kControlAreaHeight;
355 + (CGFloat)trayWidth {
356 return message_center::kNotificationWidth +
357 2 * message_center::kMarginBetweenItems;
360 // Testing API /////////////////////////////////////////////////////////////////
362 - (NSScrollView*)scrollView {
363 return scrollView_.get();
366 - (HoverImageButton*)pauseButton {
367 return pauseButton_.get();
370 - (HoverImageButton*)clearAllButton {
371 return clearAllButton_.get();
374 - (void)setAnimationDuration:(NSTimeInterval)duration {
375 animationDuration_ = duration;
378 - (void)setAnimateClearingNextNotificationDelay:(NSTimeInterval)delay {
379 animateClearingNextNotificationDelay_ = delay;
382 - (void)setAnimationEndedCallback:
383 (message_center::TrayAnimationEndedCallback)callback {
384 testingAnimationEndedCallback_.reset(Block_copy(callback));
387 // Private /////////////////////////////////////////////////////////////////////
389 - (void)layoutControlArea {
390 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
391 NSView* view = [self view];
393 // Create the "Notifications" label at the top of the tray.
394 NSFont* font = [NSFont labelFontOfSize:message_center::kTitleFontSize];
395 title_.reset([[NSTextField alloc] initWithFrame:NSZeroRect]);
396 [title_ setAutoresizingMask:NSViewMinYMargin];
397 [title_ setBezeled:NO];
398 [title_ setBordered:NO];
399 [title_ setDrawsBackground:NO];
400 [title_ setEditable:NO];
401 [title_ setFont:font];
402 [title_ setSelectable:NO];
403 [title_ setStringValue:
404 l10n_util::GetNSString(IDS_MESSAGE_CENTER_FOOTER_TITLE)];
405 [title_ setTextColor:gfx::SkColorToCalibratedNSColor(
406 message_center::kRegularTextColor)];
409 NSRect titleFrame = [title_ frame];
410 titleFrame.origin.x = message_center::kMarginBetweenItems;
411 titleFrame.origin.y = kControlAreaHeight/2 - NSMidY(titleFrame);
412 [title_ setFrame:titleFrame];
413 [view addSubview:title_];
415 auto configureButton = ^(HoverImageButton* button) {
416 [[button cell] setHighlightsBy:NSOnState];
417 [button setTrackingEnabled:YES];
418 [button setBordered:NO];
419 [button setAutoresizingMask:NSViewMinYMargin];
420 [button setTarget:self];
423 // Back button. On top of the "Notifications" label, hidden by default.
424 NSRect backButtonFrame =
425 NSMakeRect(NSMinX(titleFrame),
426 (kControlAreaHeight - kBackButtonSize) / 2,
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)];
443 accessibilitySetOverrideValue:[backButton_ toolTip]
444 forAttribute:NSAccessibilityDescriptionAttribute];
445 [[self view] addSubview:backButton_];
447 // Create the divider line between the control area and the notifications.
448 base::scoped_nsobject<NSBox> divider(
449 [[NSBox alloc] initWithFrame:NSMakeRect(0, 0, NSWidth([view frame]), 1)]);
450 [divider setAutoresizingMask:NSViewMinYMargin];
451 [divider setBorderType:NSNoBorder];
452 [divider setBoxType:NSBoxCustom];
453 [divider setContentViewMargins:NSZeroSize];
454 [divider setFillColor:gfx::SkColorToCalibratedNSColor(
455 message_center::kFooterDelimiterColor)];
456 [divider setTitlePosition:NSNoTitle];
457 [view addSubview:divider];
459 auto getButtonFrame = ^NSRect(CGFloat maxX, NSImage* image) {
460 NSSize size = [image size];
463 kControlAreaHeight/2 - size.height/2,
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,
474 settingsButton_.reset(
475 [[HoverImageButton alloc] initWithFrame:settingsButtonFrame]);
476 [settingsButton_ setDefaultImage:defaultImage];
477 [settingsButton_ setHoverImage:
478 rb.GetNativeImageNamed(IDR_NOTIFICATION_SETTINGS_HOVER).ToNSImage()];
479 [settingsButton_ setPressedImage:
480 rb.GetNativeImageNamed(IDR_NOTIFICATION_SETTINGS_PRESSED).ToNSImage()];
481 [settingsButton_ setToolTip:
482 l10n_util::GetNSString(IDS_MESSAGE_CENTER_SETTINGS_BUTTON_LABEL)];
483 [[settingsButton_ cell]
484 accessibilitySetOverrideValue:[settingsButton_ toolTip]
485 forAttribute:NSAccessibilityDescriptionAttribute];
486 [settingsButton_ setAction:@selector(showSettings:)];
487 configureButton(settingsButton_);
488 [view addSubview:settingsButton_];
490 // Create the clear all button.
491 defaultImage = rb.GetNativeImageNamed(IDR_NOTIFICATION_CLEAR_ALL).ToNSImage();
492 NSRect clearAllButtonFrame = getButtonFrame(
493 NSMinX(settingsButtonFrame) - kButtonXMargin,
495 clearAllButton_.reset(
496 [[HoverImageButton alloc] initWithFrame:clearAllButtonFrame]);
497 [clearAllButton_ setDefaultImage:defaultImage];
498 [clearAllButton_ setHoverImage:
499 rb.GetNativeImageNamed(IDR_NOTIFICATION_CLEAR_ALL_HOVER).ToNSImage()];
500 [clearAllButton_ setPressedImage:
501 rb.GetNativeImageNamed(IDR_NOTIFICATION_CLEAR_ALL_PRESSED).ToNSImage()];
502 [clearAllButton_ setToolTip:
503 l10n_util::GetNSString(IDS_MESSAGE_CENTER_CLEAR_ALL)];
504 [[clearAllButton_ cell]
505 accessibilitySetOverrideValue:[clearAllButton_ toolTip]
506 forAttribute:NSAccessibilityDescriptionAttribute];
507 [clearAllButton_ setAction:@selector(clearAllNotifications:)];
508 configureButton(clearAllButton_);
509 [view addSubview:clearAllButton_];
511 // Create the pause button.
512 NSRect pauseButtonFrame = getButtonFrame(
513 NSMinX(clearAllButtonFrame) - kButtonXMargin,
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)];
522 accessibilitySetOverrideValue:[pauseButton_ toolTip]
523 forAttribute:NSAccessibilityDescriptionAttribute];
524 [pauseButton_ setAction:@selector(toggleQuietMode:)];
525 configureButton(pauseButton_);
526 [view addSubview:pauseButton_];
529 - (void)updateTrayViewAndWindow {
530 CGFloat scrollContentHeight = 0;
531 if ([notifications_ count]) {
532 scrollContentHeight = NSMaxY([[[notifications_ lastObject] view] frame]) +
533 message_center::kMarginBetweenItems;;
536 // Resize the scroll view's content.
537 NSRect scrollViewFrame = [scrollView_ frame];
538 NSRect documentFrame = [[scrollView_ documentView] frame];
539 documentFrame.size.width = NSWidth(scrollViewFrame);
540 documentFrame.size.height = scrollContentHeight;
541 [[scrollView_ documentView] setFrame:documentFrame];
543 // Resize the container view.
544 NSRect frame = [[self view] frame];
545 CGFloat oldHeight = NSHeight(frame);
546 if (settingsController_) {
547 frame.size.height = NSHeight([[settingsController_ view] frame]);
549 frame.size.height = std::min([MCTrayViewController maxTrayClientHeight],
550 scrollContentHeight);
552 frame.size.height += kControlAreaHeight;
553 CGFloat newHeight = NSHeight(frame);
554 [[self view] setFrame:frame];
556 // Resize the scroll view.
557 scrollViewFrame.size.height = NSHeight(frame) - kControlAreaHeight;
558 [scrollView_ setFrame:scrollViewFrame];
560 // Resize the window.
561 NSRect windowFrame = [[[self view] window] frame];
562 CGFloat delta = newHeight - oldHeight;
563 windowFrame.origin.y -= delta;
564 windowFrame.size.height += delta;
566 [[[self view] window] setFrame:windowFrame display:YES];
567 // Hide the clear-all button if there are no notifications. Simply swap the
568 // X position of it and the pause button in that case.
569 BOOL hidden = ![notifications_ count];
570 if ([clearAllButton_ isHidden] != hidden) {
571 [clearAllButton_ setHidden:hidden];
573 NSRect pauseButtonFrame = [pauseButton_ frame];
574 NSRect clearAllButtonFrame = [clearAllButton_ frame];
575 std::swap(clearAllButtonFrame.origin.x, pauseButtonFrame.origin.x);
576 [pauseButton_ setFrame:pauseButtonFrame];
577 [clearAllButton_ setFrame:clearAllButtonFrame];
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];
590 // For notification removal and reposition animation.
591 if ([notificationsPendingRemoval_ count]) {
592 [self moveUpRemainingNotifications];
594 [self finalizeTrayViewAndWindow];
596 if (clearAllDelayed_)
597 [self clearAllNotifications:nil];
601 // Give the testing code a chance to do something, i.e. quitting the test
603 if (![self isAnimating] && testingAnimationEndedCallback_)
604 testingAnimationEndedCallback_.get()();
607 - (void)closeNotificationsByUser {
608 // No need to close individual notification if clear-all is in progress.
609 if (clearAllInProgress_)
612 if ([self isAnimating])
614 [self hideNotificationsPendingRemoval];
617 - (void)hideNotificationsPendingRemoval {
618 base::scoped_nsobject<NSMutableArray> animationDataArray(
619 [[NSMutableArray alloc] init]);
621 // Fade-out those notifications pending removal.
622 for (MCNotificationController* notification in notifications_.get()) {
623 if (messageCenter_->HasNotification([notification notificationID]))
625 [notificationsPendingRemoval_ addObject:notification];
626 [animationDataArray addObject:@{
627 NSViewAnimationTargetKey : [notification view],
628 NSViewAnimationEffectKey : NSViewAnimationFadeOutEffect
632 if ([notificationsPendingRemoval_ count] == 0)
635 for (MCNotificationController* notification in
636 notificationsPendingRemoval_.get()) {
637 [notifications_ removeObject:notification];
640 // Start the animation.
641 animation_.reset([[NSViewAnimation alloc]
642 initWithViewAnimations:animationDataArray]);
643 [animation_ setDuration:animationDuration_];
644 [animation_ setDelegate:self];
645 [animation_ startAnimation];
648 - (void)moveUpRemainingNotifications {
649 base::scoped_nsobject<NSMutableArray> animationDataArray(
650 [[NSMutableArray alloc] init]);
652 // Compute the position where the remaining notifications should start.
653 CGFloat minY = message_center::kMarginBetweenItems;
654 for (MCNotificationController* notification in
655 notificationsPendingRemoval_.get()) {
656 NSView* view = [notification view];
657 minY += NSHeight([view frame]) + message_center::kMarginBetweenItems;
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]
672 minY = NSMaxY(frame) + message_center::kMarginBetweenItems;
675 // Now remove notifications pending removal.
676 for (MCNotificationController* notification in
677 notificationsPendingRemoval_.get()) {
678 [[notification view] removeFromSuperview];
679 notificationsMap_.erase([notification notificationID]);
681 [notificationsPendingRemoval_ removeAllObjects];
683 // Start the animation.
684 animation_.reset([[NSViewAnimation alloc]
685 initWithViewAnimations:animationDataArray]);
686 [animation_ setDuration:animationDuration_];
687 [animation_ setDelegate:self];
688 [animation_ startAnimation];
691 - (void)finalizeTrayViewAndWindow {
692 // Reposition the remaining notifications starting at the bottom.
693 CGFloat minY = message_center::kMarginBetweenItems;
694 for (MCNotificationController* notification in notifications_.get()) {
695 NSView* view = [notification view];
696 NSRect frame = [view frame];
697 NSRect oldFrame = frame;
698 frame.origin.y = minY;
699 if (!NSEqualRects(oldFrame, frame))
700 [view setFrame:frame];
701 minY = NSMaxY(frame) + message_center::kMarginBetweenItems;
704 [self updateTrayViewAndWindow];
706 // Check if there're more notifications pending removal.
707 [self closeNotificationsByUser];
710 - (void)clearOneNotification {
711 DCHECK(!visibleNotificationsPendingClear_.empty());
713 MCNotificationController* notification =
714 visibleNotificationsPendingClear_.back();
715 visibleNotificationsPendingClear_.pop_back();
717 // Slide out the notification from left to right with fade-out simultaneously.
718 NSRect newFrame = [[notification view] frame];
719 newFrame.origin.x = NSMaxX(newFrame) + message_center::kMarginBetweenItems;
720 NSDictionary* animationDict = @{
721 NSViewAnimationTargetKey : [notification view],
722 NSViewAnimationEndFrameKey : [NSValue valueWithRect:newFrame],
723 NSViewAnimationEffectKey : NSViewAnimationFadeOutEffect
725 base::scoped_nsobject<NSViewAnimation> animation([[NSViewAnimation alloc]
726 initWithViewAnimations:[NSArray arrayWithObject:animationDict]]);
727 [animation setDuration:animationDuration_];
728 [animation setDelegate:self];
729 [animation startAnimation];
730 [clearAllAnimations_ addObject:animation];
732 // Schedule to start sliding out next notification after a short delay.
733 if (!visibleNotificationsPendingClear_.empty()) {
734 [self performSelector:@selector(clearOneNotification)
736 afterDelay:animateClearingNextNotificationDelay_];
740 - (void)finalizeClearAll {
741 DCHECK(clearAllInProgress_);
742 clearAllInProgress_ = NO;
744 DCHECK(![clearAllAnimations_ count]);
745 clearAllAnimations_.reset();
747 [pauseButton_ setEnabled:YES];
748 [clearAllButton_ setEnabled:YES];
749 [settingsButton_ setEnabled:YES];
750 [clipView_ setFrozen:NO];
752 messageCenter_->RemoveAllNotifications(true);
755 - (void)updateQuietModeButtonImage {
756 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
757 if (messageCenter_->IsQuietMode()) {
758 [pauseButton_ setTrackingEnabled:NO];
759 [pauseButton_ setDefaultImage: rb.GetNativeImageNamed(
760 IDR_NOTIFICATION_DO_NOT_DISTURB_PRESSED).ToNSImage()];
762 [pauseButton_ setTrackingEnabled:YES];
763 [pauseButton_ setDefaultImage:
764 rb.GetNativeImageNamed(IDR_NOTIFICATION_DO_NOT_DISTURB).ToNSImage()];