1 // Copyright (c) 2010 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/fullscreen_controller.h" 6 7 #include <algorithm> 8 9 #import "base/mac/mac_util.h" 10 #import "chrome/browser/ui/cocoa/browser_window_controller.h" 11 #import "third_party/GTM/AppKit/GTMNSAnimation+Duration.h" 12 13 NSString* const kWillEnterFullscreenNotification = 14 @"WillEnterFullscreenNotification"; 15 NSString* const kWillLeaveFullscreenNotification = 16 @"WillLeaveFullscreenNotification"; 17 18 namespace { 19 // The activation zone for the main menu is 4 pixels high; if we make it any 20 // smaller, then the menu can be made to appear without the bar sliding down. 21 const CGFloat kDropdownActivationZoneHeight = 4; 22 const NSTimeInterval kDropdownAnimationDuration = 0.12; 23 const NSTimeInterval kMouseExitCheckDelay = 0.1; 24 // This show delay attempts to match the delay for the main menu. 25 const NSTimeInterval kDropdownShowDelay = 0.3; 26 const NSTimeInterval kDropdownHideDelay = 0.2; 27 28 // The amount by which the floating bar is offset downwards (to avoid the menu) 29 // in fullscreen mode. (We can't use |-[NSMenu menuBarHeight]| since it returns 30 // 0 when the menu bar is hidden.) 31 const CGFloat kFloatingBarVerticalOffset = 22; 32 33 } // end namespace 34 35 36 // Helper class to manage animations for the fullscreen dropdown bar. Calls 37 // [FullscreenController changeFloatingBarShownFraction] once per animation 38 // step. 39 @interface DropdownAnimation : NSAnimation { 40 @private 41 FullscreenController* controller_; 42 CGFloat startFraction_; 43 CGFloat endFraction_; 44 } 45 46 @property(readonly, nonatomic) CGFloat startFraction; 47 @property(readonly, nonatomic) CGFloat endFraction; 48 49 // Designated initializer. Asks |controller| for the current shown fraction, so 50 // if the bar is already partially shown or partially hidden, the animation 51 // duration may be less than |fullDuration|. 52 - (id)initWithFraction:(CGFloat)fromFraction 53 fullDuration:(CGFloat)fullDuration 54 animationCurve:(NSInteger)animationCurve 55 controller:(FullscreenController*)controller; 56 57 @end 58 59 @implementation DropdownAnimation 60 61 @synthesize startFraction = startFraction_; 62 @synthesize endFraction = endFraction_; 63 64 - (id)initWithFraction:(CGFloat)toFraction 65 fullDuration:(CGFloat)fullDuration 66 animationCurve:(NSInteger)animationCurve 67 controller:(FullscreenController*)controller { 68 // Calculate the effective duration, based on the current shown fraction. 69 DCHECK(controller); 70 CGFloat fromFraction = [controller floatingBarShownFraction]; 71 CGFloat effectiveDuration = fabs(fullDuration * (fromFraction - toFraction)); 72 73 if ((self = [super gtm_initWithDuration:effectiveDuration 74 eventMask:NSLeftMouseDownMask 75 animationCurve:animationCurve])) { 76 startFraction_ = fromFraction; 77 endFraction_ = toFraction; 78 controller_ = controller; 79 } 80 return self; 81 } 82 83 // Called once per animation step. Overridden to change the floating bar's 84 // position based on the animation's progress. 85 - (void)setCurrentProgress:(NSAnimationProgress)progress { 86 CGFloat fraction = 87 startFraction_ + (progress * (endFraction_ - startFraction_)); 88 [controller_ changeFloatingBarShownFraction:fraction]; 89 } 90 91 @end 92 93 94 @interface FullscreenController (PrivateMethods) 95 96 // Returns YES if the fullscreen window is on the primary screen. 97 - (BOOL)isWindowOnPrimaryScreen; 98 99 // Returns YES if it is ok to show and hide the menu bar in response to the 100 // overlay opening and closing. Will return NO if the window is not main or not 101 // on the primary monitor. 102 - (BOOL)shouldToggleMenuBar; 103 104 // Returns |kFullScreenModeHideAll| when the overlay is hidden and 105 // |kFullScreenModeHideDock| when the overlay is shown. 106 - (base::mac::FullScreenMode)desiredFullscreenMode; 107 108 // Change the overlay to the given fraction, with or without animation. Only 109 // guaranteed to work properly with |fraction == 0| or |fraction == 1|. This 110 // performs the show/hide (animation) immediately. It does not touch the timers. 111 - (void)changeOverlayToFraction:(CGFloat)fraction 112 withAnimation:(BOOL)animate; 113 114 // Schedule the floating bar to be shown/hidden because of mouse position. 115 - (void)scheduleShowForMouse; 116 - (void)scheduleHideForMouse; 117 118 // Set up the tracking area used to activate the sliding bar or keep it active 119 // using with the rectangle in |trackingAreaBounds_|, or remove the tracking 120 // area if one was previously set up. 121 - (void)setupTrackingArea; 122 - (void)removeTrackingAreaIfNecessary; 123 124 // Returns YES if the mouse is currently in any current tracking rectangle, NO 125 // otherwise. 126 - (BOOL)mouseInsideTrackingRect; 127 128 // The tracking area can "falsely" report exits when the menu slides down over 129 // it. In that case, we have to monitor for a "real" mouse exit on a timer. 130 // |-setupMouseExitCheck| schedules a check; |-cancelMouseExitCheck| cancels any 131 // scheduled check. 132 - (void)setupMouseExitCheck; 133 - (void)cancelMouseExitCheck; 134 135 // Called (after a delay) by |-setupMouseExitCheck|, to check whether the mouse 136 // has exited or not; if it hasn't, it will schedule another check. 137 - (void)checkForMouseExit; 138 139 // Start timers for showing/hiding the floating bar. 140 - (void)startShowTimer; 141 - (void)startHideTimer; 142 - (void)cancelShowTimer; 143 - (void)cancelHideTimer; 144 - (void)cancelAllTimers; 145 146 // Methods called when the show/hide timers fire. Do not call directly. 147 - (void)showTimerFire:(NSTimer*)timer; 148 - (void)hideTimerFire:(NSTimer*)timer; 149 150 // Stops any running animations, removes tracking areas, etc. 151 - (void)cleanup; 152 153 // Shows and hides the UI associated with this window being active (having main 154 // status). This includes hiding the menu bar and displaying the "Exit 155 // Fullscreen" button. These functions are called when the window gains or 156 // loses main status as well as in |-cleanup|. 157 - (void)showActiveWindowUI; 158 - (void)hideActiveWindowUI; 159 160 @end 161 162 163 @implementation FullscreenController 164 165 @synthesize isFullscreen = isFullscreen_; 166 167 - (id)initWithBrowserController:(BrowserWindowController*)controller { 168 if ((self = [super init])) { 169 browserController_ = controller; 170 currentFullscreenMode_ = base::mac::kFullScreenModeNormal; 171 } 172 173 // Let the world know what we're up to. 174 [[NSNotificationCenter defaultCenter] 175 postNotificationName:kWillEnterFullscreenNotification 176 object:nil]; 177 178 return self; 179 } 180 181 - (void)dealloc { 182 DCHECK(!isFullscreen_); 183 DCHECK(!trackingArea_); 184 [super dealloc]; 185 } 186 187 - (void)enterFullscreenForContentView:(NSView*)contentView 188 showDropdown:(BOOL)showDropdown { 189 DCHECK(!isFullscreen_); 190 isFullscreen_ = YES; 191 contentView_ = contentView; 192 [self changeFloatingBarShownFraction:(showDropdown ? 1 : 0)]; 193 194 // Register for notifications. Self is removed as an observer in |-cleanup|. 195 NSNotificationCenter* nc = [NSNotificationCenter defaultCenter]; 196 NSWindow* window = [browserController_ window]; 197 [nc addObserver:self 198 selector:@selector(windowDidChangeScreen:) 199 name:NSWindowDidChangeScreenNotification 200 object:window]; 201 202 [nc addObserver:self 203 selector:@selector(windowDidMove:) 204 name:NSWindowDidMoveNotification 205 object:window]; 206 207 [nc addObserver:self 208 selector:@selector(windowDidBecomeMain:) 209 name:NSWindowDidBecomeMainNotification 210 object:window]; 211 212 [nc addObserver:self 213 selector:@selector(windowDidResignMain:) 214 name:NSWindowDidResignMainNotification 215 object:window]; 216 } 217 218 - (void)exitFullscreen { 219 [[NSNotificationCenter defaultCenter] 220 postNotificationName:kWillLeaveFullscreenNotification 221 object:nil]; 222 DCHECK(isFullscreen_); 223 [self cleanup]; 224 isFullscreen_ = NO; 225 } 226 227 - (void)windowDidChangeScreen:(NSNotification*)notification { 228 [browserController_ resizeFullscreenWindow]; 229 } 230 231 - (void)windowDidMove:(NSNotification*)notification { 232 [browserController_ resizeFullscreenWindow]; 233 } 234 235 - (void)windowDidBecomeMain:(NSNotification*)notification { 236 [self showActiveWindowUI]; 237 } 238 239 - (void)windowDidResignMain:(NSNotification*)notification { 240 [self hideActiveWindowUI]; 241 } 242 243 - (CGFloat)floatingBarVerticalOffset { 244 return [self isWindowOnPrimaryScreen] ? kFloatingBarVerticalOffset : 0; 245 } 246 247 - (void)overlayFrameChanged:(NSRect)frame { 248 if (!isFullscreen_) 249 return; 250 251 // Make sure |trackingAreaBounds_| always reflects either the tracking area or 252 // the desired tracking area. 253 trackingAreaBounds_ = frame; 254 // The tracking area should always be at least the height of activation zone. 255 NSRect contentBounds = [contentView_ bounds]; 256 trackingAreaBounds_.origin.y = 257 std::min(trackingAreaBounds_.origin.y, 258 NSMaxY(contentBounds) - kDropdownActivationZoneHeight); 259 trackingAreaBounds_.size.height = 260 NSMaxY(contentBounds) - trackingAreaBounds_.origin.y + 1; 261 262 // If an animation is currently running, do not set up a tracking area now. 263 // Instead, leave it to be created it in |-animationDidEnd:|. 264 if (currentAnimation_) 265 return; 266 267 [self setupTrackingArea]; 268 } 269 270 - (void)ensureOverlayShownWithAnimation:(BOOL)animate delay:(BOOL)delay { 271 if (!isFullscreen_) 272 return; 273 274 if (animate) { 275 if (delay) { 276 [self startShowTimer]; 277 } else { 278 [self cancelAllTimers]; 279 [self changeOverlayToFraction:1 withAnimation:YES]; 280 } 281 } else { 282 DCHECK(!delay); 283 [self cancelAllTimers]; 284 [self changeOverlayToFraction:1 withAnimation:NO]; 285 } 286 } 287 288 - (void)ensureOverlayHiddenWithAnimation:(BOOL)animate delay:(BOOL)delay { 289 if (!isFullscreen_) 290 return; 291 292 if (animate) { 293 if (delay) { 294 [self startHideTimer]; 295 } else { 296 [self cancelAllTimers]; 297 [self changeOverlayToFraction:0 withAnimation:YES]; 298 } 299 } else { 300 DCHECK(!delay); 301 [self cancelAllTimers]; 302 [self changeOverlayToFraction:0 withAnimation:NO]; 303 } 304 } 305 306 - (void)cancelAnimationAndTimers { 307 [self cancelAllTimers]; 308 [currentAnimation_ stopAnimation]; 309 currentAnimation_.reset(); 310 } 311 312 - (CGFloat)floatingBarShownFraction { 313 return [browserController_ floatingBarShownFraction]; 314 } 315 316 - (void)changeFloatingBarShownFraction:(CGFloat)fraction { 317 [browserController_ setFloatingBarShownFraction:fraction]; 318 319 base::mac::FullScreenMode desiredMode = [self desiredFullscreenMode]; 320 if (desiredMode != currentFullscreenMode_ && [self shouldToggleMenuBar]) { 321 if (currentFullscreenMode_ == base::mac::kFullScreenModeNormal) 322 base::mac::RequestFullScreen(desiredMode); 323 else 324 base::mac::SwitchFullScreenModes(currentFullscreenMode_, desiredMode); 325 currentFullscreenMode_ = desiredMode; 326 } 327 } 328 329 // Used to activate the floating bar in fullscreen mode. 330 - (void)mouseEntered:(NSEvent*)event { 331 DCHECK(isFullscreen_); 332 333 // Having gotten a mouse entered, we no longer need to do exit checks. 334 [self cancelMouseExitCheck]; 335 336 NSTrackingArea* trackingArea = [event trackingArea]; 337 if (trackingArea == trackingArea_) { 338 // The tracking area shouldn't be active during animation. 339 DCHECK(!currentAnimation_); 340 [self scheduleShowForMouse]; 341 } 342 } 343 344 // Used to deactivate the floating bar in fullscreen mode. 345 - (void)mouseExited:(NSEvent*)event { 346 DCHECK(isFullscreen_); 347 348 NSTrackingArea* trackingArea = [event trackingArea]; 349 if (trackingArea == trackingArea_) { 350 // The tracking area shouldn't be active during animation. 351 DCHECK(!currentAnimation_); 352 353 // We can get a false mouse exit when the menu slides down, so if the mouse 354 // is still actually over the tracking area, we ignore the mouse exit, but 355 // we set up to check the mouse position again after a delay. 356 if ([self mouseInsideTrackingRect]) { 357 [self setupMouseExitCheck]; 358 return; 359 } 360 361 [self scheduleHideForMouse]; 362 } 363 } 364 365 - (void)animationDidStop:(NSAnimation*)animation { 366 // Reset the |currentAnimation_| pointer now that the animation is over. 367 currentAnimation_.reset(); 368 369 // Invariant says that the tracking area is not installed while animations are 370 // in progress. Ensure this is true. 371 DCHECK(!trackingArea_); 372 [self removeTrackingAreaIfNecessary]; // For paranoia. 373 374 // Don't automatically set up a new tracking area. When explicitly stopped, 375 // either another animation is going to start immediately or the state will be 376 // changed immediately. 377 } 378 379 - (void)animationDidEnd:(NSAnimation*)animation { 380 [self animationDidStop:animation]; 381 382 // |trackingAreaBounds_| contains the correct tracking area bounds, including 383 // |any updates that may have come while the animation was running. Install a 384 // new tracking area with these bounds. 385 [self setupTrackingArea]; 386 387 // TODO(viettrungluu): Better would be to check during the animation; doing it 388 // here means that the timing is slightly off. 389 if (![self mouseInsideTrackingRect]) 390 [self scheduleHideForMouse]; 391 } 392 393 @end 394 395 396 @implementation FullscreenController (PrivateMethods) 397 398 - (BOOL)isWindowOnPrimaryScreen { 399 NSScreen* screen = [[browserController_ window] screen]; 400 NSScreen* primaryScreen = [[NSScreen screens] objectAtIndex:0]; 401 return (screen == primaryScreen); 402 } 403 404 - (BOOL)shouldToggleMenuBar { 405 return [self isWindowOnPrimaryScreen] && 406 [[browserController_ window] isMainWindow]; 407 } 408 409 - (base::mac::FullScreenMode)desiredFullscreenMode { 410 if ([browserController_ floatingBarShownFraction] >= 1.0) 411 return base::mac::kFullScreenModeHideDock; 412 return base::mac::kFullScreenModeHideAll; 413 } 414 415 - (void)changeOverlayToFraction:(CGFloat)fraction 416 withAnimation:(BOOL)animate { 417 // The non-animated case is really simple, so do it and return. 418 if (!animate) { 419 [currentAnimation_ stopAnimation]; 420 [self changeFloatingBarShownFraction:fraction]; 421 return; 422 } 423 424 // If we're already animating to the given fraction, then there's nothing more 425 // to do. 426 if (currentAnimation_ && [currentAnimation_ endFraction] == fraction) 427 return; 428 429 // In all other cases, we want to cancel any running animation (which may be 430 // to show or to hide). 431 [currentAnimation_ stopAnimation]; 432 433 // Now, if it happens to already be in the right state, there's nothing more 434 // to do. 435 if ([browserController_ floatingBarShownFraction] == fraction) 436 return; 437 438 // Create the animation and set it up. 439 currentAnimation_.reset( 440 [[DropdownAnimation alloc] initWithFraction:fraction 441 fullDuration:kDropdownAnimationDuration 442 animationCurve:NSAnimationEaseOut 443 controller:self]); 444 DCHECK(currentAnimation_); 445 [currentAnimation_ setAnimationBlockingMode:NSAnimationNonblocking]; 446 [currentAnimation_ setDelegate:self]; 447 448 // If there is an existing tracking area, remove it. We do not track mouse 449 // movements during animations (see class comment in the header file). 450 [self removeTrackingAreaIfNecessary]; 451 452 [currentAnimation_ startAnimation]; 453 } 454 455 - (void)scheduleShowForMouse { 456 [browserController_ lockBarVisibilityForOwner:self 457 withAnimation:YES 458 delay:YES]; 459 } 460 461 - (void)scheduleHideForMouse { 462 [browserController_ releaseBarVisibilityForOwner:self 463 withAnimation:YES 464 delay:YES]; 465 } 466 467 - (void)setupTrackingArea { 468 if (trackingArea_) { 469 // If the tracking rectangle is already |trackingAreaBounds_|, quit early. 470 NSRect oldRect = [trackingArea_ rect]; 471 if (NSEqualRects(trackingAreaBounds_, oldRect)) 472 return; 473 474 // Otherwise, remove it. 475 [self removeTrackingAreaIfNecessary]; 476 } 477 478 // Create and add a new tracking area for |frame|. 479 trackingArea_.reset( 480 [[NSTrackingArea alloc] initWithRect:trackingAreaBounds_ 481 options:NSTrackingMouseEnteredAndExited | 482 NSTrackingActiveInKeyWindow 483 owner:self 484 userInfo:nil]); 485 DCHECK(contentView_); 486 [contentView_ addTrackingArea:trackingArea_]; 487 } 488 489 - (void)removeTrackingAreaIfNecessary { 490 if (trackingArea_) { 491 DCHECK(contentView_); // |contentView_| better be valid. 492 [contentView_ removeTrackingArea:trackingArea_]; 493 trackingArea_.reset(); 494 } 495 } 496 497 - (BOOL)mouseInsideTrackingRect { 498 NSWindow* window = [browserController_ window]; 499 NSPoint mouseLoc = [window mouseLocationOutsideOfEventStream]; 500 NSPoint mousePos = [contentView_ convertPoint:mouseLoc fromView:nil]; 501 return NSMouseInRect(mousePos, trackingAreaBounds_, [contentView_ isFlipped]); 502 } 503 504 - (void)setupMouseExitCheck { 505 [self performSelector:@selector(checkForMouseExit) 506 withObject:nil 507 afterDelay:kMouseExitCheckDelay]; 508 } 509 510 - (void)cancelMouseExitCheck { 511 [NSObject cancelPreviousPerformRequestsWithTarget:self 512 selector:@selector(checkForMouseExit) object:nil]; 513 } 514 515 - (void)checkForMouseExit { 516 if ([self mouseInsideTrackingRect]) 517 [self setupMouseExitCheck]; 518 else 519 [self scheduleHideForMouse]; 520 } 521 522 - (void)startShowTimer { 523 // If there's already a show timer going, just keep it. 524 if (showTimer_) { 525 DCHECK([showTimer_ isValid]); 526 DCHECK(!hideTimer_); 527 return; 528 } 529 530 // Cancel the hide timer (if necessary) and set up the new show timer. 531 [self cancelHideTimer]; 532 showTimer_.reset( 533 [[NSTimer scheduledTimerWithTimeInterval:kDropdownShowDelay 534 target:self 535 selector:@selector(showTimerFire:) 536 userInfo:nil 537 repeats:NO] retain]); 538 DCHECK([showTimer_ isValid]); // This also checks that |showTimer_ != nil|. 539 } 540 541 - (void)startHideTimer { 542 // If there's already a hide timer going, just keep it. 543 if (hideTimer_) { 544 DCHECK([hideTimer_ isValid]); 545 DCHECK(!showTimer_); 546 return; 547 } 548 549 // Cancel the show timer (if necessary) and set up the new hide timer. 550 [self cancelShowTimer]; 551 hideTimer_.reset( 552 [[NSTimer scheduledTimerWithTimeInterval:kDropdownHideDelay 553 target:self 554 selector:@selector(hideTimerFire:) 555 userInfo:nil 556 repeats:NO] retain]); 557 DCHECK([hideTimer_ isValid]); // This also checks that |hideTimer_ != nil|. 558 } 559 560 - (void)cancelShowTimer { 561 [showTimer_ invalidate]; 562 showTimer_.reset(); 563 } 564 565 - (void)cancelHideTimer { 566 [hideTimer_ invalidate]; 567 hideTimer_.reset(); 568 } 569 570 - (void)cancelAllTimers { 571 [self cancelShowTimer]; 572 [self cancelHideTimer]; 573 } 574 575 - (void)showTimerFire:(NSTimer*)timer { 576 DCHECK_EQ(showTimer_, timer); // This better be our show timer. 577 [showTimer_ invalidate]; // Make sure it doesn't repeat. 578 showTimer_.reset(); // And get rid of it. 579 [self changeOverlayToFraction:1 withAnimation:YES]; 580 } 581 582 - (void)hideTimerFire:(NSTimer*)timer { 583 DCHECK_EQ(hideTimer_, timer); // This better be our hide timer. 584 [hideTimer_ invalidate]; // Make sure it doesn't repeat. 585 hideTimer_.reset(); // And get rid of it. 586 [self changeOverlayToFraction:0 withAnimation:YES]; 587 } 588 589 - (void)cleanup { 590 [self cancelMouseExitCheck]; 591 [self cancelAnimationAndTimers]; 592 [[NSNotificationCenter defaultCenter] removeObserver:self]; 593 594 [self removeTrackingAreaIfNecessary]; 595 contentView_ = nil; 596 597 // This isn't tracked when not in fullscreen mode. 598 [browserController_ releaseBarVisibilityForOwner:self 599 withAnimation:NO 600 delay:NO]; 601 602 // Call the main status resignation code to perform the associated cleanup, 603 // since we will no longer be receiving actual status resignation 604 // notifications. 605 [self hideActiveWindowUI]; 606 607 // No more calls back up to the BWC. 608 browserController_ = nil; 609 } 610 611 - (void)showActiveWindowUI { 612 DCHECK_EQ(currentFullscreenMode_, base::mac::kFullScreenModeNormal); 613 if (currentFullscreenMode_ != base::mac::kFullScreenModeNormal) 614 return; 615 616 if ([self shouldToggleMenuBar]) { 617 base::mac::FullScreenMode desiredMode = [self desiredFullscreenMode]; 618 base::mac::RequestFullScreen(desiredMode); 619 currentFullscreenMode_ = desiredMode; 620 } 621 622 // TODO(rohitrao): Insert the Exit Fullscreen button. http://crbug.com/35956 623 } 624 625 - (void)hideActiveWindowUI { 626 if (currentFullscreenMode_ != base::mac::kFullScreenModeNormal) { 627 base::mac::ReleaseFullScreen(currentFullscreenMode_); 628 currentFullscreenMode_ = base::mac::kFullScreenModeNormal; 629 } 630 631 // TODO(rohitrao): Remove the Exit Fullscreen button. http://crbug.com/35956 632 } 633 634 @end 635