blob: 0a655ad0bd1a14ce9a5a08066a53f97bc0f7413e [file] [log] [blame]
// 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/mac_util.h"
#include "base/mac/scoped_cftyperef.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 (BrowserAccessibilityState::GetInstance()->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)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 {
// 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)