diff options
author | mark@chromium.org <mark@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-10-12 22:27:56 +0000 |
---|---|---|
committer | mark@chromium.org <mark@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-10-12 22:27:56 +0000 |
commit | 7e9d83720b3de5ad50219cfff016535132db642f (patch) | |
tree | 867fa6a6eb1b11f1ce798d1abec9458c36db0e63 | |
parent | 76d7c661ed7a28ba703c3c4727d84d28f4320d1f (diff) | |
download | chromium_src-7e9d83720b3de5ad50219cfff016535132db642f.zip chromium_src-7e9d83720b3de5ad50219cfff016535132db642f.tar.gz chromium_src-7e9d83720b3de5ad50219cfff016535132db642f.tar.bz2 |
Status bubbles should wait before showing and hiding, and should fade in and
out on the Mac.
This fixes the fades, which were actually written but unfortunately not
working due to a silly bug. It also adds the delays, cleaning up a TODO.
It fixes some questionable logic ("hide by calling Hide() and then FadeIn()").
Finally, it allows better reuse of the status bubble NSWindow, and fixes a bug
that prevented the status bubble window from properly attaching to its parent
window if the parent was offscreen at attachment time. Unit tests for the new
behavior are included, and as a bonus, I've added better testing for some
existing behavior.
See http://dev.chromium.org/user-experience/status-bubble for the design of
the status bubble. Also, consult chrome/browser/views/status_bubble_views.cc
for the implementation used on Windows.
BUG=24495
TEST=Mouse over some links
Review URL: http://codereview.chromium.org/269045
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@28749 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r-- | chrome/browser/cocoa/status_bubble_mac.h | 76 | ||||
-rw-r--r-- | chrome/browser/cocoa/status_bubble_mac.mm | 339 | ||||
-rw-r--r-- | chrome/browser/cocoa/status_bubble_mac_unittest.mm | 306 |
3 files changed, 665 insertions, 56 deletions
diff --git a/chrome/browser/cocoa/status_bubble_mac.h b/chrome/browser/cocoa/status_bubble_mac.h index 9dc177d..f110b1d 100644 --- a/chrome/browser/cocoa/status_bubble_mac.h +++ b/chrome/browser/cocoa/status_bubble_mac.h @@ -8,7 +8,9 @@ #include <string> #import <Cocoa/Cocoa.h> +#import <QuartzCore/QuartzCore.h> +#include "base/task.h" #include "chrome/browser/status_bubble.h" class GURL; @@ -16,6 +18,17 @@ class StatusBubbleMacTest; class StatusBubbleMac : public StatusBubble { public: + // The various states that a status bubble may be in. Public for delegate + // access (for testing). + enum StatusBubbleState { + kBubbleHidden, // Fully hidden + kBubbleShowingTimer, // Waiting to fade in + kBubbleShowingFadeIn, // In a fade-in transition + kBubbleShown, // Fully visible + kBubbleHidingTimer, // Waiting to fade out + kBubbleHidingFadeOut // In a fade-out transition + }; + StatusBubbleMac(NSWindow* parent, id delegate); virtual ~StatusBubbleMac(); @@ -31,17 +44,53 @@ class StatusBubbleMac : public StatusBubble { // exist. void UpdateSizeAndPosition(); + // Delegate method called when a fade-in or fade-out transition has + // completed. This is public so that it may be visible to the CAAnimation + // delegate, which is an Objective-C object. + void AnimationDidStop(CAAnimation* animation, bool finished); + private: friend class StatusBubbleMacTest; - void SetStatus(NSString* status, bool is_url); + // Setter for state_. Use this instead of writing to state_ directly so + // that state changes can be observed by unit tests. + void SetState(StatusBubbleState state); + + // Sets the bubble text for SetStatus and SetURL. + void SetText(const std::wstring& text, bool is_url); // Construct the window/widget if it does not already exist. (Safe to call if // it does.) void Create(); - void FadeIn(); - void FadeOut(); + // Attaches the status bubble window to its parent window. + void Attach(); + + // Begins fading the status bubble window in or out depending on the value + // of |show|. This must be called from the appropriate fade state, + // kBubbleShowingFadeIn or kBubbleHidingFadeOut, or from the appropriate + // fully-shown/hidden state, kBubbleShown or kBubbleHidden. This may be + // called at any point during a fade-in or fade-out; it is even possible to + // reverse a transition before it has completed. + void Fade(bool show); + + // One-shot timer operations to manage the delays associated with the + // kBubbleShowingTimer and kBubbleHidingTimer states. StartTimer and + // TimerFired must be called from one of these states. StartTimer may be + // called while the timer is still running; in that case, the timer will be + // reset. CancelTimer may be called from any state. + void StartTimer(int64 time_ms); + void CancelTimer(); + void TimerFired(); + + // Begin the process of showing or hiding the status bubble. These may be + // called from any state, and will take the appropriate action to initiate + // any state changes that may be needed. + void StartShowing(); + void StartHiding(); + + // The timer factory used for show and hide delay timers. + ScopedRunnableMethodFactory<StatusBubbleMac> timer_factory_; // Calculate the appropriate frame for the status bubble window. NSRect CalculateWindowFrame(); @@ -61,15 +110,26 @@ class StatusBubbleMac : public StatusBubble { // The url we want to display when there is no status text to display. NSString* url_text_; - // How vertically offset the bubble is from its root position. - int offset_; + // The status bubble's current state. Do not write to this field directly; + // use SetState(). + StatusBubbleState state_; + + // True if operations are to be performed immediately rather than waiting + // for delays and transitions. Normally false, this should only be set to + // true for testing. + bool immediate_; + + DISALLOW_COPY_AND_ASSIGN(StatusBubbleMac); }; -// Delegate interface that allows the StatusBubble to query its delegate about -// the vertical offset (if any) that should be applied to the StatusBubble's -// position. +// Delegate interface @interface NSObject(StatusBubbleDelegate) +// Called to query the delegate about the vertical offset (if any) that should +// be applied to the StatusBubble's position. - (float)verticalOffsetForStatusBubble; + +// Called from SetState to notify the delegate of state changes. +- (void)statusBubbleWillEnterState:(StatusBubbleMac::StatusBubbleState)state; @end #endif // #ifndef CHROME_BROWSER_COCOA_STATUS_BUBBLE_MAC_H_ 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; diff --git a/chrome/browser/cocoa/status_bubble_mac_unittest.mm b/chrome/browser/cocoa/status_bubble_mac_unittest.mm index 8d5681b..4e30d2f 100644 --- a/chrome/browser/cocoa/status_bubble_mac_unittest.mm +++ b/chrome/browser/cocoa/status_bubble_mac_unittest.mm @@ -6,14 +6,16 @@ #include "base/scoped_nsobject.h" #include "base/scoped_ptr.h" +#import "chrome/browser/cocoa/bubble_view.h" +#import "chrome/browser/cocoa/browser_test_helper.h" #import "chrome/browser/cocoa/cocoa_test_helper.h" -#include "chrome/browser/cocoa/status_bubble_mac.h" +#import "chrome/browser/cocoa/status_bubble_mac.h" #include "googleurl/src/gurl.h" #include "testing/gtest/include/gtest/gtest.h" #include "testing/platform_test.h" #import "third_party/GTM/AppKit/GTMTheme.h" -@interface StatusBubbleMacTestWindowDelegate : NSObject <GTMThemeDelegate>; +@interface StatusBubbleMacTestWindowDelegate : NSObject <GTMThemeDelegate> @end @implementation StatusBubbleMacTestWindowDelegate - (GTMTheme*)gtm_themeForWindow:(NSWindow*)window { @@ -25,17 +27,47 @@ } @end +// The test delegate records all of the status bubble object's state +// transitions. +@interface StatusBubbleMacTestDelegate : NSObject { + @private + std::vector<StatusBubbleMac::StatusBubbleState> states_; +} +- (void)statusBubbleWillEnterState:(StatusBubbleMac::StatusBubbleState)state; +@end +@implementation StatusBubbleMacTestDelegate +- (void)statusBubbleWillEnterState:(StatusBubbleMac::StatusBubbleState)state { + states_.push_back(state); +} +- (std::vector<StatusBubbleMac::StatusBubbleState>*)states { + return &states_; +} +@end + class StatusBubbleMacTest : public PlatformTest { public: StatusBubbleMacTest() { NSWindow* window = cocoa_helper_.window(); - bubble_.reset(new StatusBubbleMac(window, nil)); + EXPECT_TRUE(window); + delegate_.reset([[StatusBubbleMacTestDelegate alloc] init]); + EXPECT_TRUE(delegate_.get()); + bubble_.reset(new StatusBubbleMac(window, delegate_)); EXPECT_TRUE(bubble_.get()); + + // Turn off delays and transitions for test mode. This doesn't just speed + // things along, it's actually required to get StatusBubbleMac to behave + // synchronously, because the tests here don't know how to wait for + // results. This allows these tests to be much more complete with a + // minimal loss of coverage and without any heinous rearchitecting. + bubble_->immediate_ = true; + EXPECT_FALSE(bubble_->window_); // lazily creates window } bool IsVisible() { - return [bubble_->window_ isVisible] ? true: false; + if (![bubble_->window_ isVisible]) + return false; + return [bubble_->window_ alphaValue] > 0.0; } NSString* GetText() { return bubble_->status_text_; @@ -43,13 +75,31 @@ class StatusBubbleMacTest : public PlatformTest { NSString* GetURLText() { return bubble_->url_text_; } + NSString* GetBubbleViewText() { + BubbleView* bubbleView = [bubble_->window_ contentView]; + return [bubbleView content]; + } NSWindow* GetWindow() { return bubble_->window_; } NSWindow* GetParent() { return bubble_->parent_; } + StatusBubbleMac::StatusBubbleState GetState() { + return bubble_->state_; + } + void SetState(StatusBubbleMac::StatusBubbleState state) { + bubble_->SetState(state); + } + std::vector<StatusBubbleMac::StatusBubbleState>* States() { + return [delegate_ states]; + } + StatusBubbleMac::StatusBubbleState StateAt(int index) { + return (*States())[index]; + } CocoaTestHelper cocoa_helper_; // Inits Cocoa, creates window, etc... + BrowserTestHelper browser_helper_; + scoped_nsobject<StatusBubbleMacTestDelegate> delegate_; scoped_ptr<StatusBubbleMac> bubble_; }; @@ -74,7 +124,6 @@ TEST_F(StatusBubbleMacTest, SetStatus) { // Hide it bubble_->SetStatus(L""); EXPECT_FALSE(IsVisible()); - EXPECT_FALSE(GetText()); } TEST_F(StatusBubbleMacTest, SetURL) { @@ -106,6 +155,253 @@ TEST_F(StatusBubbleMacTest, Hides) { EXPECT_FALSE(IsVisible()); } +// Test the "main"/"backup" behavior in StatusBubbleMac::SetText(). +TEST_F(StatusBubbleMacTest, SetStatusAndURL) { + EXPECT_FALSE(IsVisible()); + bubble_->SetStatus(L"Status"); + EXPECT_TRUE(IsVisible()); + EXPECT_TRUE([GetBubbleViewText() isEqualToString:@"Status"]); + bubble_->SetURL(GURL("http://www.nytimes.com/"), L""); + EXPECT_TRUE(IsVisible()); + EXPECT_TRUE([GetBubbleViewText() isEqualToString:@"http://www.nytimes.com/"]); + bubble_->SetURL(GURL(), L""); + EXPECT_TRUE(IsVisible()); + EXPECT_TRUE([GetBubbleViewText() isEqualToString:@"Status"]); + bubble_->SetStatus(L""); + EXPECT_FALSE(IsVisible()); + bubble_->SetURL(GURL("http://www.nytimes.com/"), L""); + EXPECT_TRUE(IsVisible()); + EXPECT_TRUE([GetBubbleViewText() isEqualToString:@"http://www.nytimes.com/"]); + bubble_->SetStatus(L"Status"); + EXPECT_TRUE(IsVisible()); + EXPECT_TRUE([GetBubbleViewText() isEqualToString:@"Status"]); + bubble_->SetStatus(L""); + EXPECT_TRUE(IsVisible()); + EXPECT_TRUE([GetBubbleViewText() isEqualToString:@"http://www.nytimes.com/"]); + bubble_->SetURL(GURL(), L""); + EXPECT_FALSE(IsVisible()); +} + +// Test that the status bubble goes through the correct delay and fade states. +// The delay and fade duration are simulated and not actually experienced +// during the test because StatusBubbleMacTest sets immediate_ mode. +TEST_F(StatusBubbleMacTest, StateTransitions) { + // First, some sanity + + EXPECT_FALSE(IsVisible()); + EXPECT_EQ(StatusBubbleMac::kBubbleHidden, GetState()); + + States()->clear(); + EXPECT_TRUE(States()->empty()); + + bubble_->SetStatus(L""); + EXPECT_FALSE(IsVisible()); + EXPECT_EQ(StatusBubbleMac::kBubbleHidden, GetState()); + EXPECT_TRUE(States()->empty()); // no change from initial kBubbleHidden state + + // Next, a few ordinary cases + + // Test StartShowing from kBubbleHidden + bubble_->SetStatus(L"Status"); + EXPECT_TRUE(IsVisible()); + // Check GetState before checking States to make sure that all state + // transitions have been flushed to States. + EXPECT_EQ(StatusBubbleMac::kBubbleShown, GetState()); + EXPECT_EQ(3u, States()->size()); + EXPECT_EQ(StatusBubbleMac::kBubbleShowingTimer, StateAt(0)); + EXPECT_EQ(StatusBubbleMac::kBubbleShowingFadeIn, StateAt(1)); + EXPECT_EQ(StatusBubbleMac::kBubbleShown, StateAt(2)); + + // Test StartShowing from kBubbleShown with the same message + States()->clear(); + bubble_->SetStatus(L"Status"); + EXPECT_TRUE(IsVisible()); + EXPECT_EQ(StatusBubbleMac::kBubbleShown, GetState()); + EXPECT_TRUE(States()->empty()); + + // Test StartShowing from kBubbleShown with a different message + bubble_->SetStatus(L"New Status"); + EXPECT_TRUE(IsVisible()); + EXPECT_EQ(StatusBubbleMac::kBubbleShown, GetState()); + EXPECT_TRUE(States()->empty()); + + // Test StartHiding from kBubbleShown + bubble_->SetStatus(L""); + EXPECT_FALSE(IsVisible()); + // Check GetState before checking States to make sure that all state + // transitions have been flushed to States. + EXPECT_EQ(StatusBubbleMac::kBubbleHidden, GetState()); + EXPECT_EQ(3u, States()->size()); + EXPECT_EQ(StatusBubbleMac::kBubbleHidingTimer, StateAt(0)); + EXPECT_EQ(StatusBubbleMac::kBubbleHidingFadeOut, StateAt(1)); + EXPECT_EQ(StatusBubbleMac::kBubbleHidden, StateAt(2)); + + // Test StartHiding from kBubbleHidden + States()->clear(); + bubble_->SetStatus(L""); + EXPECT_FALSE(IsVisible()); + EXPECT_EQ(StatusBubbleMac::kBubbleHidden, GetState()); + EXPECT_TRUE(States()->empty()); + + // Now, the edge cases + + // Test StartShowing from kBubbleShowingTimer + bubble_->SetStatus(L"Status"); + SetState(StatusBubbleMac::kBubbleShowingTimer); + [GetWindow() setAlphaValue:0.0]; + States()->clear(); + EXPECT_TRUE(States()->empty()); + bubble_->SetStatus(L"Status"); + EXPECT_EQ(StatusBubbleMac::kBubbleShown, GetState()); + EXPECT_EQ(2u, States()->size()); + EXPECT_EQ(StatusBubbleMac::kBubbleShowingFadeIn, StateAt(0)); + EXPECT_EQ(StatusBubbleMac::kBubbleShown, StateAt(1)); + + // Test StartShowing from kBubbleShowingFadeIn + bubble_->SetStatus(L"Status"); + SetState(StatusBubbleMac::kBubbleShowingFadeIn); + [GetWindow() setAlphaValue:0.5]; + States()->clear(); + EXPECT_TRUE(States()->empty()); + bubble_->SetStatus(L"Status"); + // The actual state values can't be tested in immediate_ mode because + // the window wasn't actually fading in. Without immediate_ mode, + // expect kBubbleShown. + bubble_->SetStatus(L""); // Go back to a deterministic state. + + // Test StartShowing from kBubbleHidingTimer + bubble_->SetStatus(L""); + SetState(StatusBubbleMac::kBubbleHidingTimer); + [GetWindow() setAlphaValue:1.0]; + States()->clear(); + EXPECT_TRUE(States()->empty()); + bubble_->SetStatus(L"Status"); + EXPECT_EQ(StatusBubbleMac::kBubbleShown, GetState()); + EXPECT_EQ(1u, States()->size()); + EXPECT_EQ(StatusBubbleMac::kBubbleShown, StateAt(0)); + + // Test StartShowing from kBubbleHidingFadeOut + bubble_->SetStatus(L""); + SetState(StatusBubbleMac::kBubbleHidingFadeOut); + [GetWindow() setAlphaValue:0.5]; + States()->clear(); + EXPECT_TRUE(States()->empty()); + bubble_->SetStatus(L"Status"); + EXPECT_EQ(StatusBubbleMac::kBubbleShown, GetState()); + EXPECT_EQ(2u, States()->size()); + EXPECT_EQ(StatusBubbleMac::kBubbleShowingFadeIn, StateAt(0)); + EXPECT_EQ(StatusBubbleMac::kBubbleShown, StateAt(1)); + + // Test StartHiding from kBubbleShowingTimer + bubble_->SetStatus(L"Status"); + SetState(StatusBubbleMac::kBubbleShowingTimer); + [GetWindow() setAlphaValue:0.0]; + States()->clear(); + EXPECT_TRUE(States()->empty()); + bubble_->SetStatus(L""); + EXPECT_EQ(StatusBubbleMac::kBubbleHidden, GetState()); + EXPECT_EQ(1u, States()->size()); + EXPECT_EQ(StatusBubbleMac::kBubbleHidden, StateAt(0)); + + // Test StartHiding from kBubbleShowingFadeIn + bubble_->SetStatus(L"Status"); + SetState(StatusBubbleMac::kBubbleShowingFadeIn); + [GetWindow() setAlphaValue:0.5]; + States()->clear(); + EXPECT_TRUE(States()->empty()); + bubble_->SetStatus(L""); + EXPECT_EQ(StatusBubbleMac::kBubbleHidden, GetState()); + EXPECT_EQ(2u, States()->size()); + EXPECT_EQ(StatusBubbleMac::kBubbleHidingFadeOut, StateAt(0)); + EXPECT_EQ(StatusBubbleMac::kBubbleHidden, StateAt(1)); + + // Test StartHiding from kBubbleHidingTimer + bubble_->SetStatus(L""); + SetState(StatusBubbleMac::kBubbleHidingTimer); + [GetWindow() setAlphaValue:1.0]; + States()->clear(); + EXPECT_TRUE(States()->empty()); + bubble_->SetStatus(L""); + // The actual state values can't be tested in immediate_ mode because + // the timer wasn't actually running. Without immediate_ mode, expect + // kBubbleHidingFadeOut and kBubbleHidden. + bubble_->SetStatus(L"Status"); // Go back to a deterministic state. + + // Test StartHiding from kBubbleHidingFadeOut + bubble_->SetStatus(L""); + SetState(StatusBubbleMac::kBubbleHidingFadeOut); + [GetWindow() setAlphaValue:0.5]; + States()->clear(); + EXPECT_TRUE(States()->empty()); + bubble_->SetStatus(L""); + // The actual state values can't be tested in immediate_ mode because + // the window wasn't actually fading out. Without immediate_ mode, expect + // kBubbleHidden. + bubble_->SetStatus(L"Status"); // Go back to a deterministic state. + + // Test Hide from kBubbleHidden + bubble_->SetStatus(L""); + EXPECT_EQ(StatusBubbleMac::kBubbleHidden, GetState()); + States()->clear(); + EXPECT_TRUE(States()->empty()); + bubble_->Hide(); + EXPECT_EQ(StatusBubbleMac::kBubbleHidden, GetState()); + EXPECT_TRUE(States()->empty()); + + // Test Hide from kBubbleShowingTimer + bubble_->SetStatus(L"Status"); + SetState(StatusBubbleMac::kBubbleShowingTimer); + [GetWindow() setAlphaValue:0.0]; + States()->clear(); + EXPECT_TRUE(States()->empty()); + bubble_->Hide(); + EXPECT_EQ(StatusBubbleMac::kBubbleHidden, GetState()); + EXPECT_EQ(1u, States()->size()); + EXPECT_EQ(StatusBubbleMac::kBubbleHidden, StateAt(0)); + + // Test Hide from kBubbleShowingFadeIn + bubble_->SetStatus(L"Status"); + SetState(StatusBubbleMac::kBubbleShowingFadeIn); + [GetWindow() setAlphaValue:0.5]; + States()->clear(); + EXPECT_TRUE(States()->empty()); + bubble_->Hide(); + EXPECT_EQ(StatusBubbleMac::kBubbleHidden, GetState()); + EXPECT_EQ(2u, States()->size()); + EXPECT_EQ(StatusBubbleMac::kBubbleHidingFadeOut, StateAt(0)); + EXPECT_EQ(StatusBubbleMac::kBubbleHidden, StateAt(1)); + + // Test Hide from kBubbleShown + bubble_->SetStatus(L"Status"); + States()->clear(); + EXPECT_TRUE(States()->empty()); + bubble_->Hide(); + EXPECT_EQ(StatusBubbleMac::kBubbleHidden, GetState()); + EXPECT_EQ(1u, States()->size()); + EXPECT_EQ(StatusBubbleMac::kBubbleHidden, StateAt(0)); + + // Test Hide from kBubbleHidingTimer + bubble_->SetStatus(L"Status"); + SetState(StatusBubbleMac::kBubbleHidingTimer); + States()->clear(); + EXPECT_TRUE(States()->empty()); + bubble_->Hide(); + EXPECT_EQ(StatusBubbleMac::kBubbleHidden, GetState()); + EXPECT_EQ(1u, States()->size()); + EXPECT_EQ(StatusBubbleMac::kBubbleHidden, StateAt(0)); + + // Test Hide from kBubbleHidingFadeOut + bubble_->SetStatus(L"Status"); + SetState(StatusBubbleMac::kBubbleHidingFadeOut); + [GetWindow() setAlphaValue:0.5]; + States()->clear(); + EXPECT_TRUE(States()->empty()); + bubble_->Hide(); + EXPECT_EQ(StatusBubbleMac::kBubbleHidden, GetState()); + EXPECT_EQ(1u, States()->size()); + EXPECT_EQ(StatusBubbleMac::kBubbleHidden, StateAt(0)); +} + TEST_F(StatusBubbleMacTest, MouseMove) { // TODO(pinkerton): Not sure what to do here since it relies on // [NSEvent currentEvent] and the current mouse location. |