diff options
Diffstat (limited to 'chrome/browser')
-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. |