| Index: chrome/browser/cocoa/status_bubble_mac.mm
|
| ===================================================================
|
| --- chrome/browser/cocoa/status_bubble_mac.mm (revision 28701)
|
| +++ chrome/browser/cocoa/status_bubble_mac.mm (working copy)
|
| @@ -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 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 @@
|
| [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,31 +152,53 @@
|
| 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 release];
|
| - *main = [status retain];
|
| - if ([*main length] > 0) {
|
| + [*main autorelease];
|
| + *main = [text_ns retain];
|
| +
|
| + 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();
|
|
|
| - if (window_) {
|
| - [parent_ removeChildWindow:window_];
|
| - [window_ release];
|
| - window_ = nil;
|
| + 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 (!fade_out) {
|
| + // No animation is in progress, so the opacity can be set directly.
|
| + [window_ setAlphaValue:0.0];
|
| + SetState(kBubbleHidden);
|
| + }
|
| +
|
| [status_text_ release];
|
| status_text_ = nil;
|
| [url_text_ release];
|
| @@ -165,10 +255,8 @@
|
| [[window_ contentView] setCornerFlags:kRoundedTopRightCorner];
|
| }
|
|
|
| - offset_ = offset;
|
| window_frame.origin.y -= offset;
|
| } else {
|
| - offset_ = 0;
|
| [[window_ contentView] setCornerFlags:kRoundedTopRightCorner];
|
| }
|
|
|
| @@ -201,29 +289,194 @@
|
| [[BubbleView alloc] initWithFrame:NSZeroRect themeProvider:parent_]);
|
| [window_ setContentView:view];
|
|
|
| - [parent_ addChildWindow:window_ ordered:NSWindowAbove];
|
| + [window_ setAlphaValue:0.0];
|
|
|
| - [window_ setAlphaValue:0.0f];
|
| + // 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];
|
|
|
| - offset_ = 0;
|
| + Attach();
|
| +
|
| [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::FadeOut() {
|
| +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::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;
|
|
|