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 #if !defined(MAC_OS_X_VERSION_10_7) || \
17 MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_7
19 NSWindowCollectionBehaviorFullScreenAuxiliary = 1 << 8
21 #endif // MAC_OS_X_VERSION_10_7
23 ////////////////////////////////////////////////////////////////////////////////
25 @interface MCPopupController (Private)
26 - (void)notificationSwipeStarted;
27 - (void)notificationSwipeMoved:(CGFloat)amount;
28 - (void)notificationSwipeEnded:(BOOL)ended complete:(BOOL)isComplete;
31 // Window Subclass /////////////////////////////////////////////////////////////
33 @interface MCPopupWindow : NSPanel {
34 // The cumulative X and Y scrollingDeltas since the -scrollWheel: event began.
35 NSPoint totalScrollDelta_;
39 @implementation MCPopupWindow
41 - (void)scrollWheel:(NSEvent*)event {
42 // Gesture swiping only exists on 10.7+.
43 if (![event respondsToSelector:@selector(phase)])
46 NSEventPhase phase = [event phase];
47 BOOL shouldTrackSwipe = NO;
49 if (phase == NSEventPhaseBegan) {
50 totalScrollDelta_ = NSZeroPoint;
51 } else if (phase == NSEventPhaseChanged) {
52 shouldTrackSwipe = YES;
53 totalScrollDelta_.x += [event scrollingDeltaX];
54 totalScrollDelta_.y += [event scrollingDeltaY];
57 // Only allow horizontal scrolling.
58 if (std::abs(totalScrollDelta_.x) < std::abs(totalScrollDelta_.y))
61 if (shouldTrackSwipe) {
62 MCPopupController* controller =
63 base::mac::ObjCCastStrict<MCPopupController>([self windowController]);
64 BOOL directionInverted = [event isDirectionInvertedFromDevice];
66 auto handler = ^(CGFloat gestureAmount, NSEventPhase phase,
67 BOOL isComplete, BOOL* stop) {
68 // The swipe direction should match the direction the user's fingers
69 // are moving, not the interpreted scroll direction.
70 if (directionInverted)
73 if (phase == NSEventPhaseBegan) {
74 [controller notificationSwipeStarted];
78 [controller notificationSwipeMoved:gestureAmount];
80 BOOL ended = phase == NSEventPhaseEnded;
81 if (ended || isComplete)
82 [controller notificationSwipeEnded:ended complete:isComplete];
84 [event trackSwipeEventWithOptions:NSEventSwipeTrackingLockDirection
85 dampenAmountThresholdMin:-1
87 usingHandler:handler];
93 ////////////////////////////////////////////////////////////////////////////////
95 @implementation MCPopupController
97 - (id)initWithNotification:(const message_center::Notification*)notification
98 messageCenter:(message_center::MessageCenter*)messageCenter
99 popupCollection:(MCPopupCollection*)popupCollection {
100 base::scoped_nsobject<MCPopupWindow> window(
101 [[MCPopupWindow alloc] initWithContentRect:ui::kWindowSizeDeterminedLater
102 styleMask:NSBorderlessWindowMask |
103 NSNonactivatingPanelMask
104 backing:NSBackingStoreBuffered
106 if ((self = [super initWithWindow:window])) {
107 messageCenter_ = messageCenter;
108 popupCollection_ = popupCollection;
109 notificationController_.reset(
110 [[MCNotificationController alloc] initWithNotification:notification
111 messageCenter:messageCenter_]);
113 bounds_ = [[notificationController_ view] frame];
115 [window setReleasedWhenClosed:NO];
117 [window setLevel:NSFloatingWindowLevel];
118 [window setExcludedFromWindowsMenu:YES];
119 [window setCollectionBehavior:
120 NSWindowCollectionBehaviorIgnoresCycle |
121 NSWindowCollectionBehaviorFullScreenAuxiliary];
123 [window setHasShadow:YES];
124 [window setContentView:[notificationController_ view]];
127 [[CrTrackingArea alloc] initWithRect:NSZeroRect
128 options:NSTrackingInVisibleRect |
129 NSTrackingMouseEnteredAndExited |
130 NSTrackingActiveAlways
133 [[window contentView] addTrackingArea:trackingArea_.get()];
139 if (boundsAnimation_) {
140 [boundsAnimation_ stopAnimation];
141 [boundsAnimation_ setDelegate:nil];
142 boundsAnimation_.reset();
144 if (trackingArea_.get())
145 [[[self window] contentView] removeTrackingArea:trackingArea_.get()];
147 [self performSelectorOnMainThread:@selector(release)
150 modes:@[ NSDefaultRunLoopMode ]];
153 - (MCNotificationController*)notificationController {
154 return notificationController_.get();
157 - (const message_center::Notification*)notification {
158 return [notificationController_ notification];
161 - (const std::string&)notificationID {
162 return [notificationController_ notificationID];
165 // Private /////////////////////////////////////////////////////////////////////
167 - (void)notificationSwipeStarted {
168 originalFrame_ = [[self window] frame];
169 swipeGestureEnded_ = NO;
172 - (void)notificationSwipeMoved:(CGFloat)amount {
173 NSWindow* window = [self window];
175 [window setAlphaValue:1.0 - std::abs(amount)];
176 NSRect frame = [window frame];
177 CGFloat originalMin = NSMinX(originalFrame_);
178 frame.origin.x = originalMin + (NSMidX(originalFrame_) - originalMin) *
180 [window setFrame:frame display:YES];
183 - (void)notificationSwipeEnded:(BOOL)ended complete:(BOOL)isComplete {
184 swipeGestureEnded_ |= ended;
185 if (swipeGestureEnded_ && isComplete) {
186 messageCenter_->RemoveNotification([self notificationID], /*by_user=*/true);
187 [popupCollection_ onPopupAnimationEnded:[self notificationID]];
191 - (void)animationDidEnd:(NSAnimation*)animation {
192 if (animation != boundsAnimation_.get())
194 boundsAnimation_.reset();
196 [popupCollection_ onPopupAnimationEnded:[self notificationID]];
202 - (void)showWithAnimation:(NSRect)newBounds {
204 NSRect startBounds = newBounds;
205 startBounds.origin.x += startBounds.size.width;
206 [[self window] setFrame:startBounds display:NO];
207 [[self window] setAlphaValue:0];
208 [self showWindow:nil];
210 // Slide-in and fade-in simultaneously.
211 NSDictionary* animationDict = @{
212 NSViewAnimationTargetKey : [self window],
213 NSViewAnimationEndFrameKey : [NSValue valueWithRect:newBounds],
214 NSViewAnimationEffectKey : NSViewAnimationFadeInEffect
216 DCHECK(!boundsAnimation_);
217 boundsAnimation_.reset([[NSViewAnimation alloc]
218 initWithViewAnimations:[NSArray arrayWithObject:animationDict]]);
219 [boundsAnimation_ setDuration:[popupCollection_ popupAnimationDuration]];
220 [boundsAnimation_ setDelegate:self];
221 [boundsAnimation_ startAnimation];
224 - (void)closeWithAnimation {
230 // If the notification was swiped closed, do not animate it as the
231 // notification has already faded out.
232 if (swipeGestureEnded_) {
237 NSDictionary* animationDict = @{
238 NSViewAnimationTargetKey : [self window],
239 NSViewAnimationEffectKey : NSViewAnimationFadeOutEffect
241 DCHECK(!boundsAnimation_);
242 boundsAnimation_.reset([[NSViewAnimation alloc]
243 initWithViewAnimations:[NSArray arrayWithObject:animationDict]]);
244 [boundsAnimation_ setDuration:[popupCollection_ popupAnimationDuration]];
245 [boundsAnimation_ setDelegate:self];
246 [boundsAnimation_ startAnimation];
249 - (void)markPopupCollectionGone {
250 popupCollection_ = nil;
257 - (void)setBounds:(NSRect)newBounds {
258 if (isClosing_ || NSEqualRects(bounds_ , newBounds))
262 NSDictionary* animationDict = @{
263 NSViewAnimationTargetKey : [self window],
264 NSViewAnimationEndFrameKey : [NSValue valueWithRect:newBounds]
266 DCHECK(!boundsAnimation_);
267 boundsAnimation_.reset([[NSViewAnimation alloc]
268 initWithViewAnimations:[NSArray arrayWithObject:animationDict]]);
269 [boundsAnimation_ setDuration:[popupCollection_ popupAnimationDuration]];
270 [boundsAnimation_ setDelegate:self];
271 [boundsAnimation_ startAnimation];
274 - (void)mouseEntered:(NSEvent*)event {
275 messageCenter_->PausePopupTimers();
278 - (void)mouseExited:(NSEvent*)event {
279 messageCenter_->RestartPopupTimers();