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