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