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