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