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 #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