Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(2173)

Unified Diff: chrome/browser/cocoa/status_bubble_mac.mm

Issue 269045: Mac status bubble delays and fades (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src/
Patch Set: '' Created 11 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « chrome/browser/cocoa/status_bubble_mac.h ('k') | chrome/browser/cocoa/status_bubble_mac_unittest.mm » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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;
« no previous file with comments | « chrome/browser/cocoa/status_bubble_mac.h ('k') | chrome/browser/cocoa/status_bubble_mac_unittest.mm » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698