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; |