// Copyright (c) 2011 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" #import "base/mac/mac_util.h" #include "base/mac/scoped_cftyperef.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" #include "ui/base/l10n/l10n_util.h" namespace { // 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; const CGFloat kTearDistance = 36.0; const NSTimeInterval kTearDuration = 0.333; // 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; } // namespace @interface TabView(Private) - (void)resetLastGlowUpdateTime; - (NSTimeInterval)timeElapsedSinceLastGlowUpdate; - (void)adjustGlowValue; // TODO(davidben): When we stop supporting 10.5, this can be removed. - (int)getWorkspaceID:(NSWindow*)window useCache:(BOOL)useCache; - (NSBezierPath*)bezierPathForRect:(NSRect)rect; @end // TabView(Private) @implementation TabView @synthesize state = state_; @synthesize hoverAlpha = hoverAlpha_; @synthesize alertAlpha = alertAlpha_; @synthesize closing = closing_; - (id)initWithFrame:(NSRect)frame { self = [super initWithFrame:frame]; if (self) { [self setShowsDivider:NO]; // TODO(alcor): register for theming } return self; } - (void)awakeFromNib { [self setShowsDivider:NO]; } - (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]; } // 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]]; NSRect frame = [self frame]; // Reduce the width of the hit rect slightly to remove the overlap // between adjacent tabs. The drawing code in TabCell has the top // corners of the tab inset by height*2/3, so we inset by half of // that here. This doesn't completely eliminate the overlap, but it // works well enough. NSRect hitRect = NSInsetRect(frame, frame.size.height / 3.0f, 0); if (![closeButton_ isHidden]) if (NSPointInRect(viewPoint, [closeButton_ frame])) return closeButton_; if (NSPointInRect(aPoint, hitRect)) return self; return nil; } // Returns |YES| if this tab can be torn away into a new window. - (BOOL)canBeDragged { if ([self isClosing]) return NO; NSWindowController* controller = [sourceWindow_ windowController]; if ([controller isKindOfClass:[TabWindowController class]]) { TabWindowController* realController = static_cast(controller); return [realController isTabDraggable:self]; } return YES; } // Returns an array of controllers that could be a drop target, ordered front to // back. It has to be of the appropriate class, and visible (obviously). Note // that the window cannot be a target for itself. - (NSArray*)dropTargetsForController:(TabWindowController*)dragController { NSMutableArray* targets = [NSMutableArray array]; NSWindow* dragWindow = [dragController window]; for (NSWindow* window in [NSApp orderedWindows]) { if (window == dragWindow) continue; if (![window isVisible]) continue; // Skip windows on the wrong space. if ([window respondsToSelector:@selector(isOnActiveSpace)]) { if (![window performSelector:@selector(isOnActiveSpace)]) continue; } else { // TODO(davidben): When we stop supporting 10.5, this can be // removed. // // We don't cache the workspace of |dragWindow| because it may // move around spaces. if ([self getWorkspaceID:dragWindow useCache:NO] != [self getWorkspaceID:window useCache:YES]) continue; } NSWindowController* controller = [window windowController]; if ([controller isKindOfClass:[TabWindowController class]]) { TabWindowController* realController = static_cast(controller); if ([realController canReceiveFrom:dragController]) [targets addObject:controller]; } } return targets; } // Call to clear out transient weak references we hold during drags. - (void)resetDragControllers { draggedController_ = nil; dragWindow_ = nil; dragOverlay_ = nil; sourceController_ = nil; sourceWindow_ = nil; targetController_ = nil; workspaceIDCache_.clear(); } // Sets whether the window background should be visible or invisible when // dragging a tab. The background should be invisible when the mouse is over a // potential drop target for the tab (the tab strip). It should be visible when // there's no drop target so the window looks more fully realized and ready to // become a stand-alone window. - (void)setWindowBackgroundVisibility:(BOOL)shouldBeVisible { if (chromeIsVisible_ == shouldBeVisible) return; // There appears to be a race-condition in CoreAnimation where if we use // animators to set the alpha values, we can't guarantee that we cancel them. // This has the side effect of sometimes leaving the dragged window // translucent or invisible. As a result, don't animate the alpha change. [[draggedController_ overlayWindow] setAlphaValue:1.0]; if (targetController_) { [dragWindow_ setAlphaValue:0.0]; [[draggedController_ overlayWindow] setHasShadow:YES]; [[targetController_ window] makeMainWindow]; } else { [dragWindow_ setAlphaValue:0.5]; [[draggedController_ overlayWindow] setHasShadow:NO]; [[draggedController_ window] makeMainWindow]; } chromeIsVisible_ = shouldBeVisible; } // 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; NSPoint downLocation = [theEvent locationInWindow]; // Record the state of the close button here, because selecting the tab will // unhide it. BOOL closeButtonActive = [closeButton_ isHidden] ? NO : YES; // 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:downLocation fromView:nil]; if ([self hitTest:hitLocation] == closeButton_) { [closeButton_ mouseDown:theEvent]; return; } } // Fire the action to select the tab. if ([[controller_ target] respondsToSelector:[controller_ action]]) [[controller_ target] performSelector:[controller_ action] withObject:self]; [self resetDragControllers]; // Resolve overlay back to original window. sourceWindow_ = [self window]; if ([sourceWindow_ isKindOfClass:[NSPanel class]]) { sourceWindow_ = [sourceWindow_ parentWindow]; } sourceWindowFrame_ = [sourceWindow_ frame]; sourceTabFrame_ = [self frame]; sourceController_ = [sourceWindow_ windowController]; tabWasDragged_ = NO; tearTime_ = 0.0; draggingWithinTabStrip_ = YES; chromeIsVisible_ = NO; // If there's more than one potential window to be a drop target, we want to // treat a drag of a tab just like dragging around a tab that's already // detached. Note that unit tests might have |-numberOfTabs| reporting zero // since the model won't be fully hooked up. We need to be prepared for that // and not send them into the "magnetic" codepath. NSArray* targets = [self dropTargetsForController:sourceController_]; moveWindowOnDrag_ = ([sourceController_ numberOfTabs] < 2 && ![targets count]) || ![self canBeDragged] || ![sourceController_ tabDraggingAllowed]; // If we are dragging a tab, a window with a single tab should immediately // snap off and not drag within the tab strip. if (!moveWindowOnDrag_) draggingWithinTabStrip_ = [sourceController_ numberOfTabs] > 1; dragOrigin_ = [NSEvent mouseLocation]; // 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. scoped_nsobject controller([controller_ retain]); // Because we move views between windows, we need to handle the event loop // ourselves. Ideally we should use the standard event loop. while (1) { const NSUInteger mask = NSLeftMouseUpMask | NSLeftMouseDraggedMask | NSKeyUpMask; theEvent = [NSApp nextEventMatchingMask:mask untilDate:[NSDate distantFuture] inMode:NSDefaultRunLoopMode dequeue:YES]; NSEventType type = [theEvent type]; if (type == NSKeyUp) { if ([theEvent keyCode] == kVK_Escape) { // Cancel the drag and restore the previous state. if (draggingWithinTabStrip_) { // Simply pretend the tab wasn't dragged (far enough). tabWasDragged_ = NO; } else { [targetController_ removePlaceholder]; if ([sourceController_ numberOfTabs] < 2) { // Revert to a single-tab window. targetController_ = nil; } else { // Change the target to the source controller. targetController_ = sourceController_; [targetController_ insertPlaceholderForTab:self frame:sourceTabFrame_ yStretchiness:0]; } } // Call the |mouseUp:| code to end the drag. [self mouseUp:theEvent]; break; } } else if (type == NSLeftMouseDragged) { [self mouseDragged:theEvent]; } else if (type == NSLeftMouseUp) { NSPoint upLocation = [theEvent locationInWindow]; CGFloat dx = upLocation.x - downLocation.x; CGFloat dy = upLocation.y - downLocation.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 (closeButtonActive && (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]; break; } } [self mouseUp:theEvent]; break; } else { // TODO(viettrungluu): [crbug.com/23830] We can receive right-mouse-ups // (and maybe even others?) for reasons I don't understand. So we // explicitly check for both events we're expecting, and log others. We // should figure out what's going on. LOG(WARNING) << "Spurious event received of type " << type << "."; } } } - (void)mouseDragged:(NSEvent*)theEvent { // Special-case this to keep the logic below simpler. if (moveWindowOnDrag_) { if ([sourceController_ windowMovementAllowed]) { NSPoint thisPoint = [NSEvent mouseLocation]; NSPoint origin = sourceWindowFrame_.origin; origin.x += (thisPoint.x - dragOrigin_.x); origin.y += (thisPoint.y - dragOrigin_.y); [sourceWindow_ setFrameOrigin:NSMakePoint(origin.x, origin.y)]; } // else do nothing. return; } // First, go through the magnetic drag cycle. We break out of this if // "stretchiness" ever exceeds a set amount. tabWasDragged_ = YES; if (draggingWithinTabStrip_) { NSPoint thisPoint = [NSEvent mouseLocation]; CGFloat stretchiness = thisPoint.y - dragOrigin_.y; stretchiness = copysign(sqrtf(fabs(stretchiness))/sqrtf(kTearDistance), stretchiness) / 2.0; CGFloat offset = thisPoint.x - dragOrigin_.x; if (fabsf(offset) > 100) stretchiness = 0; [sourceController_ insertPlaceholderForTab:self frame:NSOffsetRect(sourceTabFrame_, offset, 0) yStretchiness:stretchiness]; // Check that we haven't pulled the tab too far to start a drag. This // can include either pulling it too far down, or off the side of the tab // strip that would cause it to no longer be fully visible. BOOL stillVisible = [sourceController_ isTabFullyVisible:self]; CGFloat tearForce = fabs(thisPoint.y - dragOrigin_.y); if ([sourceController_ tabTearingAllowed] && (tearForce > kTearDistance || !stillVisible)) { draggingWithinTabStrip_ = NO; // When you finally leave the strip, we treat that as the origin. dragOrigin_.x = thisPoint.x; } else { // Still dragging within the tab strip, wait for the next drag event. return; } } // Do not start dragging until the user has "torn" the tab off by // moving more than 3 pixels. NSDate* targetDwellDate = nil; // The date this target was first chosen. NSPoint thisPoint = [NSEvent mouseLocation]; // Iterate over possible targets checking for the one the mouse is in. // If the tab is just in the frame, bring the window forward to make it // easier to drop something there. If it's in the tab strip, set the new // target so that it pops into that window. We can't cache this because we // need the z-order to be correct. NSArray* targets = [self dropTargetsForController:draggedController_]; TabWindowController* newTarget = nil; for (TabWindowController* target in targets) { NSRect windowFrame = [[target window] frame]; if (NSPointInRect(thisPoint, windowFrame)) { [[target window] orderFront:self]; NSRect tabStripFrame = [[target tabStripView] frame]; tabStripFrame.origin = [[target window] convertBaseToScreen:tabStripFrame.origin]; if (NSPointInRect(thisPoint, tabStripFrame)) { newTarget = target; } break; } } // If we're now targeting a new window, re-layout the tabs in the old // target and reset how long we've been hovering over this new one. if (targetController_ != newTarget) { targetDwellDate = [NSDate date]; [targetController_ removePlaceholder]; targetController_ = newTarget; if (!newTarget) { tearTime_ = [NSDate timeIntervalSinceReferenceDate]; tearOrigin_ = [dragWindow_ frame].origin; } } // Create or identify the dragged controller. if (!draggedController_) { // Get rid of any placeholder remaining in the original source window. [sourceController_ removePlaceholder]; // Detach from the current window and put it in a new window. If there are // no more tabs remaining after detaching, the source window is about to // go away (it's been autoreleased) so we need to ensure we don't reference // it any more. In that case the new controller becomes our source // controller. draggedController_ = [sourceController_ detachTabToNewWindow:self]; dragWindow_ = [draggedController_ window]; [dragWindow_ setAlphaValue:0.0]; if (![sourceController_ hasLiveTabs]) { sourceController_ = draggedController_; sourceWindow_ = dragWindow_; } // If dragging the tab only moves the current window, do not show overlay // so that sheets stay on top of the window. // Bring the target window to the front and make sure it has a border. [dragWindow_ setLevel:NSFloatingWindowLevel]; [dragWindow_ setHasShadow:YES]; [dragWindow_ orderFront:nil]; [dragWindow_ makeMainWindow]; [draggedController_ showOverlay]; dragOverlay_ = [draggedController_ overlayWindow]; // Force the new tab button to be hidden. We'll reset it on mouse up. [draggedController_ showNewTabButton:NO]; tearTime_ = [NSDate timeIntervalSinceReferenceDate]; tearOrigin_ = sourceWindowFrame_.origin; } // TODO(pinkerton): http://crbug.com/25682 demonstrates a way to get here by // some weird circumstance that doesn't first go through mouseDown:. We // really shouldn't go any farther. if (!draggedController_ || !sourceController_) return; // When the user first tears off the window, we want slide the window to // the current mouse location (to reduce the jarring appearance). We do this // by calling ourselves back with additional mouseDragged calls (not actual // events). |tearProgress| is a normalized measure of how far through this // tear "animation" (of length kTearDuration) we are and has values [0..1]. // We use sqrt() so the animation is non-linear (slow down near the end // point). NSTimeInterval tearProgress = [NSDate timeIntervalSinceReferenceDate] - tearTime_; tearProgress /= kTearDuration; // Normalize. tearProgress = sqrtf(MAX(MIN(tearProgress, 1.0), 0.0)); // Move the dragged window to the right place on the screen. NSPoint origin = sourceWindowFrame_.origin; origin.x += (thisPoint.x - dragOrigin_.x); origin.y += (thisPoint.y - dragOrigin_.y); if (tearProgress < 1) { // If the tear animation is not complete, call back to ourself with the // same event to animate even if the mouse isn't moving. We need to make // sure these get cancelled in mouseUp:. [NSObject cancelPreviousPerformRequestsWithTarget:self]; [self performSelector:@selector(mouseDragged:) withObject:theEvent afterDelay:1.0f/30.0f]; // Set the current window origin based on how far we've progressed through // the tear animation. origin.x = (1 - tearProgress) * tearOrigin_.x + tearProgress * origin.x; origin.y = (1 - tearProgress) * tearOrigin_.y + tearProgress * origin.y; } if (targetController_) { // In order to "snap" two windows of different sizes together at their // toolbar, we can't just use the origin of the target frame. We also have // to take into consideration the difference in height. NSRect targetFrame = [[targetController_ window] frame]; NSRect sourceFrame = [dragWindow_ frame]; origin.y = NSMinY(targetFrame) + (NSHeight(targetFrame) - NSHeight(sourceFrame)); } [dragWindow_ setFrameOrigin:NSMakePoint(origin.x, origin.y)]; // If we're not hovering over any window, make the window fully // opaque. Otherwise, find where the tab might be dropped and insert // a placeholder so it appears like it's part of that window. if (targetController_) { if (![[targetController_ window] isKeyWindow]) { // && ([targetDwellDate timeIntervalSinceNow] < -REQUIRED_DWELL)) { [[targetController_ window] orderFront:nil]; targetDwellDate = nil; } // Compute where placeholder should go and insert it into the // destination tab strip. TabView* draggedTabView = (TabView*)[draggedController_ selectedTabView]; NSRect tabFrame = [draggedTabView frame]; tabFrame.origin = [dragWindow_ convertBaseToScreen:tabFrame.origin]; tabFrame.origin = [[targetController_ window] convertScreenToBase:tabFrame.origin]; tabFrame = [[targetController_ tabStripView] convertRect:tabFrame fromView:nil]; [targetController_ insertPlaceholderForTab:self frame:tabFrame yStretchiness:0]; [targetController_ layoutTabs]; } else { [dragWindow_ makeKeyAndOrderFront:nil]; } // Adjust the visibility of the window background. If there is a drop target, // we want to hide the window background so the tab stands out for // positioning. If not, we want to show it so it looks like a new window will // be realized. BOOL chromeShouldBeVisible = targetController_ == nil; [self setWindowBackgroundVisibility:chromeShouldBeVisible]; } - (void)mouseUp:(NSEvent*)theEvent { // The drag/click is done. If the user dragged the mouse, finalize the drag // and clean up. // Special-case this to keep the logic below simpler. if (moveWindowOnDrag_) return; // Cancel any delayed -mouseDragged: requests that may still be pending. [NSObject cancelPreviousPerformRequestsWithTarget:self]; // TODO(pinkerton): http://crbug.com/25682 demonstrates a way to get here by // some weird circumstance that doesn't first go through mouseDown:. We // really shouldn't go any farther. if (!sourceController_) return; // We are now free to re-display the new tab button in the window we're // dragging. It will show when the next call to -layoutTabs (which happens // indrectly by several of the calls below, such as removing the placeholder). [draggedController_ showNewTabButton:YES]; if (draggingWithinTabStrip_) { if (tabWasDragged_) { // Move tab to new location. DCHECK([sourceController_ numberOfTabs]); TabWindowController* dropController = sourceController_; [dropController moveTabView:[dropController selectedTabView] fromController:nil]; } } else if (targetController_) { // Move between windows. If |targetController_| is nil, we're not dropping // into any existing window. NSView* draggedTabView = [draggedController_ selectedTabView]; [targetController_ moveTabView:draggedTabView fromController:draggedController_]; // Force redraw to avoid flashes of old content before returning to event // loop. [[targetController_ window] display]; [targetController_ showWindow:nil]; [draggedController_ removeOverlay]; } else { // Only move the window around on screen. Make sure it's set back to // normal state (fully opaque, has shadow, has key, etc). [draggedController_ removeOverlay]; // Don't want to re-show the window if it was closed during the drag. if ([dragWindow_ isVisible]) { [dragWindow_ setAlphaValue:1.0]; [dragOverlay_ setHasShadow:NO]; [dragWindow_ setHasShadow:YES]; [dragWindow_ makeKeyAndOrderFront:nil]; } [[draggedController_ window] setLevel:NSNormalWindowLevel]; [draggedController_ removePlaceholder]; } [sourceController_ removePlaceholder]; chromeIsVisible_ = YES; [self resetDragControllers]; } - (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]; } } - (void)drawRect:(NSRect)dirtyRect { const CGFloat lineWidth = [self cr_lineWidth]; NSGraphicsContext* context = [NSGraphicsContext currentContext]; [context saveGraphicsState]; ThemeService* themeProvider = static_cast([[self window] themeProvider]); [context setPatternPhase:[[self window] themePatternPhase]]; NSRect rect = [self bounds]; NSBezierPath* path = [self bezierPathForRect:rect]; BOOL selected = [self state]; // Don't draw the window/tab bar background when selected, since the tab // background overlay drawn over it (see below) will be fully opaque. BOOL hasBackgroundImage = NO; if (!selected) { // 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. hasBackgroundImage = themeProvider && (themeProvider->HasCustomImage(IDR_THEME_TAB_BACKGROUND) || themeProvider->HasCustomImage(IDR_THEME_FRAME)); NSColor* backgroundImageColor = hasBackgroundImage ? themeProvider->GetNSImageColorNamed(IDR_THEME_TAB_BACKGROUND, true) : nil; if (backgroundImageColor) { [backgroundImageColor set]; [path fill]; } else { // Use the window's background color rather than |[NSColor // windowBackgroundColor]|, which gets confused by the fullscreen window. // (The result is the same for normal, non-fullscreen windows.) [[[self window] backgroundColor] set]; [path fill]; [[NSColor colorWithCalibratedWhite:1.0 alpha:0.3] set]; [path fill]; } } [context saveGraphicsState]; [path addClip]; // Use the same overlay for the selected state and for hover and alert glows; // for the selected state, it's fully opaque. CGFloat hoverAlpha = [self hoverAlpha]; CGFloat alertAlpha = [self alertAlpha]; if (selected || hoverAlpha > 0 || alertAlpha > 0) { // Draw the selected background / glow overlay. [context saveGraphicsState]; CGContextRef cgContext = static_cast([context graphicsPort]); CGContextBeginTransparencyLayer(cgContext, 0); if (!selected) { // 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); } [path addClip]; [context saveGraphicsState]; [super drawBackground]; [context restoreGraphicsState]; // Draw a mouse hover gradient for the default themes. if (!selected && hoverAlpha > 0) { if (themeProvider && !hasBackgroundImage) { scoped_nsobject glow([NSGradient alloc]); [glow initWithStartingColor:[NSColor colorWithCalibratedWhite:1.0 alpha:1.0 * hoverAlpha] endingColor:[NSColor colorWithCalibratedWhite:1.0 alpha:0.0]]; NSPoint point = hoverPoint_; point.y = NSHeight(rect); [glow drawFromCenter:point radius:0.0 toCenter:point radius:NSWidth(rect) / 3.0 options:NSGradientDrawsBeforeStartingLocation]; [glow drawInBezierPath:path relativeCenterPosition:hoverPoint_]; } } CGContextEndTransparencyLayer(cgContext); [context restoreGraphicsState]; } BOOL active = [[self window] isKeyWindow] || [[self window] isMainWindow]; CGFloat borderAlpha = selected ? (active ? 0.3 : 0.2) : 0.2; NSColor* borderColor = [NSColor colorWithDeviceWhite:0.0 alpha:borderAlpha]; NSColor* highlightColor = themeProvider ? themeProvider->GetNSColor( themeProvider->UsingDefaultTheme() ? ThemeService::COLOR_TOOLBAR_BEZEL : ThemeService::COLOR_TOOLBAR, true) : nil; // Draw the top inner highlight within the currently selected tab if using // the default theme. if (selected && themeProvider && themeProvider->UsingDefaultTheme()) { NSAffineTransform* highlightTransform = [NSAffineTransform transform]; [highlightTransform translateXBy:lineWidth yBy:-lineWidth]; scoped_nsobject highlightPath([path copy]); [highlightPath transformUsingAffineTransform:highlightTransform]; [highlightColor setStroke]; [highlightPath setLineWidth:lineWidth]; [highlightPath stroke]; highlightTransform = [NSAffineTransform transform]; [highlightTransform translateXBy:-2 * lineWidth yBy:0.0]; [highlightPath transformUsingAffineTransform:highlightTransform]; [highlightPath stroke]; } [context restoreGraphicsState]; // Draw the top stroke. [context saveGraphicsState]; [borderColor set]; [path setLineWidth:lineWidth]; [path stroke]; [context restoreGraphicsState]; // Mimic the tab strip's bottom border, which consists of a dark border // and light highlight. if (!selected) { [path addClip]; NSRect borderRect = rect; borderRect.origin.y = lineWidth; borderRect.size.height = lineWidth; [borderColor set]; NSRectFillUsingOperation(borderRect, NSCompositeSourceOver); borderRect.origin.y = 0; [highlightColor set]; NSRectFillUsingOperation(borderRect, NSCompositeSourceOver); } [context restoreGraphicsState]; } - (void)viewDidMoveToWindow { [super viewDidMoveToWindow]; if ([self window]) { [controller_ updateTitleColor]; } } - (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]; return attributes; } - (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute { if ([attribute isEqual:NSAccessibilityTitleAttribute]) return NO; if ([attribute isEqual:NSAccessibilityEnabledAttribute]) return NO; return [super accessibilityIsAttributeSettable:attribute]; } - (id)accessibilityAttributeValue:(NSString*)attribute { if ([attribute isEqual:NSAccessibilityRoleAttribute]) return l10n_util::GetNSStringWithFixup(IDS_ACCNAME_TAB); if ([attribute isEqual:NSAccessibilityTitleAttribute]) return [controller_ title]; 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): -- 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]; } // Returns the workspace id of |window|. If |useCache|, then lookup // and remember the value in |workspaceIDCache_| until the end of the // current drag. - (int)getWorkspaceID:(NSWindow*)window useCache:(BOOL)useCache { CGWindowID windowID = [window windowNumber]; if (useCache) { std::map::iterator iter = workspaceIDCache_.find(windowID); if (iter != workspaceIDCache_.end()) return iter->second; } int workspace = -1; // It's possible to query in bulk, but probably not necessary. base::mac::ScopedCFTypeRef windowIDs(CFArrayCreate( NULL, reinterpret_cast(&windowID), 1, NULL)); base::mac::ScopedCFTypeRef descriptions( CGWindowListCreateDescriptionFromArray(windowIDs)); DCHECK(CFArrayGetCount(descriptions.get()) <= 1); if (CFArrayGetCount(descriptions.get()) > 0) { CFDictionaryRef dict = static_cast( CFArrayGetValueAtIndex(descriptions.get(), 0)); DCHECK(CFGetTypeID(dict) == CFDictionaryGetTypeID()); // Sanity check the ID. CFNumberRef otherIDRef = (CFNumberRef)base::mac::GetValueFromDictionary( dict, kCGWindowNumber, CFNumberGetTypeID()); CGWindowID otherID; if (otherIDRef && CFNumberGetValue(otherIDRef, kCGWindowIDCFNumberType, &otherID) && otherID == windowID) { // And then get the workspace. CFNumberRef workspaceRef = (CFNumberRef)base::mac::GetValueFromDictionary( dict, kCGWindowWorkspace, CFNumberGetTypeID()); if (!workspaceRef || !CFNumberGetValue(workspaceRef, kCFNumberIntType, &workspace)) { workspace = -1; } } else { NOTREACHED(); } } if (useCache) { workspaceIDCache_[windowID] = workspace; } return workspace; } // Returns the bezier path used to draw the tab given the bounds to draw it in. - (NSBezierPath*)bezierPathForRect:(NSRect)rect { const CGFloat lineWidth = [self cr_lineWidth]; const CGFloat halfLineWidth = lineWidth / 2.0; // Outset by halfLineWidth in order to draw on pixels rather than on borders // (which would cause blurry pixels). Subtract lineWidth of height to // compensate, otherwise clipping will occur. rect = NSInsetRect(rect, -halfLineWidth, -halfLineWidth); rect.size.height -= lineWidth; NSPoint bottomLeft = NSMakePoint(NSMinX(rect), NSMinY(rect) + 2 * lineWidth); NSPoint bottomRight = NSMakePoint(NSMaxX(rect), NSMinY(rect) + 2 * lineWidth); NSPoint topRight = NSMakePoint(NSMaxX(rect) - kInsetMultiplier * NSHeight(rect), NSMaxY(rect)); NSPoint topLeft = NSMakePoint(NSMinX(rect) + kInsetMultiplier * NSHeight(rect), NSMaxY(rect)); CGFloat baseControlPointOutset = NSHeight(rect) * kControlPoint1Multiplier; CGFloat bottomControlPointInset = NSHeight(rect) * kControlPoint2Multiplier; // Outset many of these values by lineWidth to cause the fill to bleed outside // the clip area. NSBezierPath* path = [NSBezierPath bezierPath]; [path moveToPoint:NSMakePoint(bottomLeft.x - lineWidth, bottomLeft.y - (2 * lineWidth))]; [path lineToPoint:NSMakePoint(bottomLeft.x - lineWidth, bottomLeft.y)]; [path lineToPoint:bottomLeft]; [path curveToPoint:topLeft controlPoint1:NSMakePoint(bottomLeft.x + baseControlPointOutset, bottomLeft.y) controlPoint2:NSMakePoint(topLeft.x - bottomControlPointInset, topLeft.y)]; [path lineToPoint:topRight]; [path curveToPoint:bottomRight controlPoint1:NSMakePoint(topRight.x + bottomControlPointInset, topRight.y) controlPoint2:NSMakePoint(bottomRight.x - baseControlPointOutset, bottomRight.y)]; [path lineToPoint:NSMakePoint(bottomRight.x + lineWidth, bottomRight.y)]; [path lineToPoint:NSMakePoint(bottomRight.x + lineWidth, bottomRight.y - (2 * lineWidth))]; return path; } @end // @implementation TabView(Private)