- add sources.
[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 "chrome/browser/ui/cocoa/tabs/tab_controller.h"
10 #import "chrome/browser/ui/cocoa/tabs/tab_controller_target.h"
11 #import "chrome/browser/ui/cocoa/tabs/tab_view.h"
12 #import "chrome/browser/ui/cocoa/tabs/tab_window_controller.h"
13
14 // Replicate specific 10.7 SDK declarations for building with prior SDKs.
15 #if !defined(MAC_OS_X_VERSION_10_7) || \
16 MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_7
17
18 enum {
19   NSWindowAnimationBehaviorDefault = 0,
20   NSWindowAnimationBehaviorNone = 2,
21   NSWindowAnimationBehaviorDocumentWindow = 3,
22   NSWindowAnimationBehaviorUtilityWindow = 4,
23   NSWindowAnimationBehaviorAlertPanel = 5
24 };
25 typedef NSInteger NSWindowAnimationBehavior;
26
27 @interface NSWindow (LionSDKDeclarations)
28 - (NSWindowAnimationBehavior)animationBehavior;
29 - (void)setAnimationBehavior:(NSWindowAnimationBehavior)newAnimationBehavior;
30 @end
31
32 #endif  // MAC_OS_X_VERSION_10_7
33
34 const CGFloat kTearDistance = 36.0;
35 const NSTimeInterval kTearDuration = 0.333;
36
37 @interface TabStripDragController (Private)
38 - (void)resetDragControllers;
39 - (NSArray*)dropTargetsForController:(TabWindowController*)dragController;
40 - (void)setWindowBackgroundVisibility:(BOOL)shouldBeVisible;
41 - (void)endDrag:(NSEvent*)event;
42 - (void)continueDrag:(NSEvent*)event;
43 @end
44
45 ////////////////////////////////////////////////////////////////////////////////
46
47 @implementation TabStripDragController
48
49 - (id)initWithTabStripController:(TabStripController*)controller {
50   if ((self = [super init])) {
51     tabStrip_ = controller;
52   }
53   return self;
54 }
55
56 - (void)dealloc {
57   [NSObject cancelPreviousPerformRequestsWithTarget:self];
58   [super dealloc];
59 }
60
61 - (BOOL)tabCanBeDragged:(TabController*)tab {
62   if ([[tab tabView] isClosing])
63     return NO;
64   NSWindowController* controller = [sourceWindow_ windowController];
65   if ([controller isKindOfClass:[TabWindowController class]]) {
66     TabWindowController* realController =
67         static_cast<TabWindowController*>(controller);
68     return [realController isTabDraggable:[tab tabView]];
69   }
70   return YES;
71 }
72
73 - (void)maybeStartDrag:(NSEvent*)theEvent forTab:(TabController*)tab {
74   [self resetDragControllers];
75
76   // Resolve overlay back to original window.
77   sourceWindow_ = [[tab view] window];
78   if ([sourceWindow_ isKindOfClass:[NSPanel class]]) {
79     sourceWindow_ = [sourceWindow_ parentWindow];
80   }
81
82   sourceWindowFrame_ = [sourceWindow_ frame];
83   sourceTabFrame_ = [[tab view] frame];
84   sourceController_ = [sourceWindow_ windowController];
85   draggedTab_ = tab;
86   tabWasDragged_ = NO;
87   tearTime_ = 0.0;
88   draggingWithinTabStrip_ = YES;
89   chromeIsVisible_ = NO;
90
91   // If there's more than one potential window to be a drop target, we want to
92   // treat a drag of a tab just like dragging around a tab that's already
93   // detached. Note that unit tests might have |-numberOfTabs| reporting zero
94   // since the model won't be fully hooked up. We need to be prepared for that
95   // and not send them into the "magnetic" codepath.
96   NSArray* targets = [self dropTargetsForController:sourceController_];
97   moveWindowOnDrag_ =
98       ([sourceController_ numberOfTabs] < 2 && ![targets count]) ||
99       ![self tabCanBeDragged:tab] ||
100       ![sourceController_ tabDraggingAllowed];
101   // If we are dragging a tab, a window with a single tab should immediately
102   // snap off and not drag within the tab strip.
103   if (!moveWindowOnDrag_)
104     draggingWithinTabStrip_ = [sourceController_ numberOfTabs] > 1;
105
106   dragOrigin_ = [NSEvent mouseLocation];
107
108   // When spinning the event loop, a tab can get detached, which could lead to
109   // our own destruction. Keep ourselves around while spinning the loop as well
110   // as the tab controller being dragged.
111   base::scoped_nsobject<TabStripDragController> keepAlive([self retain]);
112   base::scoped_nsobject<TabController> keepAliveTab([tab retain]);
113
114   // Because we move views between windows, we need to handle the event loop
115   // ourselves. Ideally we should use the standard event loop.
116   while (1) {
117     const NSUInteger mask =
118         NSLeftMouseUpMask | NSLeftMouseDraggedMask | NSKeyUpMask;
119     theEvent =
120         [NSApp nextEventMatchingMask:mask
121                            untilDate:[NSDate distantFuture]
122                               inMode:NSDefaultRunLoopMode
123                              dequeue:YES];
124     NSEventType type = [theEvent type];
125     if (type == NSKeyUp) {
126       if ([theEvent keyCode] == kVK_Escape) {
127         // Cancel the drag and restore the previous state.
128         if (draggingWithinTabStrip_) {
129           // Simply pretend the tab wasn't dragged (far enough).
130           tabWasDragged_ = NO;
131         } else {
132           [targetController_ removePlaceholder];
133           if ([sourceController_ numberOfTabs] < 2) {
134             // Revert to a single-tab window.
135             targetController_ = nil;
136           } else {
137             // Change the target to the source controller.
138             targetController_ = sourceController_;
139             [targetController_ insertPlaceholderForTab:[tab tabView]
140                                                  frame:sourceTabFrame_];
141           }
142         }
143         // Simply end the drag at this point.
144         [self endDrag:theEvent];
145         break;
146       }
147     } else if (type == NSLeftMouseDragged) {
148       [self continueDrag:theEvent];
149     } else if (type == NSLeftMouseUp) {
150       [tab selectTab:self];
151       [self endDrag:theEvent];
152       break;
153     } else {
154       // TODO(viettrungluu): [crbug.com/23830] We can receive right-mouse-ups
155       // (and maybe even others?) for reasons I don't understand. So we
156       // explicitly check for both events we're expecting, and log others. We
157       // should figure out what's going on.
158       LOG(WARNING) << "Spurious event received of type " << type << ".";
159     }
160   }
161 }
162
163 - (void)continueDrag:(NSEvent*)theEvent {
164   CHECK(draggedTab_);
165
166   // Cancel any delayed -continueDrag: requests that may still be pending.
167   [NSObject cancelPreviousPerformRequestsWithTarget:self];
168
169   // Special-case this to keep the logic below simpler.
170   if (moveWindowOnDrag_) {
171     if ([sourceController_ windowMovementAllowed]) {
172       NSPoint thisPoint = [NSEvent mouseLocation];
173       NSPoint origin = sourceWindowFrame_.origin;
174       origin.x += (thisPoint.x - dragOrigin_.x);
175       origin.y += (thisPoint.y - dragOrigin_.y);
176       [sourceWindow_ setFrameOrigin:NSMakePoint(origin.x, origin.y)];
177     }  // else do nothing.
178     return;
179   }
180
181   // First, go through the magnetic drag cycle. We break out of this if
182   // "stretchiness" ever exceeds a set amount.
183   tabWasDragged_ = YES;
184
185   if (draggingWithinTabStrip_) {
186     NSPoint thisPoint = [NSEvent mouseLocation];
187     CGFloat offset = thisPoint.x - dragOrigin_.x;
188     [sourceController_ insertPlaceholderForTab:[draggedTab_ tabView]
189                                          frame:NSOffsetRect(sourceTabFrame_,
190                                                             offset, 0)];
191     // Check that we haven't pulled the tab too far to start a drag. This
192     // can include either pulling it too far down, or off the side of the tab
193     // strip that would cause it to no longer be fully visible.
194     BOOL stillVisible =
195         [sourceController_ isTabFullyVisible:[draggedTab_ tabView]];
196     CGFloat tearForce = fabs(thisPoint.y - dragOrigin_.y);
197     if ([sourceController_ tabTearingAllowed] &&
198         (tearForce > kTearDistance || !stillVisible)) {
199       draggingWithinTabStrip_ = NO;
200       // When you finally leave the strip, we treat that as the origin.
201       dragOrigin_.x = thisPoint.x;
202     } else {
203       // Still dragging within the tab strip, wait for the next drag event.
204       return;
205     }
206   }
207
208   // Do not start dragging until the user has "torn" the tab off by
209   // moving more than 3 pixels.
210   NSPoint thisPoint = [NSEvent mouseLocation];
211
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       NSRect tabStripFrame = [[target tabStripView] frame];
224       tabStripFrame.origin = [[target window]
225                               convertBaseToScreen:tabStripFrame.origin];
226       if (NSPointInRect(thisPoint, tabStripFrame)) {
227         newTarget = target;
228       }
229       break;
230     }
231   }
232
233   // If we're now targeting a new window, re-layout the tabs in the old
234   // target and reset how long we've been hovering over this new one.
235   if (targetController_ != newTarget) {
236     [targetController_ removePlaceholder];
237     targetController_ = newTarget;
238     if (!newTarget) {
239       tearTime_ = [NSDate timeIntervalSinceReferenceDate];
240       tearOrigin_ = [dragWindow_ frame].origin;
241     }
242   }
243
244   // Create or identify the dragged controller.
245   if (!draggedController_) {
246     // Get rid of any placeholder remaining in the original source window.
247     [sourceController_ removePlaceholder];
248
249     // Detach from the current window and put it in a new window. If there are
250     // no more tabs remaining after detaching, the source window is about to
251     // go away (it's been autoreleased) so we need to ensure we don't reference
252     // it any more. In that case the new controller becomes our source
253     // controller.
254     draggedController_ =
255         [sourceController_ detachTabToNewWindow:[draggedTab_ tabView]];
256     dragWindow_ = [draggedController_ window];
257     [dragWindow_ setAlphaValue:0.0];
258     if (![sourceController_ hasLiveTabs]) {
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     TabView* draggedTabView = (TabView*)[draggedController_ activeTabView];
353     NSRect tabFrame = [draggedTabView frame];
354     tabFrame.origin = [dragWindow_ convertBaseToScreen:tabFrame.origin];
355     tabFrame.origin = [[targetController_ window]
356                         convertScreenToBase:tabFrame.origin];
357     tabFrame = [[targetController_ tabStripView]
358                 convertRect:tabFrame fromView:nil];
359     [targetController_ insertPlaceholderForTab:[draggedTab_ tabView]
360                                          frame:tabFrame];
361     [targetController_ layoutTabs];
362   } else {
363     [dragWindow_ makeKeyAndOrderFront:nil];
364   }
365
366   // Adjust the visibility of the window background. If there is a drop target,
367   // we want to hide the window background so the tab stands out for
368   // positioning. If not, we want to show it so it looks like a new window will
369   // be realized.
370   BOOL chromeShouldBeVisible = targetController_ == nil;
371   [self setWindowBackgroundVisibility:chromeShouldBeVisible];
372 }
373
374 - (void)endDrag:(NSEvent*)event {
375   // Cancel any delayed -continueDrag: requests that may still be pending.
376   [NSObject cancelPreviousPerformRequestsWithTarget:self];
377
378   // Special-case this to keep the logic below simpler.
379   if (moveWindowOnDrag_) {
380     [self resetDragControllers];
381     return;
382   }
383
384   // TODO(pinkerton): http://crbug.com/25682 demonstrates a way to get here by
385   // some weird circumstance that doesn't first go through mouseDown:. We
386   // really shouldn't go any farther.
387   if (!sourceController_)
388     return;
389
390   // We are now free to re-display the new tab button in the window we're
391   // dragging. It will show when the next call to -layoutTabs (which happens
392   // indirectly by several of the calls below, such as removing the
393   // placeholder).
394   [draggedController_ showNewTabButton:YES];
395
396   if (draggingWithinTabStrip_) {
397     if (tabWasDragged_) {
398       // Move tab to new location.
399       DCHECK([sourceController_ numberOfTabs]);
400       TabWindowController* dropController = sourceController_;
401       [dropController moveTabView:[dropController activeTabView]
402                    fromController:nil];
403     }
404   } else if (targetController_) {
405     // Move between windows. If |targetController_| is nil, we're not dropping
406     // into any existing window.
407     NSView* draggedTabView = [draggedController_ activeTabView];
408     [targetController_ moveTabView:draggedTabView
409                     fromController:draggedController_];
410     // Force redraw to avoid flashes of old content before returning to event
411     // loop.
412     [[targetController_ window] display];
413     [targetController_ showWindow:nil];
414     [draggedController_ removeOverlay];
415   } else {
416     // Only move the window around on screen. Make sure it's set back to
417     // normal state (fully opaque, has shadow, has key, etc).
418     [draggedController_ removeOverlay];
419     // Don't want to re-show the window if it was closed during the drag.
420     if ([dragWindow_ isVisible]) {
421       [dragWindow_ setAlphaValue:1.0];
422       [dragOverlay_ setHasShadow:NO];
423       [dragWindow_ setHasShadow:YES];
424       [dragWindow_ makeKeyAndOrderFront:nil];
425     }
426     [[draggedController_ window] setLevel:NSNormalWindowLevel];
427     [draggedController_ removePlaceholder];
428   }
429   [sourceController_ removePlaceholder];
430   chromeIsVisible_ = YES;
431
432   [self resetDragControllers];
433 }
434
435 // Private /////////////////////////////////////////////////////////////////////
436
437 // Call to clear out transient weak references we hold during drags.
438 - (void)resetDragControllers {
439   draggedTab_ = nil;
440   draggedController_ = nil;
441   dragWindow_ = nil;
442   dragOverlay_ = nil;
443   sourceController_ = nil;
444   sourceWindow_ = nil;
445   targetController_ = nil;
446 }
447
448 // Returns an array of controllers that could be a drop target, ordered front to
449 // back. It has to be of the appropriate class, and visible (obviously). Note
450 // that the window cannot be a target for itself.
451 - (NSArray*)dropTargetsForController:(TabWindowController*)dragController {
452   NSMutableArray* targets = [NSMutableArray array];
453   NSWindow* dragWindow = [dragController window];
454   for (NSWindow* window in [NSApp orderedWindows]) {
455     if (window == dragWindow) continue;
456     if (![window isVisible]) continue;
457     // Skip windows on the wrong space.
458     if (![window isOnActiveSpace])
459       continue;
460     NSWindowController* controller = [window windowController];
461     if ([controller isKindOfClass:[TabWindowController class]]) {
462       TabWindowController* realController =
463           static_cast<TabWindowController*>(controller);
464       if ([realController canReceiveFrom:dragController])
465         [targets addObject:controller];
466     }
467   }
468   return targets;
469 }
470
471 // Sets whether the window background should be visible or invisible when
472 // dragging a tab. The background should be invisible when the mouse is over a
473 // potential drop target for the tab (the tab strip). It should be visible when
474 // there's no drop target so the window looks more fully realized and ready to
475 // become a stand-alone window.
476 - (void)setWindowBackgroundVisibility:(BOOL)shouldBeVisible {
477   if (chromeIsVisible_ == shouldBeVisible)
478     return;
479
480   // There appears to be a race-condition in CoreAnimation where if we use
481   // animators to set the alpha values, we can't guarantee that we cancel them.
482   // This has the side effect of sometimes leaving the dragged window
483   // translucent or invisible. As a result, don't animate the alpha change.
484   [[draggedController_ overlayWindow] setAlphaValue:1.0];
485   if (targetController_) {
486     [dragWindow_ setAlphaValue:0.0];
487     [[draggedController_ overlayWindow] setHasShadow:YES];
488     [[targetController_ window] makeMainWindow];
489   } else {
490     [dragWindow_ setAlphaValue:0.5];
491     [[draggedController_ overlayWindow] setHasShadow:NO];
492     [[draggedController_ window] makeMainWindow];
493   }
494   chromeIsVisible_ = shouldBeVisible;
495 }
496
497 @end