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