Home | History | Annotate | Download | only in tabs
      1 // Copyright (c) 2011 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 #import "chrome/browser/ui/cocoa/tabs/tab_view.h"
      6 
      7 #include "base/logging.h"
      8 #import "base/mac/mac_util.h"
      9 #include "base/mac/scoped_cftyperef.h"
     10 #include "chrome/browser/themes/theme_service.h"
     11 #import "chrome/browser/ui/cocoa/nsview_additions.h"
     12 #import "chrome/browser/ui/cocoa/tabs/tab_controller.h"
     13 #import "chrome/browser/ui/cocoa/tabs/tab_window_controller.h"
     14 #import "chrome/browser/ui/cocoa/themed_window.h"
     15 #import "chrome/browser/ui/cocoa/view_id_util.h"
     16 #include "grit/generated_resources.h"
     17 #include "grit/theme_resources.h"
     18 #include "ui/base/l10n/l10n_util.h"
     19 
     20 namespace {
     21 
     22 // Constants for inset and control points for tab shape.
     23 const CGFloat kInsetMultiplier = 2.0/3.0;
     24 const CGFloat kControlPoint1Multiplier = 1.0/3.0;
     25 const CGFloat kControlPoint2Multiplier = 3.0/8.0;
     26 
     27 // The amount of time in seconds during which each type of glow increases, holds
     28 // steady, and decreases, respectively.
     29 const NSTimeInterval kHoverShowDuration = 0.2;
     30 const NSTimeInterval kHoverHoldDuration = 0.02;
     31 const NSTimeInterval kHoverHideDuration = 0.4;
     32 const NSTimeInterval kAlertShowDuration = 0.4;
     33 const NSTimeInterval kAlertHoldDuration = 0.4;
     34 const NSTimeInterval kAlertHideDuration = 0.4;
     35 
     36 // The default time interval in seconds between glow updates (when
     37 // increasing/decreasing).
     38 const NSTimeInterval kGlowUpdateInterval = 0.025;
     39 
     40 const CGFloat kTearDistance = 36.0;
     41 const NSTimeInterval kTearDuration = 0.333;
     42 
     43 // This is used to judge whether the mouse has moved during rapid closure; if it
     44 // has moved less than the threshold, we want to close the tab.
     45 const CGFloat kRapidCloseDist = 2.5;
     46 
     47 }  // namespace
     48 
     49 @interface TabView(Private)
     50 
     51 - (void)resetLastGlowUpdateTime;
     52 - (NSTimeInterval)timeElapsedSinceLastGlowUpdate;
     53 - (void)adjustGlowValue;
     54 // TODO(davidben): When we stop supporting 10.5, this can be removed.
     55 - (int)getWorkspaceID:(NSWindow*)window useCache:(BOOL)useCache;
     56 - (NSBezierPath*)bezierPathForRect:(NSRect)rect;
     57 
     58 @end  // TabView(Private)
     59 
     60 @implementation TabView
     61 
     62 @synthesize state = state_;
     63 @synthesize hoverAlpha = hoverAlpha_;
     64 @synthesize alertAlpha = alertAlpha_;
     65 @synthesize closing = closing_;
     66 
     67 - (id)initWithFrame:(NSRect)frame {
     68   self = [super initWithFrame:frame];
     69   if (self) {
     70     [self setShowsDivider:NO];
     71     // TODO(alcor): register for theming
     72   }
     73   return self;
     74 }
     75 
     76 - (void)awakeFromNib {
     77   [self setShowsDivider:NO];
     78 }
     79 
     80 - (void)dealloc {
     81   // Cancel any delayed requests that may still be pending (drags or hover).
     82   [NSObject cancelPreviousPerformRequestsWithTarget:self];
     83   [super dealloc];
     84 }
     85 
     86 // Called to obtain the context menu for when the user hits the right mouse
     87 // button (or control-clicks). (Note that -rightMouseDown: is *not* called for
     88 // control-click.)
     89 - (NSMenu*)menu {
     90   if ([self isClosing])
     91     return nil;
     92 
     93   // Sheets, being window-modal, should block contextual menus. For some reason
     94   // they do not. Disallow them ourselves.
     95   if ([[self window] attachedSheet])
     96     return nil;
     97 
     98   return [controller_ menu];
     99 }
    100 
    101 // Overridden so that mouse clicks come to this view (the parent of the
    102 // hierarchy) first. We want to handle clicks and drags in this class and
    103 // leave the background button for display purposes only.
    104 - (BOOL)acceptsFirstMouse:(NSEvent*)theEvent {
    105   return YES;
    106 }
    107 
    108 - (void)mouseEntered:(NSEvent*)theEvent {
    109   isMouseInside_ = YES;
    110   [self resetLastGlowUpdateTime];
    111   [self adjustGlowValue];
    112 }
    113 
    114 - (void)mouseMoved:(NSEvent*)theEvent {
    115   hoverPoint_ = [self convertPoint:[theEvent locationInWindow]
    116                           fromView:nil];
    117   [self setNeedsDisplay:YES];
    118 }
    119 
    120 - (void)mouseExited:(NSEvent*)theEvent {
    121   isMouseInside_ = NO;
    122   hoverHoldEndTime_ =
    123       [NSDate timeIntervalSinceReferenceDate] + kHoverHoldDuration;
    124   [self resetLastGlowUpdateTime];
    125   [self adjustGlowValue];
    126 }
    127 
    128 - (void)setTrackingEnabled:(BOOL)enabled {
    129   if (![closeButton_ isHidden]) {
    130     [closeButton_ setTrackingEnabled:enabled];
    131   }
    132 }
    133 
    134 // Determines which view a click in our frame actually hit. It's either this
    135 // view or our child close button.
    136 - (NSView*)hitTest:(NSPoint)aPoint {
    137   NSPoint viewPoint = [self convertPoint:aPoint fromView:[self superview]];
    138   NSRect frame = [self frame];
    139 
    140   // Reduce the width of the hit rect slightly to remove the overlap
    141   // between adjacent tabs.  The drawing code in TabCell has the top
    142   // corners of the tab inset by height*2/3, so we inset by half of
    143   // that here.  This doesn't completely eliminate the overlap, but it
    144   // works well enough.
    145   NSRect hitRect = NSInsetRect(frame, frame.size.height / 3.0f, 0);
    146   if (![closeButton_ isHidden])
    147     if (NSPointInRect(viewPoint, [closeButton_ frame])) return closeButton_;
    148   if (NSPointInRect(aPoint, hitRect)) return self;
    149   return nil;
    150 }
    151 
    152 // Returns |YES| if this tab can be torn away into a new window.
    153 - (BOOL)canBeDragged {
    154   if ([self isClosing])
    155     return NO;
    156   NSWindowController* controller = [sourceWindow_ windowController];
    157   if ([controller isKindOfClass:[TabWindowController class]]) {
    158     TabWindowController* realController =
    159         static_cast<TabWindowController*>(controller);
    160     return [realController isTabDraggable:self];
    161   }
    162   return YES;
    163 }
    164 
    165 // Returns an array of controllers that could be a drop target, ordered front to
    166 // back. It has to be of the appropriate class, and visible (obviously). Note
    167 // that the window cannot be a target for itself.
    168 - (NSArray*)dropTargetsForController:(TabWindowController*)dragController {
    169   NSMutableArray* targets = [NSMutableArray array];
    170   NSWindow* dragWindow = [dragController window];
    171   for (NSWindow* window in [NSApp orderedWindows]) {
    172     if (window == dragWindow) continue;
    173     if (![window isVisible]) continue;
    174     // Skip windows on the wrong space.
    175     if ([window respondsToSelector:@selector(isOnActiveSpace)]) {
    176       if (![window performSelector:@selector(isOnActiveSpace)])
    177         continue;
    178     } else {
    179       // TODO(davidben): When we stop supporting 10.5, this can be
    180       // removed.
    181       //
    182       // We don't cache the workspace of |dragWindow| because it may
    183       // move around spaces.
    184       if ([self getWorkspaceID:dragWindow useCache:NO] !=
    185           [self getWorkspaceID:window useCache:YES])
    186         continue;
    187     }
    188     NSWindowController* controller = [window windowController];
    189     if ([controller isKindOfClass:[TabWindowController class]]) {
    190       TabWindowController* realController =
    191           static_cast<TabWindowController*>(controller);
    192       if ([realController canReceiveFrom:dragController])
    193         [targets addObject:controller];
    194     }
    195   }
    196   return targets;
    197 }
    198 
    199 // Call to clear out transient weak references we hold during drags.
    200 - (void)resetDragControllers {
    201   draggedController_ = nil;
    202   dragWindow_ = nil;
    203   dragOverlay_ = nil;
    204   sourceController_ = nil;
    205   sourceWindow_ = nil;
    206   targetController_ = nil;
    207   workspaceIDCache_.clear();
    208 }
    209 
    210 // Sets whether the window background should be visible or invisible when
    211 // dragging a tab. The background should be invisible when the mouse is over a
    212 // potential drop target for the tab (the tab strip). It should be visible when
    213 // there's no drop target so the window looks more fully realized and ready to
    214 // become a stand-alone window.
    215 - (void)setWindowBackgroundVisibility:(BOOL)shouldBeVisible {
    216   if (chromeIsVisible_ == shouldBeVisible)
    217     return;
    218 
    219   // There appears to be a race-condition in CoreAnimation where if we use
    220   // animators to set the alpha values, we can't guarantee that we cancel them.
    221   // This has the side effect of sometimes leaving the dragged window
    222   // translucent or invisible. As a result, don't animate the alpha change.
    223   [[draggedController_ overlayWindow] setAlphaValue:1.0];
    224   if (targetController_) {
    225     [dragWindow_ setAlphaValue:0.0];
    226     [[draggedController_ overlayWindow] setHasShadow:YES];
    227     [[targetController_ window] makeMainWindow];
    228   } else {
    229     [dragWindow_ setAlphaValue:0.5];
    230     [[draggedController_ overlayWindow] setHasShadow:NO];
    231     [[draggedController_ window] makeMainWindow];
    232   }
    233   chromeIsVisible_ = shouldBeVisible;
    234 }
    235 
    236 // Handle clicks and drags in this button. We get here because we have
    237 // overridden acceptsFirstMouse: and the click is within our bounds.
    238 - (void)mouseDown:(NSEvent*)theEvent {
    239   if ([self isClosing])
    240     return;
    241 
    242   NSPoint downLocation = [theEvent locationInWindow];
    243 
    244   // Record the state of the close button here, because selecting the tab will
    245   // unhide it.
    246   BOOL closeButtonActive = [closeButton_ isHidden] ? NO : YES;
    247 
    248   // During the tab closure animation (in particular, during rapid tab closure),
    249   // we may get incorrectly hit with a mouse down. If it should have gone to the
    250   // close button, we send it there -- it should then track the mouse, so we
    251   // don't have to worry about mouse ups.
    252   if (closeButtonActive && [controller_ inRapidClosureMode]) {
    253     NSPoint hitLocation = [[self superview] convertPoint:downLocation
    254                                                 fromView:nil];
    255     if ([self hitTest:hitLocation] == closeButton_) {
    256       [closeButton_ mouseDown:theEvent];
    257       return;
    258     }
    259   }
    260 
    261   // Fire the action to select the tab.
    262   if ([[controller_ target] respondsToSelector:[controller_ action]])
    263     [[controller_ target] performSelector:[controller_ action]
    264                                withObject:self];
    265 
    266   [self resetDragControllers];
    267 
    268   // Resolve overlay back to original window.
    269   sourceWindow_ = [self window];
    270   if ([sourceWindow_ isKindOfClass:[NSPanel class]]) {
    271     sourceWindow_ = [sourceWindow_ parentWindow];
    272   }
    273 
    274   sourceWindowFrame_ = [sourceWindow_ frame];
    275   sourceTabFrame_ = [self frame];
    276   sourceController_ = [sourceWindow_ windowController];
    277   tabWasDragged_ = NO;
    278   tearTime_ = 0.0;
    279   draggingWithinTabStrip_ = YES;
    280   chromeIsVisible_ = NO;
    281 
    282   // If there's more than one potential window to be a drop target, we want to
    283   // treat a drag of a tab just like dragging around a tab that's already
    284   // detached. Note that unit tests might have |-numberOfTabs| reporting zero
    285   // since the model won't be fully hooked up. We need to be prepared for that
    286   // and not send them into the "magnetic" codepath.
    287   NSArray* targets = [self dropTargetsForController:sourceController_];
    288   moveWindowOnDrag_ =
    289       ([sourceController_ numberOfTabs] < 2 && ![targets count]) ||
    290       ![self canBeDragged] ||
    291       ![sourceController_ tabDraggingAllowed];
    292   // If we are dragging a tab, a window with a single tab should immediately
    293   // snap off and not drag within the tab strip.
    294   if (!moveWindowOnDrag_)
    295     draggingWithinTabStrip_ = [sourceController_ numberOfTabs] > 1;
    296 
    297   dragOrigin_ = [NSEvent mouseLocation];
    298 
    299   // If the tab gets torn off, the tab controller will be removed from the tab
    300   // strip and then deallocated. This will also result in *us* being
    301   // deallocated. Both these are bad, so we prevent this by retaining the
    302   // controller.
    303   scoped_nsobject<TabController> controller([controller_ retain]);
    304 
    305   // Because we move views between windows, we need to handle the event loop
    306   // ourselves. Ideally we should use the standard event loop.
    307   while (1) {
    308     const NSUInteger mask =
    309         NSLeftMouseUpMask | NSLeftMouseDraggedMask | NSKeyUpMask;
    310     theEvent =
    311         [NSApp nextEventMatchingMask:mask
    312                            untilDate:[NSDate distantFuture]
    313                               inMode:NSDefaultRunLoopMode dequeue:YES];
    314     NSEventType type = [theEvent type];
    315     if (type == NSKeyUp) {
    316       if ([theEvent keyCode] == kVK_Escape) {
    317         // Cancel the drag and restore the previous state.
    318         if (draggingWithinTabStrip_) {
    319           // Simply pretend the tab wasn't dragged (far enough).
    320           tabWasDragged_ = NO;
    321         } else {
    322           [targetController_ removePlaceholder];
    323           if ([sourceController_ numberOfTabs] < 2) {
    324             // Revert to a single-tab window.
    325             targetController_ = nil;
    326           } else {
    327             // Change the target to the source controller.
    328             targetController_ = sourceController_;
    329             [targetController_ insertPlaceholderForTab:self
    330                                                  frame:sourceTabFrame_
    331                                          yStretchiness:0];
    332           }
    333         }
    334         // Call the |mouseUp:| code to end the drag.
    335         [self mouseUp:theEvent];
    336         break;
    337       }
    338     } else if (type == NSLeftMouseDragged) {
    339       [self mouseDragged:theEvent];
    340     } else if (type == NSLeftMouseUp) {
    341       NSPoint upLocation = [theEvent locationInWindow];
    342       CGFloat dx = upLocation.x - downLocation.x;
    343       CGFloat dy = upLocation.y - downLocation.y;
    344 
    345       // During rapid tab closure (mashing tab close buttons), we may get hit
    346       // with a mouse down. As long as the mouse up is over the close button,
    347       // and the mouse hasn't moved too much, we close the tab.
    348       if (closeButtonActive &&
    349           (dx*dx + dy*dy) <= kRapidCloseDist*kRapidCloseDist &&
    350           [controller inRapidClosureMode]) {
    351         NSPoint hitLocation =
    352             [[self superview] convertPoint:[theEvent locationInWindow]
    353                                   fromView:nil];
    354         if ([self hitTest:hitLocation] == closeButton_) {
    355           [controller closeTab:self];
    356           break;
    357         }
    358       }
    359 
    360       [self mouseUp:theEvent];
    361       break;
    362     } else {
    363       // TODO(viettrungluu): [crbug.com/23830] We can receive right-mouse-ups
    364       // (and maybe even others?) for reasons I don't understand. So we
    365       // explicitly check for both events we're expecting, and log others. We
    366       // should figure out what's going on.
    367       LOG(WARNING) << "Spurious event received of type " << type << ".";
    368     }
    369   }
    370 }
    371 
    372 - (void)mouseDragged:(NSEvent*)theEvent {
    373   // Special-case this to keep the logic below simpler.
    374   if (moveWindowOnDrag_) {
    375     if ([sourceController_ windowMovementAllowed]) {
    376       NSPoint thisPoint = [NSEvent mouseLocation];
    377       NSPoint origin = sourceWindowFrame_.origin;
    378       origin.x += (thisPoint.x - dragOrigin_.x);
    379       origin.y += (thisPoint.y - dragOrigin_.y);
    380       [sourceWindow_ setFrameOrigin:NSMakePoint(origin.x, origin.y)];
    381     }  // else do nothing.
    382     return;
    383   }
    384 
    385   // First, go through the magnetic drag cycle. We break out of this if
    386   // "stretchiness" ever exceeds a set amount.
    387   tabWasDragged_ = YES;
    388 
    389   if (draggingWithinTabStrip_) {
    390     NSPoint thisPoint = [NSEvent mouseLocation];
    391     CGFloat stretchiness = thisPoint.y - dragOrigin_.y;
    392     stretchiness = copysign(sqrtf(fabs(stretchiness))/sqrtf(kTearDistance),
    393                             stretchiness) / 2.0;
    394     CGFloat offset = thisPoint.x - dragOrigin_.x;
    395     if (fabsf(offset) > 100) stretchiness = 0;
    396     [sourceController_ insertPlaceholderForTab:self
    397                                          frame:NSOffsetRect(sourceTabFrame_,
    398                                                             offset, 0)
    399                                  yStretchiness:stretchiness];
    400     // Check that we haven't pulled the tab too far to start a drag. This
    401     // can include either pulling it too far down, or off the side of the tab
    402     // strip that would cause it to no longer be fully visible.
    403     BOOL stillVisible = [sourceController_ isTabFullyVisible:self];
    404     CGFloat tearForce = fabs(thisPoint.y - dragOrigin_.y);
    405     if ([sourceController_ tabTearingAllowed] &&
    406         (tearForce > kTearDistance || !stillVisible)) {
    407       draggingWithinTabStrip_ = NO;
    408       // When you finally leave the strip, we treat that as the origin.
    409       dragOrigin_.x = thisPoint.x;
    410     } else {
    411       // Still dragging within the tab strip, wait for the next drag event.
    412       return;
    413     }
    414   }
    415 
    416   // Do not start dragging until the user has "torn" the tab off by
    417   // moving more than 3 pixels.
    418   NSDate* targetDwellDate = nil;  // The date this target was first chosen.
    419 
    420   NSPoint thisPoint = [NSEvent mouseLocation];
    421 
    422   // Iterate over possible targets checking for the one the mouse is in.
    423   // If the tab is just in the frame, bring the window forward to make it
    424   // easier to drop something there. If it's in the tab strip, set the new
    425   // target so that it pops into that window. We can't cache this because we
    426   // need the z-order to be correct.
    427   NSArray* targets = [self dropTargetsForController:draggedController_];
    428   TabWindowController* newTarget = nil;
    429   for (TabWindowController* target in targets) {
    430     NSRect windowFrame = [[target window] frame];
    431     if (NSPointInRect(thisPoint, windowFrame)) {
    432       [[target window] orderFront:self];
    433       NSRect tabStripFrame = [[target tabStripView] frame];
    434       tabStripFrame.origin = [[target window]
    435                               convertBaseToScreen:tabStripFrame.origin];
    436       if (NSPointInRect(thisPoint, tabStripFrame)) {
    437         newTarget = target;
    438       }
    439       break;
    440     }
    441   }
    442 
    443   // If we're now targeting a new window, re-layout the tabs in the old
    444   // target and reset how long we've been hovering over this new one.
    445   if (targetController_ != newTarget) {
    446     targetDwellDate = [NSDate date];
    447     [targetController_ removePlaceholder];
    448     targetController_ = newTarget;
    449     if (!newTarget) {
    450       tearTime_ = [NSDate timeIntervalSinceReferenceDate];
    451       tearOrigin_ = [dragWindow_ frame].origin;
    452     }
    453   }
    454 
    455   // Create or identify the dragged controller.
    456   if (!draggedController_) {
    457     // Get rid of any placeholder remaining in the original source window.
    458     [sourceController_ removePlaceholder];
    459 
    460     // Detach from the current window and put it in a new window. If there are
    461     // no more tabs remaining after detaching, the source window is about to
    462     // go away (it's been autoreleased) so we need to ensure we don't reference
    463     // it any more. In that case the new controller becomes our source
    464     // controller.
    465     draggedController_ = [sourceController_ detachTabToNewWindow:self];
    466     dragWindow_ = [draggedController_ window];
    467     [dragWindow_ setAlphaValue:0.0];
    468     if (![sourceController_ hasLiveTabs]) {
    469       sourceController_ = draggedController_;
    470       sourceWindow_ = dragWindow_;
    471     }
    472 
    473     // If dragging the tab only moves the current window, do not show overlay
    474     // so that sheets stay on top of the window.
    475     // Bring the target window to the front and make sure it has a border.
    476     [dragWindow_ setLevel:NSFloatingWindowLevel];
    477     [dragWindow_ setHasShadow:YES];
    478     [dragWindow_ orderFront:nil];
    479     [dragWindow_ makeMainWindow];
    480     [draggedController_ showOverlay];
    481     dragOverlay_ = [draggedController_ overlayWindow];
    482     // Force the new tab button to be hidden. We'll reset it on mouse up.
    483     [draggedController_ showNewTabButton:NO];
    484     tearTime_ = [NSDate timeIntervalSinceReferenceDate];
    485     tearOrigin_ = sourceWindowFrame_.origin;
    486   }
    487 
    488   // TODO(pinkerton): http://crbug.com/25682 demonstrates a way to get here by
    489   // some weird circumstance that doesn't first go through mouseDown:. We
    490   // really shouldn't go any farther.
    491   if (!draggedController_ || !sourceController_)
    492     return;
    493 
    494   // When the user first tears off the window, we want slide the window to
    495   // the current mouse location (to reduce the jarring appearance). We do this
    496   // by calling ourselves back with additional mouseDragged calls (not actual
    497   // events). |tearProgress| is a normalized measure of how far through this
    498   // tear "animation" (of length kTearDuration) we are and has values [0..1].
    499   // We use sqrt() so the animation is non-linear (slow down near the end
    500   // point).
    501   NSTimeInterval tearProgress =
    502       [NSDate timeIntervalSinceReferenceDate] - tearTime_;
    503   tearProgress /= kTearDuration;  // Normalize.
    504   tearProgress = sqrtf(MAX(MIN(tearProgress, 1.0), 0.0));
    505 
    506   // Move the dragged window to the right place on the screen.
    507   NSPoint origin = sourceWindowFrame_.origin;
    508   origin.x += (thisPoint.x - dragOrigin_.x);
    509   origin.y += (thisPoint.y - dragOrigin_.y);
    510 
    511   if (tearProgress < 1) {
    512     // If the tear animation is not complete, call back to ourself with the
    513     // same event to animate even if the mouse isn't moving. We need to make
    514     // sure these get cancelled in mouseUp:.
    515     [NSObject cancelPreviousPerformRequestsWithTarget:self];
    516     [self performSelector:@selector(mouseDragged:)
    517                withObject:theEvent
    518                afterDelay:1.0f/30.0f];
    519 
    520     // Set the current window origin based on how far we've progressed through
    521     // the tear animation.
    522     origin.x = (1 - tearProgress) * tearOrigin_.x + tearProgress * origin.x;
    523     origin.y = (1 - tearProgress) * tearOrigin_.y + tearProgress * origin.y;
    524   }
    525 
    526   if (targetController_) {
    527     // In order to "snap" two windows of different sizes together at their
    528     // toolbar, we can't just use the origin of the target frame. We also have
    529     // to take into consideration the difference in height.
    530     NSRect targetFrame = [[targetController_ window] frame];
    531     NSRect sourceFrame = [dragWindow_ frame];
    532     origin.y = NSMinY(targetFrame) +
    533                 (NSHeight(targetFrame) - NSHeight(sourceFrame));
    534   }
    535   [dragWindow_ setFrameOrigin:NSMakePoint(origin.x, origin.y)];
    536 
    537   // If we're not hovering over any window, make the window fully
    538   // opaque. Otherwise, find where the tab might be dropped and insert
    539   // a placeholder so it appears like it's part of that window.
    540   if (targetController_) {
    541     if (![[targetController_ window] isKeyWindow]) {
    542       // && ([targetDwellDate timeIntervalSinceNow] < -REQUIRED_DWELL)) {
    543       [[targetController_ window] orderFront:nil];
    544       targetDwellDate = nil;
    545     }
    546 
    547     // Compute where placeholder should go and insert it into the
    548     // destination tab strip.
    549     TabView* draggedTabView = (TabView*)[draggedController_ selectedTabView];
    550     NSRect tabFrame = [draggedTabView frame];
    551     tabFrame.origin = [dragWindow_ convertBaseToScreen:tabFrame.origin];
    552     tabFrame.origin = [[targetController_ window]
    553                         convertScreenToBase:tabFrame.origin];
    554     tabFrame = [[targetController_ tabStripView]
    555                 convertRect:tabFrame fromView:nil];
    556     [targetController_ insertPlaceholderForTab:self
    557                                          frame:tabFrame
    558                                  yStretchiness:0];
    559     [targetController_ layoutTabs];
    560   } else {
    561     [dragWindow_ makeKeyAndOrderFront:nil];
    562   }
    563 
    564   // Adjust the visibility of the window background. If there is a drop target,
    565   // we want to hide the window background so the tab stands out for
    566   // positioning. If not, we want to show it so it looks like a new window will
    567   // be realized.
    568   BOOL chromeShouldBeVisible = targetController_ == nil;
    569   [self setWindowBackgroundVisibility:chromeShouldBeVisible];
    570 }
    571 
    572 - (void)mouseUp:(NSEvent*)theEvent {
    573   // The drag/click is done. If the user dragged the mouse, finalize the drag
    574   // and clean up.
    575 
    576   // Special-case this to keep the logic below simpler.
    577   if (moveWindowOnDrag_)
    578     return;
    579 
    580   // Cancel any delayed -mouseDragged: requests that may still be pending.
    581   [NSObject cancelPreviousPerformRequestsWithTarget:self];
    582 
    583   // TODO(pinkerton): http://crbug.com/25682 demonstrates a way to get here by
    584   // some weird circumstance that doesn't first go through mouseDown:. We
    585   // really shouldn't go any farther.
    586   if (!sourceController_)
    587     return;
    588 
    589   // We are now free to re-display the new tab button in the window we're
    590   // dragging. It will show when the next call to -layoutTabs (which happens
    591   // indrectly by several of the calls below, such as removing the placeholder).
    592   [draggedController_ showNewTabButton:YES];
    593 
    594   if (draggingWithinTabStrip_) {
    595     if (tabWasDragged_) {
    596       // Move tab to new location.
    597       DCHECK([sourceController_ numberOfTabs]);
    598       TabWindowController* dropController = sourceController_;
    599       [dropController moveTabView:[dropController selectedTabView]
    600                    fromController:nil];
    601     }
    602   } else if (targetController_) {
    603     // Move between windows. If |targetController_| is nil, we're not dropping
    604     // into any existing window.
    605     NSView* draggedTabView = [draggedController_ selectedTabView];
    606     [targetController_ moveTabView:draggedTabView
    607                     fromController:draggedController_];
    608     // Force redraw to avoid flashes of old content before returning to event
    609     // loop.
    610     [[targetController_ window] display];
    611     [targetController_ showWindow:nil];
    612     [draggedController_ removeOverlay];
    613   } else {
    614     // Only move the window around on screen. Make sure it's set back to
    615     // normal state (fully opaque, has shadow, has key, etc).
    616     [draggedController_ removeOverlay];
    617     // Don't want to re-show the window if it was closed during the drag.
    618     if ([dragWindow_ isVisible]) {
    619       [dragWindow_ setAlphaValue:1.0];
    620       [dragOverlay_ setHasShadow:NO];
    621       [dragWindow_ setHasShadow:YES];
    622       [dragWindow_ makeKeyAndOrderFront:nil];
    623     }
    624     [[draggedController_ window] setLevel:NSNormalWindowLevel];
    625     [draggedController_ removePlaceholder];
    626   }
    627   [sourceController_ removePlaceholder];
    628   chromeIsVisible_ = YES;
    629 
    630   [self resetDragControllers];
    631 }
    632 
    633 - (void)otherMouseUp:(NSEvent*)theEvent {
    634   if ([self isClosing])
    635     return;
    636 
    637   // Support middle-click-to-close.
    638   if ([theEvent buttonNumber] == 2) {
    639     // |-hitTest:| takes a location in the superview's coordinates.
    640     NSPoint upLocation =
    641         [[self superview] convertPoint:[theEvent locationInWindow]
    642                               fromView:nil];
    643     // If the mouse up occurred in our view or over the close button, then
    644     // close.
    645     if ([self hitTest:upLocation])
    646       [controller_ closeTab:self];
    647   }
    648 }
    649 
    650 - (void)drawRect:(NSRect)dirtyRect {
    651   const CGFloat lineWidth = [self cr_lineWidth];
    652 
    653   NSGraphicsContext* context = [NSGraphicsContext currentContext];
    654   [context saveGraphicsState];
    655 
    656   ThemeService* themeProvider =
    657       static_cast<ThemeService*>([[self window] themeProvider]);
    658   [context setPatternPhase:[[self window] themePatternPhase]];
    659 
    660   NSRect rect = [self bounds];
    661   NSBezierPath* path = [self bezierPathForRect:rect];
    662 
    663   BOOL selected = [self state];
    664   // Don't draw the window/tab bar background when selected, since the tab
    665   // background overlay drawn over it (see below) will be fully opaque.
    666   BOOL hasBackgroundImage = NO;
    667   if (!selected) {
    668     // ui::ThemeProvider::HasCustomImage is true only if the theme provides the
    669     // image. However, even if the theme doesn't provide a tab background, the
    670     // theme machinery will make one if given a frame image. See
    671     // BrowserThemePack::GenerateTabBackgroundImages for details.
    672     hasBackgroundImage = themeProvider &&
    673         (themeProvider->HasCustomImage(IDR_THEME_TAB_BACKGROUND) ||
    674          themeProvider->HasCustomImage(IDR_THEME_FRAME));
    675 
    676     NSColor* backgroundImageColor = hasBackgroundImage ?
    677         themeProvider->GetNSImageColorNamed(IDR_THEME_TAB_BACKGROUND, true) :
    678         nil;
    679 
    680     if (backgroundImageColor) {
    681       [backgroundImageColor set];
    682       [path fill];
    683     } else {
    684       // Use the window's background color rather than |[NSColor
    685       // windowBackgroundColor]|, which gets confused by the fullscreen window.
    686       // (The result is the same for normal, non-fullscreen windows.)
    687       [[[self window] backgroundColor] set];
    688       [path fill];
    689       [[NSColor colorWithCalibratedWhite:1.0 alpha:0.3] set];
    690       [path fill];
    691     }
    692   }
    693 
    694   [context saveGraphicsState];
    695   [path addClip];
    696 
    697   // Use the same overlay for the selected state and for hover and alert glows;
    698   // for the selected state, it's fully opaque.
    699   CGFloat hoverAlpha = [self hoverAlpha];
    700   CGFloat alertAlpha = [self alertAlpha];
    701   if (selected || hoverAlpha > 0 || alertAlpha > 0) {
    702     // Draw the selected background / glow overlay.
    703     [context saveGraphicsState];
    704     CGContextRef cgContext = static_cast<CGContextRef>([context graphicsPort]);
    705     CGContextBeginTransparencyLayer(cgContext, 0);
    706     if (!selected) {
    707       // The alert glow overlay is like the selected state but at most at most
    708       // 80% opaque. The hover glow brings up the overlay's opacity at most 50%.
    709       CGFloat backgroundAlpha = 0.8 * alertAlpha;
    710       backgroundAlpha += (1 - backgroundAlpha) * 0.5 * hoverAlpha;
    711       CGContextSetAlpha(cgContext, backgroundAlpha);
    712     }
    713     [path addClip];
    714     [context saveGraphicsState];
    715     [super drawBackground];
    716     [context restoreGraphicsState];
    717 
    718     // Draw a mouse hover gradient for the default themes.
    719     if (!selected && hoverAlpha > 0) {
    720       if (themeProvider && !hasBackgroundImage) {
    721         scoped_nsobject<NSGradient> glow([NSGradient alloc]);
    722         [glow initWithStartingColor:[NSColor colorWithCalibratedWhite:1.0
    723                                         alpha:1.0 * hoverAlpha]
    724                         endingColor:[NSColor colorWithCalibratedWhite:1.0
    725                                                                 alpha:0.0]];
    726 
    727         NSPoint point = hoverPoint_;
    728         point.y = NSHeight(rect);
    729         [glow drawFromCenter:point
    730                       radius:0.0
    731                     toCenter:point
    732                       radius:NSWidth(rect) / 3.0
    733                      options:NSGradientDrawsBeforeStartingLocation];
    734 
    735         [glow drawInBezierPath:path relativeCenterPosition:hoverPoint_];
    736       }
    737     }
    738 
    739     CGContextEndTransparencyLayer(cgContext);
    740     [context restoreGraphicsState];
    741   }
    742 
    743   BOOL active = [[self window] isKeyWindow] || [[self window] isMainWindow];
    744   CGFloat borderAlpha = selected ? (active ? 0.3 : 0.2) : 0.2;
    745   NSColor* borderColor = [NSColor colorWithDeviceWhite:0.0 alpha:borderAlpha];
    746   NSColor* highlightColor = themeProvider ? themeProvider->GetNSColor(
    747       themeProvider->UsingDefaultTheme() ?
    748           ThemeService::COLOR_TOOLBAR_BEZEL :
    749           ThemeService::COLOR_TOOLBAR, true) : nil;
    750 
    751   // Draw the top inner highlight within the currently selected tab if using
    752   // the default theme.
    753   if (selected && themeProvider && themeProvider->UsingDefaultTheme()) {
    754     NSAffineTransform* highlightTransform = [NSAffineTransform transform];
    755     [highlightTransform translateXBy:lineWidth yBy:-lineWidth];
    756     scoped_nsobject<NSBezierPath> highlightPath([path copy]);
    757     [highlightPath transformUsingAffineTransform:highlightTransform];
    758     [highlightColor setStroke];
    759     [highlightPath setLineWidth:lineWidth];
    760     [highlightPath stroke];
    761     highlightTransform = [NSAffineTransform transform];
    762     [highlightTransform translateXBy:-2 * lineWidth yBy:0.0];
    763     [highlightPath transformUsingAffineTransform:highlightTransform];
    764     [highlightPath stroke];
    765   }
    766 
    767   [context restoreGraphicsState];
    768 
    769   // Draw the top stroke.
    770   [context saveGraphicsState];
    771   [borderColor set];
    772   [path setLineWidth:lineWidth];
    773   [path stroke];
    774   [context restoreGraphicsState];
    775 
    776   // Mimic the tab strip's bottom border, which consists of a dark border
    777   // and light highlight.
    778   if (!selected) {
    779     [path addClip];
    780     NSRect borderRect = rect;
    781     borderRect.origin.y = lineWidth;
    782     borderRect.size.height = lineWidth;
    783     [borderColor set];
    784     NSRectFillUsingOperation(borderRect, NSCompositeSourceOver);
    785 
    786     borderRect.origin.y = 0;
    787     [highlightColor set];
    788     NSRectFillUsingOperation(borderRect, NSCompositeSourceOver);
    789   }
    790 
    791   [context restoreGraphicsState];
    792 }
    793 
    794 - (void)viewDidMoveToWindow {
    795   [super viewDidMoveToWindow];
    796   if ([self window]) {
    797     [controller_ updateTitleColor];
    798   }
    799 }
    800 
    801 - (void)setClosing:(BOOL)closing {
    802   closing_ = closing;  // Safe because the property is nonatomic.
    803   // When closing, ensure clicks to the close button go nowhere.
    804   if (closing) {
    805     [closeButton_ setTarget:nil];
    806     [closeButton_ setAction:nil];
    807   }
    808 }
    809 
    810 - (void)startAlert {
    811   // Do not start a new alert while already alerting or while in a decay cycle.
    812   if (alertState_ == tabs::kAlertNone) {
    813     alertState_ = tabs::kAlertRising;
    814     [self resetLastGlowUpdateTime];
    815     [self adjustGlowValue];
    816   }
    817 }
    818 
    819 - (void)cancelAlert {
    820   if (alertState_ != tabs::kAlertNone) {
    821     alertState_ = tabs::kAlertFalling;
    822     alertHoldEndTime_ =
    823         [NSDate timeIntervalSinceReferenceDate] + kGlowUpdateInterval;
    824     [self resetLastGlowUpdateTime];
    825     [self adjustGlowValue];
    826   }
    827 }
    828 
    829 - (BOOL)accessibilityIsIgnored {
    830   return NO;
    831 }
    832 
    833 - (NSArray*)accessibilityActionNames {
    834   NSArray* parentActions = [super accessibilityActionNames];
    835 
    836   return [parentActions arrayByAddingObject:NSAccessibilityPressAction];
    837 }
    838 
    839 - (NSArray*)accessibilityAttributeNames {
    840   NSMutableArray* attributes =
    841       [[super accessibilityAttributeNames] mutableCopy];
    842   [attributes addObject:NSAccessibilityTitleAttribute];
    843   [attributes addObject:NSAccessibilityEnabledAttribute];
    844 
    845   return attributes;
    846 }
    847 
    848 - (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute {
    849   if ([attribute isEqual:NSAccessibilityTitleAttribute])
    850     return NO;
    851 
    852   if ([attribute isEqual:NSAccessibilityEnabledAttribute])
    853     return NO;
    854 
    855   return [super accessibilityIsAttributeSettable:attribute];
    856 }
    857 
    858 - (id)accessibilityAttributeValue:(NSString*)attribute {
    859   if ([attribute isEqual:NSAccessibilityRoleAttribute])
    860     return l10n_util::GetNSStringWithFixup(IDS_ACCNAME_TAB);
    861 
    862   if ([attribute isEqual:NSAccessibilityTitleAttribute])
    863     return [controller_ title];
    864 
    865   if ([attribute isEqual:NSAccessibilityEnabledAttribute])
    866     return [NSNumber numberWithBool:YES];
    867 
    868   return [super accessibilityAttributeValue:attribute];
    869 }
    870 
    871 - (ViewID)viewID {
    872   return VIEW_ID_TAB;
    873 }
    874 
    875 @end  // @implementation TabView
    876 
    877 @implementation TabView (TabControllerInterface)
    878 
    879 - (void)setController:(TabController*)controller {
    880   controller_ = controller;
    881 }
    882 
    883 @end  // @implementation TabView (TabControllerInterface)
    884 
    885 @implementation TabView(Private)
    886 
    887 - (void)resetLastGlowUpdateTime {
    888   lastGlowUpdate_ = [NSDate timeIntervalSinceReferenceDate];
    889 }
    890 
    891 - (NSTimeInterval)timeElapsedSinceLastGlowUpdate {
    892   return [NSDate timeIntervalSinceReferenceDate] - lastGlowUpdate_;
    893 }
    894 
    895 - (void)adjustGlowValue {
    896   // A time interval long enough to represent no update.
    897   const NSTimeInterval kNoUpdate = 1000000;
    898 
    899   // Time until next update for either glow.
    900   NSTimeInterval nextUpdate = kNoUpdate;
    901 
    902   NSTimeInterval elapsed = [self timeElapsedSinceLastGlowUpdate];
    903   NSTimeInterval currentTime = [NSDate timeIntervalSinceReferenceDate];
    904 
    905   // TODO(viettrungluu): <http://crbug.com/30617> -- split off the stuff below
    906   // into a pure function and add a unit test.
    907 
    908   CGFloat hoverAlpha = [self hoverAlpha];
    909   if (isMouseInside_) {
    910     // Increase hover glow until it's 1.
    911     if (hoverAlpha < 1) {
    912       hoverAlpha = MIN(hoverAlpha + elapsed / kHoverShowDuration, 1);
    913       [self setHoverAlpha:hoverAlpha];
    914       nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
    915     }  // Else already 1 (no update needed).
    916   } else {
    917     if (currentTime >= hoverHoldEndTime_) {
    918       // No longer holding, so decrease hover glow until it's 0.
    919       if (hoverAlpha > 0) {
    920         hoverAlpha = MAX(hoverAlpha - elapsed / kHoverHideDuration, 0);
    921         [self setHoverAlpha:hoverAlpha];
    922         nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
    923       }  // Else already 0 (no update needed).
    924     } else {
    925       // Schedule update for end of hold time.
    926       nextUpdate = MIN(hoverHoldEndTime_ - currentTime, nextUpdate);
    927     }
    928   }
    929 
    930   CGFloat alertAlpha = [self alertAlpha];
    931   if (alertState_ == tabs::kAlertRising) {
    932     // Increase alert glow until it's 1 ...
    933     alertAlpha = MIN(alertAlpha + elapsed / kAlertShowDuration, 1);
    934     [self setAlertAlpha:alertAlpha];
    935 
    936     // ... and having reached 1, switch to holding.
    937     if (alertAlpha >= 1) {
    938       alertState_ = tabs::kAlertHolding;
    939       alertHoldEndTime_ = currentTime + kAlertHoldDuration;
    940       nextUpdate = MIN(kAlertHoldDuration, nextUpdate);
    941     } else {
    942       nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
    943     }
    944   } else if (alertState_ != tabs::kAlertNone) {
    945     if (alertAlpha > 0) {
    946       if (currentTime >= alertHoldEndTime_) {
    947         // Stop holding, then decrease alert glow (until it's 0).
    948         if (alertState_ == tabs::kAlertHolding) {
    949           alertState_ = tabs::kAlertFalling;
    950           nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
    951         } else {
    952           DCHECK_EQ(tabs::kAlertFalling, alertState_);
    953           alertAlpha = MAX(alertAlpha - elapsed / kAlertHideDuration, 0);
    954           [self setAlertAlpha:alertAlpha];
    955           nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
    956         }
    957       } else {
    958         // Schedule update for end of hold time.
    959         nextUpdate = MIN(alertHoldEndTime_ - currentTime, nextUpdate);
    960       }
    961     } else {
    962       // Done the alert decay cycle.
    963       alertState_ = tabs::kAlertNone;
    964     }
    965   }
    966 
    967   if (nextUpdate < kNoUpdate)
    968     [self performSelector:_cmd withObject:nil afterDelay:nextUpdate];
    969 
    970   [self resetLastGlowUpdateTime];
    971   [self setNeedsDisplay:YES];
    972 }
    973 
    974 // Returns the workspace id of |window|. If |useCache|, then lookup
    975 // and remember the value in |workspaceIDCache_| until the end of the
    976 // current drag.
    977 - (int)getWorkspaceID:(NSWindow*)window useCache:(BOOL)useCache {
    978   CGWindowID windowID = [window windowNumber];
    979   if (useCache) {
    980     std::map<CGWindowID, int>::iterator iter =
    981         workspaceIDCache_.find(windowID);
    982     if (iter != workspaceIDCache_.end())
    983       return iter->second;
    984   }
    985 
    986   int workspace = -1;
    987   // It's possible to query in bulk, but probably not necessary.
    988   base::mac::ScopedCFTypeRef<CFArrayRef> windowIDs(CFArrayCreate(
    989       NULL, reinterpret_cast<const void **>(&windowID), 1, NULL));
    990   base::mac::ScopedCFTypeRef<CFArrayRef> descriptions(
    991       CGWindowListCreateDescriptionFromArray(windowIDs));
    992   DCHECK(CFArrayGetCount(descriptions.get()) <= 1);
    993   if (CFArrayGetCount(descriptions.get()) > 0) {
    994     CFDictionaryRef dict = static_cast<CFDictionaryRef>(
    995         CFArrayGetValueAtIndex(descriptions.get(), 0));
    996     DCHECK(CFGetTypeID(dict) == CFDictionaryGetTypeID());
    997 
    998     // Sanity check the ID.
    999     CFNumberRef otherIDRef = (CFNumberRef)base::mac::GetValueFromDictionary(
   1000         dict, kCGWindowNumber, CFNumberGetTypeID());
   1001     CGWindowID otherID;
   1002     if (otherIDRef &&
   1003         CFNumberGetValue(otherIDRef, kCGWindowIDCFNumberType, &otherID) &&
   1004         otherID == windowID) {
   1005       // And then get the workspace.
   1006       CFNumberRef workspaceRef = (CFNumberRef)base::mac::GetValueFromDictionary(
   1007           dict, kCGWindowWorkspace, CFNumberGetTypeID());
   1008       if (!workspaceRef ||
   1009           !CFNumberGetValue(workspaceRef, kCFNumberIntType, &workspace)) {
   1010         workspace = -1;
   1011       }
   1012     } else {
   1013       NOTREACHED();
   1014     }
   1015   }
   1016   if (useCache) {
   1017     workspaceIDCache_[windowID] = workspace;
   1018   }
   1019   return workspace;
   1020 }
   1021 
   1022 // Returns the bezier path used to draw the tab given the bounds to draw it in.
   1023 - (NSBezierPath*)bezierPathForRect:(NSRect)rect {
   1024   const CGFloat lineWidth = [self cr_lineWidth];
   1025   const CGFloat halfLineWidth = lineWidth / 2.0;
   1026 
   1027   // Outset by halfLineWidth in order to draw on pixels rather than on borders
   1028   // (which would cause blurry pixels). Subtract lineWidth of height to
   1029   // compensate, otherwise clipping will occur.
   1030   rect = NSInsetRect(rect, -halfLineWidth, -halfLineWidth);
   1031   rect.size.height -= lineWidth;
   1032 
   1033   NSPoint bottomLeft = NSMakePoint(NSMinX(rect), NSMinY(rect) + 2 * lineWidth);
   1034   NSPoint bottomRight = NSMakePoint(NSMaxX(rect), NSMinY(rect) + 2 * lineWidth);
   1035   NSPoint topRight =
   1036       NSMakePoint(NSMaxX(rect) - kInsetMultiplier * NSHeight(rect),
   1037                   NSMaxY(rect));
   1038   NSPoint topLeft =
   1039       NSMakePoint(NSMinX(rect)  + kInsetMultiplier * NSHeight(rect),
   1040                   NSMaxY(rect));
   1041 
   1042   CGFloat baseControlPointOutset = NSHeight(rect) * kControlPoint1Multiplier;
   1043   CGFloat bottomControlPointInset = NSHeight(rect) * kControlPoint2Multiplier;
   1044 
   1045   // Outset many of these values by lineWidth to cause the fill to bleed outside
   1046   // the clip area.
   1047   NSBezierPath* path = [NSBezierPath bezierPath];
   1048   [path moveToPoint:NSMakePoint(bottomLeft.x - lineWidth,
   1049                                 bottomLeft.y - (2 * lineWidth))];
   1050   [path lineToPoint:NSMakePoint(bottomLeft.x - lineWidth, bottomLeft.y)];
   1051   [path lineToPoint:bottomLeft];
   1052   [path curveToPoint:topLeft
   1053        controlPoint1:NSMakePoint(bottomLeft.x + baseControlPointOutset,
   1054                                  bottomLeft.y)
   1055        controlPoint2:NSMakePoint(topLeft.x - bottomControlPointInset,
   1056                                  topLeft.y)];
   1057   [path lineToPoint:topRight];
   1058   [path curveToPoint:bottomRight
   1059        controlPoint1:NSMakePoint(topRight.x + bottomControlPointInset,
   1060                                  topRight.y)
   1061        controlPoint2:NSMakePoint(bottomRight.x - baseControlPointOutset,
   1062                                  bottomRight.y)];
   1063   [path lineToPoint:NSMakePoint(bottomRight.x + lineWidth, bottomRight.y)];
   1064   [path lineToPoint:NSMakePoint(bottomRight.x + lineWidth,
   1065                                 bottomRight.y - (2 * lineWidth))];
   1066   return path;
   1067 }
   1068 
   1069 @end  // @implementation TabView(Private)
   1070