diff options
author | jianli@chromium.org <jianli@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-06-14 23:21:00 +0000 |
---|---|---|
committer | jianli@chromium.org <jianli@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-06-14 23:21:00 +0000 |
commit | 00f0caa6a7ccb0333306951688fbb0765e4e8b10 (patch) | |
tree | 4206489eb85f9e1c670ca445c406353ca5d086c0 /ui/message_center | |
parent | 9bf0b36044584756132943f37a06f2fcaef7e5fb (diff) | |
download | chromium_src-00f0caa6a7ccb0333306951688fbb0765e4e8b10.zip chromium_src-00f0caa6a7ccb0333306951688fbb0765e4e8b10.tar.gz chromium_src-00f0caa6a7ccb0333306951688fbb0765e4e8b10.tar.bz2 |
Animate clearing all notifications for Mac.
It starts by sliding out the topmost visible notification in the tray view. Then the notification below will start to slide out after a short delay. After the bottom-most visible notification has been slided out, the tray will be closed.
When the clear-all animation is in progress, the tray view buttons are disabled and the scroll view becomes frozen.
BUG=244606
TEST=existing tests updated to wait for animation to finish
R=dewittj@chromium.org, rsesek@chromium.org
Review URL: https://codereview.chromium.org/17017002
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@206514 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'ui/message_center')
-rw-r--r-- | ui/message_center/cocoa/popup_collection.mm | 3 | ||||
-rw-r--r-- | ui/message_center/cocoa/popup_collection_unittest.mm | 8 | ||||
-rw-r--r-- | ui/message_center/cocoa/tray_controller.mm | 5 | ||||
-rw-r--r-- | ui/message_center/cocoa/tray_view_controller.h | 60 | ||||
-rw-r--r-- | ui/message_center/cocoa/tray_view_controller.mm | 198 | ||||
-rw-r--r-- | ui/message_center/cocoa/tray_view_controller_unittest.mm | 25 |
6 files changed, 276 insertions, 23 deletions
diff --git a/ui/message_center/cocoa/popup_collection.mm b/ui/message_center/cocoa/popup_collection.mm index cc54b34..4b81ac3 100644 --- a/ui/message_center/cocoa/popup_collection.mm +++ b/ui/message_center/cocoa/popup_collection.mm @@ -124,7 +124,8 @@ class PopupCollectionObserver : public message_center::MessageCenterObserver { if (![self isAnimating]) [self layoutNotifications]; - // Quit the test run loop when no more animation is being played. + // Give the testing code a chance to do something, i.e. quitting the test + // run loop. if (![self isAnimating] && testingAnimationEndedCallback_) testingAnimationEndedCallback_.get()(); } diff --git a/ui/message_center/cocoa/popup_collection_unittest.mm b/ui/message_center/cocoa/popup_collection_unittest.mm index c7f4014..fada0ef 100644 --- a/ui/message_center/cocoa/popup_collection_unittest.mm +++ b/ui/message_center/cocoa/popup_collection_unittest.mm @@ -33,7 +33,8 @@ class PopupCollectionTest : public ui::CocoaTest { [[MCPopupCollection alloc] initWithMessageCenter:center_]); [collection_ setAnimationDuration:0.001]; [collection_ setAnimationEndedCallback:^{ - OnAnimationEnded(); + if (nested_run_loop_.get()) + nested_run_loop_->Quit(); }]; } @@ -107,11 +108,6 @@ class PopupCollectionTest : public ui::CocoaTest { nested_run_loop_.reset(); } - void OnAnimationEnded() { - if (nested_run_loop_.get()) - nested_run_loop_->Quit(); - } - base::MessageLoop message_loop_; scoped_ptr<base::RunLoop> nested_run_loop_; message_center::MessageCenter* center_; diff --git a/ui/message_center/cocoa/tray_controller.mm b/ui/message_center/cocoa/tray_controller.mm index 362c22a..8ece243 100644 --- a/ui/message_center/cocoa/tray_controller.mm +++ b/ui/message_center/cocoa/tray_controller.mm @@ -52,6 +52,11 @@ return self; } +- (void)close { + [viewController_ onWindowClosing]; + [super close]; +} + - (void)showTrayAtRightOf:(NSPoint)rightPoint atLeftOf:(NSPoint)leftPoint { NSScreen* screen = [[NSScreen screens] objectAtIndex:0]; NSRect screenFrame = [screen visibleFrame]; diff --git a/ui/message_center/cocoa/tray_view_controller.h b/ui/message_center/cocoa/tray_view_controller.h index a20f3e4..2446e90 100644 --- a/ui/message_center/cocoa/tray_view_controller.h +++ b/ui/message_center/cocoa/tray_view_controller.h @@ -7,9 +7,11 @@ #import <Cocoa/Cocoa.h> +#include <list> #include <map> #include <string> +#include "base/mac/scoped_block.h" #import "base/memory/scoped_nsobject.h" #include "ui/message_center/message_center_export.h" @@ -22,6 +24,11 @@ class MessageCenter; } @class HoverImageButton; +@class MCClipView; + +namespace message_center { +typedef void(^TrayAnimationEndedCallback)(); +} // The view controller responsible for the content of the message center tray // UI. This hosts a scroll view of all the notifications, as well as buttons @@ -41,6 +48,9 @@ MESSAGE_CENTER_EXPORT // The scroll view that contains all the notifications in its documentView. scoped_nsobject<NSScrollView> scrollView_; + // The clip view that manages how scrollView_'s documentView is clipped. + scoped_nsobject<MCClipView> clipView_; + // Array of MCNotificationController objects, which the array owns. scoped_nsobject<NSMutableArray> notifications_; @@ -54,6 +64,9 @@ MESSAGE_CENTER_EXPORT // The clear all notifications button. Hidden when there are no notifications. scoped_nsobject<HoverImageButton> clearAllButton_; + // The settings button that shows the settings UI. + scoped_nsobject<HoverImageButton> settingsButton_; + // Array of MCNotificationController objects pending removal by the user. // The object is owned by the array. scoped_nsobject<NSMutableArray> notificationsPendingRemoval_; @@ -64,11 +77,42 @@ MESSAGE_CENTER_EXPORT // The controller of the settings view. Only set while the view is open. scoped_nsobject<MCSettingsController> settingsController_; + + // The flag which is set when the notification removal animation is still + // in progress and the user clicks "Clear All" button. The clear-all animation + // will be delayed till the existing animation completes. + BOOL clearAllDelayed_; + + // The flag which is set when the clear-all animation is in progress. + BOOL clearAllInProgress_; + + // List of weak pointers of the view controllers that are visible in the + // scroll view and waiting to slide off one by one when the user clicks + // "Clear All" button. + std::list<MCNotificationController*> visibleNotificationsPendingClear_; + + // Array of NSViewAnimation objects, which the array owns. + scoped_nsobject<NSMutableArray> clearAllAnimations_; + + // The duration of the bounds animation, in the number of seconds. + NSTimeInterval animationDuration_; + + // The delay to start animating clearing next notification, in the number of + // seconds. + NSTimeInterval animateClearingNextNotificationDelay_; + + // For testing only. If set, the callback will be called when the animation + // ends. + base::mac::ScopedBlock<message_center::TrayAnimationEndedCallback> + testingAnimationEndedCallback_; } // Designated initializer. - (id)initWithMessageCenter:(message_center::MessageCenter*)messageCenter; +// Called when the window is being closed. +- (void)onWindowClosing; + // Callback for when the MessageCenter model changes. - (void)onMessageCenterTrayChanged; @@ -87,6 +131,9 @@ MESSAGE_CENTER_EXPORT // Scroll to the topmost notification in the tray. - (void)scrollToTop; +// Returns true if an animation is being played. +- (BOOL)isAnimating; + // Returns the maximum height of the client area of the notifications tray. + (CGFloat)maxTrayClientHeight; @@ -101,6 +148,19 @@ MESSAGE_CENTER_EXPORT - (NSScrollView*)scrollView; - (HoverImageButton*)pauseButton; - (HoverImageButton*)clearAllButton; + +// Setter for changing the animation duration. The testing code could set it +// to a very small value to expedite the test running. +- (void)setAnimationDuration:(NSTimeInterval)duration; + +// Setter for changing the clear-all animation delay. The testing code could set +// it to a very small value to expedite the test running. +- (void)setAnimateClearingNextNotificationDelay:(NSTimeInterval)delay; + +// Setter for testingAnimationEndedCallback_. The testing code could set it +// to get called back when the animation ends. +- (void)setAnimationEndedCallback: + (message_center::TrayAnimationEndedCallback)callback; @end #endif // UI_MESSAGE_CENTER_COCOA_TRAY_VIEW_CONTROLLER_H_ diff --git a/ui/message_center/cocoa/tray_view_controller.mm b/ui/message_center/cocoa/tray_view_controller.mm index beef76c..2103e7a 100644 --- a/ui/message_center/cocoa/tray_view_controller.mm +++ b/ui/message_center/cocoa/tray_view_controller.mm @@ -21,6 +21,25 @@ const int kBackButtonSize = 16; +// NSClipView subclass. +@interface MCClipView : NSClipView { + // If this is set, the visible document area will remain intact no matter how + // the user scrolls or drags the thumb. + BOOL frozen_; +} +@end + +@implementation MCClipView +- (void)setFrozen:(BOOL)frozen { + frozen_ = frozen; +} + +-(NSPoint)constrainScrollPoint:(NSPoint)proposedNewOrigin { + return frozen_ ? [self documentVisibleRect].origin : + [super constrainScrollPoint:proposedNewOrigin]; +} +@end + @interface MCTrayViewController (Private) // Creates all the views for the control area of the tray. - (void)layoutControlArea; @@ -41,6 +60,14 @@ const int kBackButtonSize = 16; // Step 3: finalize the tray view and window to get rid of the empty space. - (void)finalizeTrayViewAndWindow; + +// Clear a notification by sliding it out from left to right. This occurs when +// "Clear All" is clicked. +- (void)clearOneNotification; + +// When all visible notificatons slide out, re-enable controls and remove +// notifications from the message center. +- (void)finalizeClearAll; @end namespace { @@ -48,6 +75,10 @@ namespace { // The duration of fade-out and bounds animation. const NSTimeInterval kAnimationDuration = 0.2; +// The delay to start animating clearing next notification since current +// animation starts. +const NSTimeInterval kAnimateClearingNextNotificationDelay = 0.04; + // The height of the bar at the top of the tray that contains buttons. const CGFloat kControlAreaHeight = 50; @@ -66,12 +97,31 @@ const CGFloat kTrayBottomMargin = 75; - (id)initWithMessageCenter:(message_center::MessageCenter*)messageCenter { if ((self = [super initWithNibName:nil bundle:nil])) { messageCenter_ = messageCenter; + animationDuration_ = kAnimationDuration; + animateClearingNextNotificationDelay_ = + kAnimateClearingNextNotificationDelay; notifications_.reset([[NSMutableArray alloc] init]); notificationsPendingRemoval_.reset([[NSMutableArray alloc] init]); } return self; } +- (void)onWindowClosing { + if (animation_) { + [animation_ stopAnimation]; + [animation_ setDelegate:nil]; + animation_.reset(); + } + if (clearAllInProgress_) { + for (NSViewAnimation* animation in clearAllAnimations_.get()) { + [animation stopAnimation]; + [animation setDelegate:nil]; + } + [clearAllAnimations_ removeAllObjects]; + [self finalizeClearAll]; + } +} + - (void)loadView { // Configure the root view as a background-colored box. scoped_nsobject<NSBox> view([[NSBox alloc] initWithFrame:NSMakeRect( @@ -91,6 +141,9 @@ const CGFloat kTrayBottomMargin = 75; scoped_nsobject<NSView> documentView( [[NSView alloc] initWithFrame:NSZeroRect]); scrollView_.reset([[NSScrollView alloc] initWithFrame:[view frame]]); + clipView_.reset( + [[MCClipView alloc] initWithFrame:[[scrollView_ contentView] frame]]); + [scrollView_ setContentView:clipView_]; [scrollView_ setAutohidesScrollers:YES]; [scrollView_ setAutoresizingMask:NSViewHeightSizable | NSViewMaxYMargin]; [scrollView_ setDocumentView:documentView]; @@ -192,7 +245,35 @@ const CGFloat kTrayBottomMargin = 75; } - (void)clearAllNotifications:(id)sender { - messageCenter_->RemoveAllNotifications(true); + if ([self isAnimating]) { + clearAllDelayed_ = YES; + return; + } + + // Build a list for all notifications within the visible scroll range + // in preparation to slide them out one by one. + NSRect visibleScrollRect = [scrollView_ documentVisibleRect]; + for (MCNotificationController* notification in notifications_.get()) { + NSRect rect = [[notification view] frame]; + if (!NSIsEmptyRect(NSIntersectionRect(visibleScrollRect, rect))) { + visibleNotificationsPendingClear_.push_back(notification); + } + } + if (visibleNotificationsPendingClear_.empty()) + return; + + // Disbale buttons and freeze scroll bar to prevent the user from clicking on + // them accidentally. + [pauseButton_ setEnabled:NO]; + [clearAllButton_ setEnabled:NO]; + [settingsButton_ setEnabled:NO]; + [clipView_ setFrozen:YES]; + + // Start sliding out the top notification. + clearAllAnimations_.reset([[NSMutableArray alloc] init]); + [self clearOneNotification]; + + clearAllInProgress_ = YES; } - (void)showSettings:(id)sender { @@ -247,6 +328,10 @@ const CGFloat kTrayBottomMargin = 75; [[scrollView_ documentView] scrollPoint:topPoint]; } +- (BOOL)isAnimating { + return [animation_ isAnimating] || [clearAllAnimations_ count]; +} + + (CGFloat)maxTrayClientHeight { NSRect screenFrame = [[[NSScreen screens] objectAtIndex:0] visibleFrame]; return NSHeight(screenFrame) - kTrayBottomMargin - kControlAreaHeight; @@ -271,6 +356,19 @@ const CGFloat kTrayBottomMargin = 75; return clearAllButton_.get(); } +- (void)setAnimationDuration:(NSTimeInterval)duration { + animationDuration_ = duration; +} + +- (void)setAnimateClearingNextNotificationDelay:(NSTimeInterval)delay { + animateClearingNextNotificationDelay_ = delay; +} + +- (void)setAnimationEndedCallback: + (message_center::TrayAnimationEndedCallback)callback { + testingAnimationEndedCallback_.reset(Block_copy(callback)); +} + // Private ///////////////////////////////////////////////////////////////////// - (void)layoutControlArea { @@ -353,18 +451,18 @@ const CGFloat kTrayBottomMargin = 75; NSRect settingsButtonFrame = getButtonFrame( NSWidth([view frame]) - message_center::kMarginBetweenItems, defaultImage); - scoped_nsobject<HoverImageButton> settingsButton( + settingsButton_.reset( [[HoverImageButton alloc] initWithFrame:settingsButtonFrame]); - [settingsButton setDefaultImage:defaultImage]; - [settingsButton setHoverImage: + [settingsButton_ setDefaultImage:defaultImage]; + [settingsButton_ setHoverImage: rb.GetNativeImageNamed(IDR_NOTIFICATION_SETTINGS_HOVER).ToNSImage()]; - [settingsButton setPressedImage: + [settingsButton_ setPressedImage: rb.GetNativeImageNamed(IDR_NOTIFICATION_SETTINGS_PRESSED).ToNSImage()]; - [settingsButton setToolTip: + [settingsButton_ setToolTip: l10n_util::GetNSString(IDS_MESSAGE_CENTER_SETTINGS_BUTTON_LABEL)]; - [settingsButton setAction:@selector(showSettings:)]; - configureButton(settingsButton); - [view addSubview:settingsButton]; + [settingsButton_ setAction:@selector(showSettings:)]; + configureButton(settingsButton_); + [view addSubview:settingsButton_]; // Create the clear all button. defaultImage = rb.GetNativeImageNamed(IDR_NOTIFICATION_CLEAR_ALL).ToNSImage(); @@ -455,14 +553,37 @@ const CGFloat kTrayBottomMargin = 75; } - (void)animationDidEnd:(NSAnimation*)animation { - if ([notificationsPendingRemoval_ count]) - [self moveUpRemainingNotifications]; - else - [self finalizeTrayViewAndWindow]; + if (clearAllInProgress_) { + // For clear-all animation. + [clearAllAnimations_ removeObject:animation]; + if (![clearAllAnimations_ count] && + visibleNotificationsPendingClear_.empty()) { + [self finalizeClearAll]; + } + } else { + // For notification removal and reposition animation. + if ([notificationsPendingRemoval_ count]) { + [self moveUpRemainingNotifications]; + } else { + [self finalizeTrayViewAndWindow]; + + if (clearAllDelayed_) + [self clearAllNotifications:nil]; + } + } + + // Give the testing code a chance to do something, i.e. quitting the test + // run loop. + if (![self isAnimating] && testingAnimationEndedCallback_) + testingAnimationEndedCallback_.get()(); } - (void)closeNotificationsByUser { - if ([animation_ isAnimating]) + // No need to close individual notification if clear-all is in progress. + if (clearAllInProgress_) + return; + + if ([self isAnimating]) return; [self hideNotificationsPendingRemoval]; } @@ -493,7 +614,7 @@ const CGFloat kTrayBottomMargin = 75; // Start the animation. animation_.reset([[NSViewAnimation alloc] initWithViewAnimations:animationDataArray]); - [animation_ setDuration:kAnimationDuration]; + [animation_ setDuration:animationDuration_]; [animation_ setDelegate:self]; [animation_ startAnimation]; } @@ -536,7 +657,7 @@ const CGFloat kTrayBottomMargin = 75; // Start the animation. animation_.reset([[NSViewAnimation alloc] initWithViewAnimations:animationDataArray]); - [animation_ setDuration:kAnimationDuration]; + [animation_ setDuration:animationDuration_]; [animation_ setDelegate:self]; [animation_ startAnimation]; } @@ -560,4 +681,49 @@ const CGFloat kTrayBottomMargin = 75; [self closeNotificationsByUser]; } +- (void)clearOneNotification { + DCHECK(!visibleNotificationsPendingClear_.empty()); + + MCNotificationController* notification = + visibleNotificationsPendingClear_.back(); + visibleNotificationsPendingClear_.pop_back(); + + // Slide out the notification from left to right with fade-out simultaneously. + NSRect newFrame = [[notification view] frame]; + newFrame.origin.x = NSMaxX(newFrame) + message_center::kMarginBetweenItems; + NSDictionary* animationDict = @{ + NSViewAnimationTargetKey : [notification view], + NSViewAnimationEndFrameKey : [NSValue valueWithRect:newFrame], + NSViewAnimationEffectKey : NSViewAnimationFadeOutEffect + }; + scoped_nsobject<NSViewAnimation> animation([[NSViewAnimation alloc] + initWithViewAnimations:[NSArray arrayWithObject:animationDict]]); + [animation setDuration:animationDuration_]; + [animation setDelegate:self]; + [animation startAnimation]; + [clearAllAnimations_ addObject:animation]; + + // Schedule to start sliding out next notification after a short delay. + if (!visibleNotificationsPendingClear_.empty()) { + [self performSelector:@selector(clearOneNotification) + withObject:nil + afterDelay:animateClearingNextNotificationDelay_]; + } +} + +- (void)finalizeClearAll { + DCHECK(clearAllInProgress_); + clearAllInProgress_ = NO; + + DCHECK(![clearAllAnimations_ count]); + clearAllAnimations_.reset(); + + [pauseButton_ setEnabled:YES]; + [clearAllButton_ setEnabled:YES]; + [settingsButton_ setEnabled:YES]; + [clipView_ setFrozen:NO]; + + messageCenter_->RemoveAllNotifications(true); +} + @end diff --git a/ui/message_center/cocoa/tray_view_controller_unittest.mm b/ui/message_center/cocoa/tray_view_controller_unittest.mm index 822e101..607133a 100644 --- a/ui/message_center/cocoa/tray_view_controller_unittest.mm +++ b/ui/message_center/cocoa/tray_view_controller_unittest.mm @@ -5,6 +5,8 @@ #import "ui/message_center/cocoa/tray_view_controller.h" #include "base/memory/scoped_nsobject.h" +#include "base/message_loop.h" +#include "base/run_loop.h" #include "base/strings/utf_string_conversions.h" #import "ui/base/test/ui_cocoa_test_helper.h" #include "ui/message_center/fake_notifier_settings_provider.h" @@ -16,12 +18,23 @@ class TrayViewControllerTest : public ui::CocoaTest { public: + TrayViewControllerTest() + : center_(NULL), + message_loop_(base::MessageLoop::TYPE_UI) { + } + virtual void SetUp() OVERRIDE { ui::CocoaTest::SetUp(); message_center::MessageCenter::Initialize(); center_ = message_center::MessageCenter::Get(); center_->DisableTimersForTest(); tray_.reset([[MCTrayViewController alloc] initWithMessageCenter:center_]); + [tray_ setAnimationDuration:0.002]; + [tray_ setAnimateClearingNextNotificationDelay:0.001]; + [tray_ setAnimationEndedCallback:^{ + if (nested_run_loop_.get()) + nested_run_loop_->Quit(); + }]; [tray_ view]; // Create the view. } @@ -31,9 +44,19 @@ class TrayViewControllerTest : public ui::CocoaTest { ui::CocoaTest::TearDown(); } + void WaitForAnimationEnded() { + if (![tray_ isAnimating]) + return; + nested_run_loop_.reset(new base::RunLoop()); + nested_run_loop_->Run(); + nested_run_loop_.reset(); + } + protected: message_center::MessageCenter* center_; // Weak, global. + base::MessageLoop message_loop_; + scoped_ptr<base::RunLoop> nested_run_loop_; scoped_nsobject<MCTrayViewController> tray_; }; @@ -112,6 +135,7 @@ TEST_F(TrayViewControllerTest, AddThreeClearAll) { ASSERT_EQ(3u, [[view subviews] count]); [tray_ clearAllNotifications:nil]; + WaitForAnimationEnded(); [tray_ onMessageCenterTrayChanged]; EXPECT_EQ(0u, [[view subviews] count]); @@ -166,6 +190,7 @@ TEST_F(TrayViewControllerTest, NoClearAllWhenNoNotifications) { // Clear all notifications. [tray_ clearAllNotifications:nil]; + WaitForAnimationEnded(); [tray_ onMessageCenterTrayChanged]; // The button should be hidden again. |