#include "base/bind.h"
#include "base/compiler_specific.h"
#include "base/mac/mac_util.h"
+#include "base/mac/scoped_block.h"
+#include "base/mac/sdk_forward_declarations.h"
#include "base/message_loop/message_loop.h"
#include "base/strings/string_util.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#import "chrome/browser/ui/cocoa/bubble_view.h"
+#include "chrome/browser/ui/elide_url.h"
#include "net/base/net_util.h"
#import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSAnimation+Duration.h"
#import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSBezierPath+RoundRect.h"
const int kTextPadding = 3;
-// The animation key used for fade-in and fade-out transitions.
-NSString* const kFadeAnimationKey = @"alphaValue";
-
// The status bubble's maximum opacity, when fully faded in.
const CGFloat kBubbleOpacity = 1.0;
// Delay before showing or hiding the bubble after a SetStatus or SetURL call.
-const int64 kShowDelayMilliseconds = 80;
-const int64 kHideDelayMilliseconds = 250;
+const int64 kShowDelayMS = 80;
+const int64 kHideDelayMS = 250;
// How long each fade should last.
const NSTimeInterval kShowFadeInDurationSeconds = 0.120;
const NSTimeInterval kMinimumTimeInterval =
std::numeric_limits<NSTimeInterval>::min();
-// How quickly the status bubble should expand, in seconds.
-const CGFloat kExpansionDuration = 0.125;
+// How quickly the status bubble should expand.
+const CGFloat kExpansionDurationSeconds = 0.125;
} // namespace
@interface StatusBubbleAnimationDelegate : NSObject {
@private
- StatusBubbleMac* statusBubble_; // weak; owns us indirectly
+ base::mac::ScopedBlock<void (^)(void)> completionHandler_;
}
-- (id)initWithStatusBubble:(StatusBubbleMac*)statusBubble;
-
-// Invalidates this object so that no further calls will be made to
-// statusBubble_. This should be called when statusBubble_ is released, to
-// prevent attempts to call into the released object.
-- (void)invalidate;
+- (id)initWithCompletionHandler:(void (^)(void))completionHandler;
// CAAnimation delegate method
- (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished;
@implementation StatusBubbleAnimationDelegate
-- (id)initWithStatusBubble:(StatusBubbleMac*)statusBubble {
+- (id)initWithCompletionHandler:(void (^)(void))completionHandler {
if ((self = [super init])) {
- statusBubble_ = statusBubble;
+ completionHandler_.reset(completionHandler, base::scoped_policy::RETAIN);
}
return self;
}
-- (void)invalidate {
- statusBubble_ = NULL;
+- (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished {
+ completionHandler_.get()();
}
-- (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished {
- if (statusBubble_)
- statusBubble_->AnimationDidStop(animation, finished ? true : false);
+@end
+
+@interface StatusBubbleWindow : NSWindow {
+ @private
+ void (^completionHandler_)(void);
+}
+
+- (id)animationForKey:(NSString *)key;
+- (void)runAnimationGroup:(void (^)(NSAnimationContext *context))changes
+ completionHandler:(void (^)(void))completionHandler;
+@end
+
+@implementation StatusBubbleWindow
+
+- (id)animationForKey:(NSString *)key {
+ CAAnimation* animation = [super animationForKey:key];
+ // If completionHandler_ isn't nil, then this is the first of (potentially)
+ // multiple animations in a grouping; give it the completion handler. If
+ // completionHandler_ is nil, then some other animation was tagged with the
+ // completion handler.
+ if (completionHandler_) {
+ DCHECK(![NSAnimationContext respondsToSelector:
+ @selector(runAnimationGroup:completionHandler:)]);
+ StatusBubbleAnimationDelegate* animation_delegate =
+ [[StatusBubbleAnimationDelegate alloc]
+ initWithCompletionHandler:completionHandler_];
+ [animation setDelegate:animation_delegate];
+ completionHandler_ = nil;
+ }
+ return animation;
+}
+
+- (void)runAnimationGroup:(void (^)(NSAnimationContext *context))changes
+ completionHandler:(void (^)(void))completionHandler {
+ if ([NSAnimationContext respondsToSelector:
+ @selector(runAnimationGroup:completionHandler:)]) {
+ [NSAnimationContext runAnimationGroup:changes
+ completionHandler:completionHandler];
+ } else {
+ // Mac OS 10.6 does not have completion handler callbacks at the Cocoa
+ // level, only at the CoreAnimation level. So intercept calls made to
+ // -animationForKey: and tag one of the animations with a delegate that will
+ // execute the completion handler.
+ completionHandler_ = completionHandler;
+ [NSAnimationContext beginGrouping];
+ changes([NSAnimationContext currentContext]);
+ // At this point, -animationForKey should have been called by CoreAnimation
+ // to set up the animation to run. Verify this.
+ DCHECK(completionHandler_ == nil);
+ [NSAnimationContext endGrouping];
+ }
}
@end
StatusBubbleMac::StatusBubbleMac(NSWindow* parent, id delegate)
: timer_factory_(this),
expand_timer_factory_(this),
+ completion_handler_factory_(this),
parent_(parent),
delegate_(delegate),
window_(nil),
Hide();
- [[[window_ animationForKey:kFadeAnimationKey] delegate] invalidate];
+ completion_handler_factory_.InvalidateWeakPtrs();
Detach();
[window_ release];
window_ = nil;
base::string16 original_url_text = net::FormatUrl(url, languages);
base::string16 status =
- gfx::ElideUrl(url, font_list_chr, text_width, languages);
+ ElideUrl(url, font_list_chr, text_width, languages);
SetText(status, true);
base::MessageLoop::current()->PostDelayedTask(FROM_HERE,
base::Bind(&StatusBubbleMac::ExpandBubble,
expand_timer_factory_.GetWeakPtr()),
- base::TimeDelta::FromMilliseconds(kExpandHoverDelay));
+ base::TimeDelta::FromMilliseconds(kExpandHoverDelayMS));
}
}
// An animation is in progress. Cancel it by starting a new animation.
// Use kMinimumTimeInterval to set the opacity as rapidly as possible.
fade_out = true;
- [NSAnimationContext beginGrouping];
- [[NSAnimationContext currentContext] setDuration:kMinimumTimeInterval];
- [[window_ animator] setAlphaValue:0.0];
- [NSAnimationContext endGrouping];
+ AnimateWindowAlpha(0.0, kMinimumTimeInterval);
}
}
void StatusBubbleMac::Create() {
DCHECK(!window_);
- window_ = [[NSWindow alloc] initWithContentRect:ui::kWindowSizeDeterminedLater
- styleMask:NSBorderlessWindowMask
- backing:NSBackingStoreBuffered
- defer:YES];
+ window_ = [[StatusBubbleWindow alloc]
+ initWithContentRect:ui::kWindowSizeDeterminedLater
+ styleMask:NSBorderlessWindowMask
+ backing:NSBackingStoreBuffered
+ defer:YES];
[window_ setMovableByWindowBackground:NO];
[window_ setBackgroundColor:[NSColor clearColor]];
[window_ setLevel:NSNormalWindowLevel];
// TODO(dtseng): Ignore until we provide NSAccessibility support.
[window_ accessibilitySetOverrideValue:NSAccessibilityUnknownRole
- forAttribute:NSAccessibilityRoleAttribute];
-
- // Set a delegate for the fade-in and fade-out transitions to be notified
- // when fades are complete. The ownership model is for window_ to own
- // animation_dictionary, which owns animation, which owns
- // animation_delegate.
- CAAnimation* animation = [[window_ animationForKey:kFadeAnimationKey] copy];
- [animation autorelease];
- StatusBubbleAnimationDelegate* animation_delegate =
- [[StatusBubbleAnimationDelegate alloc] initWithStatusBubble:this];
- [animation_delegate autorelease];
- [animation setDelegate:animation_delegate];
- NSMutableDictionary* animation_dictionary =
- [NSMutableDictionary dictionaryWithDictionary:[window_ animations]];
- [animation_dictionary setObject:animation forKey:kFadeAnimationKey];
- [window_ setAnimations:animation_dictionary];
+ forAttribute:NSAccessibilityRoleAttribute];
[view setCornerFlags:kRoundedTopRightCorner];
MouseMoved(gfx::Point(), false);
void StatusBubbleMac::Detach() {
DCHECK(is_attached());
- // Magic setFrame: See crbug.com/58506, and codereview.chromium.org/3564021
+ // Magic setFrame: See http://crbug.com/58506 and http://crrev.com/3564021 .
[window_ setFrame:CalculateWindowFrame(/*expand=*/false) display:NO];
[parent_ removeChildWindow:window_]; // See crbug.com/28107 ...
[window_ orderOut:nil]; // ... and crbug.com/29054.
[[window_ contentView] setThemeProvider:nil];
}
-void StatusBubbleMac::AnimationDidStop(CAAnimation* animation, bool finished) {
+void StatusBubbleMac::AnimationDidStop() {
DCHECK([NSThread isMainThread]);
DCHECK(state_ == kBubbleShowingFadeIn || state_ == kBubbleHidingFadeOut);
DCHECK(is_attached());
- if (finished) {
- // Because of the mechanism used to interrupt animations, this is never
- // actually called with finished set to false. If animations ever become
- // directly interruptible, the check will ensure that state_ remains
- // properly synchronized.
- if (state_ == kBubbleShowingFadeIn) {
- DCHECK_EQ([[window_ animator] alphaValue], kBubbleOpacity);
- SetState(kBubbleShown);
- } else {
- DCHECK_EQ([[window_ animator] alphaValue], 0.0);
- SetState(kBubbleHidden);
- }
+ if (state_ == kBubbleShowingFadeIn) {
+ DCHECK_EQ([[window_ animator] alphaValue], kBubbleOpacity);
+ SetState(kBubbleShown);
+ } else {
+ DCHECK_EQ([[window_ animator] alphaValue], 0.0);
+ SetState(kBubbleHidden);
}
}
// Also, doing it this way instead of detaching the window avoids bugs with
// Spaces and Cmd-`. See http://crbug.com/31821 and http://crbug.com/61629.
NSRect frame = [window_ frame];
- frame.size = NSMakeSize(1, 1);
+ frame.size = ui::kWindowSizeDeterminedLater.size;
[window_ setFrame:frame display:YES];
}
if (duration == 0.0)
duration = kMinimumTimeInterval;
- // This will cancel an in-progress transition and replace it with this fade.
- [NSAnimationContext beginGrouping];
- // Don't use the GTM additon for the "Steve" slowdown because this can happen
- // async from user actions and the effects could be a surprise.
- [[NSAnimationContext currentContext] setDuration:duration];
- [[window_ animator] setAlphaValue:opacity];
- [NSAnimationContext endGrouping];
+ // Cancel an in-progress transition and replace it with this fade.
+ AnimateWindowAlpha(opacity, duration);
+}
+
+void StatusBubbleMac::AnimateWindowAlpha(CGFloat alpha,
+ NSTimeInterval duration) {
+ completion_handler_factory_.InvalidateWeakPtrs();
+ base::WeakPtr<StatusBubbleMac> weak_ptr(
+ completion_handler_factory_.GetWeakPtr());
+ [window_
+ runAnimationGroup:^(NSAnimationContext* context) {
+ [context setDuration:duration];
+ [[window_ animator] setAlphaValue:alpha];
+ }
+ completionHandler:^{
+ if (weak_ptr)
+ weak_ptr->AnimationDidStop();
+ }];
}
void StatusBubbleMac::StartTimer(int64 delay_ms) {
if (state_ == kBubbleHidden) {
// Arrange to begin fading in after a delay.
SetState(kBubbleShowingTimer);
- StartTimer(kShowDelayMilliseconds);
+ StartTimer(kShowDelayMS);
} else if (state_ == kBubbleHidingFadeOut) {
// Cancel the fade-out in progress and replace it with a fade in.
SetState(kBubbleShowingFadeIn);
// this point means that there is a new request to show something. Start
// over again by resetting the timer, effectively invalidating the earlier
// request.
- StartTimer(kShowDelayMilliseconds);
+ StartTimer(kShowDelayMS);
}
// If the state is kBubbleShown or kBubbleShowingFadeIn, leave everything
if (state_ == kBubbleShown) {
// Arrange to begin fading out after a delay.
SetState(kBubbleHidingTimer);
- StartTimer(kHideDelayMilliseconds);
+ StartTimer(kHideDelayMS);
} else if (state_ == kBubbleShowingFadeIn) {
// Cancel the fade-in in progress and replace it with a fade out.
SetState(kBubbleHidingFadeOut);
NSFont* font = [[window_ contentView] font];
gfx::FontList font_list_chr(
gfx::Font(base::SysNSStringToUTF8([font fontName]), [font pointSize]));
- base::string16 expanded_url = gfx::ElideUrl(
+ base::string16 expanded_url = ElideUrl(
url_, font_list_chr, max_bubble_width, languages_);
// Scale width from gfx::Font in view coordinates to window coordinates.
}
[NSAnimationContext beginGrouping];
- [[NSAnimationContext currentContext] setDuration:kExpansionDuration];
+ [[NSAnimationContext currentContext] setDuration:kExpansionDurationSeconds];
[[window_ animator] setFrame:actual_window_frame display:YES];
[NSAnimationContext endGrouping];
}