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 "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"
24 const int kBackButtonSize = 16;
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.
34 @implementation MCClipView
35 - (void)setFrozen:(BOOL)frozen {
39 - (NSPoint)constrainScrollPoint:(NSPoint)proposedNewOrigin {
40 return frozen_ ? [self documentVisibleRect].origin :
41 [super constrainScrollPoint:proposedNewOrigin];
45 @interface MCTrayViewController (Private)
46 // Creates all the views for the control area of the tray.
47 - (void)layoutControlArea;
49 // Update both tray view and window by resizing it to fit its content.
50 - (void)updateTrayViewAndWindow;
52 // Remove notifications dismissed by the user. It is done in the following
54 - (void)closeNotificationsByUser;
56 // Step 1: hide all notifications pending removal with fade-out animation.
57 - (void)hideNotificationsPendingRemoval;
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;
63 // Step 3: finalize the tray view and window to get rid of the empty space.
64 - (void)finalizeTrayViewAndWindow;
66 // Clear a notification by sliding it out from left to right. This occurs when
67 // "Clear All" is clicked.
68 - (void)clearOneNotification;
70 // When all visible notifications slide out, re-enable controls and remove
71 // notifications from the message center.
72 - (void)finalizeClearAll;
74 // Sets the images of the quiet mode button based on the message center state.
75 - (void)updateQuietModeButtonImage;
80 // The duration of fade-out and bounds animation.
81 const NSTimeInterval kAnimationDuration = 0.2;
83 // The delay to start animating clearing next notification since current
85 const NSTimeInterval kAnimateClearingNextNotificationDelay = 0.04;
87 // The height of the bar at the top of the tray that contains buttons.
88 const CGFloat kControlAreaHeight = 50;
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;
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;
100 @implementation MCTrayViewController
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]);
114 - (NSString*)trayTitle {
115 return [title_ stringValue];
118 - (void)setTrayTitle:(NSString*)title {
119 [title_ setStringValue:title];
123 - (void)onWindowClosing {
125 [animation_ stopAnimation];
126 [animation_ setDelegate:nil];
129 if (clearAllInProgress_) {
130 // To stop chain of clearOneNotification calls to start new animations.
131 [NSObject cancelPreviousPerformRequestsWithTarget:self];
133 for (NSViewAnimation* animation in clearAllAnimations_.get()) {
134 [animation stopAnimation];
135 [animation setDelegate:nil];
137 [clearAllAnimations_ removeAllObjects];
138 [self finalizeClearAll];
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.
155 [self layoutControlArea];
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]]);
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_];
172 [self onMessageCenterTrayChanged];
175 - (void)onMessageCenterTrayChanged {
176 if (settingsController_)
177 return [self updateTrayViewAndWindow];
179 std::map<std::string, MCNotificationController*> newMap;
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];
186 CGFloat minY = message_center::kMarginBetweenItems;
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();
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]];
207 [notifications_ addObject:controller]; // Transfer ownership.
208 messageCenter_->DisplayedNotification(
209 (*it)->id(), message_center::DISPLAY_SOURCE_MESSAGE_CENTER);
211 notification = controller.get();
213 notification = existing->second;
214 [notification updateNotification:*it];
215 notificationsMap_.erase(existing);
218 DCHECK(notification);
220 NSRect frame = [[notification view] frame];
221 frame.origin.x = message_center::kMarginBetweenItems;
222 frame.origin.y = minY;
223 [[notification view] setFrame:frame];
225 newMap.insert(std::make_pair((*it)->id(), notification));
227 minY = NSMaxY(frame) + message_center::kMarginBetweenItems;
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];
236 // Copy the new map of notifications to replace the old.
237 notificationsMap_ = newMap;
239 [self updateTrayViewAndWindow];
242 - (void)toggleQuietMode:(id)sender {
243 if (messageCenter_->IsQuietMode())
244 messageCenter_->SetQuietMode(false);
246 messageCenter_->EnterQuietModeWithExpire(base::TimeDelta::FromDays(1));
248 [self updateQuietModeButtonImage];
251 - (void)clearAllNotifications:(id)sender {
252 if ([self isAnimating]) {
253 clearAllDelayed_ = YES;
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);
266 if (visibleNotificationsPendingClear_.empty())
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];
276 // Start sliding out the top notification.
277 clearAllAnimations_.reset([[NSMutableArray alloc] init]);
278 [self clearOneNotification];
280 clearAllInProgress_ = YES;
283 - (void)showSettings:(id)sender {
284 if (settingsController_)
285 return [self showMessages:sender];
287 message_center::NotifierSettingsProvider* provider =
288 messageCenter_->GetNotifierSettingsProvider();
289 settingsController_.reset(
290 [[MCSettingsController alloc] initWithProvider:provider
291 trayViewController:self]);
293 [[self view] addSubview:[settingsController_ view]];
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];
302 [scrollView_ setHidden:YES];
304 [[[self view] window] recalculateKeyViewLoop];
305 messageCenter_->SetVisibility(message_center::VISIBILITY_SETTINGS);
307 [self updateTrayViewAndWindow];
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]];
318 [self updateTrayViewAndWindow];
321 - (void)showMessages:(id)sender {
322 messageCenter_->SetVisibility(message_center::VISIBILITY_MESSAGE_CENTER);
323 [self cleanupSettings];
324 [[[self view] window] recalculateKeyViewLoop];
325 [self updateTrayViewAndWindow];
328 - (void)cleanupSettings {
329 [scrollView_ setHidden:NO];
331 [[settingsController_ view] removeFromSuperview];
332 settingsController_.reset();
334 NSRect titleFrame = [title_ frame];
335 titleFrame.origin.x = NSMinX([backButton_ frame]);
336 [title_ setFrame:titleFrame];
337 [backButton_ setHidden:YES];
338 [clearAllButton_ setEnabled:YES];
342 - (void)scrollToTop {
344 NSMakePoint(0.0, [[scrollView_ documentView] bounds].size.height);
345 [[scrollView_ documentView] scrollPoint:topPoint];
348 - (BOOL)isAnimating {
349 return [animation_ isAnimating] || [clearAllAnimations_ count];
352 + (CGFloat)maxTrayClientHeight {
353 NSRect screenFrame = [[[NSScreen screens] objectAtIndex:0] visibleFrame];
354 return NSHeight(screenFrame) - kTrayBottomMargin - kControlAreaHeight;
357 + (CGFloat)trayWidth {
358 return message_center::kNotificationWidth +
359 2 * message_center::kMarginBetweenItems;
362 // Testing API /////////////////////////////////////////////////////////////////
365 return divider_.get();
368 - (NSTextField*)emptyDescription {
369 return emptyDescription_.get();
372 - (NSScrollView*)scrollView {
373 return scrollView_.get();
376 - (HoverImageButton*)pauseButton {
377 return pauseButton_.get();
380 - (HoverImageButton*)clearAllButton {
381 return clearAllButton_.get();
384 - (void)setAnimationDuration:(NSTimeInterval)duration {
385 animationDuration_ = duration;
388 - (void)setAnimateClearingNextNotificationDelay:(NSTimeInterval)delay {
389 animateClearingNextNotificationDelay_ = delay;
392 - (void)setAnimationEndedCallback:
393 (message_center::TrayAnimationEndedCallback)callback {
394 testingAnimationEndedCallback_.reset(Block_copy(callback));
397 // Private /////////////////////////////////////////////////////////////////////
399 - (void)layoutControlArea {
400 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
401 NSView* view = [self view];
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);
408 [[MCTextField alloc] initWithFrame:NSZeroRect backgroundColor:color]);
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)];
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_];
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];
431 // Back button. On top of the "Notifications" label, hidden by default.
432 NSRect backButtonFrame =
433 NSMakeRect(NSMinX(titleFrame),
434 (kControlAreaHeight - kBackButtonSize) / 2,
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)];
451 accessibilitySetOverrideValue:[backButton_ toolTip]
452 forAttribute:NSAccessibilityDescriptionAttribute];
453 [[self view] addSubview:backButton_];
455 // Create the divider line between the control area and the notifications.
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_];
468 auto getButtonFrame = ^NSRect(CGFloat maxX, NSImage* image) {
469 NSSize size = [image size];
472 kControlAreaHeight/2 - size.height/2,
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,
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_];
499 // Create the clear all button.
500 defaultImage = rb.GetNativeImageNamed(IDR_NOTIFICATION_CLEAR_ALL).ToNSImage();
501 NSRect clearAllButtonFrame = getButtonFrame(
502 NSMinX(settingsButtonFrame) - kButtonXMargin,
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_];
520 // Create the pause button.
521 NSRect pauseButtonFrame = getButtonFrame(
522 NSMinX(clearAllButtonFrame) - kButtonXMargin,
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)];
531 accessibilitySetOverrideValue:[pauseButton_ toolTip]
532 forAttribute:NSAccessibilityDescriptionAttribute];
533 [pauseButton_ setAction:@selector(toggleQuietMode:)];
534 configureButton(pauseButton_);
535 [view addSubview:pauseButton_];
537 // Create the description field for the empty message center. Initially it is
539 emptyDescription_.reset(
540 [[MCTextField alloc] initWithFrame:NSZeroRect backgroundColor:color]);
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];
552 [view addSubview:emptyDescription_];
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;;
564 [emptyDescription_ setHidden:NO];
565 [scrollView_ setHidden:YES];
566 [divider_ setHidden:YES];
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));
573 centeredFrame.origin = centeredOrigin;
574 [emptyDescription_ setFrame:centeredFrame];
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];
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]);
590 frame.size.height = std::min([MCTrayViewController maxTrayClientHeight],
591 scrollContentHeight);
593 frame.size.height += kControlAreaHeight;
594 CGFloat newHeight = NSHeight(frame);
595 [[self view] setFrame:frame];
597 // Resize the scroll view.
598 scrollViewFrame.size.height = NSHeight(frame) - kControlAreaHeight;
599 [scrollView_ setFrame:scrollViewFrame];
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;
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];
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];
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];
631 // For notification removal and reposition animation.
632 if ([notificationsPendingRemoval_ count]) {
633 [self moveUpRemainingNotifications];
635 [self finalizeTrayViewAndWindow];
637 if (clearAllDelayed_)
638 [self clearAllNotifications:nil];
642 // Give the testing code a chance to do something, i.e. quitting the test
644 if (![self isAnimating] && testingAnimationEndedCallback_)
645 testingAnimationEndedCallback_.get()();
648 - (void)closeNotificationsByUser {
649 // No need to close individual notification if clear-all is in progress.
650 if (clearAllInProgress_)
653 if ([self isAnimating])
655 [self hideNotificationsPendingRemoval];
658 - (void)hideNotificationsPendingRemoval {
659 base::scoped_nsobject<NSMutableArray> animationDataArray(
660 [[NSMutableArray alloc] init]);
662 // Fade-out those notifications pending removal.
663 for (MCNotificationController* notification in notifications_.get()) {
664 if (messageCenter_->FindVisibleNotificationById(
665 [notification notificationID]))
667 [notificationsPendingRemoval_ addObject:notification];
668 [animationDataArray addObject:@{
669 NSViewAnimationTargetKey : [notification view],
670 NSViewAnimationEffectKey : NSViewAnimationFadeOutEffect
674 if ([notificationsPendingRemoval_ count] == 0)
677 for (MCNotificationController* notification in
678 notificationsPendingRemoval_.get()) {
679 [notifications_ removeObject:notification];
682 // Start the animation.
683 animation_.reset([[NSViewAnimation alloc]
684 initWithViewAnimations:animationDataArray]);
685 [animation_ setDuration:animationDuration_];
686 [animation_ setDelegate:self];
687 [animation_ startAnimation];
690 - (void)moveUpRemainingNotifications {
691 base::scoped_nsobject<NSMutableArray> animationDataArray(
692 [[NSMutableArray alloc] init]);
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;
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]
714 minY = NSMaxY(frame) + message_center::kMarginBetweenItems;
717 // Now remove notifications pending removal.
718 for (MCNotificationController* notification in
719 notificationsPendingRemoval_.get()) {
720 [[notification view] removeFromSuperview];
721 notificationsMap_.erase([notification notificationID]);
723 [notificationsPendingRemoval_ removeAllObjects];
725 // Start the animation.
726 animation_.reset([[NSViewAnimation alloc]
727 initWithViewAnimations:animationDataArray]);
728 [animation_ setDuration:animationDuration_];
729 [animation_ setDelegate:self];
730 [animation_ startAnimation];
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;
746 [self updateTrayViewAndWindow];
748 // Check if there're more notifications pending removal.
749 [self closeNotificationsByUser];
752 - (void)clearOneNotification {
753 DCHECK(!visibleNotificationsPendingClear_.empty());
755 MCNotificationController* notification =
756 visibleNotificationsPendingClear_.back();
757 visibleNotificationsPendingClear_.pop_back();
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
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];
774 // Schedule to start sliding out next notification after a short delay.
775 if (!visibleNotificationsPendingClear_.empty()) {
776 [self performSelector:@selector(clearOneNotification)
778 afterDelay:animateClearingNextNotificationDelay_];
782 - (void)finalizeClearAll {
783 DCHECK(clearAllInProgress_);
784 clearAllInProgress_ = NO;
786 DCHECK(![clearAllAnimations_ count]);
787 clearAllAnimations_.reset();
789 [pauseButton_ setEnabled:YES];
790 [clearAllButton_ setEnabled:YES];
791 [settingsButton_ setEnabled:YES];
792 [clipView_ setFrozen:NO];
794 messageCenter_->RemoveAllVisibleNotifications(true);
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()];
804 [pauseButton_ setTrackingEnabled:YES];
805 [pauseButton_ setDefaultImage:
806 rb.GetNativeImageNamed(IDR_NOTIFICATION_DO_NOT_DISTURB).ToNSImage()];