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.
5 #import "chrome/browser/ui/cocoa/info_bubble_window.h"
7 #include <Carbon/Carbon.h>
9 #include "base/basictypes.h"
10 #include "base/logging.h"
11 #include "base/mac/scoped_nsobject.h"
12 #include "chrome/browser/chrome_notification_types.h"
13 #include "content/public/browser/notification_observer.h"
14 #include "content/public/browser/notification_registrar.h"
15 #include "content/public/browser/notification_service.h"
16 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSAnimation+Duration.h"
19 const CGFloat kOrderInSlideOffset = 10;
20 const NSTimeInterval kOrderInAnimationDuration = 0.075;
21 const NSTimeInterval kOrderOutAnimationDuration = 0.15;
22 // The minimum representable time interval. This can be used as the value
23 // passed to +[NSAnimationContext setDuration:] to stop an in-progress
24 // animation as quickly as possible.
25 const NSTimeInterval kMinimumTimeInterval =
26 std::numeric_limits<NSTimeInterval>::min();
29 @interface InfoBubbleWindow (Private)
30 - (void)appIsTerminating;
31 - (void)finishCloseAfterAnimation;
34 // A helper class to proxy app notifications to the window.
35 class AppNotificationBridge : public content::NotificationObserver {
37 explicit AppNotificationBridge(InfoBubbleWindow* owner) : owner_(owner) {
38 registrar_.Add(this, chrome::NOTIFICATION_APP_TERMINATING,
39 content::NotificationService::AllSources());
42 // Overridden from content::NotificationObserver.
43 void Observe(int type,
44 const content::NotificationSource& source,
45 const content::NotificationDetails& details) override {
47 case chrome::NOTIFICATION_APP_TERMINATING:
48 [owner_ appIsTerminating];
51 NOTREACHED() << L"Unexpected notification";
56 // The object we need to inform when we get a notification. Weak. Owns us.
57 InfoBubbleWindow* owner_;
59 // Used for registering to receive notifications and automatic clean up.
60 content::NotificationRegistrar registrar_;
62 DISALLOW_COPY_AND_ASSIGN(AppNotificationBridge);
65 // A delegate object for watching the alphaValue animation on InfoBubbleWindows.
66 // An InfoBubbleWindow instance cannot be the delegate for its own animation
67 // because CAAnimations retain their delegates, and since the InfoBubbleWindow
68 // retains its animations a retain loop would be formed.
69 @interface InfoBubbleWindowCloser : NSObject {
71 InfoBubbleWindow* window_; // Weak. Window to close.
73 - (id)initWithWindow:(InfoBubbleWindow*)window;
76 @implementation InfoBubbleWindowCloser
78 - (id)initWithWindow:(InfoBubbleWindow*)window {
79 if ((self = [super init])) {
85 // Callback for the alpha animation. Closes window_ if appropriate.
86 - (void)animationDidStop:(CAAnimation*)anim finished:(BOOL)flag {
87 // When alpha reaches zero, close window_.
88 if ([window_ alphaValue] == 0.0) {
89 [window_ finishCloseAfterAnimation];
96 @implementation InfoBubbleWindow
98 @synthesize allowedAnimations = allowedAnimations_;
99 @synthesize canBecomeKeyWindow = canBecomeKeyWindow_;
100 @synthesize allowShareParentKeyState = allowShareParentKeyState_;
102 - (id)initWithContentRect:(NSRect)contentRect
103 styleMask:(NSUInteger)aStyle
104 backing:(NSBackingStoreType)bufferingType
106 if ((self = [super initWithContentRect:contentRect
107 styleMask:NSBorderlessWindowMask
108 backing:bufferingType
110 [self setBackgroundColor:[NSColor clearColor]];
111 [self setExcludedFromWindowsMenu:YES];
112 [self setAllowShareParentKeyState:YES];
114 [self setHasShadow:YES];
115 canBecomeKeyWindow_ = YES;
116 allowedAnimations_ = info_bubble::kAnimateOrderIn |
117 info_bubble::kAnimateOrderOut;
118 notificationBridge_.reset(new AppNotificationBridge(self));
120 // Start invisible. Will be made visible when ordered front.
121 [self setAlphaValue:0.0];
123 // Set up alphaValue animation so that self is delegate for the animation.
124 // Setting up the delegate is required so that the
125 // animationDidStop:finished: callback can be handled.
126 // Notice that only the alphaValue Animation is replaced in case
127 // superclasses set up animations.
128 CAAnimation* alphaAnimation = [CABasicAnimation animation];
129 base::scoped_nsobject<InfoBubbleWindowCloser> delegate(
130 [[InfoBubbleWindowCloser alloc] initWithWindow:self]);
131 [alphaAnimation setDelegate:delegate];
132 NSMutableDictionary* animations =
133 [NSMutableDictionary dictionaryWithDictionary:[self animations]];
134 [animations setObject:alphaAnimation forKey:@"alphaValue"];
135 [self setAnimations:animations];
140 - (BOOL)performKeyEquivalent:(NSEvent*)event {
141 if (([event keyCode] == kVK_Escape) ||
142 (([event keyCode] == kVK_ANSI_Period) &&
143 (([event modifierFlags] & NSCommandKeyMask) != 0))) {
144 [[self windowController] cancel:self];
147 return [super performKeyEquivalent:event];
151 // http://www.cocoabuilder.com/archive/message/cocoa/2006/6/19/165953,
152 // NSBorderlessWindowMask windows cannot become key or main. In this
153 // case, this is not necessarily a desired behavior. As an example, the
154 // bubble could have buttons.
155 - (BOOL)canBecomeKeyWindow {
156 return canBecomeKeyWindow_;
159 // Lets the traffic light buttons on the browser window keep their "active"
160 // state while an info bubble is open. Only has an effect on 10.7.
161 - (BOOL)_sharesParentKeyState {
162 return allowShareParentKeyState_;
166 // Block the window from receiving events while it fades out.
169 if ((allowedAnimations_ & info_bubble::kAnimateOrderOut) == 0) {
170 [self finishCloseAfterAnimation];
172 // Apply animations to hide self.
173 [NSAnimationContext beginGrouping];
174 [[NSAnimationContext currentContext]
175 gtm_setDuration:kOrderOutAnimationDuration
176 eventMask:NSLeftMouseUpMask];
177 [[self animator] setAlphaValue:0.0];
178 [NSAnimationContext endGrouping];
182 // If the app is terminating but the window is still fading out, cancel the
183 // animation and close the window to prevent it from leaking.
184 // See http://crbug.com/37717
185 - (void)appIsTerminating {
186 if ((allowedAnimations_ & info_bubble::kAnimateOrderOut) == 0)
187 return; // The close has already happened with no Core Animation.
189 // Cancel the current animation so that it closes immediately, triggering
190 // |finishCloseAfterAnimation|.
191 [NSAnimationContext beginGrouping];
192 [[NSAnimationContext currentContext] setDuration:kMinimumTimeInterval];
193 [[self animator] setAlphaValue:0.0];
194 [NSAnimationContext endGrouping];
197 // Called by InfoBubbleWindowCloser when the window is to be really closed
198 // after the fading animation is complete.
199 - (void)finishCloseAfterAnimation {
201 [[self parentWindow] removeChildWindow:self];
206 // Adds animation for info bubbles being ordered to the front.
207 - (void)orderWindow:(NSWindowOrderingMode)orderingMode
208 relativeTo:(NSInteger)otherWindowNumber {
209 // According to the documentation '0' is the otherWindowNumber when the window
211 if (orderingMode == NSWindowAbove && otherWindowNumber == 0) {
212 // Order self appropriately assuming that its alpha is zero as set up
213 // in the designated initializer.
214 [super orderWindow:orderingMode relativeTo:otherWindowNumber];
216 // Set up frame so it can be adjust down by a few pixels.
217 NSRect frame = [self frame];
218 NSPoint newOrigin = frame.origin;
219 newOrigin.y += kOrderInSlideOffset;
220 [self setFrameOrigin:newOrigin];
222 // Apply animations to show and move self.
223 [NSAnimationContext beginGrouping];
224 // The star currently triggers on mouse down, not mouse up.
225 NSTimeInterval duration =
226 (allowedAnimations_ & info_bubble::kAnimateOrderIn)
227 ? kOrderInAnimationDuration : kMinimumTimeInterval;
228 [[NSAnimationContext currentContext]
229 gtm_setDuration:duration
230 eventMask:NSLeftMouseUpMask | NSLeftMouseDownMask];
231 [[self animator] setAlphaValue:1.0];
232 [[self animator] setFrame:frame display:YES];
233 [NSAnimationContext endGrouping];
235 [super orderWindow:orderingMode relativeTo:otherWindowNumber];
239 // If the window is currently animating a close, block all UI events to the
241 - (void)sendEvent:(NSEvent*)theEvent {
243 [super sendEvent:theEvent];