diff options
Diffstat (limited to 'chrome/browser/cocoa/status_bubble_mac.mm')
-rw-r--r-- | chrome/browser/cocoa/status_bubble_mac.mm | 339 |
1 files changed, 296 insertions, 43 deletions
diff --git a/chrome/browser/cocoa/status_bubble_mac.mm b/chrome/browser/cocoa/status_bubble_mac.mm index 3eef461..fb954cc 100644 --- a/chrome/browser/cocoa/status_bubble_mac.mm +++ b/chrome/browser/cocoa/status_bubble_mac.mm @@ -4,7 +4,11 @@ #include "chrome/browser/cocoa/status_bubble_mac.h" +#include <limits> + #include "app/gfx/text_elider.h" +#include "base/compiler_specific.h" +#include "base/message_loop.h" #include "base/string_util.h" #include "base/sys_string_conversions.h" #import "chrome/browser/cocoa/bubble_view.h" @@ -16,8 +20,9 @@ namespace { const int kWindowHeight = 18; + // The width of the bubble in relation to the width of the parent window. -const float kWindowWidthPercent = 1.0f/3.0f; +const double kWindowWidthPercent = 1.0 / 3.0; // How close the mouse can get to the infobubble before it starts sliding // off-screen. @@ -25,33 +30,91 @@ const int kMousePadding = 20; const int kTextPadding = 3; -// How long each fade should last for. -const int kShowFadeDuration = 0.120f; -const int kHideFadeDuration = 0.200f; +// The animation key used for fade-in and fade-out transitions. +const NSString* 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; + +// How long each fade should last. +const NSTimeInterval kShowFadeInDurationSeconds = 0.120; +const NSTimeInterval kHideFadeOutDurationSeconds = 0.200; + +// 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(); +} // namespace + +@interface StatusBubbleAnimationDelegate : NSObject { + @private + StatusBubbleMac* statusBubble_; // weak; owns us indirectly } -// TODO(avi): -// - do display delay +- (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; + +// CAAnimation delegate method +- (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished; +@end + +@implementation StatusBubbleAnimationDelegate + +- (id)initWithStatusBubble:(StatusBubbleMac*)statusBubble { + if ((self = [super init])) { + statusBubble_ = statusBubble; + } + + return self; +} + +- (void)invalidate { + statusBubble_ = NULL; +} + +- (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished { + if (statusBubble_) + statusBubble_->AnimationDidStop(animation, finished ? true : false); +} + +@end StatusBubbleMac::StatusBubbleMac(NSWindow* parent, id delegate) - : parent_(parent), + : ALLOW_THIS_IN_INITIALIZER_LIST(timer_factory_(this)), + parent_(parent), delegate_(delegate), window_(nil), status_text_(nil), - url_text_(nil) { + url_text_(nil), + state_(kBubbleHidden), + immediate_(false) { } StatusBubbleMac::~StatusBubbleMac() { Hide(); + + if (window_) { + [[[window_ animationForKey:kFadeAnimationKey] delegate] invalidate]; + [parent_ removeChildWindow:window_]; + [window_ release]; + window_ = nil; + } } void StatusBubbleMac::SetStatus(const std::wstring& status) { Create(); - NSString* status_ns = base::SysWideToNSString(status); - - SetStatus(status_ns, false); + SetText(status, false); } void StatusBubbleMac::SetURL(const GURL& url, const std::wstring& languages) { @@ -67,12 +130,17 @@ void StatusBubbleMac::SetURL(const GURL& url, const std::wstring& languages) { [font pointSize]); std::wstring status = gfx::ElideUrl(url, font_chr, text_width, languages); - NSString* status_ns = base::SysWideToNSString(status); - SetStatus(status_ns, true); + SetText(status, true); } -void StatusBubbleMac::SetStatus(NSString* status, bool is_url) { +void StatusBubbleMac::SetText(const std::wstring& text, bool is_url) { + // The status bubble allows the status and URL strings to be set + // independently. Whichever was set non-empty most recently will be the + // value displayed. When both are empty, the status bubble hides. + + NSString* text_ns = base::SysWideToNSString(text); + NSString** main; NSString** backup; @@ -84,29 +152,51 @@ void StatusBubbleMac::SetStatus(NSString* status, bool is_url) { backup = &url_text_; } - if ([status isEqualToString:*main]) - return; + // Don't return from this function early. It's important to make sure that + // all calls to StartShowing and StartHiding are made, so that all delays + // are observed properly. Specifically, if the state is currently + // kBubbleShowingTimer, the timer will need to be restarted even if + // [text_ns isEqualToString:*main] is true. + + [*main autorelease]; + *main = [text_ns retain]; - [*main release]; - *main = [status retain]; - if ([*main length] > 0) { + bool show = true; + if ([*main length] > 0) [[window_ contentView] setContent:*main]; - } else if ([*backup length] > 0) { + else if ([*backup length] > 0) [[window_ contentView] setContent:*backup]; - } else { - Hide(); - } + else + show = false; - FadeIn(); + if (show) + StartShowing(); + else + StartHiding(); } void StatusBubbleMac::Hide() { - FadeOut(); + CancelTimer(); + + bool fade_out = false; + if (state_ == kBubbleHidingFadeOut || state_ == kBubbleShowingFadeIn) { + SetState(kBubbleHidingFadeOut); + + if (!immediate_) { + // 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]; + } + } - if (window_) { - [parent_ removeChildWindow:window_]; - [window_ release]; - window_ = nil; + if (!fade_out) { + // No animation is in progress, so the opacity can be set directly. + [window_ setAlphaValue:0.0]; + SetState(kBubbleHidden); } [status_text_ release]; @@ -165,10 +255,8 @@ void StatusBubbleMac::MouseMoved() { [[window_ contentView] setCornerFlags:kRoundedTopRightCorner]; } - offset_ = offset; window_frame.origin.y -= offset; } else { - offset_ = 0; [[window_ contentView] setCornerFlags:kRoundedTopRightCorner]; } @@ -201,29 +289,194 @@ void StatusBubbleMac::Create() { [[BubbleView alloc] initWithFrame:NSZeroRect themeProvider:parent_]); [window_ setContentView:view]; - [parent_ addChildWindow:window_ ordered:NSWindowAbove]; - - [window_ setAlphaValue:0.0f]; + [window_ setAlphaValue:0.0]; + + // 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]; + + Attach(); - offset_ = 0; [view setCornerFlags:kRoundedTopRightCorner]; MouseMoved(); } -void StatusBubbleMac::FadeIn() { - [NSAnimationContext beginGrouping]; - [[NSAnimationContext currentContext] setDuration:kShowFadeDuration]; - [[window_ animator] setAlphaValue:1.0f]; - [NSAnimationContext endGrouping]; +void StatusBubbleMac::Attach() { + // If the parent window is offscreen when the child is added, the child will + // never be displayed, even when the parent moves on-screen. This method + // may be called several times during the process of creating or showing a + // status bubble to attach the bubble to its parent window. + if (![window_ parentWindow] && [parent_ isVisible]) + [parent_ addChildWindow:window_ ordered:NSWindowAbove]; +} + +void StatusBubbleMac::AnimationDidStop(CAAnimation* animation, bool finished) { + DCHECK(state_ == kBubbleShowingFadeIn || state_ == kBubbleHidingFadeOut); + + 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); + state_ = kBubbleShown; + } else { + DCHECK_EQ([[window_ animator] alphaValue], 0.0); + state_ = kBubbleHidden; + } + } } -void StatusBubbleMac::FadeOut() { +void StatusBubbleMac::SetState(StatusBubbleState state) { + if (state == state_) + return; + + if ([delegate_ respondsToSelector:@selector(statusBubbleWillEnterState:)]) + [delegate_ statusBubbleWillEnterState:state]; + + state_ = state; +} + +void StatusBubbleMac::Fade(bool show) { + StatusBubbleState fade_state = kBubbleShowingFadeIn; + StatusBubbleState target_state = kBubbleShown; + NSTimeInterval full_duration = kShowFadeInDurationSeconds; + CGFloat opacity = kBubbleOpacity; + + if (!show) { + fade_state = kBubbleHidingFadeOut; + target_state = kBubbleHidden; + full_duration = kHideFadeOutDurationSeconds; + opacity = 0.0; + } + + DCHECK(state_ == fade_state || state_ == target_state); + + if (state_ == target_state) + return; + + Attach(); + + if (immediate_) { + [window_ setAlphaValue:opacity]; + SetState(target_state); + return; + } + + // If an incomplete transition has left the opacity somewhere between 0 and + // kBubbleOpacity, the fade rate is kept constant by shortening the duration. + NSTimeInterval duration = + full_duration * + fabs(opacity - [[window_ animator] alphaValue]) / kBubbleOpacity; + + // 0.0 will not cancel an in-progress animation. + if (duration == 0.0) + duration = kMinimumTimeInterval; + + // This will cancel an in-progress transition and replace it with this fade. [NSAnimationContext beginGrouping]; - [[NSAnimationContext currentContext] setDuration:kHideFadeDuration]; - [[window_ animator] setAlphaValue:0.0f]; + [[NSAnimationContext currentContext] setDuration:duration]; + [[window_ animator] setAlphaValue:opacity]; [NSAnimationContext endGrouping]; } +void StatusBubbleMac::StartTimer(int64 delay_ms) { + DCHECK(state_ == kBubbleShowingTimer || state_ == kBubbleHidingTimer); + + if (immediate_) { + TimerFired(); + return; + } + + // There can only be one running timer. + CancelTimer(); + + MessageLoop::current()->PostDelayedTask( + FROM_HERE, + timer_factory_.NewRunnableMethod(&StatusBubbleMac::TimerFired), + delay_ms); +} + +void StatusBubbleMac::CancelTimer() { + if (!timer_factory_.empty()) + timer_factory_.RevokeAll(); +} + +void StatusBubbleMac::TimerFired() { + DCHECK(state_ == kBubbleShowingTimer || state_ == kBubbleHidingTimer); + + if (state_ == kBubbleShowingTimer) { + SetState(kBubbleShowingFadeIn); + Fade(true); + } else { + SetState(kBubbleHidingFadeOut); + Fade(false); + } +} + +void StatusBubbleMac::StartShowing() { + Attach(); + + if (state_ == kBubbleHidden) { + // Arrange to begin fading in after a delay. + SetState(kBubbleShowingTimer); + StartTimer(kShowDelayMilliseconds); + } else if (state_ == kBubbleHidingFadeOut) { + // Cancel the fade-out in progress and replace it with a fade in. + SetState(kBubbleShowingFadeIn); + Fade(true); + } else if (state_ == kBubbleHidingTimer) { + // The bubble was already shown but was waiting to begin fading out. It's + // given a stay of execution. + SetState(kBubbleShown); + CancelTimer(); + } else if (state_ == kBubbleShowingTimer) { + // The timer was already running but nothing was showing yet. Reaching + // 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); + } + + // If the state is kBubbleShown or kBubbleShowingFadeIn, leave everything + // alone. +} + +void StatusBubbleMac::StartHiding() { + if (state_ == kBubbleShown) { + // Arrange to begin fading out after a delay. + SetState(kBubbleHidingTimer); + StartTimer(kHideDelayMilliseconds); + } else if (state_ == kBubbleShowingFadeIn) { + // Cancel the fade-in in progress and replace it with a fade out. + SetState(kBubbleHidingFadeOut); + Fade(false); + } else if (state_ == kBubbleShowingTimer) { + // The bubble was already hidden but was waiting to begin fading in. Too + // bad, it won't get the opportunity now. + SetState(kBubbleHidden); + CancelTimer(); + } + + // If the state is kBubbleHidden, kBubbleHidingFadeOut, or + // kBubbleHidingTimer, leave everything alone. The timer is not reset as + // with kBubbleShowingTimer in StartShowing() because a subsequent request + // to hide something while one is already in flight does not invalidate the + // earlier request. +} + void StatusBubbleMac::UpdateSizeAndPosition() { if (!window_) return; |