diff options
author | viettrungluu@chromium.org <viettrungluu@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-12-16 22:26:56 +0000 |
---|---|---|
committer | viettrungluu@chromium.org <viettrungluu@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-12-16 22:26:56 +0000 |
commit | 812742f88a7b3b6457f5d5cfcef88ee7848ba764 (patch) | |
tree | c76f4535f41da799ff23ce0737935cc653b9de25 /chrome/browser | |
parent | 33e1fe807e303f9bd31abce0c6e7033a084a269c (diff) | |
download | chromium_src-812742f88a7b3b6457f5d5cfcef88ee7848ba764.zip chromium_src-812742f88a7b3b6457f5d5cfcef88ee7848ba764.tar.gz chromium_src-812742f88a7b3b6457f5d5cfcef88ee7848ba764.tar.bz2 |
Mac: make unselected pinned tabs glow when their title changes.
When an unselected pinned tab changes its title, that tab should briefly "glow" to indicate this (see what Win/Chrome does).
BUG=29261,28154
TEST=Go to <http://www.surflocal.net/Awards/submit/animatedtitle.html>, pin it, and select another tab; the pinned tab should glow (pulsate in this case) nicely. Also make sure that hover effects work as normal. Try this on a number of themes, with multiple tabs, etc.
Review URL: http://codereview.chromium.org/455042
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@34762 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser')
-rw-r--r-- | chrome/browser/cocoa/tab_controller.mm | 7 | ||||
-rw-r--r-- | chrome/browser/cocoa/tab_view.h | 41 | ||||
-rw-r--r-- | chrome/browser/cocoa/tab_view.mm | 186 | ||||
-rw-r--r-- | chrome/browser/cocoa/tab_view_unittest.mm | 28 |
4 files changed, 215 insertions, 47 deletions
diff --git a/chrome/browser/cocoa/tab_controller.mm b/chrome/browser/cocoa/tab_controller.mm index 49ce170..c9469e0 100644 --- a/chrome/browser/cocoa/tab_controller.mm +++ b/chrome/browser/cocoa/tab_controller.mm @@ -105,7 +105,9 @@ class MenuDelegate : public menus::SimpleMenuModel::Delegate { - (void)internalSetSelected:(BOOL)selected { selected_ = selected; TabView* tabView = static_cast<TabView*>([self view]); + DCHECK([tabView isKindOfClass:[TabView class]]); [tabView setState:selected]; + [tabView cancelAlert]; [self updateVisibility]; [self updateTitleColor]; } @@ -149,6 +151,11 @@ class MenuDelegate : public menus::SimpleMenuModel::Delegate { - (void)setTitle:(NSString*)title { [[self view] setToolTip:title]; + if ([self pinned] && ![self selected]) { + TabView* tabView = static_cast<TabView*>([self view]); + DCHECK([tabView isKindOfClass:[TabView class]]); + [tabView startAlert]; + } [super setTitle:title]; } diff --git a/chrome/browser/cocoa/tab_view.h b/chrome/browser/cocoa/tab_view.h index 58383af..b43053e 100644 --- a/chrome/browser/cocoa/tab_view.h +++ b/chrome/browser/cocoa/tab_view.h @@ -11,6 +11,26 @@ #import "chrome/browser/cocoa/background_gradient_view.h" #import "chrome/browser/cocoa/hover_close_button.h" +namespace tabs { + +// Nomenclature: +// Tabs _glow_ under two different circumstances, when they are _hovered_ (by +// the mouse) and when they are _alerted_ (to show that the tab's title has +// changed). + +// The state of alerting (to show a title change on an unselected, pinned tab). +// This is more complicated than a simple on/off since we want to allow the +// alert glow to go through a full rise-hold-fall cycle to avoid flickering (or +// always holding). +enum AlertState { + kAlertNone = 0, // Obj-C initializes to this. + kAlertRising, + kAlertHolding, + kAlertFalling +}; + +} // namespace tabs + @class TabController, TabWindowController; // A view that handles the event tracking (clicking and dragging) for a tab @@ -29,8 +49,16 @@ scoped_nsobject<NSTrackingArea> closeTrackingArea_; BOOL isMouseInside_; // Is the mouse hovering over? - CGFloat hoverAlpha_; // How strong the mouse hover state is. - NSTimeInterval lastHoverUpdate_; // Time the hover value was last updated. + tabs::AlertState alertState_; + + CGFloat hoverAlpha_; // How strong the hover glow is. + NSTimeInterval hoverHoldEndTime_; // When the hover glow will begin dimming. + + CGFloat alertAlpha_; // How strong the alert glow is. + NSTimeInterval alertHoldEndTime_; // When the hover glow will begin dimming. + + NSTimeInterval lastGlowUpdate_; // Time either glow was last updated. + NSPoint hoverPoint_; // Current location of hover in view coords. // All following variables are valid for the duration of a drag. @@ -60,6 +88,7 @@ @property(assign, nonatomic) NSCellStateValue state; @property(assign, nonatomic) CGFloat hoverAlpha; +@property(assign, nonatomic) CGFloat alertAlpha; // Determines if the tab is in the process of animating closed. It may still // be visible on-screen, but should not respond to/initiate any events. Upon @@ -70,6 +99,14 @@ // Enables/Disables tracking regions for the tab. - (void)setTrackingEnabled:(BOOL)enabled; +// Begin showing an "alert" glow (shown to call attention to an unselected +// pinned tab whose title changed). +- (void)startAlert; + +// Stop showing the "alert" glow; this won't immediately wipe out any glow, but +// will make it fade away. +- (void)cancelAlert; + @end #endif // CHROME_BROWSER_COCOA_TAB_VIEW_H_ diff --git a/chrome/browser/cocoa/tab_view.mm b/chrome/browser/cocoa/tab_view.mm index 28dca36..1f6c29a 100644 --- a/chrome/browser/cocoa/tab_view.mm +++ b/chrome/browser/cocoa/tab_view.mm @@ -16,8 +16,18 @@ const CGFloat kInsetMultiplier = 2.0/3.0; const CGFloat kControlPoint1Multiplier = 1.0/3.0; const CGFloat kControlPoint2Multiplier = 3.0/8.0; -const NSTimeInterval kAnimationShowDuration = 0.2; -const NSTimeInterval kAnimationHideDuration = 0.4; +// The amount of time in seconds during which each type of glow increases, holds +// steady, and decreases, respectively. +const NSTimeInterval kHoverShowDuration = 0.2; +const NSTimeInterval kHoverHoldDuration = 0.02; +const NSTimeInterval kHoverHideDuration = 0.4; +const NSTimeInterval kAlertShowDuration = 0.4; +const NSTimeInterval kAlertHoldDuration = 0.4; +const NSTimeInterval kAlertHideDuration = 0.4; + +// The default time interval in seconds between glow updates (when +// increasing/decreasing). +const NSTimeInterval kGlowUpdateInterval = 0.025; const CGFloat kTearDistance = 36.0; const NSTimeInterval kTearDuration = 0.333; @@ -28,10 +38,19 @@ const CGFloat kRapidCloseDist = 2.5; } // namespace +@interface TabView(Private) + +- (void)resetLastGlowUpdateTime; +- (NSTimeInterval)timeElapsedSinceLastGlowUpdate; +- (void)adjustGlowValue; + +@end // TabView(Private) + @implementation TabView @synthesize state = state_; @synthesize hoverAlpha = hoverAlpha_; +@synthesize alertAlpha = alertAlpha_; @synthesize closing = closing_; - (id)initWithFrame:(NSRect)frame { @@ -67,36 +86,10 @@ const CGFloat kRapidCloseDist = 2.5; return YES; } -- (void)adjustHoverValue { - NSTimeInterval thisUpdate = [NSDate timeIntervalSinceReferenceDate]; - - NSTimeInterval elapsed = thisUpdate - lastHoverUpdate_; - - CGFloat opacity = [self hoverAlpha]; - if (isMouseInside_) { - opacity += elapsed / kAnimationShowDuration; - } else { - opacity -= elapsed / kAnimationHideDuration; - } - - if (!isMouseInside_ && opacity < 0) { - opacity = 0; - } else if (isMouseInside_ && opacity > 1) { - opacity = 1; - } else { - [self performSelector:_cmd withObject:nil afterDelay:0.02]; - } - lastHoverUpdate_ = thisUpdate; - [self setHoverAlpha:opacity]; - - [self setNeedsDisplay:YES]; -} - - (void)mouseEntered:(NSEvent*)theEvent { - lastHoverUpdate_ = [NSDate timeIntervalSinceReferenceDate]; isMouseInside_ = YES; - [self adjustHoverValue]; - [self setNeedsDisplay:YES]; + [self resetLastGlowUpdateTime]; + [self adjustGlowValue]; } - (void)mouseMoved:(NSEvent*)theEvent { @@ -106,10 +99,11 @@ const CGFloat kRapidCloseDist = 2.5; } - (void)mouseExited:(NSEvent*)theEvent { - lastHoverUpdate_ = [NSDate timeIntervalSinceReferenceDate]; isMouseInside_ = NO; - [self adjustHoverValue]; - [self setNeedsDisplay:YES]; + hoverHoldEndTime_ = + [NSDate timeIntervalSinceReferenceDate] + kHoverHoldDuration; + [self resetLastGlowUpdateTime]; + [self adjustGlowValue]; } - (void)setTrackingEnabled:(BOOL)enabled { @@ -678,9 +672,14 @@ const CGFloat kRapidCloseDist = 2.5; [context saveGraphicsState]; [path addClip]; - if (selected || hoverAlpha_ > 0) { + // Use the same overlay for both hover and alert glows by combining their + // opacities. Note that h + a - h*a = h + a*(1-h) = a + h*(1-a). + CGFloat hoverAlpha = [self hoverAlpha]; + CGFloat alertAlpha = [self alertAlpha]; + CGFloat glowAlpha = hoverAlpha + alertAlpha - hoverAlpha * alertAlpha; + if (selected || glowAlpha > 0) { // Draw the background. - CGFloat backgroundAlpha = hoverAlpha_ * 0.5; + CGFloat backgroundAlpha = glowAlpha * 0.5; [context saveGraphicsState]; CGContextRef cgContext = (CGContextRef)([context graphicsPort]); @@ -693,13 +692,12 @@ const CGFloat kRapidCloseDist = 2.5; [context restoreGraphicsState]; // Draw a mouse hover gradient for the default themes - if (!selected) { + if (!selected && hoverAlpha > 0) { if (![theme backgroundImageForStyle:GTMThemeStyleTabBarDeselected state:GTMThemeStateActiveWindow]) { scoped_nsobject<NSGradient> glow([NSGradient alloc]); [glow initWithStartingColor:[NSColor colorWithCalibratedWhite:1.0 - alpha:1.0 * - hoverAlpha_] + alpha:1.0 * hoverAlpha] endingColor:[NSColor colorWithCalibratedWhite:1.0 alpha:0.0]]; @@ -724,7 +722,7 @@ const CGFloat kRapidCloseDist = 2.5; [highlightTransform translateXBy:1 yBy:-1]; scoped_nsobject<NSBezierPath> highlightPath([path copy]); [highlightPath transformUsingAffineTransform:highlightTransform]; - [[NSColor colorWithCalibratedWhite:1.0 alpha:0.2 + 0.3 * hoverAlpha_] + [[NSColor colorWithCalibratedWhite:1.0 alpha:0.2 + 0.3 * glowAlpha] setStroke]; [highlightPath stroke]; @@ -777,4 +775,114 @@ const CGFloat kRapidCloseDist = 2.5; } } +- (void)startAlert { + // Do not start a new alert while already alerting or while in a decay cycle. + if (alertState_ == tabs::kAlertNone) { + alertState_ = tabs::kAlertRising; + [self resetLastGlowUpdateTime]; + [self adjustGlowValue]; + } +} + +- (void)cancelAlert { + if (alertState_ != tabs::kAlertNone) { + alertState_ = tabs::kAlertFalling; + alertHoldEndTime_ = + [NSDate timeIntervalSinceReferenceDate] + kGlowUpdateInterval; + [self resetLastGlowUpdateTime]; + [self adjustGlowValue]; + } +} + @end // @implementation TabView + +@implementation TabView(Private) + +- (void)resetLastGlowUpdateTime { + lastGlowUpdate_ = [NSDate timeIntervalSinceReferenceDate]; +} + +- (NSTimeInterval)timeElapsedSinceLastGlowUpdate { + return [NSDate timeIntervalSinceReferenceDate] - lastGlowUpdate_; +} + +- (void)adjustGlowValue { + // A time interval long enough to represent no update. + const NSTimeInterval kNoUpdate = 1000000; + + // Time until next update for either glow. + NSTimeInterval nextUpdate = kNoUpdate; + + NSTimeInterval elapsed = [self timeElapsedSinceLastGlowUpdate]; + NSTimeInterval currentTime = [NSDate timeIntervalSinceReferenceDate]; + + // TODO(viettrungluu): <http://crbug.com/30617> -- split off the stuff below + // into a pure function and add a unit test. + + CGFloat hoverAlpha = [self hoverAlpha]; + if (isMouseInside_) { + // Increase hover glow until it's 1. + if (hoverAlpha < 1) { + hoverAlpha = MIN(hoverAlpha + elapsed / kHoverShowDuration, 1); + [self setHoverAlpha:hoverAlpha]; + nextUpdate = MIN(kGlowUpdateInterval, nextUpdate); + } // Else already 1 (no update needed). + } else { + if (currentTime >= hoverHoldEndTime_) { + // No longer holding, so decrease hover glow until it's 0. + if (hoverAlpha > 0) { + hoverAlpha = MAX(hoverAlpha - elapsed / kHoverHideDuration, 0); + [self setHoverAlpha:hoverAlpha]; + nextUpdate = MIN(kGlowUpdateInterval, nextUpdate); + } // Else already 0 (no update needed). + } else { + // Schedule update for end of hold time. + nextUpdate = MIN(hoverHoldEndTime_ - currentTime, nextUpdate); + } + } + + CGFloat alertAlpha = [self alertAlpha]; + if (alertState_ == tabs::kAlertRising) { + // Increase alert glow until it's 1 ... + alertAlpha = MIN(alertAlpha + elapsed / kAlertShowDuration, 1); + [self setAlertAlpha:alertAlpha]; + + // ... and having reached 1, switch to holding. + if (alertAlpha >= 1) { + alertState_ = tabs::kAlertHolding; + alertHoldEndTime_ = currentTime + kAlertHoldDuration; + nextUpdate = MIN(kAlertHoldDuration, nextUpdate); + } else { + nextUpdate = MIN(kGlowUpdateInterval, nextUpdate); + } + } else if (alertState_ != tabs::kAlertNone) { + if (alertAlpha > 0) { + if (currentTime >= alertHoldEndTime_) { + // Stop holding, then decrease alert glow (until it's 0). + if (alertState_ == tabs::kAlertHolding) { + alertState_ = tabs::kAlertFalling; + nextUpdate = MIN(kGlowUpdateInterval, nextUpdate); + } else { + DCHECK_EQ(tabs::kAlertFalling, alertState_); + alertAlpha = MAX(alertAlpha - elapsed / kAlertHideDuration, 0); + [self setAlertAlpha:alertAlpha]; + nextUpdate = MIN(kGlowUpdateInterval, nextUpdate); + } + } else { + // Schedule update for end of hold time. + nextUpdate = MIN(alertHoldEndTime_ - currentTime, nextUpdate); + } + } else { + // Done the alert decay cycle. + alertState_ = tabs::kAlertNone; + } + } + + if (nextUpdate < kNoUpdate) + [self performSelector:_cmd withObject:nil afterDelay:nextUpdate]; + + [self resetLastGlowUpdateTime]; + [self setNeedsDisplay:YES]; +} + +@end // @implementation TabView(Private) diff --git a/chrome/browser/cocoa/tab_view_unittest.mm b/chrome/browser/cocoa/tab_view_unittest.mm index 0c4d768..128274b 100644 --- a/chrome/browser/cocoa/tab_view_unittest.mm +++ b/chrome/browser/cocoa/tab_view_unittest.mm @@ -28,12 +28,13 @@ TEST_VIEW(TabViewTest, view_) // Test drawing, mostly to ensure nothing leaks or crashes. TEST_F(TabViewTest, Display) { - [view_ setHoverAlpha:0.0]; - [view_ display]; - [view_ setHoverAlpha:0.5]; - [view_ display]; - [view_ setHoverAlpha:1.0]; - [view_ display]; + for (int i = 0; i < 5; i++) { + for (int j = 0; j < 5; j++) { + [view_ setHoverAlpha:i*0.2]; + [view_ setAlertAlpha:j*0.2]; + [view_ display]; + } + } } // Test dragging and mouse tracking. @@ -46,4 +47,19 @@ TEST_F(TabViewTest, Menu) { EXPECT_FALSE([view_ menu]); } +TEST_F(TabViewTest, Glow) { + // TODO(viettrungluu): Figure out how to test this, which is timing-sensitive + // and which moreover uses |-performSelector:withObject:afterDelay:|. + + // Call |-startAlert|/|-cancelAlert| and make sure it doesn't crash. + for (int i = 0; i < 5; i++) { + [view_ startAlert]; + [view_ cancelAlert]; + } + [view_ startAlert]; + [view_ startAlert]; + [view_ cancelAlert]; + [view_ cancelAlert]; +} + } // namespace |