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