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