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//////////////////////////////////////////////////////////////////////////////// 17 18@interface MCPopupController (Private) 19- (void)notificationSwipeStarted; 20- (void)notificationSwipeMoved:(CGFloat)amount; 21- (void)notificationSwipeEnded:(BOOL)ended complete:(BOOL)isComplete; 22@end 23 24// Window Subclass ///////////////////////////////////////////////////////////// 25 26@interface MCPopupWindow : NSPanel { 27 // The cumulative X and Y scrollingDeltas since the -scrollWheel: event began. 28 NSPoint totalScrollDelta_; 29} 30@end 31 32@implementation MCPopupWindow 33 34- (void)scrollWheel:(NSEvent*)event { 35 // Gesture swiping only exists on 10.7+. 36 if (![event respondsToSelector:@selector(phase)]) 37 return; 38 39 NSEventPhase phase = [event phase]; 40 BOOL shouldTrackSwipe = NO; 41 42 if (phase == NSEventPhaseBegan) { 43 totalScrollDelta_ = NSZeroPoint; 44 } else if (phase == NSEventPhaseChanged) { 45 shouldTrackSwipe = YES; 46 totalScrollDelta_.x += [event scrollingDeltaX]; 47 totalScrollDelta_.y += [event scrollingDeltaY]; 48 } 49 50 // Only allow horizontal scrolling. 51 if (std::abs(totalScrollDelta_.x) < std::abs(totalScrollDelta_.y)) 52 return; 53 54 if (shouldTrackSwipe) { 55 MCPopupController* controller = 56 base::mac::ObjCCastStrict<MCPopupController>([self windowController]); 57 BOOL directionInverted = [event isDirectionInvertedFromDevice]; 58 59 auto handler = ^(CGFloat gestureAmount, NSEventPhase phase, 60 BOOL isComplete, BOOL* stop) { 61 // The swipe direction should match the direction the user's fingers 62 // are moving, not the interpreted scroll direction. 63 if (directionInverted) 64 gestureAmount *= -1; 65 66 if (phase == NSEventPhaseBegan) { 67 [controller notificationSwipeStarted]; 68 return; 69 } 70 71 [controller notificationSwipeMoved:gestureAmount]; 72 73 BOOL ended = phase == NSEventPhaseEnded; 74 if (ended || isComplete) 75 [controller notificationSwipeEnded:ended complete:isComplete]; 76 }; 77 [event trackSwipeEventWithOptions:NSEventSwipeTrackingLockDirection 78 dampenAmountThresholdMin:-1 79 max:1 80 usingHandler:handler]; 81 } 82} 83 84@end 85 86//////////////////////////////////////////////////////////////////////////////// 87 88@implementation MCPopupController 89 90- (id)initWithNotification:(const message_center::Notification*)notification 91 messageCenter:(message_center::MessageCenter*)messageCenter 92 popupCollection:(MCPopupCollection*)popupCollection { 93 base::scoped_nsobject<MCPopupWindow> window( 94 [[MCPopupWindow alloc] initWithContentRect:ui::kWindowSizeDeterminedLater 95 styleMask:NSBorderlessWindowMask | 96 NSNonactivatingPanelMask 97 backing:NSBackingStoreBuffered 98 defer:YES]); 99 if ((self = [super initWithWindow:window])) { 100 messageCenter_ = messageCenter; 101 popupCollection_ = popupCollection; 102 notificationController_.reset( 103 [[MCNotificationController alloc] initWithNotification:notification 104 messageCenter:messageCenter_]); 105 isClosing_ = NO; 106 bounds_ = [[notificationController_ view] frame]; 107 108 [window setReleasedWhenClosed:NO]; 109 110 [window setLevel:NSFloatingWindowLevel]; 111 [window setExcludedFromWindowsMenu:YES]; 112 [window setCollectionBehavior: 113 NSWindowCollectionBehaviorIgnoresCycle | 114 NSWindowCollectionBehaviorFullScreenAuxiliary]; 115 116 [window setHasShadow:YES]; 117 [window setContentView:[notificationController_ view]]; 118 119 trackingArea_.reset( 120 [[CrTrackingArea alloc] initWithRect:NSZeroRect 121 options:NSTrackingInVisibleRect | 122 NSTrackingMouseEnteredAndExited | 123 NSTrackingActiveAlways 124 owner:self 125 userInfo:nil]); 126 [[window contentView] addTrackingArea:trackingArea_.get()]; 127 } 128 return self; 129} 130 131- (void)close { 132 if (boundsAnimation_) { 133 [boundsAnimation_ stopAnimation]; 134 [boundsAnimation_ setDelegate:nil]; 135 boundsAnimation_.reset(); 136 } 137 if (trackingArea_.get()) 138 [[[self window] contentView] removeTrackingArea:trackingArea_.get()]; 139 [super close]; 140 [self performSelectorOnMainThread:@selector(release) 141 withObject:nil 142 waitUntilDone:NO 143 modes:@[ NSDefaultRunLoopMode ]]; 144} 145 146- (MCNotificationController*)notificationController { 147 return notificationController_.get(); 148} 149 150- (const message_center::Notification*)notification { 151 return [notificationController_ notification]; 152} 153 154- (const std::string&)notificationID { 155 return [notificationController_ notificationID]; 156} 157 158// Private ///////////////////////////////////////////////////////////////////// 159 160- (void)notificationSwipeStarted { 161 originalFrame_ = [[self window] frame]; 162 swipeGestureEnded_ = NO; 163} 164 165- (void)notificationSwipeMoved:(CGFloat)amount { 166 NSWindow* window = [self window]; 167 168 [window setAlphaValue:1.0 - std::abs(amount)]; 169 NSRect frame = [window frame]; 170 CGFloat originalMin = NSMinX(originalFrame_); 171 frame.origin.x = originalMin + (NSMidX(originalFrame_) - originalMin) * 172 -amount; 173 [window setFrame:frame display:YES]; 174} 175 176- (void)notificationSwipeEnded:(BOOL)ended complete:(BOOL)isComplete { 177 swipeGestureEnded_ |= ended; 178 if (swipeGestureEnded_ && isComplete) { 179 messageCenter_->RemoveNotification([self notificationID], /*by_user=*/true); 180 [popupCollection_ onPopupAnimationEnded:[self notificationID]]; 181 } 182} 183 184- (void)animationDidEnd:(NSAnimation*)animation { 185 if (animation != boundsAnimation_.get()) 186 return; 187 boundsAnimation_.reset(); 188 189 [popupCollection_ onPopupAnimationEnded:[self notificationID]]; 190 191 if (isClosing_) 192 [self close]; 193} 194 195- (void)showWithAnimation:(NSRect)newBounds { 196 bounds_ = newBounds; 197 NSRect startBounds = newBounds; 198 startBounds.origin.x += startBounds.size.width; 199 [[self window] setFrame:startBounds display:NO]; 200 [[self window] setAlphaValue:0]; 201 [self showWindow:nil]; 202 203 // Slide-in and fade-in simultaneously. 204 NSDictionary* animationDict = @{ 205 NSViewAnimationTargetKey : [self window], 206 NSViewAnimationEndFrameKey : [NSValue valueWithRect:newBounds], 207 NSViewAnimationEffectKey : NSViewAnimationFadeInEffect 208 }; 209 DCHECK(!boundsAnimation_); 210 boundsAnimation_.reset([[NSViewAnimation alloc] 211 initWithViewAnimations:[NSArray arrayWithObject:animationDict]]); 212 [boundsAnimation_ setDuration:[popupCollection_ popupAnimationDuration]]; 213 [boundsAnimation_ setDelegate:self]; 214 [boundsAnimation_ startAnimation]; 215} 216 217- (void)closeWithAnimation { 218 if (isClosing_) 219 return; 220 221 isClosing_ = YES; 222 223 // If the notification was swiped closed, do not animate it as the 224 // notification has already faded out. 225 if (swipeGestureEnded_) { 226 [self close]; 227 return; 228 } 229 230 NSDictionary* animationDict = @{ 231 NSViewAnimationTargetKey : [self window], 232 NSViewAnimationEffectKey : NSViewAnimationFadeOutEffect 233 }; 234 DCHECK(!boundsAnimation_); 235 boundsAnimation_.reset([[NSViewAnimation alloc] 236 initWithViewAnimations:[NSArray arrayWithObject:animationDict]]); 237 [boundsAnimation_ setDuration:[popupCollection_ popupAnimationDuration]]; 238 [boundsAnimation_ setDelegate:self]; 239 [boundsAnimation_ startAnimation]; 240} 241 242- (void)markPopupCollectionGone { 243 popupCollection_ = nil; 244} 245 246- (NSRect)bounds { 247 return bounds_; 248} 249 250- (void)setBounds:(NSRect)newBounds { 251 if (isClosing_ || NSEqualRects(bounds_ , newBounds)) 252 return; 253 bounds_ = newBounds; 254 255 NSDictionary* animationDict = @{ 256 NSViewAnimationTargetKey : [self window], 257 NSViewAnimationEndFrameKey : [NSValue valueWithRect:newBounds] 258 }; 259 DCHECK(!boundsAnimation_); 260 boundsAnimation_.reset([[NSViewAnimation alloc] 261 initWithViewAnimations:[NSArray arrayWithObject:animationDict]]); 262 [boundsAnimation_ setDuration:[popupCollection_ popupAnimationDuration]]; 263 [boundsAnimation_ setDelegate:self]; 264 [boundsAnimation_ startAnimation]; 265} 266 267- (void)mouseEntered:(NSEvent*)event { 268 messageCenter_->PausePopupTimers(); 269} 270 271- (void)mouseExited:(NSEvent*)event { 272 messageCenter_->RestartPopupTimers(); 273} 274 275@end 276