Home | History | Annotate | Download | only in cocoa
      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