diff options
Diffstat (limited to 'chrome/browser/ui/cocoa/presentation_mode_controller.mm')
-rw-r--r-- | chrome/browser/ui/cocoa/presentation_mode_controller.mm | 649 |
1 files changed, 649 insertions, 0 deletions
diff --git a/chrome/browser/ui/cocoa/presentation_mode_controller.mm b/chrome/browser/ui/cocoa/presentation_mode_controller.mm new file mode 100644 index 0000000..35a20a5 --- /dev/null +++ b/chrome/browser/ui/cocoa/presentation_mode_controller.mm @@ -0,0 +1,649 @@ +// Copyright (c) 2011 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> + +#import "base/mac/mac_util.h" +#import "chrome/browser/ui/cocoa/browser_window_controller.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:(NSInteger)animationCurve + controller:(PresentationModeController*)controller; + +@end + +@implementation DropdownAnimation + +@synthesize startFraction = startFraction_; +@synthesize endFraction = endFraction_; + +- (id)initWithFraction:(CGFloat)toFraction + fullDuration:(CGFloat)fullDuration + animationCurve:(NSInteger)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::IsOSSnowLeopardOrEarlier()) { + [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 (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 base::mac::IsOSSnowLeopardOrEarlier() && + [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 |