Index: chrome/browser/ui/cocoa/tabs/tab_strip_drag_controller.mm |
diff --git a/chrome/browser/ui/cocoa/tabs/tab_strip_drag_controller.mm b/chrome/browser/ui/cocoa/tabs/tab_strip_drag_controller.mm |
new file mode 100644 |
index 0000000000000000000000000000000000000000..f9b26628c050df6fc558cd84c90d59f1472ce70e |
--- /dev/null |
+++ b/chrome/browser/ui/cocoa/tabs/tab_strip_drag_controller.mm |
@@ -0,0 +1,523 @@ |
+// Copyright (c) 2011 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/tabs/tab_strip_drag_controller.h" |
+ |
+#import "base/mac/mac_util.h" |
+#include "base/mac/scoped_cftyperef.h" |
+#import "chrome/browser/ui/cocoa/tabs/tab_controller.h" |
+#import "chrome/browser/ui/cocoa/tabs/tab_controller_target.h" |
+#import "chrome/browser/ui/cocoa/tabs/tab_view.h" |
+#import "chrome/browser/ui/cocoa/tabs/tab_window_controller.h" |
+ |
+const CGFloat kTearDistance = 36.0; |
+const NSTimeInterval kTearDuration = 0.333; |
+ |
+@interface TabStripDragController (Private) |
+- (void)resetDragControllers; |
+- (NSArray*)dropTargetsForController:(TabWindowController*)dragController; |
+- (void)setWindowBackgroundVisibility:(BOOL)shouldBeVisible; |
+// TODO(davidben): When we stop supporting 10.5, this can be removed. |
+- (int)getWorkspaceID:(NSWindow*)window useCache:(BOOL)useCache; |
+@end |
+ |
+//////////////////////////////////////////////////////////////////////////////// |
+ |
+@implementation TabStripDragController |
+ |
+- (id)initWithTabStripController:(TabStripController*)controller { |
+ if ((self = [super init])) { |
+ tabStrip_ = controller; |
+ } |
+ return self; |
+} |
+ |
+- (void)dealloc { |
+ [NSObject cancelPreviousPerformRequestsWithTarget:self]; |
+ [super dealloc]; |
+} |
+ |
+- (BOOL)tabCanBeDragged:(TabController*)tab { |
+ if ([[tab tabView] isClosing]) |
+ return NO; |
+ NSWindowController* controller = [sourceWindow_ windowController]; |
+ if ([controller isKindOfClass:[TabWindowController class]]) { |
+ TabWindowController* realController = |
+ static_cast<TabWindowController*>(controller); |
+ return [realController isTabDraggable:[tab tabView]]; |
+ } |
+ return YES; |
+} |
+ |
+- (void)maybeStartDrag:(NSEvent*)theEvent forTab:(TabController*)tab { |
+ [self resetDragControllers]; |
+ |
+ // Resolve overlay back to original window. |
+ sourceWindow_ = [[tab view] window]; |
+ if ([sourceWindow_ isKindOfClass:[NSPanel class]]) { |
+ sourceWindow_ = [sourceWindow_ parentWindow]; |
+ } |
+ |
+ sourceWindowFrame_ = [sourceWindow_ frame]; |
+ sourceTabFrame_ = [[tab view] frame]; |
+ sourceController_ = [sourceWindow_ windowController]; |
+ draggedTab_ = tab; |
+ 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 tabCanBeDragged:tab] || |
+ ![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]; |
+ |
+ // When spinning the event loop, a tab can get detached, which could lead to |
+ // our own destruction. Keep ourselves around while spinning the loop. |
+ scoped_nsobject<TabStripDragController> keepAlive([self 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) { |
+ const NSUInteger mask = |
+ NSLeftMouseUpMask | NSLeftMouseDraggedMask | NSKeyUpMask; |
+ theEvent = |
+ [NSApp nextEventMatchingMask:mask |
+ untilDate:[NSDate distantFuture] |
+ inMode:NSDefaultRunLoopMode |
+ dequeue:YES]; |
+ NSEventType type = [theEvent type]; |
+ if (type == NSKeyUp) { |
+ if ([theEvent keyCode] == kVK_Escape) { |
+ // Cancel the drag and restore the previous state. |
+ if (draggingWithinTabStrip_) { |
+ // Simply pretend the tab wasn't dragged (far enough). |
+ tabWasDragged_ = NO; |
+ } else { |
+ [targetController_ removePlaceholder]; |
+ if ([sourceController_ numberOfTabs] < 2) { |
+ // Revert to a single-tab window. |
+ targetController_ = nil; |
+ } else { |
+ // Change the target to the source controller. |
+ targetController_ = sourceController_; |
+ [targetController_ insertPlaceholderForTab:[tab tabView] |
+ frame:sourceTabFrame_ |
+ yStretchiness:0]; |
+ } |
+ } |
+ // Simply end the drag at this point. |
+ [self endDrag:theEvent]; |
+ break; |
+ } |
+ } else if (type == NSLeftMouseDragged) { |
+ [self continueDrag:theEvent]; |
+ } else if (type == NSLeftMouseUp) { |
+ [[tab view] mouseUp:theEvent]; |
+ [self endDrag: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)continueDrag:(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:[draggedTab_ tabView] |
+ 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:[draggedTab_ tabView]]; |
+ 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:[draggedTab_ tabView]]; |
+ 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(continueDrag:) |
+ 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_ activeTabView]; |
+ 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:[draggedTab_ tabView] |
+ 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)endDrag:(NSEvent*)event { |
+ // 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 activeTabView] |
+ fromController:nil]; |
+ } |
+ } else if (targetController_) { |
+ // Move between windows. If |targetController_| is nil, we're not dropping |
+ // into any existing window. |
+ NSView* draggedTabView = [draggedController_ activeTabView]; |
+ [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]; |
+} |
+ |
+// Private ///////////////////////////////////////////////////////////////////// |
+ |
+// 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(); |
+} |
+ |
+// 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; |
+} |
+ |
+// 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; |
+} |
+ |
+// 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; |
+} |
+ |
+@end |