1 // Copyright (c) 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #import "ui/message_center/cocoa/popup_controller.h"
9 #import "base/mac/foundation_util.h"
10 #import "base/mac/sdk_forward_declarations.h"
11 #import "ui/base/cocoa/window_size_constants.h"
12 #import "ui/message_center/cocoa/notification_controller.h"
13 #import "ui/message_center/cocoa/popup_collection.h"
14 #include "ui/message_center/message_center.h"
16 ////////////////////////////////////////////////////////////////////////////////
18 @interface MCPopupController (Private)
19 - (void)notificationSwipeStarted;
20 - (void)notificationSwipeMoved:(CGFloat)amount;
21 - (void)notificationSwipeEnded:(BOOL)ended complete:(BOOL)isComplete;
23 // This setter for |boundsAnimation_| also cleans up the state of the previous
24 // |boundsAnimation_|.
25 - (void)setBoundsAnimation:(NSViewAnimation*)animation;
27 // Constructs an NSViewAnimation from |dictionary|, which should be a view
28 // animation dictionary.
29 - (NSViewAnimation*)animationWithDictionary:(NSDictionary*)dictionary;
32 // Window Subclass /////////////////////////////////////////////////////////////
34 @interface MCPopupWindow : NSPanel {
35 // The cumulative X and Y scrollingDeltas since the -scrollWheel: event began.
36 NSPoint totalScrollDelta_;
40 @implementation MCPopupWindow
42 - (void)scrollWheel:(NSEvent*)event {
43 // Gesture swiping only exists on 10.7+.
44 if (![event respondsToSelector:@selector(phase)])
47 NSEventPhase phase = [event phase];
48 BOOL shouldTrackSwipe = NO;
50 if (phase == NSEventPhaseBegan) {
51 totalScrollDelta_ = NSZeroPoint;
52 } else if (phase == NSEventPhaseChanged) {
53 shouldTrackSwipe = YES;
54 totalScrollDelta_.x += [event scrollingDeltaX];
55 totalScrollDelta_.y += [event scrollingDeltaY];
58 // Only allow horizontal scrolling.
59 if (std::abs(totalScrollDelta_.x) < std::abs(totalScrollDelta_.y))
62 if (shouldTrackSwipe) {
63 MCPopupController* controller =
64 base::mac::ObjCCastStrict<MCPopupController>([self windowController]);
65 BOOL directionInverted = [event isDirectionInvertedFromDevice];
67 auto handler = ^(CGFloat gestureAmount, NSEventPhase phase,
68 BOOL isComplete, BOOL* stop) {
69 // The swipe direction should match the direction the user's fingers
70 // are moving, not the interpreted scroll direction.
71 if (directionInverted)
74 if (phase == NSEventPhaseBegan) {
75 [controller notificationSwipeStarted];
79 [controller notificationSwipeMoved:gestureAmount];
81 BOOL ended = phase == NSEventPhaseEnded;
82 if (ended || isComplete)
83 [controller notificationSwipeEnded:ended complete:isComplete];
85 [event trackSwipeEventWithOptions:NSEventSwipeTrackingLockDirection
86 dampenAmountThresholdMin:-1
88 usingHandler:handler];
94 ////////////////////////////////////////////////////////////////////////////////
96 @implementation MCPopupController
98 - (id)initWithNotification:(const message_center::Notification*)notification
99 messageCenter:(message_center::MessageCenter*)messageCenter
100 popupCollection:(MCPopupCollection*)popupCollection {
101 base::scoped_nsobject<MCPopupWindow> window(
102 [[MCPopupWindow alloc] initWithContentRect:ui::kWindowSizeDeterminedLater
103 styleMask:NSBorderlessWindowMask |
104 NSNonactivatingPanelMask
105 backing:NSBackingStoreBuffered
107 if ((self = [super initWithWindow:window])) {
108 messageCenter_ = messageCenter;
109 popupCollection_ = popupCollection;
110 notificationController_.reset(
111 [[MCNotificationController alloc] initWithNotification:notification
112 messageCenter:messageCenter_]);
114 bounds_ = [[notificationController_ view] frame];
116 [window setReleasedWhenClosed:NO];
118 [window setLevel:NSFloatingWindowLevel];
119 [window setExcludedFromWindowsMenu:YES];
120 [window setCollectionBehavior:
121 NSWindowCollectionBehaviorIgnoresCycle |
122 NSWindowCollectionBehaviorFullScreenAuxiliary];
124 [window setHasShadow:YES];
125 [window setContentView:[notificationController_ view]];
128 [[CrTrackingArea alloc] initWithRect:NSZeroRect
129 options:NSTrackingInVisibleRect |
130 NSTrackingMouseEnteredAndExited |
131 NSTrackingActiveAlways
134 [[window contentView] addTrackingArea:trackingArea_.get()];
140 [self setBoundsAnimation:nil];
141 if (trackingArea_.get())
142 [[[self window] contentView] removeTrackingArea:trackingArea_.get()];
144 [self performSelectorOnMainThread:@selector(release)
147 modes:@[ NSDefaultRunLoopMode ]];
150 - (MCNotificationController*)notificationController {
151 return notificationController_.get();
154 - (const message_center::Notification*)notification {
155 return [notificationController_ notification];
158 - (const std::string&)notificationID {
159 return [notificationController_ notificationID];
162 // Private /////////////////////////////////////////////////////////////////////
164 - (void)notificationSwipeStarted {
165 originalFrame_ = [[self window] frame];
166 swipeGestureEnded_ = NO;
169 - (void)notificationSwipeMoved:(CGFloat)amount {
170 NSWindow* window = [self window];
172 [window setAlphaValue:1.0 - std::abs(amount)];
173 NSRect frame = [window frame];
174 CGFloat originalMin = NSMinX(originalFrame_);
175 frame.origin.x = originalMin + (NSMidX(originalFrame_) - originalMin) *
177 [window setFrame:frame display:YES];
180 - (void)notificationSwipeEnded:(BOOL)ended complete:(BOOL)isComplete {
181 swipeGestureEnded_ |= ended;
182 if (swipeGestureEnded_ && isComplete) {
183 messageCenter_->RemoveNotification([self notificationID], /*by_user=*/true);
184 [popupCollection_ onPopupAnimationEnded:[self notificationID]];
188 - (void)setBoundsAnimation:(NSViewAnimation*)animation {
189 [boundsAnimation_ stopAnimation];
190 [boundsAnimation_ setDelegate:nil];
191 boundsAnimation_.reset([animation retain]);
194 - (NSViewAnimation*)animationWithDictionary:(NSDictionary*)dictionary {
195 return [[[NSViewAnimation alloc]
196 initWithViewAnimations:@[ dictionary ]] autorelease];
199 - (void)animationDidEnd:(NSAnimation*)animation {
200 DCHECK_EQ(animation, boundsAnimation_.get());
201 [self setBoundsAnimation:nil];
203 [popupCollection_ onPopupAnimationEnded:[self notificationID]];
209 - (void)animationDidStop:(NSAnimation*)animation {
210 // We can arrive here if animation was stopped in [self close] call.
211 DCHECK_EQ(animation, boundsAnimation_.get());
212 [self setBoundsAnimation:nil];
214 [popupCollection_ onPopupAnimationEnded:[self notificationID]];
217 - (void)showWithAnimation:(NSRect)newBounds {
219 NSRect startBounds = newBounds;
220 startBounds.origin.x += startBounds.size.width;
221 [[self window] setFrame:startBounds display:NO];
222 [[self window] setAlphaValue:0];
223 [self showWindow:nil];
225 // Slide-in and fade-in simultaneously.
226 NSDictionary* animationDict = @{
227 NSViewAnimationTargetKey : [self window],
228 NSViewAnimationEndFrameKey : [NSValue valueWithRect:newBounds],
229 NSViewAnimationEffectKey : NSViewAnimationFadeInEffect
231 NSViewAnimation* animation = [self animationWithDictionary:animationDict];
232 [self setBoundsAnimation:animation];
233 [boundsAnimation_ setDuration:[popupCollection_ popupAnimationDuration]];
234 [boundsAnimation_ setDelegate:self];
235 [boundsAnimation_ startAnimation];
238 - (void)closeWithAnimation {
244 // If the notification was swiped closed, do not animate it as the
245 // notification has already faded out.
246 if (swipeGestureEnded_) {
251 NSDictionary* animationDict = @{
252 NSViewAnimationTargetKey : [self window],
253 NSViewAnimationEffectKey : NSViewAnimationFadeOutEffect
255 NSViewAnimation* animation = [self animationWithDictionary:animationDict];
256 [self setBoundsAnimation:animation];
257 [boundsAnimation_ setDuration:[popupCollection_ popupAnimationDuration]];
258 [boundsAnimation_ setDelegate:self];
259 [boundsAnimation_ startAnimation];
262 - (void)markPopupCollectionGone {
263 popupCollection_ = nil;
270 - (void)setBounds:(NSRect)newBounds {
271 if (isClosing_ || NSEqualRects(bounds_ , newBounds))
275 NSDictionary* animationDict = @{
276 NSViewAnimationTargetKey : [self window],
277 NSViewAnimationEndFrameKey : [NSValue valueWithRect:newBounds]
279 NSViewAnimation* animation = [self animationWithDictionary:animationDict];
280 [self setBoundsAnimation:animation];
281 [boundsAnimation_ setDuration:[popupCollection_ popupAnimationDuration]];
282 [boundsAnimation_ setDelegate:self];
283 [boundsAnimation_ startAnimation];
286 - (void)mouseEntered:(NSEvent*)event {
287 messageCenter_->PausePopupTimers();
290 - (void)mouseExited:(NSEvent*)event {
291 messageCenter_->RestartPopupTimers();