| // Copyright (c) 2011 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "chrome/browser/ui/cocoa/info_bubble_window.h" |
| |
| #include "base/basictypes.h" |
| #include "base/logging.h" |
| #include "base/memory/scoped_nsobject.h" |
| #include "content/common/notification_observer.h" |
| #include "content/common/notification_registrar.h" |
| #include "content/common/notification_service.h" |
| #include "content/common/notification_type.h" |
| #import "third_party/GTM/AppKit/GTMNSAnimation+Duration.h" |
| |
| namespace { |
| const CGFloat kOrderInSlideOffset = 10; |
| const NSTimeInterval kOrderInAnimationDuration = 0.2; |
| const NSTimeInterval kOrderOutAnimationDuration = 0.15; |
| // The minimum representable time interval. This can be used as the value |
| // passed to +[NSAnimationContext setDuration:] to stop an in-progress |
| // animation as quickly as possible. |
| const NSTimeInterval kMinimumTimeInterval = |
| std::numeric_limits<NSTimeInterval>::min(); |
| } |
| |
| @interface InfoBubbleWindow(Private) |
| - (void)appIsTerminating; |
| - (void)finishCloseAfterAnimation; |
| @end |
| |
| // A helper class to proxy app notifications to the window. |
| class AppNotificationBridge : public NotificationObserver { |
| public: |
| explicit AppNotificationBridge(InfoBubbleWindow* owner) : owner_(owner) { |
| registrar_.Add(this, NotificationType::APP_TERMINATING, |
| NotificationService::AllSources()); |
| } |
| |
| // Overridden from NotificationObserver. |
| void Observe(NotificationType type, |
| const NotificationSource& source, |
| const NotificationDetails& details) { |
| switch (type.value) { |
| case NotificationType::APP_TERMINATING: |
| [owner_ appIsTerminating]; |
| break; |
| default: |
| NOTREACHED() << L"Unexpected notification"; |
| } |
| } |
| |
| private: |
| // The object we need to inform when we get a notification. Weak. Owns us. |
| InfoBubbleWindow* owner_; |
| |
| // Used for registering to receive notifications and automatic clean up. |
| NotificationRegistrar registrar_; |
| |
| DISALLOW_COPY_AND_ASSIGN(AppNotificationBridge); |
| }; |
| |
| // A delegate object for watching the alphaValue animation on InfoBubbleWindows. |
| // An InfoBubbleWindow instance cannot be the delegate for its own animation |
| // because CAAnimations retain their delegates, and since the InfoBubbleWindow |
| // retains its animations a retain loop would be formed. |
| @interface InfoBubbleWindowCloser : NSObject { |
| @private |
| InfoBubbleWindow* window_; // Weak. Window to close. |
| } |
| - (id)initWithWindow:(InfoBubbleWindow*)window; |
| @end |
| |
| @implementation InfoBubbleWindowCloser |
| |
| - (id)initWithWindow:(InfoBubbleWindow*)window { |
| if ((self = [super init])) { |
| window_ = window; |
| } |
| return self; |
| } |
| |
| // Callback for the alpha animation. Closes window_ if appropriate. |
| - (void)animationDidStop:(CAAnimation*)anim finished:(BOOL)flag { |
| // When alpha reaches zero, close window_. |
| if ([window_ alphaValue] == 0.0) { |
| [window_ finishCloseAfterAnimation]; |
| } |
| } |
| |
| @end |
| |
| |
| @implementation InfoBubbleWindow |
| |
| @synthesize delayOnClose = delayOnClose_; |
| |
| - (id)initWithContentRect:(NSRect)contentRect |
| styleMask:(NSUInteger)aStyle |
| backing:(NSBackingStoreType)bufferingType |
| defer:(BOOL)flag { |
| if ((self = [super initWithContentRect:contentRect |
| styleMask:NSBorderlessWindowMask |
| backing:bufferingType |
| defer:flag])) { |
| [self setBackgroundColor:[NSColor clearColor]]; |
| [self setExcludedFromWindowsMenu:YES]; |
| [self setOpaque:NO]; |
| [self setHasShadow:YES]; |
| delayOnClose_ = YES; |
| notificationBridge_.reset(new AppNotificationBridge(self)); |
| |
| // Start invisible. Will be made visible when ordered front. |
| [self setAlphaValue:0.0]; |
| |
| // Set up alphaValue animation so that self is delegate for the animation. |
| // Setting up the delegate is required so that the |
| // animationDidStop:finished: callback can be handled. |
| // Notice that only the alphaValue Animation is replaced in case |
| // superclasses set up animations. |
| CAAnimation* alphaAnimation = [CABasicAnimation animation]; |
| scoped_nsobject<InfoBubbleWindowCloser> delegate( |
| [[InfoBubbleWindowCloser alloc] initWithWindow:self]); |
| [alphaAnimation setDelegate:delegate]; |
| NSMutableDictionary* animations = |
| [NSMutableDictionary dictionaryWithDictionary:[self animations]]; |
| [animations setObject:alphaAnimation forKey:@"alphaValue"]; |
| [self setAnimations:animations]; |
| } |
| return self; |
| } |
| |
| // According to |
| // http://www.cocoabuilder.com/archive/message/cocoa/2006/6/19/165953, |
| // NSBorderlessWindowMask windows cannot become key or main. In this |
| // case, this is not a desired behavior. As an example, the bubble could have |
| // buttons. |
| - (BOOL)canBecomeKeyWindow { |
| return YES; |
| } |
| |
| - (void)close { |
| // Block the window from receiving events while it fades out. |
| closing_ = YES; |
| |
| if (!delayOnClose_) { |
| [self finishCloseAfterAnimation]; |
| } else { |
| // Apply animations to hide self. |
| [NSAnimationContext beginGrouping]; |
| [[NSAnimationContext currentContext] |
| gtm_setDuration:kOrderOutAnimationDuration |
| eventMask:NSLeftMouseUpMask]; |
| [[self animator] setAlphaValue:0.0]; |
| [NSAnimationContext endGrouping]; |
| } |
| } |
| |
| // If the app is terminating but the window is still fading out, cancel the |
| // animation and close the window to prevent it from leaking. |
| // See http://crbug.com/37717 |
| - (void)appIsTerminating { |
| if (!delayOnClose_) |
| return; // The close has already happened with no Core Animation. |
| |
| // Cancel the current animation so that it closes immediately, triggering |
| // |finishCloseAfterAnimation|. |
| [NSAnimationContext beginGrouping]; |
| [[NSAnimationContext currentContext] setDuration:kMinimumTimeInterval]; |
| [[self animator] setAlphaValue:0.0]; |
| [NSAnimationContext endGrouping]; |
| } |
| |
| // Called by InfoBubbleWindowCloser when the window is to be really closed |
| // after the fading animation is complete. |
| - (void)finishCloseAfterAnimation { |
| if (closing_) |
| [super close]; |
| } |
| |
| // Adds animation for info bubbles being ordered to the front. |
| - (void)orderWindow:(NSWindowOrderingMode)orderingMode |
| relativeTo:(NSInteger)otherWindowNumber { |
| // According to the documentation '0' is the otherWindowNumber when the window |
| // is ordered front. |
| if (orderingMode == NSWindowAbove && otherWindowNumber == 0) { |
| // Order self appropriately assuming that its alpha is zero as set up |
| // in the designated initializer. |
| [super orderWindow:orderingMode relativeTo:otherWindowNumber]; |
| |
| // Set up frame so it can be adjust down by a few pixels. |
| NSRect frame = [self frame]; |
| NSPoint newOrigin = frame.origin; |
| newOrigin.y += kOrderInSlideOffset; |
| [self setFrameOrigin:newOrigin]; |
| |
| // Apply animations to show and move self. |
| [NSAnimationContext beginGrouping]; |
| // The star currently triggers on mouse down, not mouse up. |
| [[NSAnimationContext currentContext] |
| gtm_setDuration:kOrderInAnimationDuration |
| eventMask:NSLeftMouseUpMask|NSLeftMouseDownMask]; |
| [[self animator] setAlphaValue:1.0]; |
| [[self animator] setFrame:frame display:YES]; |
| [NSAnimationContext endGrouping]; |
| } else { |
| [super orderWindow:orderingMode relativeTo:otherWindowNumber]; |
| } |
| } |
| |
| // If the window is currently animating a close, block all UI events to the |
| // window. |
| - (void)sendEvent:(NSEvent*)theEvent { |
| if (!closing_) |
| [super sendEvent:theEvent]; |
| } |
| |
| - (BOOL)isClosing { |
| return closing_; |
| } |
| |
| @end |