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