Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(324)

Side by Side Diff: chrome/browser/ui/cocoa/tabs/tab_strip_drag_controller.mm

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

Powered by Google App Engine
This is Rietveld 408576698