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/popup_controller.h" 6 7 #include <cmath> 8 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" 15 16 #if !defined(MAC_OS_X_VERSION_10_7) || \ 17 MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_7 18 enum { 19 NSWindowCollectionBehaviorFullScreenAuxiliary = 1 << 8 20 }; 21 #endif // MAC_OS_X_VERSION_10_7 22 23 //////////////////////////////////////////////////////////////////////////////// 24 25 @interface MCPopupController (Private) 26 - (void)notificationSwipeStarted; 27 - (void)notificationSwipeMoved:(CGFloat)amount; 28 - (void)notificationSwipeEnded:(BOOL)ended complete:(BOOL)isComplete; 29 @end 30 31 // Window Subclass ///////////////////////////////////////////////////////////// 32 33 @interface MCPopupWindow : NSPanel { 34 // The cumulative X and Y scrollingDeltas since the -scrollWheel: event began. 35 NSPoint totalScrollDelta_; 36 } 37 @end 38 39 @implementation MCPopupWindow 40 41 - (void)scrollWheel:(NSEvent*)event { 42 // Gesture swiping only exists on 10.7+. 43 if (![event respondsToSelector:@selector(phase)]) 44 return; 45 46 NSEventPhase phase = [event phase]; 47 BOOL shouldTrackSwipe = NO; 48 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]; 55 } 56 57 // Only allow horizontal scrolling. 58 if (std::abs(totalScrollDelta_.x) < std::abs(totalScrollDelta_.y)) 59 return; 60 61 if (shouldTrackSwipe) { 62 MCPopupController* controller = 63 base::mac::ObjCCastStrict<MCPopupController>([self windowController]); 64 BOOL directionInverted = [event isDirectionInvertedFromDevice]; 65 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) 71 gestureAmount *= -1; 72 73 if (phase == NSEventPhaseBegan) { 74 [controller notificationSwipeStarted]; 75 return; 76 } 77 78 [controller notificationSwipeMoved:gestureAmount]; 79 80 BOOL ended = phase == NSEventPhaseEnded; 81 if (ended || isComplete) 82 [controller notificationSwipeEnded:ended complete:isComplete]; 83 }; 84 [event trackSwipeEventWithOptions:NSEventSwipeTrackingLockDirection 85 dampenAmountThresholdMin:-1 86 max:1 87 usingHandler:handler]; 88 } 89 } 90 91 @end 92 93 //////////////////////////////////////////////////////////////////////////////// 94 95 @implementation MCPopupController 96 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 105 defer:YES]); 106 if ((self = [super initWithWindow:window])) { 107 messageCenter_ = messageCenter; 108 popupCollection_ = popupCollection; 109 notificationController_.reset( 110 [[MCNotificationController alloc] initWithNotification:notification 111 messageCenter:messageCenter_]); 112 isClosing_ = NO; 113 bounds_ = [[notificationController_ view] frame]; 114 115 [window setReleasedWhenClosed:NO]; 116 117 [window setLevel:NSFloatingWindowLevel]; 118 [window setExcludedFromWindowsMenu:YES]; 119 [window setCollectionBehavior: 120 NSWindowCollectionBehaviorIgnoresCycle | 121 NSWindowCollectionBehaviorFullScreenAuxiliary]; 122 123 [window setHasShadow:YES]; 124 [window setContentView:[notificationController_ view]]; 125 126 trackingArea_.reset( 127 [[CrTrackingArea alloc] initWithRect:NSZeroRect 128 options:NSTrackingInVisibleRect | 129 NSTrackingMouseEnteredAndExited | 130 NSTrackingActiveAlways 131 owner:self 132 userInfo:nil]); 133 [[window contentView] addTrackingArea:trackingArea_.get()]; 134 } 135 return self; 136 } 137 138 - (void)close { 139 if (boundsAnimation_) { 140 [boundsAnimation_ stopAnimation]; 141 [boundsAnimation_ setDelegate:nil]; 142 boundsAnimation_.reset(); 143 } 144 if (trackingArea_.get()) 145 [[[self window] contentView] removeTrackingArea:trackingArea_.get()]; 146 [super close]; 147 [self performSelectorOnMainThread:@selector(release) 148 withObject:nil 149 waitUntilDone:NO 150 modes:@[ NSDefaultRunLoopMode ]]; 151 } 152 153 - (MCNotificationController*)notificationController { 154 return notificationController_.get(); 155 } 156 157 - (const message_center::Notification*)notification { 158 return [notificationController_ notification]; 159 } 160 161 - (const std::string&)notificationID { 162 return [notificationController_ notificationID]; 163 } 164 165 // Private ///////////////////////////////////////////////////////////////////// 166 167 - (void)notificationSwipeStarted { 168 originalFrame_ = [[self window] frame]; 169 swipeGestureEnded_ = NO; 170 } 171 172 - (void)notificationSwipeMoved:(CGFloat)amount { 173 NSWindow* window = [self window]; 174 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) * 179 -amount; 180 [window setFrame:frame display:YES]; 181 } 182 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]]; 188 } 189 } 190 191 - (void)animationDidEnd:(NSAnimation*)animation { 192 if (animation != boundsAnimation_.get()) 193 return; 194 boundsAnimation_.reset(); 195 196 [popupCollection_ onPopupAnimationEnded:[self notificationID]]; 197 198 if (isClosing_) 199 [self close]; 200 } 201 202 - (void)showWithAnimation:(NSRect)newBounds { 203 bounds_ = 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]; 209 210 // Slide-in and fade-in simultaneously. 211 NSDictionary* animationDict = @{ 212 NSViewAnimationTargetKey : [self window], 213 NSViewAnimationEndFrameKey : [NSValue valueWithRect:newBounds], 214 NSViewAnimationEffectKey : NSViewAnimationFadeInEffect 215 }; 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]; 222 } 223 224 - (void)closeWithAnimation { 225 if (isClosing_) 226 return; 227 228 isClosing_ = YES; 229 230 // If the notification was swiped closed, do not animate it as the 231 // notification has already faded out. 232 if (swipeGestureEnded_) { 233 [self close]; 234 return; 235 } 236 237 NSDictionary* animationDict = @{ 238 NSViewAnimationTargetKey : [self window], 239 NSViewAnimationEffectKey : NSViewAnimationFadeOutEffect 240 }; 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]; 247 } 248 249 - (void)markPopupCollectionGone { 250 popupCollection_ = nil; 251 } 252 253 - (NSRect)bounds { 254 return bounds_; 255 } 256 257 - (void)setBounds:(NSRect)newBounds { 258 if (isClosing_ || NSEqualRects(bounds_ , newBounds)) 259 return; 260 bounds_ = newBounds; 261 262 NSDictionary* animationDict = @{ 263 NSViewAnimationTargetKey : [self window], 264 NSViewAnimationEndFrameKey : [NSValue valueWithRect:newBounds] 265 }; 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]; 272 } 273 274 - (void)mouseEntered:(NSEvent*)event { 275 messageCenter_->PausePopupTimers(); 276 } 277 278 - (void)mouseExited:(NSEvent*)event { 279 messageCenter_->RestartPopupTimers(); 280 } 281 282 @end 283