Home | History | Annotate | Download | only in cocoa
      1 // Copyright (c) 2011 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 "chrome/browser/ui/cocoa/info_bubble_window.h"
      6 
      7 #include "base/basictypes.h"
      8 #include "base/logging.h"
      9 #include "base/memory/scoped_nsobject.h"
     10 #include "content/common/notification_observer.h"
     11 #include "content/common/notification_registrar.h"
     12 #include "content/common/notification_service.h"
     13 #include "content/common/notification_type.h"
     14 #import "third_party/GTM/AppKit/GTMNSAnimation+Duration.h"
     15 
     16 namespace {
     17 const CGFloat kOrderInSlideOffset = 10;
     18 const NSTimeInterval kOrderInAnimationDuration = 0.2;
     19 const NSTimeInterval kOrderOutAnimationDuration = 0.15;
     20 // The minimum representable time interval.  This can be used as the value
     21 // passed to +[NSAnimationContext setDuration:] to stop an in-progress
     22 // animation as quickly as possible.
     23 const NSTimeInterval kMinimumTimeInterval =
     24     std::numeric_limits<NSTimeInterval>::min();
     25 }
     26 
     27 @interface InfoBubbleWindow(Private)
     28 - (void)appIsTerminating;
     29 - (void)finishCloseAfterAnimation;
     30 @end
     31 
     32 // A helper class to proxy app notifications to the window.
     33 class AppNotificationBridge : public NotificationObserver {
     34  public:
     35   explicit AppNotificationBridge(InfoBubbleWindow* owner) : owner_(owner) {
     36     registrar_.Add(this, NotificationType::APP_TERMINATING,
     37                    NotificationService::AllSources());
     38   }
     39 
     40   // Overridden from NotificationObserver.
     41   void Observe(NotificationType type,
     42                const NotificationSource& source,
     43                const NotificationDetails& details) {
     44     switch (type.value) {
     45       case NotificationType::APP_TERMINATING:
     46         [owner_ appIsTerminating];
     47         break;
     48       default:
     49         NOTREACHED() << L"Unexpected notification";
     50     }
     51   }
     52 
     53  private:
     54   // The object we need to inform when we get a notification. Weak. Owns us.
     55   InfoBubbleWindow* owner_;
     56 
     57   // Used for registering to receive notifications and automatic clean up.
     58   NotificationRegistrar registrar_;
     59 
     60   DISALLOW_COPY_AND_ASSIGN(AppNotificationBridge);
     61 };
     62 
     63 // A delegate object for watching the alphaValue animation on InfoBubbleWindows.
     64 // An InfoBubbleWindow instance cannot be the delegate for its own animation
     65 // because CAAnimations retain their delegates, and since the InfoBubbleWindow
     66 // retains its animations a retain loop would be formed.
     67 @interface InfoBubbleWindowCloser : NSObject {
     68  @private
     69   InfoBubbleWindow* window_;  // Weak. Window to close.
     70 }
     71 - (id)initWithWindow:(InfoBubbleWindow*)window;
     72 @end
     73 
     74 @implementation InfoBubbleWindowCloser
     75 
     76 - (id)initWithWindow:(InfoBubbleWindow*)window {
     77   if ((self = [super init])) {
     78     window_ = window;
     79   }
     80   return self;
     81 }
     82 
     83 // Callback for the alpha animation. Closes window_ if appropriate.
     84 - (void)animationDidStop:(CAAnimation*)anim finished:(BOOL)flag {
     85   // When alpha reaches zero, close window_.
     86   if ([window_ alphaValue] == 0.0) {
     87     [window_ finishCloseAfterAnimation];
     88   }
     89 }
     90 
     91 @end
     92 
     93 
     94 @implementation InfoBubbleWindow
     95 
     96 @synthesize delayOnClose = delayOnClose_;
     97 
     98 - (id)initWithContentRect:(NSRect)contentRect
     99                 styleMask:(NSUInteger)aStyle
    100                   backing:(NSBackingStoreType)bufferingType
    101                     defer:(BOOL)flag {
    102   if ((self = [super initWithContentRect:contentRect
    103                                styleMask:NSBorderlessWindowMask
    104                                  backing:bufferingType
    105                                    defer:flag])) {
    106     [self setBackgroundColor:[NSColor clearColor]];
    107     [self setExcludedFromWindowsMenu:YES];
    108     [self setOpaque:NO];
    109     [self setHasShadow:YES];
    110     delayOnClose_ = YES;
    111     notificationBridge_.reset(new AppNotificationBridge(self));
    112 
    113     // Start invisible. Will be made visible when ordered front.
    114     [self setAlphaValue:0.0];
    115 
    116     // Set up alphaValue animation so that self is delegate for the animation.
    117     // Setting up the delegate is required so that the
    118     // animationDidStop:finished: callback can be handled.
    119     // Notice that only the alphaValue Animation is replaced in case
    120     // superclasses set up animations.
    121     CAAnimation* alphaAnimation = [CABasicAnimation animation];
    122     scoped_nsobject<InfoBubbleWindowCloser> delegate(
    123         [[InfoBubbleWindowCloser alloc] initWithWindow:self]);
    124     [alphaAnimation setDelegate:delegate];
    125     NSMutableDictionary* animations =
    126         [NSMutableDictionary dictionaryWithDictionary:[self animations]];
    127     [animations setObject:alphaAnimation forKey:@"alphaValue"];
    128     [self setAnimations:animations];
    129   }
    130   return self;
    131 }
    132 
    133 // According to
    134 // http://www.cocoabuilder.com/archive/message/cocoa/2006/6/19/165953,
    135 // NSBorderlessWindowMask windows cannot become key or main. In this
    136 // case, this is not a desired behavior. As an example, the bubble could have
    137 // buttons.
    138 - (BOOL)canBecomeKeyWindow {
    139   return YES;
    140 }
    141 
    142 - (void)close {
    143   // Block the window from receiving events while it fades out.
    144   closing_ = YES;
    145 
    146   if (!delayOnClose_) {
    147     [self finishCloseAfterAnimation];
    148   } else {
    149     // Apply animations to hide self.
    150     [NSAnimationContext beginGrouping];
    151     [[NSAnimationContext currentContext]
    152         gtm_setDuration:kOrderOutAnimationDuration
    153               eventMask:NSLeftMouseUpMask];
    154     [[self animator] setAlphaValue:0.0];
    155     [NSAnimationContext endGrouping];
    156   }
    157 }
    158 
    159 // If the app is terminating but the window is still fading out, cancel the
    160 // animation and close the window to prevent it from leaking.
    161 // See http://crbug.com/37717
    162 - (void)appIsTerminating {
    163   if (!delayOnClose_)
    164     return;  // The close has already happened with no Core Animation.
    165 
    166   // Cancel the current animation so that it closes immediately, triggering
    167   // |finishCloseAfterAnimation|.
    168   [NSAnimationContext beginGrouping];
    169   [[NSAnimationContext currentContext] setDuration:kMinimumTimeInterval];
    170   [[self animator] setAlphaValue:0.0];
    171   [NSAnimationContext endGrouping];
    172 }
    173 
    174 // Called by InfoBubbleWindowCloser when the window is to be really closed
    175 // after the fading animation is complete.
    176 - (void)finishCloseAfterAnimation {
    177   if (closing_)
    178     [super close];
    179 }
    180 
    181 // Adds animation for info bubbles being ordered to the front.
    182 - (void)orderWindow:(NSWindowOrderingMode)orderingMode
    183          relativeTo:(NSInteger)otherWindowNumber {
    184   // According to the documentation '0' is the otherWindowNumber when the window
    185   // is ordered front.
    186   if (orderingMode == NSWindowAbove && otherWindowNumber == 0) {
    187     // Order self appropriately assuming that its alpha is zero as set up
    188     // in the designated initializer.
    189     [super orderWindow:orderingMode relativeTo:otherWindowNumber];
    190 
    191     // Set up frame so it can be adjust down by a few pixels.
    192     NSRect frame = [self frame];
    193     NSPoint newOrigin = frame.origin;
    194     newOrigin.y += kOrderInSlideOffset;
    195     [self setFrameOrigin:newOrigin];
    196 
    197     // Apply animations to show and move self.
    198     [NSAnimationContext beginGrouping];
    199     // The star currently triggers on mouse down, not mouse up.
    200     [[NSAnimationContext currentContext]
    201         gtm_setDuration:kOrderInAnimationDuration
    202               eventMask:NSLeftMouseUpMask|NSLeftMouseDownMask];
    203     [[self animator] setAlphaValue:1.0];
    204     [[self animator] setFrame:frame display:YES];
    205     [NSAnimationContext endGrouping];
    206   } else {
    207     [super orderWindow:orderingMode relativeTo:otherWindowNumber];
    208   }
    209 }
    210 
    211 // If the window is currently animating a close, block all UI events to the
    212 // window.
    213 - (void)sendEvent:(NSEvent*)theEvent {
    214   if (!closing_)
    215     [super sendEvent:theEvent];
    216 }
    217 
    218 - (BOOL)isClosing {
    219   return closing_;
    220 }
    221 
    222 @end
    223