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 13NSString* const kWillEnterFullscreenNotification = 14 @"WillEnterFullscreenNotification"; 15NSString* const kWillLeaveFullscreenNotification = 16 @"WillLeaveFullscreenNotification"; 17 18namespace { 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. 21const CGFloat kDropdownActivationZoneHeight = 4; 22const NSTimeInterval kDropdownAnimationDuration = 0.12; 23const NSTimeInterval kMouseExitCheckDelay = 0.1; 24// This show delay attempts to match the delay for the main menu. 25const NSTimeInterval kDropdownShowDelay = 0.3; 26const 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.) 31const 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