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