Popular sites on the NTP: check that experiment group StartsWith (rather than IS...
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / tabs / tab_strip_drag_controller.mm
blob9fc8cadee747ed62cc01ae5f728ab908014339ba
1 // Copyright (c) 2012 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.
5 #import "chrome/browser/ui/cocoa/tabs/tab_strip_drag_controller.h"
7 #include <Carbon/Carbon.h>
9 #include "base/mac/scoped_cftyperef.h"
10 #import "base/mac/sdk_forward_declarations.h"
11 #import "chrome/browser/ui/cocoa/tabs/tab_controller.h"
12 #import "chrome/browser/ui/cocoa/tabs/tab_controller_target.h"
13 #import "chrome/browser/ui/cocoa/tabs/tab_strip_controller.h"
14 #import "chrome/browser/ui/cocoa/tabs/tab_strip_view.h"
15 #import "chrome/browser/ui/cocoa/tabs/tab_view.h"
16 #import "chrome/browser/ui/cocoa/tabs/tab_window_controller.h"
17 #include "ui/gfx/mac/scoped_cocoa_disable_screen_updates.h"
19 const CGFloat kTearDistance = 36.0;
20 const NSTimeInterval kTearDuration = 0.333;
22 // Returns whether |screenPoint| is inside the bounds of |view|.
23 static BOOL PointIsInsideView(NSPoint screenPoint, NSView* view) {
24   if ([view window] == nil)
25     return NO;
26   NSPoint windowPoint = [[view window] convertScreenToBase:screenPoint];
27   NSPoint viewPoint = [view convertPoint:windowPoint fromView:nil];
28   return [view mouse:viewPoint inRect:[view bounds]];
31 @interface TabStripDragController (Private)
32 - (NSArray*)selectedTabViews;
33 - (BOOL)canDragSelectedTabs;
34 - (void)resetDragControllers;
35 - (NSArray*)dropTargetsForController:(TabWindowController*)dragController;
36 - (void)setWindowBackgroundVisibility:(BOOL)shouldBeVisible;
37 - (void)endDrag:(NSEvent*)event;
38 - (void)continueDrag:(NSEvent*)event;
39 @end
41 ////////////////////////////////////////////////////////////////////////////////
43 @implementation TabStripDragController
45 @synthesize draggedTab = draggedTab_;
47 - (id)initWithTabStripController:(TabStripController*)controller {
48   if ((self = [super init])) {
49     tabStrip_ = controller;
50   }
51   return self;
54 - (void)dealloc {
55   [NSObject cancelPreviousPerformRequestsWithTarget:self];
56   [super dealloc];
59 - (void)maybeStartDrag:(NSEvent*)theEvent forTab:(TabController*)tab {
60   [self resetDragControllers];
62   // Resolve overlay back to original window.
63   sourceWindow_ = [[tab view] window];
64   if ([sourceWindow_ isKindOfClass:[NSPanel class]]) {
65     sourceWindow_ = [sourceWindow_ parentWindow];
66   }
68   sourceWindowFrame_ = [sourceWindow_ frame];
69   sourceTabFrame_ = [[tab view] frame];
70   sourceController_ = [sourceWindow_ windowController];
71   draggedTab_ = tab;
72   tabWasDragged_ = NO;
73   tearTime_ = 0.0;
74   draggingWithinTabStrip_ = YES;
75   chromeIsVisible_ = NO;
77   moveWindowOnDrag_ = ![self canDragSelectedTabs] ||
78                       ![sourceController_ tabDraggingAllowed];
79   // If we are dragging a tab, a window with a single tab should immediately
80   // snap off and not drag within the tab strip.
81   if (!moveWindowOnDrag_)
82     draggingWithinTabStrip_ = [sourceController_ numberOfTabs] > 1;
84   dragOrigin_ = [NSEvent mouseLocation];
86   // When spinning the event loop, a tab can get detached, which could lead to
87   // our own destruction. Keep ourselves around while spinning the loop as well
88   // as the tab controller being dragged.
89   base::scoped_nsobject<TabStripDragController> keepAlive([self retain]);
90   base::scoped_nsobject<TabController> keepAliveTab([tab retain]);
92   // Because we move views between windows, we need to handle the event loop
93   // ourselves. Ideally we should use the standard event loop.
94   while (1) {
95     const NSUInteger mask =
96         NSLeftMouseUpMask | NSLeftMouseDraggedMask | NSKeyUpMask;
97     theEvent =
98         [NSApp nextEventMatchingMask:mask
99                            untilDate:[NSDate distantFuture]
100                               inMode:NSDefaultRunLoopMode
101                              dequeue:YES];
103     // Ensure that any window changes that happen while handling this event
104     // appear atomically.
105     gfx::ScopedCocoaDisableScreenUpdates disabler;
107     NSEventType type = [theEvent type];
108     if (type == NSKeyUp) {
109       if ([theEvent keyCode] == kVK_Escape) {
110         // Cancel the drag and restore the previous state.
111         if (draggingWithinTabStrip_) {
112           // Simply pretend the tab wasn't dragged (far enough).
113           tabWasDragged_ = NO;
114         } else {
115           [targetController_ removePlaceholder];
116           [[sourceController_ window] makeMainWindow];
117           if ([sourceController_ numberOfTabs] < 2) {
118             // Revert to a single-tab window.
119             targetController_ = nil;
120           } else {
121             // Change the target to the source controller.
122             targetController_ = sourceController_;
123             [targetController_ insertPlaceholderForTab:[tab tabView]
124                                                  frame:sourceTabFrame_];
125           }
126         }
127         // Simply end the drag at this point.
128         [self endDrag:theEvent];
129         break;
130       }
131     } else if (type == NSLeftMouseDragged) {
132       [self continueDrag:theEvent];
133     } else if (type == NSLeftMouseUp) {
134       [tab selectTab:self];
135       [self endDrag:theEvent];
136       break;
137     } else {
138       // TODO(viettrungluu): [crbug.com/23830] We can receive right-mouse-ups
139       // (and maybe even others?) for reasons I don't understand. So we
140       // explicitly check for both events we're expecting, and log others. We
141       // should figure out what's going on.
142       LOG(WARNING) << "Spurious event received of type " << type << ".";
143     }
144   }
147 - (void)continueDrag:(NSEvent*)theEvent {
148   CHECK(draggedTab_);
150   // Cancel any delayed -continueDrag: requests that may still be pending.
151   [NSObject cancelPreviousPerformRequestsWithTarget:self];
153   // Special-case this to keep the logic below simpler.
154   if (moveWindowOnDrag_) {
155     if ([sourceController_ windowMovementAllowed]) {
156       NSPoint thisPoint = [NSEvent mouseLocation];
157       NSPoint origin = sourceWindowFrame_.origin;
158       origin.x += (thisPoint.x - dragOrigin_.x);
159       origin.y += (thisPoint.y - dragOrigin_.y);
160       [sourceWindow_ setFrameOrigin:NSMakePoint(origin.x, origin.y)];
161     }  // else do nothing.
162     return;
163   }
165   // First, go through the magnetic drag cycle. We break out of this if
166   // "stretchiness" ever exceeds a set amount.
167   tabWasDragged_ = YES;
169   if (draggingWithinTabStrip_) {
170     NSPoint thisPoint = [NSEvent mouseLocation];
171     CGFloat offset = thisPoint.x - dragOrigin_.x;
172     [sourceController_ insertPlaceholderForTab:[draggedTab_ tabView]
173                                          frame:NSOffsetRect(sourceTabFrame_,
174                                                             offset, 0)];
175     // Check that we haven't pulled the tab too far to start a drag. This
176     // can include either pulling it too far down, or off the side of the tab
177     // strip that would cause it to no longer be fully visible.
178     BOOL stillVisible =
179         [sourceController_ isTabFullyVisible:[draggedTab_ tabView]];
180     CGFloat tearForce = fabs(thisPoint.y - dragOrigin_.y);
181     if ([sourceController_ tabTearingAllowed] &&
182         (tearForce > kTearDistance || !stillVisible)) {
183       draggingWithinTabStrip_ = NO;
184       // When you finally leave the strip, we treat that as the origin.
185       dragOrigin_.x = thisPoint.x;
186     } else {
187       // Still dragging within the tab strip, wait for the next drag event.
188       return;
189     }
190   }
192   NSPoint thisPoint = [NSEvent mouseLocation];
194   // Iterate over possible targets checking for the one the mouse is in.
195   // If the tab is just in the frame, bring the window forward to make it
196   // easier to drop something there. If it's in the tab strip, set the new
197   // target so that it pops into that window. We can't cache this because we
198   // need the z-order to be correct.
199   NSArray* targets = [self dropTargetsForController:draggedController_];
200   TabWindowController* newTarget = nil;
201   for (TabWindowController* target in targets) {
202     NSRect windowFrame = [[target window] frame];
203     if (NSPointInRect(thisPoint, windowFrame)) {
204       [[target window] orderFront:self];
205       if (PointIsInsideView(thisPoint, [target tabStripView])) {
206         newTarget = target;
207       }
208       break;
209     }
210   }
212   // If we're now targeting a new window, re-layout the tabs in the old
213   // target and reset how long we've been hovering over this new one.
214   if (targetController_ != newTarget) {
215     [targetController_ removePlaceholder];
216     targetController_ = newTarget;
217     if (!newTarget) {
218       tearTime_ = [NSDate timeIntervalSinceReferenceDate];
219       tearOrigin_ = [dragWindow_ frame].origin;
220     }
221   }
223   // Create or identify the dragged controller.
224   if (!draggedController_) {
225     // Detach from the current window and put it in a new window. If there are
226     // no more tabs remaining after detaching, the source window is about to
227     // go away (it's been autoreleased) so we need to ensure we don't reference
228     // it any more. In that case the new controller becomes our source
229     // controller.
230     NSArray* tabs = [self selectedTabViews];
231     draggedController_ =
232         [sourceController_ detachTabsToNewWindow:tabs
233                                       draggedTab:[draggedTab_ tabView]];
235     dragWindow_ = [draggedController_ window];
236     [dragWindow_ setAlphaValue:0.0];
237     if ([sourceController_ hasLiveTabs]) {
238       if (PointIsInsideView(thisPoint, [sourceController_ tabStripView])) {
239         // We don't want to remove the source window's placeholder here because
240         // the new tab button may briefly flash in and out if we remove and add
241         // back the placeholder.
242         // Instead, we will remove the placeholder later when the target window
243         // actually changes.
244         targetController_ = sourceController_;
245       } else {
246         [sourceController_ removePlaceholder];
247       }
248     } else {
249       [sourceController_ removePlaceholder];
250       sourceController_ = draggedController_;
251       sourceWindow_ = dragWindow_;
252     }
254     // Disable window animation before calling |orderFront:| when detaching
255     // to a new window.
256     NSWindowAnimationBehavior savedAnimationBehavior =
257         NSWindowAnimationBehaviorDefault;
258     bool didSaveAnimationBehavior = false;
259     if ([dragWindow_ respondsToSelector:@selector(animationBehavior)] &&
260         [dragWindow_ respondsToSelector:@selector(setAnimationBehavior:)]) {
261       didSaveAnimationBehavior = true;
262       savedAnimationBehavior = [dragWindow_ animationBehavior];
263       [dragWindow_ setAnimationBehavior:NSWindowAnimationBehaviorNone];
264     }
266     // If dragging the tab only moves the current window, do not show overlay
267     // so that sheets stay on top of the window.
268     // Bring the target window to the front and make sure it has a border.
269     [dragWindow_ setLevel:NSFloatingWindowLevel];
270     [dragWindow_ setHasShadow:YES];
271     [dragWindow_ orderFront:nil];
272     [dragWindow_ makeMainWindow];
273     [draggedController_ showOverlay];
274     dragOverlay_ = [draggedController_ overlayWindow];
275     [dragOverlay_ setHasShadow:YES];
276     // Force the new tab button to be hidden. We'll reset it on mouse up.
277     [draggedController_ showNewTabButton:NO];
278     tearTime_ = [NSDate timeIntervalSinceReferenceDate];
279     tearOrigin_ = sourceWindowFrame_.origin;
281     // Restore window animation behavior.
282     if (didSaveAnimationBehavior)
283       [dragWindow_ setAnimationBehavior:savedAnimationBehavior];
284   }
286   // TODO(pinkerton): http://crbug.com/25682 demonstrates a way to get here by
287   // some weird circumstance that doesn't first go through mouseDown:. We
288   // really shouldn't go any farther.
289   if (!draggedController_ || !sourceController_)
290     return;
292   // When the user first tears off the window, we want slide the window to
293   // the current mouse location (to reduce the jarring appearance). We do this
294   // by calling ourselves back with additional -continueDrag: calls (not actual
295   // events). |tearProgress| is a normalized measure of how far through this
296   // tear "animation" (of length kTearDuration) we are and has values [0..1].
297   // We use sqrt() so the animation is non-linear (slow down near the end
298   // point).
299   NSTimeInterval tearProgress =
300       [NSDate timeIntervalSinceReferenceDate] - tearTime_;
301   tearProgress /= kTearDuration;  // Normalize.
302   tearProgress = sqrtf(MAX(MIN(tearProgress, 1.0), 0.0));
304   // Move the dragged window to the right place on the screen.
305   NSPoint origin = sourceWindowFrame_.origin;
306   origin.x += (thisPoint.x - dragOrigin_.x);
307   origin.y += (thisPoint.y - dragOrigin_.y);
309   if (tearProgress < 1) {
310     // If the tear animation is not complete, call back to ourself with the
311     // same event to animate even if the mouse isn't moving. We need to make
312     // sure these get cancelled in -endDrag:.
313     [NSObject cancelPreviousPerformRequestsWithTarget:self];
314     [self performSelector:@selector(continueDrag:)
315                withObject:theEvent
316                afterDelay:1.0f/30.0f];
318     // Set the current window origin based on how far we've progressed through
319     // the tear animation.
320     origin.x = (1 - tearProgress) * tearOrigin_.x + tearProgress * origin.x;
321     origin.y = (1 - tearProgress) * tearOrigin_.y + tearProgress * origin.y;
322   }
324   if (targetController_) {
325     // In order to "snap" two windows of different sizes together at their
326     // toolbar, we can't just use the origin of the target frame. We also have
327     // to take into consideration the difference in height.
328     NSRect targetFrame = [[targetController_ window] frame];
329     NSRect sourceFrame = [dragWindow_ frame];
330     origin.y = NSMinY(targetFrame) +
331                 (NSHeight(targetFrame) - NSHeight(sourceFrame));
332   }
333   [dragWindow_ setFrameOrigin:NSMakePoint(origin.x, origin.y)];
335   // If we're not hovering over any window, make the window fully
336   // opaque. Otherwise, find where the tab might be dropped and insert
337   // a placeholder so it appears like it's part of that window.
338   if (targetController_) {
339     if (![[targetController_ window] isKeyWindow])
340       [[targetController_ window] orderFront:nil];
342     // Compute where placeholder should go and insert it into the
343     // destination tab strip.
344     // The placeholder frame is the rect that contains all dragged tabs.
345     NSRect tabFrame = NSZeroRect;
346     for (NSView* tabView in [draggedController_ tabViews]) {
347       tabFrame = NSUnionRect(tabFrame, [tabView frame]);
348     }
349     tabFrame.origin = [dragWindow_ convertBaseToScreen:tabFrame.origin];
350     tabFrame.origin = [[targetController_ window]
351                         convertScreenToBase:tabFrame.origin];
352     tabFrame = [[targetController_ tabStripView]
353                 convertRect:tabFrame fromView:nil];
354     [targetController_ insertPlaceholderForTab:[draggedTab_ tabView]
355                                          frame:tabFrame];
356     [targetController_ layoutTabs];
357   } else {
358     [dragWindow_ makeKeyAndOrderFront:nil];
359   }
361   // Adjust the visibility of the window background. If there is a drop target,
362   // we want to hide the window background so the tab stands out for
363   // positioning. If not, we want to show it so it looks like a new window will
364   // be realized.
365   BOOL chromeShouldBeVisible = targetController_ == nil;
366   [self setWindowBackgroundVisibility:chromeShouldBeVisible];
369 - (void)endDrag:(NSEvent*)event {
370   // Cancel any delayed -continueDrag: requests that may still be pending.
371   [NSObject cancelPreviousPerformRequestsWithTarget:self];
373   // Special-case this to keep the logic below simpler.
374   if (moveWindowOnDrag_) {
375     [self resetDragControllers];
376     return;
377   }
379   // TODO(pinkerton): http://crbug.com/25682 demonstrates a way to get here by
380   // some weird circumstance that doesn't first go through mouseDown:. We
381   // really shouldn't go any farther.
382   if (!sourceController_)
383     return;
385   // We are now free to re-display the new tab button in the window we're
386   // dragging. It will show when the next call to -layoutTabs (which happens
387   // indirectly by several of the calls below, such as removing the
388   // placeholder).
389   [draggedController_ showNewTabButton:YES];
391   if (draggingWithinTabStrip_) {
392     if (tabWasDragged_) {
393       // Move tab to new location.
394       DCHECK([sourceController_ numberOfTabs]);
395       TabWindowController* dropController = sourceController_;
396       [dropController moveTabViews:@[ [dropController activeTabView] ]
397                     fromController:nil];
398     }
399   } else if (targetController_) {
400     // Move between windows. If |targetController_| is nil, we're not dropping
401     // into any existing window.
402     [targetController_ moveTabViews:[draggedController_ tabViews]
403                      fromController:draggedController_];
404     // Force redraw to avoid flashes of old content before returning to event
405     // loop.
406     [[targetController_ window] display];
407     [targetController_ showWindow:nil];
408     [draggedController_ removeOverlay];
409   } else {
410     // Only move the window around on screen. Make sure it's set back to
411     // normal state (fully opaque, has shadow, has key, etc).
412     [draggedController_ removeOverlay];
413     // Don't want to re-show the window if it was closed during the drag.
414     if ([dragWindow_ isVisible]) {
415       [dragWindow_ setAlphaValue:1.0];
416       [dragOverlay_ setHasShadow:NO];
417       [dragWindow_ setHasShadow:YES];
418       [dragWindow_ makeKeyAndOrderFront:nil];
419     }
420     [[draggedController_ window] setLevel:NSNormalWindowLevel];
421     [draggedController_ removePlaceholder];
422   }
423   [sourceController_ removePlaceholder];
424   chromeIsVisible_ = YES;
426   [self resetDragControllers];
429 // Private /////////////////////////////////////////////////////////////////////
431 - (NSArray*)selectedTabViews {
432   return [draggedTab_ selected] ? [tabStrip_ selectedViews]
433                                 : @[ [draggedTab_ tabView] ];
436 - (BOOL)canDragSelectedTabs {
437   NSArray* tabs = [self selectedTabViews];
439   // If there's more than one potential window to be a drop target, we want to
440   // treat a drag of a tab just like dragging around a tab that's already
441   // detached. Note that unit tests might have |-numberOfTabs| reporting zero
442   // since the model won't be fully hooked up. We need to be prepared for that
443   // and not send them into the "magnetic" codepath.
444   NSArray* targets = [self dropTargetsForController:sourceController_];
445   if (![targets count] && [sourceController_ numberOfTabs] - [tabs count] == 0)
446     return NO;  // I.e. ignore dragging *all* tabs in the last Browser window.
448   for (TabView* tabView in tabs) {
449     if ([tabView isClosing])
450       return NO;
451   }
453   NSWindowController* controller = [sourceWindow_ windowController];
454   if ([controller isKindOfClass:[TabWindowController class]]) {
455     TabWindowController* realController =
456         static_cast<TabWindowController*>(controller);
457     for (TabView* tabView in tabs) {
458       if (![realController isTabDraggable:tabView])
459         return NO;
460     }
461   }
462   return YES;
465 // Call to clear out transient weak references we hold during drags.
466 - (void)resetDragControllers {
467   draggedTab_ = nil;
468   draggedController_ = nil;
469   dragWindow_ = nil;
470   dragOverlay_ = nil;
471   sourceController_ = nil;
472   sourceWindow_ = nil;
473   targetController_ = nil;
476 // Returns an array of controllers that could be a drop target, ordered front to
477 // back. It has to be of the appropriate class, and visible (obviously). Note
478 // that the window cannot be a target for itself.
479 - (NSArray*)dropTargetsForController:(TabWindowController*)dragController {
480   NSMutableArray* targets = [NSMutableArray array];
481   NSWindow* dragWindow = [dragController window];
482   for (NSWindow* window in [NSApp orderedWindows]) {
483     if (window == dragWindow) continue;
484     if (![window isVisible]) continue;
485     // Skip windows on the wrong space.
486     if (![window isOnActiveSpace])
487       continue;
488     NSWindowController* controller = [window windowController];
489     if ([controller isKindOfClass:[TabWindowController class]]) {
490       TabWindowController* realController =
491           static_cast<TabWindowController*>(controller);
492       if ([realController canReceiveFrom:dragController])
493         [targets addObject:controller];
494     }
495   }
496   return targets;
499 // Sets whether the window background should be visible or invisible when
500 // dragging a tab. The background should be invisible when the mouse is over a
501 // potential drop target for the tab (the tab strip). It should be visible when
502 // there's no drop target so the window looks more fully realized and ready to
503 // become a stand-alone window.
504 - (void)setWindowBackgroundVisibility:(BOOL)shouldBeVisible {
505   if (chromeIsVisible_ == shouldBeVisible)
506     return;
508   // There appears to be a race-condition in CoreAnimation where if we use
509   // animators to set the alpha values, we can't guarantee that we cancel them.
510   // This has the side effect of sometimes leaving the dragged window
511   // translucent or invisible. As a result, don't animate the alpha change.
512   [[draggedController_ overlayWindow] setAlphaValue:1.0];
513   if (targetController_) {
514     [dragWindow_ setAlphaValue:0.0];
515     [[draggedController_ overlayWindow] setHasShadow:YES];
516     [[targetController_ window] makeMainWindow];
517   } else {
518     [dragWindow_ setAlphaValue:0.5];
519     [[draggedController_ overlayWindow] setHasShadow:NO];
520     [[draggedController_ window] makeMainWindow];
521   }
522   chromeIsVisible_ = shouldBeVisible;
525 @end