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