diff options
Diffstat (limited to 'chrome/browser/ui/cocoa/tab_view.mm')
-rw-r--r-- | chrome/browser/ui/cocoa/tab_view.mm | 1057 |
1 files changed, 1057 insertions, 0 deletions
diff --git a/chrome/browser/ui/cocoa/tab_view.mm b/chrome/browser/ui/cocoa/tab_view.mm new file mode 100644 index 0000000..58659ca --- /dev/null +++ b/chrome/browser/ui/cocoa/tab_view.mm @@ -0,0 +1,1057 @@ +// Copyright (c) 2010 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/tab_view.h" + +#include "base/logging.h" +#import "base/mac_util.h" +#include "base/mac/scoped_cftyperef.h" +#include "base/nsimage_cache_mac.h" +#include "chrome/browser/accessibility/browser_accessibility_state.h" +#include "chrome/browser/themes/browser_theme_provider.h" +#import "chrome/browser/ui/cocoa/tab_controller.h" +#import "chrome/browser/ui/cocoa/tab_window_controller.h" +#import "chrome/browser/ui/cocoa/themed_window.h" +#import "chrome/browser/ui/cocoa/view_id_util.h" +#include "grit/theme_resources.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]; + + // It is desirable for us to remove the close button from the cocoa hierarchy, + // so that VoiceOver does not encounter it. + // TODO(dtseng): crbug.com/59978. + // Retain in case we remove it from its superview. + closeButtonRetainer_.reset([closeButton_ retain]); + if (Singleton<BrowserAccessibilityState>::get()->IsAccessibleBrowser()) { + // The superview gives up ownership of the closeButton here. + [closeButton_ removeFromSuperview]; + } +} + +- (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 { + [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<TabWindowController*>(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<TabWindowController*>(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<TabController> 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) { + theEvent = + [NSApp nextEventMatchingMask:NSLeftMouseUpMask | NSLeftMouseDraggedMask + untilDate:[NSDate distantFuture] + inMode:NSDefaultRunLoopMode dequeue:YES]; + NSEventType type = [theEvent type]; + 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 { + NSGraphicsContext* context = [NSGraphicsContext currentContext]; + [context saveGraphicsState]; + + BrowserThemeProvider* themeProvider = + static_cast<BrowserThemeProvider*>([[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) { + // 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<CGContextRef>([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<NSGradient> 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() ? + BrowserThemeProvider::COLOR_TOOLBAR_BEZEL : + BrowserThemeProvider::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:1.0 yBy:-1.0]; + scoped_nsobject<NSBezierPath> highlightPath([path copy]); + [highlightPath transformUsingAffineTransform:highlightTransform]; + [highlightColor setStroke]; + [highlightPath setLineWidth:1.0]; + [highlightPath stroke]; + highlightTransform = [NSAffineTransform transform]; + [highlightTransform translateXBy:-2.0 yBy:0.0]; + [highlightPath transformUsingAffineTransform:highlightTransform]; + [highlightPath stroke]; + } + + [context restoreGraphicsState]; + + // Draw the top stroke. + [context saveGraphicsState]; + [borderColor set]; + [path setLineWidth:1.0]; + [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 = 1; + borderRect.size.height = 1; + [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 NSAccessibilityButtonRole; + + if ([attribute isEqual:NSAccessibilityTitleAttribute]) + return [controller_ title]; + + if ([attribute isEqual:NSAccessibilityEnabledAttribute]) + return [NSNumber numberWithBool:YES]; + + if ([attribute isEqual:NSAccessibilityChildrenAttribute]) { + // The subviews (icon and text) are clutter; filter out everything but + // useful controls. + NSArray* children = [super accessibilityAttributeValue:attribute]; + NSMutableArray* okChildren = [NSMutableArray array]; + for (id child in children) { + if ([child isKindOfClass:[NSButtonCell class]]) + [okChildren addObject:child]; + } + + return okChildren; + } + + 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]; +} + +// 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<CGWindowID, int>::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<CFArrayRef> windowIDs(CFArrayCreate( + NULL, reinterpret_cast<const void **>(&windowID), 1, NULL)); + base::mac::ScopedCFTypeRef<CFArrayRef> descriptions( + CGWindowListCreateDescriptionFromArray(windowIDs)); + DCHECK(CFArrayGetCount(descriptions.get()) <= 1); + if (CFArrayGetCount(descriptions.get()) > 0) { + CFDictionaryRef dict = static_cast<CFDictionaryRef>( + CFArrayGetValueAtIndex(descriptions.get(), 0)); + DCHECK(CFGetTypeID(dict) == CFDictionaryGetTypeID()); + + // Sanity check the ID. + CFNumberRef otherIDRef = (CFNumberRef)mac_util::GetValueFromDictionary( + dict, kCGWindowNumber, CFNumberGetTypeID()); + CGWindowID otherID; + if (otherIDRef && + CFNumberGetValue(otherIDRef, kCGWindowIDCFNumberType, &otherID) && + otherID == windowID) { + // And then get the workspace. + CFNumberRef workspaceRef = (CFNumberRef)mac_util::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 { + // Outset by 0.5 in order to draw on pixels rather than on borders (which + // would cause blurry pixels). Subtract 1px of height to compensate, otherwise + // clipping will occur. + rect = NSInsetRect(rect, -0.5, -0.5); + rect.size.height -= 1.0; + + NSPoint bottomLeft = NSMakePoint(NSMinX(rect), NSMinY(rect) + 2); + NSPoint bottomRight = NSMakePoint(NSMaxX(rect), NSMinY(rect) + 2); + 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 1 to cause the fill to bleed outside the + // clip area. + NSBezierPath* path = [NSBezierPath bezierPath]; + [path moveToPoint:NSMakePoint(bottomLeft.x - 1, bottomLeft.y - 2)]; + [path lineToPoint:NSMakePoint(bottomLeft.x - 1, 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 + 1, bottomRight.y)]; + [path lineToPoint:NSMakePoint(bottomRight.x + 1, bottomRight.y - 2)]; + return path; +} + +@end // @implementation TabView(Private) |