| // 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/presentation_mode_controller.h" |
| |
| #include <algorithm> |
| |
| #include "base/command_line.h" |
| #import "base/mac/mac_util.h" |
| #include "chrome/browser/fullscreen.h" |
| #import "chrome/browser/ui/cocoa/browser_window_controller.h" |
| #include "chrome/common/chrome_switches.h" |
| #import "third_party/GTM/AppKit/GTMNSAnimation+Duration.h" |
| |
| NSString* const kWillEnterFullscreenNotification = |
| @"WillEnterFullscreenNotification"; |
| NSString* const kWillLeaveFullscreenNotification = |
| @"WillLeaveFullscreenNotification"; |
| |
| namespace { |
| // The activation zone for the main menu is 4 pixels high; if we make it any |
| // smaller, then the menu can be made to appear without the bar sliding down. |
| const CGFloat kDropdownActivationZoneHeight = 4; |
| const NSTimeInterval kDropdownAnimationDuration = 0.12; |
| const NSTimeInterval kMouseExitCheckDelay = 0.1; |
| // This show delay attempts to match the delay for the main menu. |
| const NSTimeInterval kDropdownShowDelay = 0.3; |
| const NSTimeInterval kDropdownHideDelay = 0.2; |
| |
| // The amount by which the floating bar is offset downwards (to avoid the menu) |
| // in presentation mode. (We can't use |-[NSMenu menuBarHeight]| since it |
| // returns 0 when the menu bar is hidden.) |
| const CGFloat kFloatingBarVerticalOffset = 22; |
| |
| } // end namespace |
| |
| |
| // Helper class to manage animations for the dropdown bar. Calls |
| // [PresentationModeController changeFloatingBarShownFraction] once per |
| // animation step. |
| @interface DropdownAnimation : NSAnimation { |
| @private |
| PresentationModeController* controller_; |
| CGFloat startFraction_; |
| CGFloat endFraction_; |
| } |
| |
| @property(readonly, nonatomic) CGFloat startFraction; |
| @property(readonly, nonatomic) CGFloat endFraction; |
| |
| // Designated initializer. Asks |controller| for the current shown fraction, so |
| // if the bar is already partially shown or partially hidden, the animation |
| // duration may be less than |fullDuration|. |
| - (id)initWithFraction:(CGFloat)fromFraction |
| fullDuration:(CGFloat)fullDuration |
| animationCurve:(NSAnimationCurve)animationCurve |
| controller:(PresentationModeController*)controller; |
| |
| @end |
| |
| @implementation DropdownAnimation |
| |
| @synthesize startFraction = startFraction_; |
| @synthesize endFraction = endFraction_; |
| |
| - (id)initWithFraction:(CGFloat)toFraction |
| fullDuration:(CGFloat)fullDuration |
| animationCurve:(NSAnimationCurve)animationCurve |
| controller:(PresentationModeController*)controller { |
| // Calculate the effective duration, based on the current shown fraction. |
| DCHECK(controller); |
| CGFloat fromFraction = [controller floatingBarShownFraction]; |
| CGFloat effectiveDuration = fabs(fullDuration * (fromFraction - toFraction)); |
| |
| if ((self = [super gtm_initWithDuration:effectiveDuration |
| eventMask:NSLeftMouseDownMask |
| animationCurve:animationCurve])) { |
| startFraction_ = fromFraction; |
| endFraction_ = toFraction; |
| controller_ = controller; |
| } |
| return self; |
| } |
| |
| // Called once per animation step. Overridden to change the floating bar's |
| // position based on the animation's progress. |
| - (void)setCurrentProgress:(NSAnimationProgress)progress { |
| CGFloat fraction = |
| startFraction_ + (progress * (endFraction_ - startFraction_)); |
| [controller_ changeFloatingBarShownFraction:fraction]; |
| } |
| |
| @end |
| |
| |
| @interface PresentationModeController (PrivateMethods) |
| |
| // Returns YES if the window is on the primary screen. |
| - (BOOL)isWindowOnPrimaryScreen; |
| |
| // Returns YES if it is ok to show and hide the menu bar in response to the |
| // overlay opening and closing. Will return NO if the window is not main or not |
| // on the primary monitor. |
| - (BOOL)shouldToggleMenuBar; |
| |
| // Returns |kFullScreenModeHideAll| when the overlay is hidden and |
| // |kFullScreenModeHideDock| when the overlay is shown. |
| - (base::mac::FullScreenMode)desiredSystemFullscreenMode; |
| |
| // Change the overlay to the given fraction, with or without animation. Only |
| // guaranteed to work properly with |fraction == 0| or |fraction == 1|. This |
| // performs the show/hide (animation) immediately. It does not touch the timers. |
| - (void)changeOverlayToFraction:(CGFloat)fraction |
| withAnimation:(BOOL)animate; |
| |
| // Schedule the floating bar to be shown/hidden because of mouse position. |
| - (void)scheduleShowForMouse; |
| - (void)scheduleHideForMouse; |
| |
| // Set up the tracking area used to activate the sliding bar or keep it active |
| // using with the rectangle in |trackingAreaBounds_|, or remove the tracking |
| // area if one was previously set up. |
| - (void)setupTrackingArea; |
| - (void)removeTrackingAreaIfNecessary; |
| |
| // Returns YES if the mouse is currently in any current tracking rectangle, NO |
| // otherwise. |
| - (BOOL)mouseInsideTrackingRect; |
| |
| // The tracking area can "falsely" report exits when the menu slides down over |
| // it. In that case, we have to monitor for a "real" mouse exit on a timer. |
| // |-setupMouseExitCheck| schedules a check; |-cancelMouseExitCheck| cancels any |
| // scheduled check. |
| - (void)setupMouseExitCheck; |
| - (void)cancelMouseExitCheck; |
| |
| // Called (after a delay) by |-setupMouseExitCheck|, to check whether the mouse |
| // has exited or not; if it hasn't, it will schedule another check. |
| - (void)checkForMouseExit; |
| |
| // Start timers for showing/hiding the floating bar. |
| - (void)startShowTimer; |
| - (void)startHideTimer; |
| - (void)cancelShowTimer; |
| - (void)cancelHideTimer; |
| - (void)cancelAllTimers; |
| |
| // Methods called when the show/hide timers fire. Do not call directly. |
| - (void)showTimerFire:(NSTimer*)timer; |
| - (void)hideTimerFire:(NSTimer*)timer; |
| |
| // Stops any running animations, removes tracking areas, etc. |
| - (void)cleanup; |
| |
| // Shows and hides the UI associated with this window being active (having main |
| // status). This includes hiding the menu bar. These functions are called when |
| // the window gains or loses main status as well as in |-cleanup|. |
| - (void)showActiveWindowUI; |
| - (void)hideActiveWindowUI; |
| |
| @end |
| |
| |
| @implementation PresentationModeController |
| |
| @synthesize inPresentationMode = inPresentationMode_; |
| |
| - (id)initWithBrowserController:(BrowserWindowController*)controller { |
| if ((self = [super init])) { |
| browserController_ = controller; |
| systemFullscreenMode_ = base::mac::kFullScreenModeNormal; |
| } |
| |
| // Let the world know what we're up to. |
| [[NSNotificationCenter defaultCenter] |
| postNotificationName:kWillEnterFullscreenNotification |
| object:nil]; |
| |
| return self; |
| } |
| |
| - (void)dealloc { |
| DCHECK(!inPresentationMode_); |
| DCHECK(!trackingArea_); |
| [super dealloc]; |
| } |
| |
| - (void)enterPresentationModeForContentView:(NSView*)contentView |
| showDropdown:(BOOL)showDropdown { |
| DCHECK(!inPresentationMode_); |
| enteringPresentationMode_ = YES; |
| inPresentationMode_ = YES; |
| contentView_ = contentView; |
| [self changeFloatingBarShownFraction:(showDropdown ? 1 : 0)]; |
| |
| // Register for notifications. Self is removed as an observer in |-cleanup|. |
| NSNotificationCenter* nc = [NSNotificationCenter defaultCenter]; |
| NSWindow* window = [browserController_ window]; |
| |
| // Disable these notifications on Lion as they cause crashes. |
| // TODO(rohitrao): Figure out what happens if a fullscreen window changes |
| // monitors on Lion. |
| if (base::mac::IsOSSnowLeopard()) { |
| [nc addObserver:self |
| selector:@selector(windowDidChangeScreen:) |
| name:NSWindowDidChangeScreenNotification |
| object:window]; |
| |
| [nc addObserver:self |
| selector:@selector(windowDidMove:) |
| name:NSWindowDidMoveNotification |
| object:window]; |
| } |
| |
| [nc addObserver:self |
| selector:@selector(windowDidBecomeMain:) |
| name:NSWindowDidBecomeMainNotification |
| object:window]; |
| |
| [nc addObserver:self |
| selector:@selector(windowDidResignMain:) |
| name:NSWindowDidResignMainNotification |
| object:window]; |
| |
| enteringPresentationMode_ = NO; |
| } |
| |
| - (void)exitPresentationMode { |
| [[NSNotificationCenter defaultCenter] |
| postNotificationName:kWillLeaveFullscreenNotification |
| object:nil]; |
| DCHECK(inPresentationMode_); |
| inPresentationMode_ = NO; |
| [self cleanup]; |
| } |
| |
| - (void)windowDidChangeScreen:(NSNotification*)notification { |
| [browserController_ resizeFullscreenWindow]; |
| } |
| |
| - (void)windowDidMove:(NSNotification*)notification { |
| [browserController_ resizeFullscreenWindow]; |
| } |
| |
| - (void)windowDidBecomeMain:(NSNotification*)notification { |
| [self showActiveWindowUI]; |
| } |
| |
| - (void)windowDidResignMain:(NSNotification*)notification { |
| [self hideActiveWindowUI]; |
| } |
| |
| - (CGFloat)floatingBarVerticalOffset { |
| return [self isWindowOnPrimaryScreen] ? kFloatingBarVerticalOffset : 0; |
| } |
| |
| - (void)overlayFrameChanged:(NSRect)frame { |
| if (!inPresentationMode_) |
| return; |
| |
| // Make sure |trackingAreaBounds_| always reflects either the tracking area or |
| // the desired tracking area. |
| trackingAreaBounds_ = frame; |
| // The tracking area should always be at least the height of activation zone. |
| NSRect contentBounds = [contentView_ bounds]; |
| trackingAreaBounds_.origin.y = |
| std::min(trackingAreaBounds_.origin.y, |
| NSMaxY(contentBounds) - kDropdownActivationZoneHeight); |
| trackingAreaBounds_.size.height = |
| NSMaxY(contentBounds) - trackingAreaBounds_.origin.y + 1; |
| |
| // If an animation is currently running, do not set up a tracking area now. |
| // Instead, leave it to be created it in |-animationDidEnd:|. |
| if (currentAnimation_) |
| return; |
| |
| // If this is part of the initial setup, lock bar visibility if the mouse is |
| // within the tracking area bounds. |
| if (enteringPresentationMode_ && [self mouseInsideTrackingRect]) |
| [browserController_ lockBarVisibilityForOwner:self |
| withAnimation:NO |
| delay:NO]; |
| [self setupTrackingArea]; |
| } |
| |
| - (void)ensureOverlayShownWithAnimation:(BOOL)animate delay:(BOOL)delay { |
| if (!inPresentationMode_) |
| return; |
| |
| if (CommandLine::ForCurrentProcess()->HasSwitch(switches::kKioskMode)) |
| return; |
| |
| if (animate) { |
| if (delay) { |
| [self startShowTimer]; |
| } else { |
| [self cancelAllTimers]; |
| [self changeOverlayToFraction:1 withAnimation:YES]; |
| } |
| } else { |
| DCHECK(!delay); |
| [self cancelAllTimers]; |
| [self changeOverlayToFraction:1 withAnimation:NO]; |
| } |
| } |
| |
| - (void)ensureOverlayHiddenWithAnimation:(BOOL)animate delay:(BOOL)delay { |
| if (!inPresentationMode_) |
| return; |
| |
| if (animate) { |
| if (delay) { |
| [self startHideTimer]; |
| } else { |
| [self cancelAllTimers]; |
| [self changeOverlayToFraction:0 withAnimation:YES]; |
| } |
| } else { |
| DCHECK(!delay); |
| [self cancelAllTimers]; |
| [self changeOverlayToFraction:0 withAnimation:NO]; |
| } |
| } |
| |
| - (void)cancelAnimationAndTimers { |
| [self cancelAllTimers]; |
| [currentAnimation_ stopAnimation]; |
| currentAnimation_.reset(); |
| } |
| |
| - (CGFloat)floatingBarShownFraction { |
| return [browserController_ floatingBarShownFraction]; |
| } |
| |
| - (void)changeFloatingBarShownFraction:(CGFloat)fraction { |
| [browserController_ setFloatingBarShownFraction:fraction]; |
| |
| base::mac::FullScreenMode desiredMode = [self desiredSystemFullscreenMode]; |
| if (desiredMode != systemFullscreenMode_ && [self shouldToggleMenuBar]) { |
| if (systemFullscreenMode_ == base::mac::kFullScreenModeNormal) |
| base::mac::RequestFullScreen(desiredMode); |
| else |
| base::mac::SwitchFullScreenModes(systemFullscreenMode_, desiredMode); |
| systemFullscreenMode_ = desiredMode; |
| } |
| } |
| |
| // Used to activate the floating bar in presentation mode. |
| - (void)mouseEntered:(NSEvent*)event { |
| DCHECK(inPresentationMode_); |
| |
| // Having gotten a mouse entered, we no longer need to do exit checks. |
| [self cancelMouseExitCheck]; |
| |
| NSTrackingArea* trackingArea = [event trackingArea]; |
| if (trackingArea == trackingArea_) { |
| // The tracking area shouldn't be active during animation. |
| DCHECK(!currentAnimation_); |
| [self scheduleShowForMouse]; |
| } |
| } |
| |
| // Used to deactivate the floating bar in presentation mode. |
| - (void)mouseExited:(NSEvent*)event { |
| DCHECK(inPresentationMode_); |
| |
| NSTrackingArea* trackingArea = [event trackingArea]; |
| if (trackingArea == trackingArea_) { |
| // The tracking area shouldn't be active during animation. |
| DCHECK(!currentAnimation_); |
| |
| // We can get a false mouse exit when the menu slides down, so if the mouse |
| // is still actually over the tracking area, we ignore the mouse exit, but |
| // we set up to check the mouse position again after a delay. |
| if ([self mouseInsideTrackingRect]) { |
| [self setupMouseExitCheck]; |
| return; |
| } |
| |
| [self scheduleHideForMouse]; |
| } |
| } |
| |
| - (void)animationDidStop:(NSAnimation*)animation { |
| // Reset the |currentAnimation_| pointer now that the animation is over. |
| currentAnimation_.reset(); |
| |
| // Invariant says that the tracking area is not installed while animations are |
| // in progress. Ensure this is true. |
| DCHECK(!trackingArea_); |
| [self removeTrackingAreaIfNecessary]; // For paranoia. |
| |
| // Don't automatically set up a new tracking area. When explicitly stopped, |
| // either another animation is going to start immediately or the state will be |
| // changed immediately. |
| } |
| |
| - (void)animationDidEnd:(NSAnimation*)animation { |
| [self animationDidStop:animation]; |
| |
| // |trackingAreaBounds_| contains the correct tracking area bounds, including |
| // |any updates that may have come while the animation was running. Install a |
| // new tracking area with these bounds. |
| [self setupTrackingArea]; |
| |
| // TODO(viettrungluu): Better would be to check during the animation; doing it |
| // here means that the timing is slightly off. |
| if (![self mouseInsideTrackingRect]) |
| [self scheduleHideForMouse]; |
| } |
| |
| @end |
| |
| |
| @implementation PresentationModeController (PrivateMethods) |
| |
| - (BOOL)isWindowOnPrimaryScreen { |
| NSScreen* screen = [[browserController_ window] screen]; |
| NSScreen* primaryScreen = [[NSScreen screens] objectAtIndex:0]; |
| return (screen == primaryScreen); |
| } |
| |
| - (BOOL)shouldToggleMenuBar { |
| return !chrome::mac::SupportsSystemFullscreen() && |
| [self isWindowOnPrimaryScreen] && |
| [[browserController_ window] isMainWindow]; |
| } |
| |
| - (base::mac::FullScreenMode)desiredSystemFullscreenMode { |
| if ([browserController_ floatingBarShownFraction] >= 1.0) |
| return base::mac::kFullScreenModeHideDock; |
| return base::mac::kFullScreenModeHideAll; |
| } |
| |
| - (void)changeOverlayToFraction:(CGFloat)fraction |
| withAnimation:(BOOL)animate { |
| // The non-animated case is really simple, so do it and return. |
| if (!animate) { |
| [currentAnimation_ stopAnimation]; |
| [self changeFloatingBarShownFraction:fraction]; |
| return; |
| } |
| |
| // If we're already animating to the given fraction, then there's nothing more |
| // to do. |
| if (currentAnimation_ && [currentAnimation_ endFraction] == fraction) |
| return; |
| |
| // In all other cases, we want to cancel any running animation (which may be |
| // to show or to hide). |
| [currentAnimation_ stopAnimation]; |
| |
| // Now, if it happens to already be in the right state, there's nothing more |
| // to do. |
| if ([browserController_ floatingBarShownFraction] == fraction) |
| return; |
| |
| // Create the animation and set it up. |
| currentAnimation_.reset( |
| [[DropdownAnimation alloc] initWithFraction:fraction |
| fullDuration:kDropdownAnimationDuration |
| animationCurve:NSAnimationEaseOut |
| controller:self]); |
| DCHECK(currentAnimation_); |
| [currentAnimation_ setAnimationBlockingMode:NSAnimationNonblocking]; |
| [currentAnimation_ setDelegate:self]; |
| |
| // If there is an existing tracking area, remove it. We do not track mouse |
| // movements during animations (see class comment in the header file). |
| [self removeTrackingAreaIfNecessary]; |
| |
| [currentAnimation_ startAnimation]; |
| } |
| |
| - (void)scheduleShowForMouse { |
| [browserController_ lockBarVisibilityForOwner:self |
| withAnimation:YES |
| delay:YES]; |
| } |
| |
| - (void)scheduleHideForMouse { |
| [browserController_ releaseBarVisibilityForOwner:self |
| withAnimation:YES |
| delay:YES]; |
| } |
| |
| - (void)setupTrackingArea { |
| if (trackingArea_) { |
| // If the tracking rectangle is already |trackingAreaBounds_|, quit early. |
| NSRect oldRect = [trackingArea_ rect]; |
| if (NSEqualRects(trackingAreaBounds_, oldRect)) |
| return; |
| |
| // Otherwise, remove it. |
| [self removeTrackingAreaIfNecessary]; |
| } |
| |
| // Create and add a new tracking area for |frame|. |
| trackingArea_.reset( |
| [[NSTrackingArea alloc] initWithRect:trackingAreaBounds_ |
| options:NSTrackingMouseEnteredAndExited | |
| NSTrackingActiveInKeyWindow |
| owner:self |
| userInfo:nil]); |
| DCHECK(contentView_); |
| [contentView_ addTrackingArea:trackingArea_]; |
| } |
| |
| - (void)removeTrackingAreaIfNecessary { |
| if (trackingArea_) { |
| DCHECK(contentView_); // |contentView_| better be valid. |
| [contentView_ removeTrackingArea:trackingArea_]; |
| trackingArea_.reset(); |
| } |
| } |
| |
| - (BOOL)mouseInsideTrackingRect { |
| NSWindow* window = [browserController_ window]; |
| NSPoint mouseLoc = [window mouseLocationOutsideOfEventStream]; |
| NSPoint mousePos = [contentView_ convertPoint:mouseLoc fromView:nil]; |
| return NSMouseInRect(mousePos, trackingAreaBounds_, [contentView_ isFlipped]); |
| } |
| |
| - (void)setupMouseExitCheck { |
| [self performSelector:@selector(checkForMouseExit) |
| withObject:nil |
| afterDelay:kMouseExitCheckDelay]; |
| } |
| |
| - (void)cancelMouseExitCheck { |
| [NSObject cancelPreviousPerformRequestsWithTarget:self |
| selector:@selector(checkForMouseExit) object:nil]; |
| } |
| |
| - (void)checkForMouseExit { |
| if ([self mouseInsideTrackingRect]) |
| [self setupMouseExitCheck]; |
| else |
| [self scheduleHideForMouse]; |
| } |
| |
| - (void)startShowTimer { |
| // If there's already a show timer going, just keep it. |
| if (showTimer_) { |
| DCHECK([showTimer_ isValid]); |
| DCHECK(!hideTimer_); |
| return; |
| } |
| |
| // Cancel the hide timer (if necessary) and set up the new show timer. |
| [self cancelHideTimer]; |
| showTimer_.reset( |
| [[NSTimer scheduledTimerWithTimeInterval:kDropdownShowDelay |
| target:self |
| selector:@selector(showTimerFire:) |
| userInfo:nil |
| repeats:NO] retain]); |
| DCHECK([showTimer_ isValid]); // This also checks that |showTimer_ != nil|. |
| } |
| |
| - (void)startHideTimer { |
| // If there's already a hide timer going, just keep it. |
| if (hideTimer_) { |
| DCHECK([hideTimer_ isValid]); |
| DCHECK(!showTimer_); |
| return; |
| } |
| |
| // Cancel the show timer (if necessary) and set up the new hide timer. |
| [self cancelShowTimer]; |
| hideTimer_.reset( |
| [[NSTimer scheduledTimerWithTimeInterval:kDropdownHideDelay |
| target:self |
| selector:@selector(hideTimerFire:) |
| userInfo:nil |
| repeats:NO] retain]); |
| DCHECK([hideTimer_ isValid]); // This also checks that |hideTimer_ != nil|. |
| } |
| |
| - (void)cancelShowTimer { |
| [showTimer_ invalidate]; |
| showTimer_.reset(); |
| } |
| |
| - (void)cancelHideTimer { |
| [hideTimer_ invalidate]; |
| hideTimer_.reset(); |
| } |
| |
| - (void)cancelAllTimers { |
| [self cancelShowTimer]; |
| [self cancelHideTimer]; |
| } |
| |
| - (void)showTimerFire:(NSTimer*)timer { |
| DCHECK_EQ(showTimer_, timer); // This better be our show timer. |
| [showTimer_ invalidate]; // Make sure it doesn't repeat. |
| showTimer_.reset(); // And get rid of it. |
| [self changeOverlayToFraction:1 withAnimation:YES]; |
| } |
| |
| - (void)hideTimerFire:(NSTimer*)timer { |
| DCHECK_EQ(hideTimer_, timer); // This better be our hide timer. |
| [hideTimer_ invalidate]; // Make sure it doesn't repeat. |
| hideTimer_.reset(); // And get rid of it. |
| [self changeOverlayToFraction:0 withAnimation:YES]; |
| } |
| |
| - (void)cleanup { |
| [self cancelMouseExitCheck]; |
| [self cancelAnimationAndTimers]; |
| [[NSNotificationCenter defaultCenter] removeObserver:self]; |
| |
| [self removeTrackingAreaIfNecessary]; |
| contentView_ = nil; |
| |
| // This isn't tracked when not in presentation mode. |
| [browserController_ releaseBarVisibilityForOwner:self |
| withAnimation:NO |
| delay:NO]; |
| |
| // Call the main status resignation code to perform the associated cleanup, |
| // since we will no longer be receiving actual status resignation |
| // notifications. |
| [self hideActiveWindowUI]; |
| |
| // No more calls back up to the BWC. |
| browserController_ = nil; |
| } |
| |
| - (void)showActiveWindowUI { |
| DCHECK_EQ(systemFullscreenMode_, base::mac::kFullScreenModeNormal); |
| if (systemFullscreenMode_ != base::mac::kFullScreenModeNormal) |
| return; |
| |
| if ([self shouldToggleMenuBar]) { |
| base::mac::FullScreenMode desiredMode = [self desiredSystemFullscreenMode]; |
| base::mac::RequestFullScreen(desiredMode); |
| systemFullscreenMode_ = desiredMode; |
| } |
| |
| // TODO(rohitrao): Insert the Exit Fullscreen button. http://crbug.com/35956 |
| } |
| |
| - (void)hideActiveWindowUI { |
| if (systemFullscreenMode_ != base::mac::kFullScreenModeNormal) { |
| base::mac::ReleaseFullScreen(systemFullscreenMode_); |
| systemFullscreenMode_ = base::mac::kFullScreenModeNormal; |
| } |
| |
| // TODO(rohitrao): Remove the Exit Fullscreen button. http://crbug.com/35956 |
| } |
| |
| @end |