blob: dd284383fa6f9d0fe71028e6a8441e244365f19e [file] [log] [blame]
// 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