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