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