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