| // Copyright (c) 2010 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "chrome/browser/ui/cocoa/tab_view.h" |
| |
| #include "base/logging.h" |
| #import "base/mac/mac_util.h" |
| #include "base/mac/scoped_cftyperef.h" |
| #include "chrome/browser/accessibility/browser_accessibility_state.h" |
| #include "chrome/browser/themes/browser_theme_provider.h" |
| #import "chrome/browser/ui/cocoa/tab_controller.h" |
| #import "chrome/browser/ui/cocoa/tab_window_controller.h" |
| #import "chrome/browser/ui/cocoa/themed_window.h" |
| #import "chrome/browser/ui/cocoa/view_id_util.h" |
| #include "grit/theme_resources.h" |
| |
| namespace { |
| |
| // Constants for inset and control points for tab shape. |
| const CGFloat kInsetMultiplier = 2.0/3.0; |
| const CGFloat kControlPoint1Multiplier = 1.0/3.0; |
| const CGFloat kControlPoint2Multiplier = 3.0/8.0; |
| |
| // The amount of time in seconds during which each type of glow increases, holds |
| // steady, and decreases, respectively. |
| const NSTimeInterval kHoverShowDuration = 0.2; |
| const NSTimeInterval kHoverHoldDuration = 0.02; |
| const NSTimeInterval kHoverHideDuration = 0.4; |
| const NSTimeInterval kAlertShowDuration = 0.4; |
| const NSTimeInterval kAlertHoldDuration = 0.4; |
| const NSTimeInterval kAlertHideDuration = 0.4; |
| |
| // The default time interval in seconds between glow updates (when |
| // increasing/decreasing). |
| const NSTimeInterval kGlowUpdateInterval = 0.025; |
| |
| const CGFloat kTearDistance = 36.0; |
| const NSTimeInterval kTearDuration = 0.333; |
| |
| // This is used to judge whether the mouse has moved during rapid closure; if it |
| // has moved less than the threshold, we want to close the tab. |
| const CGFloat kRapidCloseDist = 2.5; |
| |
| } // namespace |
| |
| @interface TabView(Private) |
| |
| - (void)resetLastGlowUpdateTime; |
| - (NSTimeInterval)timeElapsedSinceLastGlowUpdate; |
| - (void)adjustGlowValue; |
| // TODO(davidben): When we stop supporting 10.5, this can be removed. |
| - (int)getWorkspaceID:(NSWindow*)window useCache:(BOOL)useCache; |
| - (NSBezierPath*)bezierPathForRect:(NSRect)rect; |
| |
| @end // TabView(Private) |
| |
| @implementation TabView |
| |
| @synthesize state = state_; |
| @synthesize hoverAlpha = hoverAlpha_; |
| @synthesize alertAlpha = alertAlpha_; |
| @synthesize closing = closing_; |
| |
| - (id)initWithFrame:(NSRect)frame { |
| self = [super initWithFrame:frame]; |
| if (self) { |
| [self setShowsDivider:NO]; |
| // TODO(alcor): register for theming |
| } |
| return self; |
| } |
| |
| - (void)awakeFromNib { |
| [self setShowsDivider:NO]; |
| |
| // It is desirable for us to remove the close button from the cocoa hierarchy, |
| // so that VoiceOver does not encounter it. |
| // TODO(dtseng): crbug.com/59978. |
| // Retain in case we remove it from its superview. |
| closeButtonRetainer_.reset([closeButton_ retain]); |
| if (BrowserAccessibilityState::GetInstance()->IsAccessibleBrowser()) { |
| // The superview gives up ownership of the closeButton here. |
| [closeButton_ removeFromSuperview]; |
| } |
| } |
| |
| - (void)dealloc { |
| // Cancel any delayed requests that may still be pending (drags or hover). |
| [NSObject cancelPreviousPerformRequestsWithTarget:self]; |
| [super dealloc]; |
| } |
| |
| // Called to obtain the context menu for when the user hits the right mouse |
| // button (or control-clicks). (Note that -rightMouseDown: is *not* called for |
| // control-click.) |
| - (NSMenu*)menu { |
| if ([self isClosing]) |
| return nil; |
| |
| // Sheets, being window-modal, should block contextual menus. For some reason |
| // they do not. Disallow them ourselves. |
| if ([[self window] attachedSheet]) |
| return nil; |
| |
| return [controller_ menu]; |
| } |
| |
| // Overridden so that mouse clicks come to this view (the parent of the |
| // hierarchy) first. We want to handle clicks and drags in this class and |
| // leave the background button for display purposes only. |
| - (BOOL)acceptsFirstMouse:(NSEvent*)theEvent { |
| return YES; |
| } |
| |
| - (void)mouseEntered:(NSEvent*)theEvent { |
| isMouseInside_ = YES; |
| [self resetLastGlowUpdateTime]; |
| [self adjustGlowValue]; |
| } |
| |
| - (void)mouseMoved:(NSEvent*)theEvent { |
| hoverPoint_ = [self convertPoint:[theEvent locationInWindow] |
| fromView:nil]; |
| [self setNeedsDisplay:YES]; |
| } |
| |
| - (void)mouseExited:(NSEvent*)theEvent { |
| isMouseInside_ = NO; |
| hoverHoldEndTime_ = |
| [NSDate timeIntervalSinceReferenceDate] + kHoverHoldDuration; |
| [self resetLastGlowUpdateTime]; |
| [self adjustGlowValue]; |
| } |
| |
| - (void)setTrackingEnabled:(BOOL)enabled { |
| [closeButton_ setTrackingEnabled:enabled]; |
| } |
| |
| // Determines which view a click in our frame actually hit. It's either this |
| // view or our child close button. |
| - (NSView*)hitTest:(NSPoint)aPoint { |
| NSPoint viewPoint = [self convertPoint:aPoint fromView:[self superview]]; |
| NSRect frame = [self frame]; |
| |
| // Reduce the width of the hit rect slightly to remove the overlap |
| // between adjacent tabs. The drawing code in TabCell has the top |
| // corners of the tab inset by height*2/3, so we inset by half of |
| // that here. This doesn't completely eliminate the overlap, but it |
| // works well enough. |
| NSRect hitRect = NSInsetRect(frame, frame.size.height / 3.0f, 0); |
| if (![closeButton_ isHidden]) |
| if (NSPointInRect(viewPoint, [closeButton_ frame])) return closeButton_; |
| if (NSPointInRect(aPoint, hitRect)) return self; |
| return nil; |
| } |
| |
| // Returns |YES| if this tab can be torn away into a new window. |
| - (BOOL)canBeDragged { |
| if ([self isClosing]) |
| return NO; |
| NSWindowController* controller = [sourceWindow_ windowController]; |
| if ([controller isKindOfClass:[TabWindowController class]]) { |
| TabWindowController* realController = |
| static_cast<TabWindowController*>(controller); |
| return [realController isTabDraggable:self]; |
| } |
| return YES; |
| } |
| |
| // Returns an array of controllers that could be a drop target, ordered front to |
| // back. It has to be of the appropriate class, and visible (obviously). Note |
| // that the window cannot be a target for itself. |
| - (NSArray*)dropTargetsForController:(TabWindowController*)dragController { |
| NSMutableArray* targets = [NSMutableArray array]; |
| NSWindow* dragWindow = [dragController window]; |
| for (NSWindow* window in [NSApp orderedWindows]) { |
| if (window == dragWindow) continue; |
| if (![window isVisible]) continue; |
| // Skip windows on the wrong space. |
| if ([window respondsToSelector:@selector(isOnActiveSpace)]) { |
| if (![window performSelector:@selector(isOnActiveSpace)]) |
| continue; |
| } else { |
| // TODO(davidben): When we stop supporting 10.5, this can be |
| // removed. |
| // |
| // We don't cache the workspace of |dragWindow| because it may |
| // move around spaces. |
| if ([self getWorkspaceID:dragWindow useCache:NO] != |
| [self getWorkspaceID:window useCache:YES]) |
| continue; |
| } |
| NSWindowController* controller = [window windowController]; |
| if ([controller isKindOfClass:[TabWindowController class]]) { |
| TabWindowController* realController = |
| static_cast<TabWindowController*>(controller); |
| if ([realController canReceiveFrom:dragController]) |
| [targets addObject:controller]; |
| } |
| } |
| return targets; |
| } |
| |
| // Call to clear out transient weak references we hold during drags. |
| - (void)resetDragControllers { |
| draggedController_ = nil; |
| dragWindow_ = nil; |
| dragOverlay_ = nil; |
| sourceController_ = nil; |
| sourceWindow_ = nil; |
| targetController_ = nil; |
| workspaceIDCache_.clear(); |
| } |
| |
| // Sets whether the window background should be visible or invisible when |
| // dragging a tab. The background should be invisible when the mouse is over a |
| // potential drop target for the tab (the tab strip). It should be visible when |
| // there's no drop target so the window looks more fully realized and ready to |
| // become a stand-alone window. |
| - (void)setWindowBackgroundVisibility:(BOOL)shouldBeVisible { |
| if (chromeIsVisible_ == shouldBeVisible) |
| return; |
| |
| // There appears to be a race-condition in CoreAnimation where if we use |
| // animators to set the alpha values, we can't guarantee that we cancel them. |
| // This has the side effect of sometimes leaving the dragged window |
| // translucent or invisible. As a result, don't animate the alpha change. |
| [[draggedController_ overlayWindow] setAlphaValue:1.0]; |
| if (targetController_) { |
| [dragWindow_ setAlphaValue:0.0]; |
| [[draggedController_ overlayWindow] setHasShadow:YES]; |
| [[targetController_ window] makeMainWindow]; |
| } else { |
| [dragWindow_ setAlphaValue:0.5]; |
| [[draggedController_ overlayWindow] setHasShadow:NO]; |
| [[draggedController_ window] makeMainWindow]; |
| } |
| chromeIsVisible_ = shouldBeVisible; |
| } |
| |
| // Handle clicks and drags in this button. We get here because we have |
| // overridden acceptsFirstMouse: and the click is within our bounds. |
| - (void)mouseDown:(NSEvent*)theEvent { |
| if ([self isClosing]) |
| return; |
| |
| NSPoint downLocation = [theEvent locationInWindow]; |
| |
| // Record the state of the close button here, because selecting the tab will |
| // unhide it. |
| BOOL closeButtonActive = [closeButton_ isHidden] ? NO : YES; |
| |
| // During the tab closure animation (in particular, during rapid tab closure), |
| // we may get incorrectly hit with a mouse down. If it should have gone to the |
| // close button, we send it there -- it should then track the mouse, so we |
| // don't have to worry about mouse ups. |
| if (closeButtonActive && [controller_ inRapidClosureMode]) { |
| NSPoint hitLocation = [[self superview] convertPoint:downLocation |
| fromView:nil]; |
| if ([self hitTest:hitLocation] == closeButton_) { |
| [closeButton_ mouseDown:theEvent]; |
| return; |
| } |
| } |
| |
| // Fire the action to select the tab. |
| if ([[controller_ target] respondsToSelector:[controller_ action]]) |
| [[controller_ target] performSelector:[controller_ action] |
| withObject:self]; |
| |
| [self resetDragControllers]; |
| |
| // Resolve overlay back to original window. |
| sourceWindow_ = [self window]; |
| if ([sourceWindow_ isKindOfClass:[NSPanel class]]) { |
| sourceWindow_ = [sourceWindow_ parentWindow]; |
| } |
| |
| sourceWindowFrame_ = [sourceWindow_ frame]; |
| sourceTabFrame_ = [self frame]; |
| sourceController_ = [sourceWindow_ windowController]; |
| tabWasDragged_ = NO; |
| tearTime_ = 0.0; |
| draggingWithinTabStrip_ = YES; |
| chromeIsVisible_ = NO; |
| |
| // If there's more than one potential window to be a drop target, we want to |
| // treat a drag of a tab just like dragging around a tab that's already |
| // detached. Note that unit tests might have |-numberOfTabs| reporting zero |
| // since the model won't be fully hooked up. We need to be prepared for that |
| // and not send them into the "magnetic" codepath. |
| NSArray* targets = [self dropTargetsForController:sourceController_]; |
| moveWindowOnDrag_ = |
| ([sourceController_ numberOfTabs] < 2 && ![targets count]) || |
| ![self canBeDragged] || |
| ![sourceController_ tabDraggingAllowed]; |
| // If we are dragging a tab, a window with a single tab should immediately |
| // snap off and not drag within the tab strip. |
| if (!moveWindowOnDrag_) |
| draggingWithinTabStrip_ = [sourceController_ numberOfTabs] > 1; |
| |
| dragOrigin_ = [NSEvent mouseLocation]; |
| |
| // If the tab gets torn off, the tab controller will be removed from the tab |
| // strip and then deallocated. This will also result in *us* being |
| // deallocated. Both these are bad, so we prevent this by retaining the |
| // controller. |
| scoped_nsobject<TabController> controller([controller_ retain]); |
| |
| // Because we move views between windows, we need to handle the event loop |
| // ourselves. Ideally we should use the standard event loop. |
| while (1) { |
| theEvent = |
| [NSApp nextEventMatchingMask:NSLeftMouseUpMask | NSLeftMouseDraggedMask |
| untilDate:[NSDate distantFuture] |
| inMode:NSDefaultRunLoopMode dequeue:YES]; |
| NSEventType type = [theEvent type]; |
| if (type == NSLeftMouseDragged) { |
| [self mouseDragged:theEvent]; |
| } else if (type == NSLeftMouseUp) { |
| NSPoint upLocation = [theEvent locationInWindow]; |
| CGFloat dx = upLocation.x - downLocation.x; |
| CGFloat dy = upLocation.y - downLocation.y; |
| |
| // During rapid tab closure (mashing tab close buttons), we may get hit |
| // with a mouse down. As long as the mouse up is over the close button, |
| // and the mouse hasn't moved too much, we close the tab. |
| if (closeButtonActive && |
| (dx*dx + dy*dy) <= kRapidCloseDist*kRapidCloseDist && |
| [controller inRapidClosureMode]) { |
| NSPoint hitLocation = |
| [[self superview] convertPoint:[theEvent locationInWindow] |
| fromView:nil]; |
| if ([self hitTest:hitLocation] == closeButton_) { |
| [controller closeTab:self]; |
| break; |
| } |
| } |
| |
| [self mouseUp:theEvent]; |
| break; |
| } else { |
| // TODO(viettrungluu): [crbug.com/23830] We can receive right-mouse-ups |
| // (and maybe even others?) for reasons I don't understand. So we |
| // explicitly check for both events we're expecting, and log others. We |
| // should figure out what's going on. |
| LOG(WARNING) << "Spurious event received of type " << type << "."; |
| } |
| } |
| } |
| |
| - (void)mouseDragged:(NSEvent*)theEvent { |
| // Special-case this to keep the logic below simpler. |
| if (moveWindowOnDrag_) { |
| if ([sourceController_ windowMovementAllowed]) { |
| NSPoint thisPoint = [NSEvent mouseLocation]; |
| NSPoint origin = sourceWindowFrame_.origin; |
| origin.x += (thisPoint.x - dragOrigin_.x); |
| origin.y += (thisPoint.y - dragOrigin_.y); |
| [sourceWindow_ setFrameOrigin:NSMakePoint(origin.x, origin.y)]; |
| } // else do nothing. |
| return; |
| } |
| |
| // First, go through the magnetic drag cycle. We break out of this if |
| // "stretchiness" ever exceeds a set amount. |
| tabWasDragged_ = YES; |
| |
| if (draggingWithinTabStrip_) { |
| NSPoint thisPoint = [NSEvent mouseLocation]; |
| CGFloat stretchiness = thisPoint.y - dragOrigin_.y; |
| stretchiness = copysign(sqrtf(fabs(stretchiness))/sqrtf(kTearDistance), |
| stretchiness) / 2.0; |
| CGFloat offset = thisPoint.x - dragOrigin_.x; |
| if (fabsf(offset) > 100) stretchiness = 0; |
| [sourceController_ insertPlaceholderForTab:self |
| frame:NSOffsetRect(sourceTabFrame_, |
| offset, 0) |
| yStretchiness:stretchiness]; |
| // Check that we haven't pulled the tab too far to start a drag. This |
| // can include either pulling it too far down, or off the side of the tab |
| // strip that would cause it to no longer be fully visible. |
| BOOL stillVisible = [sourceController_ isTabFullyVisible:self]; |
| CGFloat tearForce = fabs(thisPoint.y - dragOrigin_.y); |
| if ([sourceController_ tabTearingAllowed] && |
| (tearForce > kTearDistance || !stillVisible)) { |
| draggingWithinTabStrip_ = NO; |
| // When you finally leave the strip, we treat that as the origin. |
| dragOrigin_.x = thisPoint.x; |
| } else { |
| // Still dragging within the tab strip, wait for the next drag event. |
| return; |
| } |
| } |
| |
| // Do not start dragging until the user has "torn" the tab off by |
| // moving more than 3 pixels. |
| NSDate* targetDwellDate = nil; // The date this target was first chosen. |
| |
| NSPoint thisPoint = [NSEvent mouseLocation]; |
| |
| // Iterate over possible targets checking for the one the mouse is in. |
| // If the tab is just in the frame, bring the window forward to make it |
| // easier to drop something there. If it's in the tab strip, set the new |
| // target so that it pops into that window. We can't cache this because we |
| // need the z-order to be correct. |
| NSArray* targets = [self dropTargetsForController:draggedController_]; |
| TabWindowController* newTarget = nil; |
| for (TabWindowController* target in targets) { |
| NSRect windowFrame = [[target window] frame]; |
| if (NSPointInRect(thisPoint, windowFrame)) { |
| [[target window] orderFront:self]; |
| NSRect tabStripFrame = [[target tabStripView] frame]; |
| tabStripFrame.origin = [[target window] |
| convertBaseToScreen:tabStripFrame.origin]; |
| if (NSPointInRect(thisPoint, tabStripFrame)) { |
| newTarget = target; |
| } |
| break; |
| } |
| } |
| |
| // If we're now targeting a new window, re-layout the tabs in the old |
| // target and reset how long we've been hovering over this new one. |
| if (targetController_ != newTarget) { |
| targetDwellDate = [NSDate date]; |
| [targetController_ removePlaceholder]; |
| targetController_ = newTarget; |
| if (!newTarget) { |
| tearTime_ = [NSDate timeIntervalSinceReferenceDate]; |
| tearOrigin_ = [dragWindow_ frame].origin; |
| } |
| } |
| |
| // Create or identify the dragged controller. |
| if (!draggedController_) { |
| // Get rid of any placeholder remaining in the original source window. |
| [sourceController_ removePlaceholder]; |
| |
| // Detach from the current window and put it in a new window. If there are |
| // no more tabs remaining after detaching, the source window is about to |
| // go away (it's been autoreleased) so we need to ensure we don't reference |
| // it any more. In that case the new controller becomes our source |
| // controller. |
| draggedController_ = [sourceController_ detachTabToNewWindow:self]; |
| dragWindow_ = [draggedController_ window]; |
| [dragWindow_ setAlphaValue:0.0]; |
| if (![sourceController_ hasLiveTabs]) { |
| sourceController_ = draggedController_; |
| sourceWindow_ = dragWindow_; |
| } |
| |
| // If dragging the tab only moves the current window, do not show overlay |
| // so that sheets stay on top of the window. |
| // Bring the target window to the front and make sure it has a border. |
| [dragWindow_ setLevel:NSFloatingWindowLevel]; |
| [dragWindow_ setHasShadow:YES]; |
| [dragWindow_ orderFront:nil]; |
| [dragWindow_ makeMainWindow]; |
| [draggedController_ showOverlay]; |
| dragOverlay_ = [draggedController_ overlayWindow]; |
| // Force the new tab button to be hidden. We'll reset it on mouse up. |
| [draggedController_ showNewTabButton:NO]; |
| tearTime_ = [NSDate timeIntervalSinceReferenceDate]; |
| tearOrigin_ = sourceWindowFrame_.origin; |
| } |
| |
| // TODO(pinkerton): http://crbug.com/25682 demonstrates a way to get here by |
| // some weird circumstance that doesn't first go through mouseDown:. We |
| // really shouldn't go any farther. |
| if (!draggedController_ || !sourceController_) |
| return; |
| |
| // When the user first tears off the window, we want slide the window to |
| // the current mouse location (to reduce the jarring appearance). We do this |
| // by calling ourselves back with additional mouseDragged calls (not actual |
| // events). |tearProgress| is a normalized measure of how far through this |
| // tear "animation" (of length kTearDuration) we are and has values [0..1]. |
| // We use sqrt() so the animation is non-linear (slow down near the end |
| // point). |
| NSTimeInterval tearProgress = |
| [NSDate timeIntervalSinceReferenceDate] - tearTime_; |
| tearProgress /= kTearDuration; // Normalize. |
| tearProgress = sqrtf(MAX(MIN(tearProgress, 1.0), 0.0)); |
| |
| // Move the dragged window to the right place on the screen. |
| NSPoint origin = sourceWindowFrame_.origin; |
| origin.x += (thisPoint.x - dragOrigin_.x); |
| origin.y += (thisPoint.y - dragOrigin_.y); |
| |
| if (tearProgress < 1) { |
| // If the tear animation is not complete, call back to ourself with the |
| // same event to animate even if the mouse isn't moving. We need to make |
| // sure these get cancelled in mouseUp:. |
| [NSObject cancelPreviousPerformRequestsWithTarget:self]; |
| [self performSelector:@selector(mouseDragged:) |
| withObject:theEvent |
| afterDelay:1.0f/30.0f]; |
| |
| // Set the current window origin based on how far we've progressed through |
| // the tear animation. |
| origin.x = (1 - tearProgress) * tearOrigin_.x + tearProgress * origin.x; |
| origin.y = (1 - tearProgress) * tearOrigin_.y + tearProgress * origin.y; |
| } |
| |
| if (targetController_) { |
| // In order to "snap" two windows of different sizes together at their |
| // toolbar, we can't just use the origin of the target frame. We also have |
| // to take into consideration the difference in height. |
| NSRect targetFrame = [[targetController_ window] frame]; |
| NSRect sourceFrame = [dragWindow_ frame]; |
| origin.y = NSMinY(targetFrame) + |
| (NSHeight(targetFrame) - NSHeight(sourceFrame)); |
| } |
| [dragWindow_ setFrameOrigin:NSMakePoint(origin.x, origin.y)]; |
| |
| // If we're not hovering over any window, make the window fully |
| // opaque. Otherwise, find where the tab might be dropped and insert |
| // a placeholder so it appears like it's part of that window. |
| if (targetController_) { |
| if (![[targetController_ window] isKeyWindow]) { |
| // && ([targetDwellDate timeIntervalSinceNow] < -REQUIRED_DWELL)) { |
| [[targetController_ window] orderFront:nil]; |
| targetDwellDate = nil; |
| } |
| |
| // Compute where placeholder should go and insert it into the |
| // destination tab strip. |
| TabView* draggedTabView = (TabView*)[draggedController_ selectedTabView]; |
| NSRect tabFrame = [draggedTabView frame]; |
| tabFrame.origin = [dragWindow_ convertBaseToScreen:tabFrame.origin]; |
| tabFrame.origin = [[targetController_ window] |
| convertScreenToBase:tabFrame.origin]; |
| tabFrame = [[targetController_ tabStripView] |
| convertRect:tabFrame fromView:nil]; |
| [targetController_ insertPlaceholderForTab:self |
| frame:tabFrame |
| yStretchiness:0]; |
| [targetController_ layoutTabs]; |
| } else { |
| [dragWindow_ makeKeyAndOrderFront:nil]; |
| } |
| |
| // Adjust the visibility of the window background. If there is a drop target, |
| // we want to hide the window background so the tab stands out for |
| // positioning. If not, we want to show it so it looks like a new window will |
| // be realized. |
| BOOL chromeShouldBeVisible = targetController_ == nil; |
| [self setWindowBackgroundVisibility:chromeShouldBeVisible]; |
| } |
| |
| - (void)mouseUp:(NSEvent*)theEvent { |
| // The drag/click is done. If the user dragged the mouse, finalize the drag |
| // and clean up. |
| |
| // Special-case this to keep the logic below simpler. |
| if (moveWindowOnDrag_) |
| return; |
| |
| // Cancel any delayed -mouseDragged: requests that may still be pending. |
| [NSObject cancelPreviousPerformRequestsWithTarget:self]; |
| |
| // TODO(pinkerton): http://crbug.com/25682 demonstrates a way to get here by |
| // some weird circumstance that doesn't first go through mouseDown:. We |
| // really shouldn't go any farther. |
| if (!sourceController_) |
| return; |
| |
| // We are now free to re-display the new tab button in the window we're |
| // dragging. It will show when the next call to -layoutTabs (which happens |
| // indrectly by several of the calls below, such as removing the placeholder). |
| [draggedController_ showNewTabButton:YES]; |
| |
| if (draggingWithinTabStrip_) { |
| if (tabWasDragged_) { |
| // Move tab to new location. |
| DCHECK([sourceController_ numberOfTabs]); |
| TabWindowController* dropController = sourceController_; |
| [dropController moveTabView:[dropController selectedTabView] |
| fromController:nil]; |
| } |
| } else if (targetController_) { |
| // Move between windows. If |targetController_| is nil, we're not dropping |
| // into any existing window. |
| NSView* draggedTabView = [draggedController_ selectedTabView]; |
| [targetController_ moveTabView:draggedTabView |
| fromController:draggedController_]; |
| // Force redraw to avoid flashes of old content before returning to event |
| // loop. |
| [[targetController_ window] display]; |
| [targetController_ showWindow:nil]; |
| [draggedController_ removeOverlay]; |
| } else { |
| // Only move the window around on screen. Make sure it's set back to |
| // normal state (fully opaque, has shadow, has key, etc). |
| [draggedController_ removeOverlay]; |
| // Don't want to re-show the window if it was closed during the drag. |
| if ([dragWindow_ isVisible]) { |
| [dragWindow_ setAlphaValue:1.0]; |
| [dragOverlay_ setHasShadow:NO]; |
| [dragWindow_ setHasShadow:YES]; |
| [dragWindow_ makeKeyAndOrderFront:nil]; |
| } |
| [[draggedController_ window] setLevel:NSNormalWindowLevel]; |
| [draggedController_ removePlaceholder]; |
| } |
| [sourceController_ removePlaceholder]; |
| chromeIsVisible_ = YES; |
| |
| [self resetDragControllers]; |
| } |
| |
| - (void)otherMouseUp:(NSEvent*)theEvent { |
| if ([self isClosing]) |
| return; |
| |
| // Support middle-click-to-close. |
| if ([theEvent buttonNumber] == 2) { |
| // |-hitTest:| takes a location in the superview's coordinates. |
| NSPoint upLocation = |
| [[self superview] convertPoint:[theEvent locationInWindow] |
| fromView:nil]; |
| // If the mouse up occurred in our view or over the close button, then |
| // close. |
| if ([self hitTest:upLocation]) |
| [controller_ closeTab:self]; |
| } |
| } |
| |
| - (void)drawRect:(NSRect)dirtyRect { |
| NSGraphicsContext* context = [NSGraphicsContext currentContext]; |
| [context saveGraphicsState]; |
| |
| BrowserThemeProvider* themeProvider = |
| static_cast<BrowserThemeProvider*>([[self window] themeProvider]); |
| [context setPatternPhase:[[self window] themePatternPhase]]; |
| |
| NSRect rect = [self bounds]; |
| NSBezierPath* path = [self bezierPathForRect:rect]; |
| |
| BOOL selected = [self state]; |
| // Don't draw the window/tab bar background when selected, since the tab |
| // background overlay drawn over it (see below) will be fully opaque. |
| BOOL hasBackgroundImage = NO; |
| if (!selected) { |
| // ThemeProvider::HasCustomImage is true only if the theme provides the |
| // image. However, even if the theme doesn't provide a tab background, the |
| // theme machinery will make one if given a frame image. See |
| // BrowserThemePack::GenerateTabBackgroundImages for details. |
| hasBackgroundImage = themeProvider && |
| (themeProvider->HasCustomImage(IDR_THEME_TAB_BACKGROUND) || |
| themeProvider->HasCustomImage(IDR_THEME_FRAME)); |
| |
| NSColor* backgroundImageColor = hasBackgroundImage ? |
| themeProvider->GetNSImageColorNamed(IDR_THEME_TAB_BACKGROUND, true) : |
| nil; |
| |
| if (backgroundImageColor) { |
| [backgroundImageColor set]; |
| [path fill]; |
| } else { |
| // Use the window's background color rather than |[NSColor |
| // windowBackgroundColor]|, which gets confused by the fullscreen window. |
| // (The result is the same for normal, non-fullscreen windows.) |
| [[[self window] backgroundColor] set]; |
| [path fill]; |
| [[NSColor colorWithCalibratedWhite:1.0 alpha:0.3] set]; |
| [path fill]; |
| } |
| } |
| |
| [context saveGraphicsState]; |
| [path addClip]; |
| |
| // Use the same overlay for the selected state and for hover and alert glows; |
| // for the selected state, it's fully opaque. |
| CGFloat hoverAlpha = [self hoverAlpha]; |
| CGFloat alertAlpha = [self alertAlpha]; |
| if (selected || hoverAlpha > 0 || alertAlpha > 0) { |
| // Draw the selected background / glow overlay. |
| [context saveGraphicsState]; |
| CGContextRef cgContext = static_cast<CGContextRef>([context graphicsPort]); |
| CGContextBeginTransparencyLayer(cgContext, 0); |
| if (!selected) { |
| // The alert glow overlay is like the selected state but at most at most |
| // 80% opaque. The hover glow brings up the overlay's opacity at most 50%. |
| CGFloat backgroundAlpha = 0.8 * alertAlpha; |
| backgroundAlpha += (1 - backgroundAlpha) * 0.5 * hoverAlpha; |
| CGContextSetAlpha(cgContext, backgroundAlpha); |
| } |
| [path addClip]; |
| [context saveGraphicsState]; |
| [super drawBackground]; |
| [context restoreGraphicsState]; |
| |
| // Draw a mouse hover gradient for the default themes. |
| if (!selected && hoverAlpha > 0) { |
| if (themeProvider && !hasBackgroundImage) { |
| scoped_nsobject<NSGradient> glow([NSGradient alloc]); |
| [glow initWithStartingColor:[NSColor colorWithCalibratedWhite:1.0 |
| alpha:1.0 * hoverAlpha] |
| endingColor:[NSColor colorWithCalibratedWhite:1.0 |
| alpha:0.0]]; |
| |
| NSPoint point = hoverPoint_; |
| point.y = NSHeight(rect); |
| [glow drawFromCenter:point |
| radius:0.0 |
| toCenter:point |
| radius:NSWidth(rect) / 3.0 |
| options:NSGradientDrawsBeforeStartingLocation]; |
| |
| [glow drawInBezierPath:path relativeCenterPosition:hoverPoint_]; |
| } |
| } |
| |
| CGContextEndTransparencyLayer(cgContext); |
| [context restoreGraphicsState]; |
| } |
| |
| BOOL active = [[self window] isKeyWindow] || [[self window] isMainWindow]; |
| CGFloat borderAlpha = selected ? (active ? 0.3 : 0.2) : 0.2; |
| NSColor* borderColor = [NSColor colorWithDeviceWhite:0.0 alpha:borderAlpha]; |
| NSColor* highlightColor = themeProvider ? themeProvider->GetNSColor( |
| themeProvider->UsingDefaultTheme() ? |
| BrowserThemeProvider::COLOR_TOOLBAR_BEZEL : |
| BrowserThemeProvider::COLOR_TOOLBAR, true) : nil; |
| |
| // Draw the top inner highlight within the currently selected tab if using |
| // the default theme. |
| if (selected && themeProvider && themeProvider->UsingDefaultTheme()) { |
| NSAffineTransform* highlightTransform = [NSAffineTransform transform]; |
| [highlightTransform translateXBy:1.0 yBy:-1.0]; |
| scoped_nsobject<NSBezierPath> highlightPath([path copy]); |
| [highlightPath transformUsingAffineTransform:highlightTransform]; |
| [highlightColor setStroke]; |
| [highlightPath setLineWidth:1.0]; |
| [highlightPath stroke]; |
| highlightTransform = [NSAffineTransform transform]; |
| [highlightTransform translateXBy:-2.0 yBy:0.0]; |
| [highlightPath transformUsingAffineTransform:highlightTransform]; |
| [highlightPath stroke]; |
| } |
| |
| [context restoreGraphicsState]; |
| |
| // Draw the top stroke. |
| [context saveGraphicsState]; |
| [borderColor set]; |
| [path setLineWidth:1.0]; |
| [path stroke]; |
| [context restoreGraphicsState]; |
| |
| // Mimic the tab strip's bottom border, which consists of a dark border |
| // and light highlight. |
| if (!selected) { |
| [path addClip]; |
| NSRect borderRect = rect; |
| borderRect.origin.y = 1; |
| borderRect.size.height = 1; |
| [borderColor set]; |
| NSRectFillUsingOperation(borderRect, NSCompositeSourceOver); |
| |
| borderRect.origin.y = 0; |
| [highlightColor set]; |
| NSRectFillUsingOperation(borderRect, NSCompositeSourceOver); |
| } |
| |
| [context restoreGraphicsState]; |
| } |
| |
| - (void)viewDidMoveToWindow { |
| [super viewDidMoveToWindow]; |
| if ([self window]) { |
| [controller_ updateTitleColor]; |
| } |
| } |
| |
| - (void)setClosing:(BOOL)closing { |
| closing_ = closing; // Safe because the property is nonatomic. |
| // When closing, ensure clicks to the close button go nowhere. |
| if (closing) { |
| [closeButton_ setTarget:nil]; |
| [closeButton_ setAction:nil]; |
| } |
| } |
| |
| - (void)startAlert { |
| // Do not start a new alert while already alerting or while in a decay cycle. |
| if (alertState_ == tabs::kAlertNone) { |
| alertState_ = tabs::kAlertRising; |
| [self resetLastGlowUpdateTime]; |
| [self adjustGlowValue]; |
| } |
| } |
| |
| - (void)cancelAlert { |
| if (alertState_ != tabs::kAlertNone) { |
| alertState_ = tabs::kAlertFalling; |
| alertHoldEndTime_ = |
| [NSDate timeIntervalSinceReferenceDate] + kGlowUpdateInterval; |
| [self resetLastGlowUpdateTime]; |
| [self adjustGlowValue]; |
| } |
| } |
| |
| - (BOOL)accessibilityIsIgnored { |
| return NO; |
| } |
| |
| - (NSArray*)accessibilityActionNames { |
| NSArray* parentActions = [super accessibilityActionNames]; |
| |
| return [parentActions arrayByAddingObject:NSAccessibilityPressAction]; |
| } |
| |
| - (NSArray*)accessibilityAttributeNames { |
| NSMutableArray* attributes = |
| [[super accessibilityAttributeNames] mutableCopy]; |
| [attributes addObject:NSAccessibilityTitleAttribute]; |
| [attributes addObject:NSAccessibilityEnabledAttribute]; |
| |
| return attributes; |
| } |
| |
| - (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute { |
| if ([attribute isEqual:NSAccessibilityTitleAttribute]) |
| return NO; |
| |
| if ([attribute isEqual:NSAccessibilityEnabledAttribute]) |
| return NO; |
| |
| return [super accessibilityIsAttributeSettable:attribute]; |
| } |
| |
| - (id)accessibilityAttributeValue:(NSString*)attribute { |
| if ([attribute isEqual:NSAccessibilityRoleAttribute]) |
| return NSAccessibilityButtonRole; |
| |
| if ([attribute isEqual:NSAccessibilityTitleAttribute]) |
| return [controller_ title]; |
| |
| if ([attribute isEqual:NSAccessibilityEnabledAttribute]) |
| return [NSNumber numberWithBool:YES]; |
| |
| if ([attribute isEqual:NSAccessibilityChildrenAttribute]) { |
| // The subviews (icon and text) are clutter; filter out everything but |
| // useful controls. |
| NSArray* children = [super accessibilityAttributeValue:attribute]; |
| NSMutableArray* okChildren = [NSMutableArray array]; |
| for (id child in children) { |
| if ([child isKindOfClass:[NSButtonCell class]]) |
| [okChildren addObject:child]; |
| } |
| |
| return okChildren; |
| } |
| |
| return [super accessibilityAttributeValue:attribute]; |
| } |
| |
| - (ViewID)viewID { |
| return VIEW_ID_TAB; |
| } |
| |
| @end // @implementation TabView |
| |
| @implementation TabView (TabControllerInterface) |
| |
| - (void)setController:(TabController*)controller { |
| controller_ = controller; |
| } |
| |
| @end // @implementation TabView (TabControllerInterface) |
| |
| @implementation TabView(Private) |
| |
| - (void)resetLastGlowUpdateTime { |
| lastGlowUpdate_ = [NSDate timeIntervalSinceReferenceDate]; |
| } |
| |
| - (NSTimeInterval)timeElapsedSinceLastGlowUpdate { |
| return [NSDate timeIntervalSinceReferenceDate] - lastGlowUpdate_; |
| } |
| |
| - (void)adjustGlowValue { |
| // A time interval long enough to represent no update. |
| const NSTimeInterval kNoUpdate = 1000000; |
| |
| // Time until next update for either glow. |
| NSTimeInterval nextUpdate = kNoUpdate; |
| |
| NSTimeInterval elapsed = [self timeElapsedSinceLastGlowUpdate]; |
| NSTimeInterval currentTime = [NSDate timeIntervalSinceReferenceDate]; |
| |
| // TODO(viettrungluu): <http://crbug.com/30617> -- split off the stuff below |
| // into a pure function and add a unit test. |
| |
| CGFloat hoverAlpha = [self hoverAlpha]; |
| if (isMouseInside_) { |
| // Increase hover glow until it's 1. |
| if (hoverAlpha < 1) { |
| hoverAlpha = MIN(hoverAlpha + elapsed / kHoverShowDuration, 1); |
| [self setHoverAlpha:hoverAlpha]; |
| nextUpdate = MIN(kGlowUpdateInterval, nextUpdate); |
| } // Else already 1 (no update needed). |
| } else { |
| if (currentTime >= hoverHoldEndTime_) { |
| // No longer holding, so decrease hover glow until it's 0. |
| if (hoverAlpha > 0) { |
| hoverAlpha = MAX(hoverAlpha - elapsed / kHoverHideDuration, 0); |
| [self setHoverAlpha:hoverAlpha]; |
| nextUpdate = MIN(kGlowUpdateInterval, nextUpdate); |
| } // Else already 0 (no update needed). |
| } else { |
| // Schedule update for end of hold time. |
| nextUpdate = MIN(hoverHoldEndTime_ - currentTime, nextUpdate); |
| } |
| } |
| |
| CGFloat alertAlpha = [self alertAlpha]; |
| if (alertState_ == tabs::kAlertRising) { |
| // Increase alert glow until it's 1 ... |
| alertAlpha = MIN(alertAlpha + elapsed / kAlertShowDuration, 1); |
| [self setAlertAlpha:alertAlpha]; |
| |
| // ... and having reached 1, switch to holding. |
| if (alertAlpha >= 1) { |
| alertState_ = tabs::kAlertHolding; |
| alertHoldEndTime_ = currentTime + kAlertHoldDuration; |
| nextUpdate = MIN(kAlertHoldDuration, nextUpdate); |
| } else { |
| nextUpdate = MIN(kGlowUpdateInterval, nextUpdate); |
| } |
| } else if (alertState_ != tabs::kAlertNone) { |
| if (alertAlpha > 0) { |
| if (currentTime >= alertHoldEndTime_) { |
| // Stop holding, then decrease alert glow (until it's 0). |
| if (alertState_ == tabs::kAlertHolding) { |
| alertState_ = tabs::kAlertFalling; |
| nextUpdate = MIN(kGlowUpdateInterval, nextUpdate); |
| } else { |
| DCHECK_EQ(tabs::kAlertFalling, alertState_); |
| alertAlpha = MAX(alertAlpha - elapsed / kAlertHideDuration, 0); |
| [self setAlertAlpha:alertAlpha]; |
| nextUpdate = MIN(kGlowUpdateInterval, nextUpdate); |
| } |
| } else { |
| // Schedule update for end of hold time. |
| nextUpdate = MIN(alertHoldEndTime_ - currentTime, nextUpdate); |
| } |
| } else { |
| // Done the alert decay cycle. |
| alertState_ = tabs::kAlertNone; |
| } |
| } |
| |
| if (nextUpdate < kNoUpdate) |
| [self performSelector:_cmd withObject:nil afterDelay:nextUpdate]; |
| |
| [self resetLastGlowUpdateTime]; |
| [self setNeedsDisplay:YES]; |
| } |
| |
| // Returns the workspace id of |window|. If |useCache|, then lookup |
| // and remember the value in |workspaceIDCache_| until the end of the |
| // current drag. |
| - (int)getWorkspaceID:(NSWindow*)window useCache:(BOOL)useCache { |
| CGWindowID windowID = [window windowNumber]; |
| if (useCache) { |
| std::map<CGWindowID, int>::iterator iter = |
| workspaceIDCache_.find(windowID); |
| if (iter != workspaceIDCache_.end()) |
| return iter->second; |
| } |
| |
| int workspace = -1; |
| // It's possible to query in bulk, but probably not necessary. |
| base::mac::ScopedCFTypeRef<CFArrayRef> windowIDs(CFArrayCreate( |
| NULL, reinterpret_cast<const void **>(&windowID), 1, NULL)); |
| base::mac::ScopedCFTypeRef<CFArrayRef> descriptions( |
| CGWindowListCreateDescriptionFromArray(windowIDs)); |
| DCHECK(CFArrayGetCount(descriptions.get()) <= 1); |
| if (CFArrayGetCount(descriptions.get()) > 0) { |
| CFDictionaryRef dict = static_cast<CFDictionaryRef>( |
| CFArrayGetValueAtIndex(descriptions.get(), 0)); |
| DCHECK(CFGetTypeID(dict) == CFDictionaryGetTypeID()); |
| |
| // Sanity check the ID. |
| CFNumberRef otherIDRef = (CFNumberRef)base::mac::GetValueFromDictionary( |
| dict, kCGWindowNumber, CFNumberGetTypeID()); |
| CGWindowID otherID; |
| if (otherIDRef && |
| CFNumberGetValue(otherIDRef, kCGWindowIDCFNumberType, &otherID) && |
| otherID == windowID) { |
| // And then get the workspace. |
| CFNumberRef workspaceRef = (CFNumberRef)base::mac::GetValueFromDictionary( |
| dict, kCGWindowWorkspace, CFNumberGetTypeID()); |
| if (!workspaceRef || |
| !CFNumberGetValue(workspaceRef, kCFNumberIntType, &workspace)) { |
| workspace = -1; |
| } |
| } else { |
| NOTREACHED(); |
| } |
| } |
| if (useCache) { |
| workspaceIDCache_[windowID] = workspace; |
| } |
| return workspace; |
| } |
| |
| // Returns the bezier path used to draw the tab given the bounds to draw it in. |
| - (NSBezierPath*)bezierPathForRect:(NSRect)rect { |
| // Outset by 0.5 in order to draw on pixels rather than on borders (which |
| // would cause blurry pixels). Subtract 1px of height to compensate, otherwise |
| // clipping will occur. |
| rect = NSInsetRect(rect, -0.5, -0.5); |
| rect.size.height -= 1.0; |
| |
| NSPoint bottomLeft = NSMakePoint(NSMinX(rect), NSMinY(rect) + 2); |
| NSPoint bottomRight = NSMakePoint(NSMaxX(rect), NSMinY(rect) + 2); |
| NSPoint topRight = |
| NSMakePoint(NSMaxX(rect) - kInsetMultiplier * NSHeight(rect), |
| NSMaxY(rect)); |
| NSPoint topLeft = |
| NSMakePoint(NSMinX(rect) + kInsetMultiplier * NSHeight(rect), |
| NSMaxY(rect)); |
| |
| CGFloat baseControlPointOutset = NSHeight(rect) * kControlPoint1Multiplier; |
| CGFloat bottomControlPointInset = NSHeight(rect) * kControlPoint2Multiplier; |
| |
| // Outset many of these values by 1 to cause the fill to bleed outside the |
| // clip area. |
| NSBezierPath* path = [NSBezierPath bezierPath]; |
| [path moveToPoint:NSMakePoint(bottomLeft.x - 1, bottomLeft.y - 2)]; |
| [path lineToPoint:NSMakePoint(bottomLeft.x - 1, bottomLeft.y)]; |
| [path lineToPoint:bottomLeft]; |
| [path curveToPoint:topLeft |
| controlPoint1:NSMakePoint(bottomLeft.x + baseControlPointOutset, |
| bottomLeft.y) |
| controlPoint2:NSMakePoint(topLeft.x - bottomControlPointInset, |
| topLeft.y)]; |
| [path lineToPoint:topRight]; |
| [path curveToPoint:bottomRight |
| controlPoint1:NSMakePoint(topRight.x + bottomControlPointInset, |
| topRight.y) |
| controlPoint2:NSMakePoint(bottomRight.x - baseControlPointOutset, |
| bottomRight.y)]; |
| [path lineToPoint:NSMakePoint(bottomRight.x + 1, bottomRight.y)]; |
| [path lineToPoint:NSMakePoint(bottomRight.x + 1, bottomRight.y - 2)]; |
| return path; |
| } |
| |
| @end // @implementation TabView(Private) |