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 /////////////////////////////////////////////////////////////////
363 return divider_.get();
366 - (NSTextField*)emptyDescription {
367 return emptyDescription_.get();
370 - (NSScrollView*)scrollView {
371 return scrollView_.get();
374 - (HoverImageButton*)pauseButton {
375 return pauseButton_.get();
378 - (HoverImageButton*)clearAllButton {
379 return clearAllButton_.get();
382 - (void)setAnimationDuration:(NSTimeInterval)duration {
383 animationDuration_ = duration;
386 - (void)setAnimateClearingNextNotificationDelay:(NSTimeInterval)delay {
387 animateClearingNextNotificationDelay_ = delay;
390 - (void)setAnimationEndedCallback:
391 (message_center::TrayAnimationEndedCallback)callback {
392 testingAnimationEndedCallback_.reset(Block_copy(callback));
395 // Private /////////////////////////////////////////////////////////////////////
397 - (void)layoutControlArea {
398 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
399 NSView* view = [self view];
401 auto configureLabel = ^(NSTextField* textField) {
402 [textField setAutoresizingMask:NSViewMinYMargin];
403 [textField setBezeled:NO];
404 [textField setBordered:NO];
405 [textField setDrawsBackground:NO];
406 [textField setEditable:NO];
407 [textField setSelectable:NO];
410 // Create the "Notifications" label at the top of the tray.
411 NSFont* font = [NSFont labelFontOfSize:message_center::kTitleFontSize];
412 title_.reset([[NSTextField alloc] initWithFrame:NSZeroRect]);
413 configureLabel(title_);
415 [title_ setFont:font];
416 [title_ setStringValue:
417 l10n_util::GetNSString(IDS_MESSAGE_CENTER_FOOTER_TITLE)];
418 [title_ setTextColor:gfx::SkColorToCalibratedNSColor(
419 message_center::kRegularTextColor)];
422 NSRect titleFrame = [title_ frame];
423 titleFrame.origin.x = message_center::kMarginBetweenItems;
424 titleFrame.origin.y = kControlAreaHeight/2 - NSMidY(titleFrame);
425 [title_ setFrame:titleFrame];
426 [view addSubview:title_];
428 auto configureButton = ^(HoverImageButton* button) {
429 [[button cell] setHighlightsBy:NSOnState];
430 [button setTrackingEnabled:YES];
431 [button setBordered:NO];
432 [button setAutoresizingMask:NSViewMinYMargin];
433 [button setTarget:self];
436 // Back button. On top of the "Notifications" label, hidden by default.
437 NSRect backButtonFrame =
438 NSMakeRect(NSMinX(titleFrame),
439 (kControlAreaHeight - kBackButtonSize) / 2,
442 backButton_.reset([[HoverImageButton alloc] initWithFrame:backButtonFrame]);
443 [backButton_ setDefaultImage:
444 rb.GetNativeImageNamed(IDR_NOTIFICATION_ARROW).ToNSImage()];
445 [backButton_ setHoverImage:
446 rb.GetNativeImageNamed(IDR_NOTIFICATION_ARROW_HOVER).ToNSImage()];
447 [backButton_ setPressedImage:
448 rb.GetNativeImageNamed(IDR_NOTIFICATION_ARROW_PRESSED).ToNSImage()];
449 [backButton_ setAction:@selector(showMessages:)];
450 configureButton(backButton_);
451 [backButton_ setHidden:YES];
452 [backButton_ setKeyEquivalent:@"\e"];
453 [backButton_ setToolTip:l10n_util::GetNSString(
454 IDS_MESSAGE_CENTER_SETTINGS_GO_BACK_BUTTON_TOOLTIP)];
456 accessibilitySetOverrideValue:[backButton_ toolTip]
457 forAttribute:NSAccessibilityDescriptionAttribute];
458 [[self view] addSubview:backButton_];
460 // Create the divider line between the control area and the notifications.
462 [[NSBox alloc] initWithFrame:NSMakeRect(0, 0, NSWidth([view frame]), 1)]);
463 [divider_ setAutoresizingMask:NSViewMinYMargin];
464 [divider_ setBorderType:NSNoBorder];
465 [divider_ setBoxType:NSBoxCustom];
466 [divider_ setContentViewMargins:NSZeroSize];
467 [divider_ setFillColor:gfx::SkColorToCalibratedNSColor(
468 message_center::kFooterDelimiterColor)];
469 [divider_ setTitlePosition:NSNoTitle];
470 [view addSubview:divider_];
473 auto getButtonFrame = ^NSRect(CGFloat maxX, NSImage* image) {
474 NSSize size = [image size];
477 kControlAreaHeight/2 - size.height/2,
482 // Create the settings button at the far-right.
483 NSImage* defaultImage =
484 rb.GetNativeImageNamed(IDR_NOTIFICATION_SETTINGS).ToNSImage();
485 NSRect settingsButtonFrame = getButtonFrame(
486 NSWidth([view frame]) - message_center::kMarginBetweenItems,
488 settingsButton_.reset(
489 [[HoverImageButton alloc] initWithFrame:settingsButtonFrame]);
490 [settingsButton_ setDefaultImage:defaultImage];
491 [settingsButton_ setHoverImage:
492 rb.GetNativeImageNamed(IDR_NOTIFICATION_SETTINGS_HOVER).ToNSImage()];
493 [settingsButton_ setPressedImage:
494 rb.GetNativeImageNamed(IDR_NOTIFICATION_SETTINGS_PRESSED).ToNSImage()];
495 [settingsButton_ setToolTip:
496 l10n_util::GetNSString(IDS_MESSAGE_CENTER_SETTINGS_BUTTON_LABEL)];
497 [[settingsButton_ cell]
498 accessibilitySetOverrideValue:[settingsButton_ toolTip]
499 forAttribute:NSAccessibilityDescriptionAttribute];
500 [settingsButton_ setAction:@selector(showSettings:)];
501 configureButton(settingsButton_);
502 [view addSubview:settingsButton_];
504 // Create the clear all button.
505 defaultImage = rb.GetNativeImageNamed(IDR_NOTIFICATION_CLEAR_ALL).ToNSImage();
506 NSRect clearAllButtonFrame = getButtonFrame(
507 NSMinX(settingsButtonFrame) - kButtonXMargin,
509 clearAllButton_.reset(
510 [[HoverImageButton alloc] initWithFrame:clearAllButtonFrame]);
511 [clearAllButton_ setDefaultImage:defaultImage];
512 [clearAllButton_ setHoverImage:
513 rb.GetNativeImageNamed(IDR_NOTIFICATION_CLEAR_ALL_HOVER).ToNSImage()];
514 [clearAllButton_ setPressedImage:
515 rb.GetNativeImageNamed(IDR_NOTIFICATION_CLEAR_ALL_PRESSED).ToNSImage()];
516 [clearAllButton_ setToolTip:
517 l10n_util::GetNSString(IDS_MESSAGE_CENTER_CLEAR_ALL)];
518 [[clearAllButton_ cell]
519 accessibilitySetOverrideValue:[clearAllButton_ toolTip]
520 forAttribute:NSAccessibilityDescriptionAttribute];
521 [clearAllButton_ setAction:@selector(clearAllNotifications:)];
522 configureButton(clearAllButton_);
523 [view addSubview:clearAllButton_];
525 // Create the pause button.
526 NSRect pauseButtonFrame = getButtonFrame(
527 NSMinX(clearAllButtonFrame) - kButtonXMargin,
529 pauseButton_.reset([[HoverImageButton alloc] initWithFrame:pauseButtonFrame]);
530 [self updateQuietModeButtonImage];
531 [pauseButton_ setHoverImage: rb.GetNativeImageNamed(
532 IDR_NOTIFICATION_DO_NOT_DISTURB_HOVER).ToNSImage()];
533 [pauseButton_ setToolTip:
534 l10n_util::GetNSString(IDS_MESSAGE_CENTER_QUIET_MODE_BUTTON_TOOLTIP)];
536 accessibilitySetOverrideValue:[pauseButton_ toolTip]
537 forAttribute:NSAccessibilityDescriptionAttribute];
538 [pauseButton_ setAction:@selector(toggleQuietMode:)];
539 configureButton(pauseButton_);
540 [view addSubview:pauseButton_];
542 // Create the description field for the empty message center. Initially it is
544 emptyDescription_.reset([[NSTextField alloc] initWithFrame:NSZeroRect]);
545 configureLabel(emptyDescription_);
548 [NSFont labelFontOfSize:message_center::kEmptyCenterFontSize];
549 [emptyDescription_ setFont:smallFont];
550 [emptyDescription_ setStringValue:
551 l10n_util::GetNSString(IDS_MESSAGE_CENTER_NO_MESSAGES)];
552 [emptyDescription_ setTextColor:gfx::SkColorToCalibratedNSColor(
553 message_center::kDimTextColor)];
554 [emptyDescription_ sizeToFit];
555 [emptyDescription_ setHidden:YES];
557 [view addSubview:emptyDescription_];
560 - (void)updateTrayViewAndWindow {
561 CGFloat scrollContentHeight = message_center::kMinScrollViewHeight;
562 if ([notifications_ count]) {
563 [emptyDescription_ setHidden:YES];
564 [scrollView_ setHidden:NO];
565 [divider_ setHidden:NO];
566 scrollContentHeight = NSMaxY([[[notifications_ lastObject] view] frame]) +
567 message_center::kMarginBetweenItems;;
569 [emptyDescription_ setHidden:NO];
570 [scrollView_ setHidden:YES];
571 [divider_ setHidden:YES];
573 NSRect centeredFrame = [emptyDescription_ frame];
574 NSPoint centeredOrigin = NSMakePoint(
575 floor((NSWidth([[self view] frame]) - NSWidth(centeredFrame))/2 + 0.5),
576 floor((scrollContentHeight - NSHeight(centeredFrame))/2 + 0.5));
578 centeredFrame.origin = centeredOrigin;
579 [emptyDescription_ setFrame:centeredFrame];
582 // Resize the scroll view's content.
583 NSRect scrollViewFrame = [scrollView_ frame];
584 NSRect documentFrame = [[scrollView_ documentView] frame];
585 documentFrame.size.width = NSWidth(scrollViewFrame);
586 documentFrame.size.height = scrollContentHeight;
587 [[scrollView_ documentView] setFrame:documentFrame];
589 // Resize the container view.
590 NSRect frame = [[self view] frame];
591 CGFloat oldHeight = NSHeight(frame);
592 if (settingsController_) {
593 frame.size.height = NSHeight([[settingsController_ view] frame]);
595 frame.size.height = std::min([MCTrayViewController maxTrayClientHeight],
596 scrollContentHeight);
598 frame.size.height += kControlAreaHeight;
599 CGFloat newHeight = NSHeight(frame);
600 [[self view] setFrame:frame];
602 // Resize the scroll view.
603 scrollViewFrame.size.height = NSHeight(frame) - kControlAreaHeight;
604 [scrollView_ setFrame:scrollViewFrame];
606 // Resize the window.
607 NSRect windowFrame = [[[self view] window] frame];
608 CGFloat delta = newHeight - oldHeight;
609 windowFrame.origin.y -= delta;
610 windowFrame.size.height += delta;
612 [[[self view] window] setFrame:windowFrame display:YES];
613 // Hide the clear-all button if there are no notifications. Simply swap the
614 // X position of it and the pause button in that case.
615 BOOL hidden = ![notifications_ count];
616 if ([clearAllButton_ isHidden] != hidden) {
617 [clearAllButton_ setHidden:hidden];
619 NSRect pauseButtonFrame = [pauseButton_ frame];
620 NSRect clearAllButtonFrame = [clearAllButton_ frame];
621 std::swap(clearAllButtonFrame.origin.x, pauseButtonFrame.origin.x);
622 [pauseButton_ setFrame:pauseButtonFrame];
623 [clearAllButton_ setFrame:clearAllButtonFrame];
627 - (void)animationDidEnd:(NSAnimation*)animation {
628 if (clearAllInProgress_) {
629 // For clear-all animation.
630 [clearAllAnimations_ removeObject:animation];
631 if (![clearAllAnimations_ count] &&
632 visibleNotificationsPendingClear_.empty()) {
633 [self finalizeClearAll];
636 // For notification removal and reposition animation.
637 if ([notificationsPendingRemoval_ count]) {
638 [self moveUpRemainingNotifications];
640 [self finalizeTrayViewAndWindow];
642 if (clearAllDelayed_)
643 [self clearAllNotifications:nil];
647 // Give the testing code a chance to do something, i.e. quitting the test
649 if (![self isAnimating] && testingAnimationEndedCallback_)
650 testingAnimationEndedCallback_.get()();
653 - (void)closeNotificationsByUser {
654 // No need to close individual notification if clear-all is in progress.
655 if (clearAllInProgress_)
658 if ([self isAnimating])
660 [self hideNotificationsPendingRemoval];
663 - (void)hideNotificationsPendingRemoval {
664 base::scoped_nsobject<NSMutableArray> animationDataArray(
665 [[NSMutableArray alloc] init]);
667 // Fade-out those notifications pending removal.
668 for (MCNotificationController* notification in notifications_.get()) {
669 if (messageCenter_->HasNotification([notification notificationID]))
671 [notificationsPendingRemoval_ addObject:notification];
672 [animationDataArray addObject:@{
673 NSViewAnimationTargetKey : [notification view],
674 NSViewAnimationEffectKey : NSViewAnimationFadeOutEffect
678 if ([notificationsPendingRemoval_ count] == 0)
681 for (MCNotificationController* notification in
682 notificationsPendingRemoval_.get()) {
683 [notifications_ removeObject:notification];
686 // Start the animation.
687 animation_.reset([[NSViewAnimation alloc]
688 initWithViewAnimations:animationDataArray]);
689 [animation_ setDuration:animationDuration_];
690 [animation_ setDelegate:self];
691 [animation_ startAnimation];
694 - (void)moveUpRemainingNotifications {
695 base::scoped_nsobject<NSMutableArray> animationDataArray(
696 [[NSMutableArray alloc] init]);
698 // Compute the position where the remaining notifications should start.
699 CGFloat minY = message_center::kMarginBetweenItems;
700 for (MCNotificationController* notification in
701 notificationsPendingRemoval_.get()) {
702 NSView* view = [notification view];
703 minY += NSHeight([view frame]) + message_center::kMarginBetweenItems;
706 // Reposition the remaining notifications starting at the computed position.
707 for (MCNotificationController* notification in notifications_.get()) {
708 NSView* view = [notification view];
709 NSRect frame = [view frame];
710 NSRect oldFrame = frame;
711 frame.origin.y = minY;
712 if (!NSEqualRects(oldFrame, frame)) {
713 [animationDataArray addObject:@{
714 NSViewAnimationTargetKey : view,
715 NSViewAnimationEndFrameKey : [NSValue valueWithRect:frame]
718 minY = NSMaxY(frame) + message_center::kMarginBetweenItems;
721 // Now remove notifications pending removal.
722 for (MCNotificationController* notification in
723 notificationsPendingRemoval_.get()) {
724 [[notification view] removeFromSuperview];
725 notificationsMap_.erase([notification notificationID]);
727 [notificationsPendingRemoval_ removeAllObjects];
729 // Start the animation.
730 animation_.reset([[NSViewAnimation alloc]
731 initWithViewAnimations:animationDataArray]);
732 [animation_ setDuration:animationDuration_];
733 [animation_ setDelegate:self];
734 [animation_ startAnimation];
737 - (void)finalizeTrayViewAndWindow {
738 // Reposition the remaining notifications starting at the bottom.
739 CGFloat minY = message_center::kMarginBetweenItems;
740 for (MCNotificationController* notification in notifications_.get()) {
741 NSView* view = [notification view];
742 NSRect frame = [view frame];
743 NSRect oldFrame = frame;
744 frame.origin.y = minY;
745 if (!NSEqualRects(oldFrame, frame))
746 [view setFrame:frame];
747 minY = NSMaxY(frame) + message_center::kMarginBetweenItems;
750 [self updateTrayViewAndWindow];
752 // Check if there're more notifications pending removal.
753 [self closeNotificationsByUser];
756 - (void)clearOneNotification {
757 DCHECK(!visibleNotificationsPendingClear_.empty());
759 MCNotificationController* notification =
760 visibleNotificationsPendingClear_.back();
761 visibleNotificationsPendingClear_.pop_back();
763 // Slide out the notification from left to right with fade-out simultaneously.
764 NSRect newFrame = [[notification view] frame];
765 newFrame.origin.x = NSMaxX(newFrame) + message_center::kMarginBetweenItems;
766 NSDictionary* animationDict = @{
767 NSViewAnimationTargetKey : [notification view],
768 NSViewAnimationEndFrameKey : [NSValue valueWithRect:newFrame],
769 NSViewAnimationEffectKey : NSViewAnimationFadeOutEffect
771 base::scoped_nsobject<NSViewAnimation> animation([[NSViewAnimation alloc]
772 initWithViewAnimations:[NSArray arrayWithObject:animationDict]]);
773 [animation setDuration:animationDuration_];
774 [animation setDelegate:self];
775 [animation startAnimation];
776 [clearAllAnimations_ addObject:animation];
778 // Schedule to start sliding out next notification after a short delay.
779 if (!visibleNotificationsPendingClear_.empty()) {
780 [self performSelector:@selector(clearOneNotification)
782 afterDelay:animateClearingNextNotificationDelay_];
786 - (void)finalizeClearAll {
787 DCHECK(clearAllInProgress_);
788 clearAllInProgress_ = NO;
790 DCHECK(![clearAllAnimations_ count]);
791 clearAllAnimations_.reset();
793 [pauseButton_ setEnabled:YES];
794 [clearAllButton_ setEnabled:YES];
795 [settingsButton_ setEnabled:YES];
796 [clipView_ setFrozen:NO];
798 messageCenter_->RemoveAllVisibleNotifications(true);
801 - (void)updateQuietModeButtonImage {
802 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
803 if (messageCenter_->IsQuietMode()) {
804 [pauseButton_ setTrackingEnabled:NO];
805 [pauseButton_ setDefaultImage: rb.GetNativeImageNamed(
806 IDR_NOTIFICATION_DO_NOT_DISTURB_PRESSED).ToNSImage()];
808 [pauseButton_ setTrackingEnabled:YES];
809 [pauseButton_ setDefaultImage:
810 rb.GetNativeImageNamed(IDR_NOTIFICATION_DO_NOT_DISTURB).ToNSImage()];