• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
20namespace {
21
22// Constants for inset and control points for tab shape.
23const CGFloat kInsetMultiplier = 2.0/3.0;
24const CGFloat kControlPoint1Multiplier = 1.0/3.0;
25const 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.
29const NSTimeInterval kHoverShowDuration = 0.2;
30const NSTimeInterval kHoverHoldDuration = 0.02;
31const NSTimeInterval kHoverHideDuration = 0.4;
32const NSTimeInterval kAlertShowDuration = 0.4;
33const NSTimeInterval kAlertHoldDuration = 0.4;
34const NSTimeInterval kAlertHideDuration = 0.4;
35
36// The default time interval in seconds between glow updates (when
37// increasing/decreasing).
38const NSTimeInterval kGlowUpdateInterval = 0.025;
39
40const CGFloat kTearDistance = 36.0;
41const 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.
45const 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