summaryrefslogtreecommitdiffstats
path: root/ui/message_center
diff options
context:
space:
mode:
authorjianli@chromium.org <jianli@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-06-14 23:21:00 +0000
committerjianli@chromium.org <jianli@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-06-14 23:21:00 +0000
commit00f0caa6a7ccb0333306951688fbb0765e4e8b10 (patch)
tree4206489eb85f9e1c670ca445c406353ca5d086c0 /ui/message_center
parent9bf0b36044584756132943f37a06f2fcaef7e5fb (diff)
downloadchromium_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.mm3
-rw-r--r--ui/message_center/cocoa/popup_collection_unittest.mm8
-rw-r--r--ui/message_center/cocoa/tray_controller.mm5
-rw-r--r--ui/message_center/cocoa/tray_view_controller.h60
-rw-r--r--ui/message_center/cocoa/tray_view_controller.mm198
-rw-r--r--ui/message_center/cocoa/tray_view_controller_unittest.mm25
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.