| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "chrome/browser/ui/cocoa/tabs/tab_view.h" |
| |
| #include "base/logging.h" |
| #include "base/mac/sdk_forward_declarations.h" |
| #include "chrome/browser/themes/theme_service.h" |
| #import "chrome/browser/ui/cocoa/nsview_additions.h" |
| #import "chrome/browser/ui/cocoa/tabs/tab_controller.h" |
| #import "chrome/browser/ui/cocoa/tabs/tab_window_controller.h" |
| #import "chrome/browser/ui/cocoa/themed_window.h" |
| #import "chrome/browser/ui/cocoa/view_id_util.h" |
| #include "grit/generated_resources.h" |
| #include "grit/theme_resources.h" |
| #import "ui/base/cocoa/nsgraphics_context_additions.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/resource/resource_bundle.h" |
| #include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h" |
| |
| |
| const int kMaskHeight = 29; // Height of the mask bitmap. |
| const int kFillHeight = 25; // Height of the "mask on" part of the mask bitmap. |
| |
| // Constants for inset and control points for tab shape. |
| const CGFloat kInsetMultiplier = 2.0/3.0; |
| const CGFloat kControlPoint1Multiplier = 1.0/3.0; |
| const CGFloat kControlPoint2Multiplier = 3.0/8.0; |
| |
| // 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; |
| |
| // This is used to judge whether the mouse has moved during rapid closure; if it |
| // has moved less than the threshold, we want to close the tab. |
| const CGFloat kRapidCloseDist = 2.5; |
| |
| @interface TabView(Private) |
| |
| - (void)resetLastGlowUpdateTime; |
| - (NSTimeInterval)timeElapsedSinceLastGlowUpdate; |
| - (void)adjustGlowValue; |
| - (CGImageRef)tabClippingMask; |
| |
| @end // TabView(Private) |
| |
| @implementation TabView |
| |
| @synthesize state = state_; |
| @synthesize hoverAlpha = hoverAlpha_; |
| @synthesize alertAlpha = alertAlpha_; |
| @synthesize closing = closing_; |
| |
| + (CGFloat)insetMultiplier { |
| return kInsetMultiplier; |
| } |
| |
| - (id)initWithFrame:(NSRect)frame |
| controller:(TabController*)controller |
| closeButton:(HoverCloseButton*)closeButton { |
| self = [super initWithFrame:frame]; |
| if (self) { |
| controller_ = controller; |
| closeButton_ = closeButton; |
| } |
| return self; |
| } |
| |
| - (void)dealloc { |
| // Cancel any delayed requests that may still be pending (drags or hover). |
| [NSObject cancelPreviousPerformRequestsWithTarget:self]; |
| [super dealloc]; |
| } |
| |
| // Called to obtain the context menu for when the user hits the right mouse |
| // button (or control-clicks). (Note that -rightMouseDown: is *not* called for |
| // control-click.) |
| - (NSMenu*)menu { |
| if ([self isClosing]) |
| return nil; |
| |
| // Sheets, being window-modal, should block contextual menus. For some reason |
| // they do not. Disallow them ourselves. |
| if ([[self window] attachedSheet]) |
| return nil; |
| |
| return [controller_ menu]; |
| } |
| |
| - (void)resizeSubviewsWithOldSize:(NSSize)oldBoundsSize { |
| [super resizeSubviewsWithOldSize:oldBoundsSize]; |
| // Called when our view is resized. If it gets too small, start by hiding |
| // the close button and only show it if tab is selected. Eventually, hide the |
| // icon as well. |
| [controller_ updateVisibility]; |
| } |
| |
| // Overridden so that mouse clicks come to this view (the parent of the |
| // hierarchy) first. We want to handle clicks and drags in this class and |
| // leave the background button for display purposes only. |
| - (BOOL)acceptsFirstMouse:(NSEvent*)theEvent { |
| return YES; |
| } |
| |
| - (void)mouseEntered:(NSEvent*)theEvent { |
| isMouseInside_ = YES; |
| [self resetLastGlowUpdateTime]; |
| [self adjustGlowValue]; |
| } |
| |
| - (void)mouseMoved:(NSEvent*)theEvent { |
| hoverPoint_ = [self convertPoint:[theEvent locationInWindow] |
| fromView:nil]; |
| [self setNeedsDisplay:YES]; |
| } |
| |
| - (void)mouseExited:(NSEvent*)theEvent { |
| isMouseInside_ = NO; |
| hoverHoldEndTime_ = |
| [NSDate timeIntervalSinceReferenceDate] + kHoverHoldDuration; |
| [self resetLastGlowUpdateTime]; |
| [self adjustGlowValue]; |
| } |
| |
| - (void)setTrackingEnabled:(BOOL)enabled { |
| if (![closeButton_ isHidden]) { |
| [closeButton_ setTrackingEnabled:enabled]; |
| } |
| } |
| |
| // Determines which view a click in our frame actually hit. It's either this |
| // view or our child close button. |
| - (NSView*)hitTest:(NSPoint)aPoint { |
| NSPoint viewPoint = [self convertPoint:aPoint fromView:[self superview]]; |
| if (![closeButton_ isHidden]) |
| if (NSPointInRect(viewPoint, [closeButton_ frame])) return closeButton_; |
| |
| NSRect pointRect = NSMakeRect(viewPoint.x, viewPoint.y, 1, 1); |
| |
| ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); |
| NSImage* left = rb.GetNativeImageNamed(IDR_TAB_ALPHA_LEFT).ToNSImage(); |
| if (viewPoint.x < [left size].width) { |
| NSRect imageRect = NSMakeRect(0, 0, [left size].width, [left size].height); |
| if ([left hitTestRect:pointRect withImageDestinationRect:imageRect |
| context:nil hints:nil flipped:NO]) { |
| return self; |
| } |
| return nil; |
| } |
| |
| NSImage* right = rb.GetNativeImageNamed(IDR_TAB_ALPHA_RIGHT).ToNSImage(); |
| CGFloat rightX = NSWidth([self bounds]) - [right size].width; |
| if (viewPoint.x > rightX) { |
| NSRect imageRect = NSMakeRect( |
| rightX, 0, [right size].width, [right size].height); |
| if ([right hitTestRect:pointRect withImageDestinationRect:imageRect |
| context:nil hints:nil flipped:NO]) { |
| return self; |
| } |
| return nil; |
| } |
| |
| if (viewPoint.y < kFillHeight) |
| return self; |
| return nil; |
| } |
| |
| // Returns |YES| if this tab can be torn away into a new window. |
| - (BOOL)canBeDragged { |
| return [controller_ tabCanBeDragged:controller_]; |
| } |
| |
| // Handle clicks and drags in this button. We get here because we have |
| // overridden acceptsFirstMouse: and the click is within our bounds. |
| - (void)mouseDown:(NSEvent*)theEvent { |
| if ([self isClosing]) |
| return; |
| |
| // Record the point at which this event happened. This is used by other mouse |
| // events that are dispatched from |-maybeStartDrag::|. |
| mouseDownPoint_ = [theEvent locationInWindow]; |
| |
| // Record the state of the close button here, because selecting the tab will |
| // unhide it. |
| BOOL closeButtonActive = ![closeButton_ isHidden]; |
| |
| // During the tab closure animation (in particular, during rapid tab closure), |
| // we may get incorrectly hit with a mouse down. If it should have gone to the |
| // close button, we send it there -- it should then track the mouse, so we |
| // don't have to worry about mouse ups. |
| if (closeButtonActive && [controller_ inRapidClosureMode]) { |
| NSPoint hitLocation = [[self superview] convertPoint:mouseDownPoint_ |
| fromView:nil]; |
| if ([self hitTest:hitLocation] == closeButton_) { |
| [closeButton_ mouseDown:theEvent]; |
| return; |
| } |
| } |
| |
| // If the tab gets torn off, the tab controller will be removed from the tab |
| // strip and then deallocated. This will also result in *us* being |
| // deallocated. Both these are bad, so we prevent this by retaining the |
| // controller. |
| base::scoped_nsobject<TabController> controller([controller_ retain]); |
| |
| // Try to initiate a drag. This will spin a custom event loop and may |
| // dispatch other mouse events. |
| [controller_ maybeStartDrag:theEvent forTab:controller]; |
| |
| // The custom loop has ended, so clear the point. |
| mouseDownPoint_ = NSZeroPoint; |
| } |
| |
| - (void)mouseUp:(NSEvent*)theEvent { |
| // Check for rapid tab closure. |
| if ([theEvent type] == NSLeftMouseUp) { |
| NSPoint upLocation = [theEvent locationInWindow]; |
| CGFloat dx = upLocation.x - mouseDownPoint_.x; |
| CGFloat dy = upLocation.y - mouseDownPoint_.y; |
| |
| // During rapid tab closure (mashing tab close buttons), we may get hit |
| // with a mouse down. As long as the mouse up is over the close button, |
| // and the mouse hasn't moved too much, we close the tab. |
| if (![closeButton_ isHidden] && |
| (dx*dx + dy*dy) <= kRapidCloseDist*kRapidCloseDist && |
| [controller_ inRapidClosureMode]) { |
| NSPoint hitLocation = |
| [[self superview] convertPoint:[theEvent locationInWindow] |
| fromView:nil]; |
| if ([self hitTest:hitLocation] == closeButton_) { |
| [controller_ closeTab:self]; |
| return; |
| } |
| } |
| } |
| |
| // Fire the action to select the tab. |
| [controller_ selectTab:self]; |
| |
| // Messaging the drag controller with |-endDrag:| would seem like the right |
| // thing to do here. But, when a tab has been detached, the controller's |
| // target is nil until the drag is finalized. Since |-mouseUp:| gets called |
| // via the manual event loop inside -[TabStripDragController |
| // maybeStartDrag:forTab:], the drag controller can end the dragging session |
| // itself directly after calling this. |
| } |
| |
| - (void)otherMouseUp:(NSEvent*)theEvent { |
| if ([self isClosing]) |
| return; |
| |
| // Support middle-click-to-close. |
| if ([theEvent buttonNumber] == 2) { |
| // |-hitTest:| takes a location in the superview's coordinates. |
| NSPoint upLocation = |
| [[self superview] convertPoint:[theEvent locationInWindow] |
| fromView:nil]; |
| // If the mouse up occurred in our view or over the close button, then |
| // close. |
| if ([self hitTest:upLocation]) |
| [controller_ closeTab:self]; |
| } |
| } |
| |
| // Returns the color used to draw the background of a tab. |selected| selects |
| // between the foreground and background tabs. |
| - (NSColor*)backgroundColorForSelected:(bool)selected { |
| ThemeService* themeProvider = |
| static_cast<ThemeService*>([[self window] themeProvider]); |
| if (!themeProvider) |
| return [[self window] backgroundColor]; |
| |
| int bitmapResources[2][2] = { |
| // Background window. |
| { |
| IDR_THEME_TAB_BACKGROUND_INACTIVE, // Background tab. |
| IDR_THEME_TOOLBAR_INACTIVE, // Active tab. |
| }, |
| // Currently focused window. |
| { |
| IDR_THEME_TAB_BACKGROUND, // Background tab. |
| IDR_THEME_TOOLBAR, // Active tab. |
| }, |
| }; |
| |
| // Themes don't have an inactive image so only look for one if there's no |
| // theme. |
| bool active = [[self window] isKeyWindow] || [[self window] isMainWindow] || |
| !themeProvider->UsingDefaultTheme(); |
| return themeProvider->GetNSImageColorNamed(bitmapResources[active][selected]); |
| } |
| |
| // Draws the active tab background. |
| - (void)drawFillForActiveTab:(NSRect)dirtyRect { |
| NSColor* backgroundImageColor = [self backgroundColorForSelected:YES]; |
| [backgroundImageColor set]; |
| |
| // Themes can have partially transparent images. NSRectFill() is measurably |
| // faster though, so call it for the known-safe default theme. |
| ThemeService* themeProvider = |
| static_cast<ThemeService*>([[self window] themeProvider]); |
| if (themeProvider && themeProvider->UsingDefaultTheme()) |
| NSRectFill(dirtyRect); |
| else |
| NSRectFillUsingOperation(dirtyRect, NSCompositeSourceOver); |
| } |
| |
| // Draws the tab background. |
| - (void)drawFill:(NSRect)dirtyRect { |
| gfx::ScopedNSGraphicsContextSaveGState scopedGState; |
| NSGraphicsContext* context = [NSGraphicsContext currentContext]; |
| CGContextRef cgContext = static_cast<CGContextRef>([context graphicsPort]); |
| |
| ThemeService* themeProvider = |
| static_cast<ThemeService*>([[self window] themeProvider]); |
| NSPoint phase = [[self window] |
| themePatternPhaseForAlignment: THEME_PATTERN_ALIGN_WITH_TAB_STRIP]; |
| [context cr_setPatternPhase:phase forView:self]; |
| |
| CGImageRef mask([self tabClippingMask]); |
| CGRect maskBounds = CGRectMake(0, 0, maskCacheWidth_, kMaskHeight); |
| CGContextClipToMask(cgContext, maskBounds, mask); |
| |
| bool selected = [self state]; |
| if (selected) { |
| [self drawFillForActiveTab:dirtyRect]; |
| return; |
| } |
| |
| // Background tabs should not paint over the tab strip separator, which is |
| // two pixels high in both lodpi and hidpi. |
| if (dirtyRect.origin.y < 1) |
| dirtyRect.origin.y = 2 * [self cr_lineWidth]; |
| |
| // Draw the tab background. |
| NSColor* backgroundImageColor = [self backgroundColorForSelected:NO]; |
| [backgroundImageColor set]; |
| |
| // Themes can have partially transparent images. NSRectFill() is measurably |
| // faster though, so call it for the known-safe default theme. |
| bool usingDefaultTheme = themeProvider && themeProvider->UsingDefaultTheme(); |
| if (usingDefaultTheme) |
| NSRectFill(dirtyRect); |
| else |
| NSRectFillUsingOperation(dirtyRect, NSCompositeSourceOver); |
| |
| // Draw the glow for hover and the overlay for alerts. |
| CGFloat hoverAlpha = [self hoverAlpha]; |
| CGFloat alertAlpha = [self alertAlpha]; |
| if (hoverAlpha > 0 || alertAlpha > 0) { |
| gfx::ScopedNSGraphicsContextSaveGState contextSave; |
| CGContextBeginTransparencyLayer(cgContext, 0); |
| |
| // The alert glow overlay is like the selected state but at most at most 80% |
| // opaque. The hover glow brings up the overlay's opacity at most 50%. |
| CGFloat backgroundAlpha = 0.8 * alertAlpha; |
| backgroundAlpha += (1 - backgroundAlpha) * 0.5 * hoverAlpha; |
| CGContextSetAlpha(cgContext, backgroundAlpha); |
| |
| [self drawFillForActiveTab:dirtyRect]; |
| |
| // ui::ThemeProvider::HasCustomImage is true only if the theme provides the |
| // image. However, even if the theme doesn't provide a tab background, the |
| // theme machinery will make one if given a frame image. See |
| // BrowserThemePack::GenerateTabBackgroundImages for details. |
| BOOL hasCustomTheme = themeProvider && |
| (themeProvider->HasCustomImage(IDR_THEME_TAB_BACKGROUND) || |
| themeProvider->HasCustomImage(IDR_THEME_FRAME)); |
| // Draw a mouse hover gradient for the default themes. |
| if (hoverAlpha > 0) { |
| if (themeProvider && !hasCustomTheme) { |
| base::scoped_nsobject<NSGradient> glow([NSGradient alloc]); |
| [glow initWithStartingColor:[NSColor colorWithCalibratedWhite:1.0 |
| alpha:1.0 * hoverAlpha] |
| endingColor:[NSColor colorWithCalibratedWhite:1.0 |
| alpha:0.0]]; |
| NSRect rect = [self bounds]; |
| NSPoint point = hoverPoint_; |
| point.y = NSHeight(rect); |
| [glow drawFromCenter:point |
| radius:0.0 |
| toCenter:point |
| radius:NSWidth(rect) / 3.0 |
| options:NSGradientDrawsBeforeStartingLocation]; |
| } |
| } |
| |
| CGContextEndTransparencyLayer(cgContext); |
| } |
| } |
| |
| // Draws the tab outline. |
| - (void)drawStroke:(NSRect)dirtyRect { |
| BOOL focused = [[self window] isKeyWindow] || [[self window] isMainWindow]; |
| CGFloat alpha = focused ? 1.0 : tabs::kImageNoFocusAlpha; |
| |
| ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); |
| float height = |
| [rb.GetNativeImageNamed(IDR_TAB_ACTIVE_LEFT).ToNSImage() size].height; |
| if ([controller_ active]) { |
| NSDrawThreePartImage(NSMakeRect(0, 0, NSWidth([self bounds]), height), |
| rb.GetNativeImageNamed(IDR_TAB_ACTIVE_LEFT).ToNSImage(), |
| rb.GetNativeImageNamed(IDR_TAB_ACTIVE_CENTER).ToNSImage(), |
| rb.GetNativeImageNamed(IDR_TAB_ACTIVE_RIGHT).ToNSImage(), |
| /*vertical=*/NO, |
| NSCompositeSourceOver, |
| alpha, |
| /*flipped=*/NO); |
| } else { |
| NSDrawThreePartImage(NSMakeRect(0, 0, NSWidth([self bounds]), height), |
| rb.GetNativeImageNamed(IDR_TAB_INACTIVE_LEFT).ToNSImage(), |
| rb.GetNativeImageNamed(IDR_TAB_INACTIVE_CENTER).ToNSImage(), |
| rb.GetNativeImageNamed(IDR_TAB_INACTIVE_RIGHT).ToNSImage(), |
| /*vertical=*/NO, |
| NSCompositeSourceOver, |
| alpha, |
| /*flipped=*/NO); |
| } |
| } |
| |
| - (void)drawRect:(NSRect)dirtyRect { |
| // Text, close button, and image are drawn by subviews. |
| [self drawFill:dirtyRect]; |
| [self drawStroke:dirtyRect]; |
| } |
| |
| - (void)setFrameOrigin:(NSPoint)origin { |
| // The background color depends on the view's vertical position. |
| if (NSMinY([self frame]) != origin.y) |
| [self setNeedsDisplay:YES]; |
| [super setFrameOrigin:origin]; |
| } |
| |
| // Override this to catch the text so that we can choose when to display it. |
| - (void)setToolTip:(NSString*)string { |
| toolTipText_.reset([string retain]); |
| } |
| |
| - (NSString*)toolTipText { |
| if (!toolTipText_.get()) { |
| return @""; |
| } |
| return toolTipText_.get(); |
| } |
| |
| - (void)viewDidMoveToWindow { |
| [super viewDidMoveToWindow]; |
| if ([self window]) { |
| [controller_ updateTitleColor]; |
| } |
| } |
| |
| - (void)setState:(NSCellStateValue)state { |
| if (state_ == state) |
| return; |
| state_ = state; |
| [self setNeedsDisplay:YES]; |
| } |
| |
| - (void)setClosing:(BOOL)closing { |
| closing_ = closing; // Safe because the property is nonatomic. |
| // When closing, ensure clicks to the close button go nowhere. |
| if (closing) { |
| [closeButton_ setTarget:nil]; |
| [closeButton_ setAction:nil]; |
| } |
| } |
| |
| - (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]; |
| } |
| } |
| |
| - (BOOL)accessibilityIsIgnored { |
| return NO; |
| } |
| |
| - (NSArray*)accessibilityActionNames { |
| NSArray* parentActions = [super accessibilityActionNames]; |
| |
| return [parentActions arrayByAddingObject:NSAccessibilityPressAction]; |
| } |
| |
| - (NSArray*)accessibilityAttributeNames { |
| NSMutableArray* attributes = |
| [[super accessibilityAttributeNames] mutableCopy]; |
| [attributes addObject:NSAccessibilityTitleAttribute]; |
| [attributes addObject:NSAccessibilityEnabledAttribute]; |
| [attributes addObject:NSAccessibilityValueAttribute]; |
| |
| return [attributes autorelease]; |
| } |
| |
| - (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute { |
| if ([attribute isEqual:NSAccessibilityTitleAttribute]) |
| return NO; |
| |
| if ([attribute isEqual:NSAccessibilityEnabledAttribute]) |
| return NO; |
| |
| if ([attribute isEqual:NSAccessibilityValueAttribute]) |
| return YES; |
| |
| return [super accessibilityIsAttributeSettable:attribute]; |
| } |
| |
| - (void)accessibilityPerformAction:(NSString*)action { |
| if ([action isEqual:NSAccessibilityPressAction] && |
| [[controller_ target] respondsToSelector:[controller_ action]]) { |
| [[controller_ target] performSelector:[controller_ action] |
| withObject:self]; |
| NSAccessibilityPostNotification(self, |
| NSAccessibilityValueChangedNotification); |
| } else { |
| [super accessibilityPerformAction:action]; |
| } |
| } |
| |
| - (id)accessibilityAttributeValue:(NSString*)attribute { |
| if ([attribute isEqual:NSAccessibilityRoleAttribute]) |
| return NSAccessibilityRadioButtonRole; |
| if ([attribute isEqual:NSAccessibilityRoleDescriptionAttribute]) |
| return l10n_util::GetNSStringWithFixup(IDS_ACCNAME_TAB); |
| if ([attribute isEqual:NSAccessibilityTitleAttribute]) |
| return [controller_ title]; |
| if ([attribute isEqual:NSAccessibilityValueAttribute]) |
| return [NSNumber numberWithInt:[controller_ selected]]; |
| if ([attribute isEqual:NSAccessibilityEnabledAttribute]) |
| return [NSNumber numberWithBool:YES]; |
| |
| return [super accessibilityAttributeValue:attribute]; |
| } |
| |
| - (ViewID)viewID { |
| return VIEW_ID_TAB; |
| } |
| |
| @end // @implementation TabView |
| |
| @implementation TabView (TabControllerInterface) |
| |
| - (void)setController:(TabController*)controller { |
| controller_ = controller; |
| } |
| |
| @end // @implementation TabView (TabControllerInterface) |
| |
| @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]; |
| } |
| |
| - (CGImageRef)tabClippingMask { |
| // NOTE: NSHeight([self bounds]) doesn't match the height of the bitmaps. |
| CGFloat scale = 1; |
| if ([[self window] respondsToSelector:@selector(backingScaleFactor)]) |
| scale = [[self window] backingScaleFactor]; |
| |
| NSRect bounds = [self bounds]; |
| CGFloat tabWidth = NSWidth(bounds); |
| if (tabWidth == maskCacheWidth_ && scale == maskCacheScale_) |
| return maskCache_.get(); |
| |
| maskCacheWidth_ = tabWidth; |
| maskCacheScale_ = scale; |
| |
| ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); |
| NSImage* leftMask = rb.GetNativeImageNamed(IDR_TAB_ALPHA_LEFT).ToNSImage(); |
| NSImage* rightMask = rb.GetNativeImageNamed(IDR_TAB_ALPHA_RIGHT).ToNSImage(); |
| |
| CGFloat leftWidth = leftMask.size.width; |
| CGFloat rightWidth = rightMask.size.width; |
| |
| // Image masks must be in the DeviceGray colorspace. Create a context and |
| // draw the mask into it. |
| base::ScopedCFTypeRef<CGColorSpaceRef> colorspace( |
| CGColorSpaceCreateDeviceGray()); |
| CGContextRef maskContext = |
| CGBitmapContextCreate(NULL, tabWidth * scale, kMaskHeight * scale, |
| 8, tabWidth * scale, colorspace, 0); |
| CGContextScaleCTM(maskContext, scale, scale); |
| NSGraphicsContext* maskGraphicsContext = |
| [NSGraphicsContext graphicsContextWithGraphicsPort:maskContext |
| flipped:NO]; |
| |
| gfx::ScopedNSGraphicsContextSaveGState scopedGState; |
| [NSGraphicsContext setCurrentContext:maskGraphicsContext]; |
| |
| // Draw mask image. |
| [[NSColor blackColor] setFill]; |
| CGContextFillRect(maskContext, CGRectMake(0, 0, tabWidth, kMaskHeight)); |
| |
| NSDrawThreePartImage(NSMakeRect(0, 0, tabWidth, kMaskHeight), |
| leftMask, nil, rightMask, /*vertical=*/NO, NSCompositeSourceOver, 1.0, |
| /*flipped=*/NO); |
| |
| CGFloat middleWidth = tabWidth - leftWidth - rightWidth; |
| NSRect middleRect = NSMakeRect(leftWidth, 0, middleWidth, kFillHeight); |
| [[NSColor whiteColor] setFill]; |
| NSRectFill(middleRect); |
| |
| maskCache_.reset(CGBitmapContextCreateImage(maskContext)); |
| return maskCache_; |
| } |
| |
| @end // @implementation TabView(Private) |