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.
5 #import "chrome/browser/ui/cocoa/presentation_mode_controller.h"
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"
17 NSString* const kWillEnterFullscreenNotification =
18 @"WillEnterFullscreenNotification";
19 NSString* const kWillLeaveFullscreenNotification =
20 @"WillLeaveFullscreenNotification";
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.
25 const CGFloat kDropdownActivationZoneHeight = 4;
26 const NSTimeInterval kDropdownAnimationDuration = 0.12;
27 const NSTimeInterval kMouseExitCheckDelay = 0.1;
28 // This show delay attempts to match the delay for the main menu.
29 const NSTimeInterval kDropdownShowDelay = 0.3;
30 const NSTimeInterval kDropdownHideDelay = 0.2;
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.)
35 const CGFloat kFloatingBarVerticalOffset = 22;
40 // Helper class to manage animations for the dropdown bar. Calls
41 // [PresentationModeController changeFloatingBarShownFraction] once per
43 @interface DropdownAnimation : NSAnimation {
45 PresentationModeController* controller_;
46 CGFloat startFraction_;
50 @property(readonly, nonatomic) CGFloat startFraction;
51 @property(readonly, nonatomic) CGFloat endFraction;
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;
63 @implementation DropdownAnimation
65 @synthesize startFraction = startFraction_;
66 @synthesize endFraction = endFraction_;
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.
74 CGFloat fromFraction = [controller floatingBarShownFraction];
75 CGFloat effectiveDuration = fabs(fullDuration * (fromFraction - toFraction));
77 if ((self = [super gtm_initWithDuration:effectiveDuration
78 eventMask:NSLeftMouseDownMask
79 animationCurve:animationCurve])) {
80 startFraction_ = fromFraction;
81 endFraction_ = toFraction;
82 controller_ = controller;
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 {
91 startFraction_ + (progress * (endFraction_ - startFraction_));
92 [controller_ changeFloatingBarShownFraction:fraction];
98 @interface PresentationModeController (PrivateMethods)
100 // Returns YES if the window is on the primary screen.
101 - (BOOL)isWindowOnPrimaryScreen;
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;
108 // Returns |kFullScreenModeHideAll| when the overlay is hidden and
109 // |kFullScreenModeHideDock| when the overlay is shown.
110 - (base::mac::FullScreenMode)desiredSystemFullscreenMode;
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;
118 // Schedule the floating bar to be shown/hidden because of mouse position.
119 - (void)scheduleShowForMouse;
120 - (void)scheduleHideForMouse;
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;
128 // Returns YES if the mouse is currently in any current tracking rectangle, NO
130 - (BOOL)mouseInsideTrackingRect;
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
136 - (void)setupMouseExitCheck;
137 - (void)cancelMouseExitCheck;
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;
143 // Start timers for showing/hiding the floating bar.
144 - (void)startShowTimer;
145 - (void)startHideTimer;
146 - (void)cancelShowTimer;
147 - (void)cancelHideTimer;
148 - (void)cancelAllTimers;
150 // Methods called when the show/hide timers fire. Do not call directly.
151 - (void)showTimerFire:(NSTimer*)timer;
152 - (void)hideTimerFire:(NSTimer*)timer;
154 // Stops any running animations, removes tracking areas, etc.
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;
166 @implementation PresentationModeController
168 @synthesize inPresentationMode = inPresentationMode_;
170 - (id)initWithBrowserController:(BrowserWindowController*)controller {
171 if ((self = [super init])) {
172 browserController_ = controller;
173 systemFullscreenMode_ = base::mac::kFullScreenModeNormal;
176 // Let the world know what we're up to.
177 [[NSNotificationCenter defaultCenter]
178 postNotificationName:kWillEnterFullscreenNotification
185 DCHECK(!inPresentationMode_);
186 DCHECK(!trackingArea_);
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)];
198 // Register for notifications. Self is removed as an observer in |-cleanup|.
199 NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
200 NSWindow* window = [browserController_ window];
202 // Disable these notifications on Lion as they cause crashes.
203 // TODO(rohitrao): Figure out what happens if a fullscreen window changes
205 if (base::mac::IsOSSnowLeopard()) {
207 selector:@selector(windowDidChangeScreen:)
208 name:NSWindowDidChangeScreenNotification
212 selector:@selector(windowDidMove:)
213 name:NSWindowDidMoveNotification
218 selector:@selector(windowDidBecomeMain:)
219 name:NSWindowDidBecomeMainNotification
223 selector:@selector(windowDidResignMain:)
224 name:NSWindowDidResignMainNotification
227 enteringPresentationMode_ = NO;
230 - (void)exitPresentationMode {
231 [[NSNotificationCenter defaultCenter]
232 postNotificationName:kWillLeaveFullscreenNotification
234 DCHECK(inPresentationMode_);
235 inPresentationMode_ = NO;
240 - (void)windowDidChangeScreen:(NSNotification*)notification {
241 [browserController_ resizeFullscreenWindow];
244 - (void)windowDidMove:(NSNotification*)notification {
245 [browserController_ resizeFullscreenWindow];
248 - (void)windowDidBecomeMain:(NSNotification*)notification {
249 [self showActiveWindowUI];
252 - (void)windowDidResignMain:(NSNotification*)notification {
253 [self hideActiveWindowUI];
256 - (CGFloat)floatingBarVerticalOffset {
257 return [self isWindowOnPrimaryScreen] ? kFloatingBarVerticalOffset : 0;
260 - (void)overlayFrameChanged:(NSRect)frame {
261 if (!inPresentationMode_)
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;
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_)
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
286 [self setupTrackingArea];
289 - (void)ensureOverlayShownWithAnimation:(BOOL)animate delay:(BOOL)delay {
290 if (!inPresentationMode_)
293 if (CommandLine::ForCurrentProcess()->HasSwitch(switches::kKioskMode))
298 [self startShowTimer];
300 [self cancelAllTimers];
301 [self changeOverlayToFraction:1 withAnimation:YES];
305 [self cancelAllTimers];
306 [self changeOverlayToFraction:1 withAnimation:NO];
310 - (void)ensureOverlayHiddenWithAnimation:(BOOL)animate delay:(BOOL)delay {
311 if (!inPresentationMode_)
316 [self startHideTimer];
318 [self cancelAllTimers];
319 [self changeOverlayToFraction:0 withAnimation:YES];
323 [self cancelAllTimers];
324 [self changeOverlayToFraction:0 withAnimation:NO];
328 - (void)cancelAnimationAndTimers {
329 [self cancelAllTimers];
330 [currentAnimation_ stopAnimation];
331 currentAnimation_.reset();
334 - (CGFloat)floatingBarShownFraction {
335 return [browserController_ floatingBarShownFraction];
338 - (void)setSystemFullscreenModeTo:(base::mac::FullScreenMode)mode {
339 if (mode == systemFullscreenMode_)
341 if (systemFullscreenMode_ == base::mac::kFullScreenModeNormal)
342 base::mac::RequestFullScreen(mode);
343 else if (mode == base::mac::kFullScreenModeNormal)
344 base::mac::ReleaseFullScreen(systemFullscreenMode_);
346 base::mac::SwitchFullScreenModes(systemFullscreenMode_, mode);
347 systemFullscreenMode_ = mode;
350 - (void)changeFloatingBarShownFraction:(CGFloat)fraction {
351 [browserController_ setFloatingBarShownFraction:fraction];
353 if ([self shouldToggleMenuBar])
354 [self setSystemFullscreenModeTo:[self desiredSystemFullscreenMode]];
357 // Used to activate the floating bar in presentation mode.
358 - (void)mouseEntered:(NSEvent*)event {
359 DCHECK(inPresentationMode_);
361 // Having gotten a mouse entered, we no longer need to do exit checks.
362 [self cancelMouseExitCheck];
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];
372 // Used to deactivate the floating bar in presentation mode.
373 - (void)mouseExited:(NSEvent*)event {
374 DCHECK(inPresentationMode_);
376 NSTrackingArea* trackingArea = [event trackingArea];
377 if (trackingArea == trackingArea_) {
378 // The tracking area shouldn't be active during animation.
379 DCHECK(!currentAnimation_);
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];
389 [self scheduleHideForMouse];
393 - (void)animationDidStop:(NSAnimation*)animation {
394 // Reset the |currentAnimation_| pointer now that the animation is over.
395 currentAnimation_.reset();
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.
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.
407 - (void)animationDidEnd:(NSAnimation*)animation {
408 [self animationDidStop:animation];
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];
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];
424 @implementation PresentationModeController (PrivateMethods)
426 - (BOOL)isWindowOnPrimaryScreen {
427 NSScreen* screen = [[browserController_ window] screen];
428 NSScreen* primaryScreen = [[NSScreen screens] objectAtIndex:0];
429 return (screen == primaryScreen);
432 - (BOOL)shouldToggleMenuBar {
433 return [browserController_ isInImmersiveFullscreen] &&
434 [self isWindowOnPrimaryScreen] &&
435 [[browserController_ window] isMainWindow];
438 - (base::mac::FullScreenMode)desiredSystemFullscreenMode {
439 if ([browserController_ floatingBarShownFraction] >= 1.0)
440 return base::mac::kFullScreenModeHideDock;
441 return base::mac::kFullScreenModeHideAll;
444 - (void)changeOverlayToFraction:(CGFloat)fraction
445 withAnimation:(BOOL)animate {
446 // The non-animated case is really simple, so do it and return.
448 [currentAnimation_ stopAnimation];
449 [self changeFloatingBarShownFraction:fraction];
453 // If we're already animating to the given fraction, then there's nothing more
455 if (currentAnimation_ && [currentAnimation_ endFraction] == fraction)
458 // In all other cases, we want to cancel any running animation (which may be
459 // to show or to hide).
460 [currentAnimation_ stopAnimation];
462 // Now, if it happens to already be in the right state, there's nothing more
464 if ([browserController_ floatingBarShownFraction] == fraction)
467 // Create the animation and set it up.
468 currentAnimation_.reset(
469 [[DropdownAnimation alloc] initWithFraction:fraction
470 fullDuration:kDropdownAnimationDuration
471 animationCurve:NSAnimationEaseOut
473 DCHECK(currentAnimation_);
474 [currentAnimation_ setAnimationBlockingMode:NSAnimationNonblocking];
475 [currentAnimation_ setDelegate:self];
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];
481 [currentAnimation_ startAnimation];
484 - (void)scheduleShowForMouse {
485 [browserController_ lockBarVisibilityForOwner:self
490 - (void)scheduleHideForMouse {
491 [browserController_ releaseBarVisibilityForOwner:self
496 - (void)setupTrackingArea {
498 // If the tracking rectangle is already |trackingAreaBounds_|, quit early.
499 NSRect oldRect = [trackingArea_ rect];
500 if (NSEqualRects(trackingAreaBounds_, oldRect))
503 // Otherwise, remove it.
504 [self removeTrackingAreaIfNecessary];
507 // Create and add a new tracking area for |frame|.
509 [[NSTrackingArea alloc] initWithRect:trackingAreaBounds_
510 options:NSTrackingMouseEnteredAndExited |
511 NSTrackingActiveInKeyWindow
514 DCHECK(contentView_);
515 [contentView_ addTrackingArea:trackingArea_];
518 - (void)removeTrackingAreaIfNecessary {
520 DCHECK(contentView_); // |contentView_| better be valid.
521 [contentView_ removeTrackingArea:trackingArea_];
522 trackingArea_.reset();
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]);
533 - (void)setupMouseExitCheck {
534 [self performSelector:@selector(checkForMouseExit)
536 afterDelay:kMouseExitCheckDelay];
539 - (void)cancelMouseExitCheck {
540 [NSObject cancelPreviousPerformRequestsWithTarget:self
541 selector:@selector(checkForMouseExit) object:nil];
544 - (void)checkForMouseExit {
545 if ([self mouseInsideTrackingRect])
546 [self setupMouseExitCheck];
548 [self scheduleHideForMouse];
551 - (void)startShowTimer {
552 // If there's already a show timer going, just keep it.
554 DCHECK([showTimer_ isValid]);
559 // Cancel the hide timer (if necessary) and set up the new show timer.
560 [self cancelHideTimer];
562 [[NSTimer scheduledTimerWithTimeInterval:kDropdownShowDelay
564 selector:@selector(showTimerFire:)
566 repeats:NO] retain]);
567 DCHECK([showTimer_ isValid]); // This also checks that |showTimer_ != nil|.
570 - (void)startHideTimer {
571 // If there's already a hide timer going, just keep it.
573 DCHECK([hideTimer_ isValid]);
578 // Cancel the show timer (if necessary) and set up the new hide timer.
579 [self cancelShowTimer];
581 [[NSTimer scheduledTimerWithTimeInterval:kDropdownHideDelay
583 selector:@selector(hideTimerFire:)
585 repeats:NO] retain]);
586 DCHECK([hideTimer_ isValid]); // This also checks that |hideTimer_ != nil|.
589 - (void)cancelShowTimer {
590 [showTimer_ invalidate];
594 - (void)cancelHideTimer {
595 [hideTimer_ invalidate];
599 - (void)cancelAllTimers {
600 [self cancelShowTimer];
601 [self cancelHideTimer];
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];
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];
619 [self cancelMouseExitCheck];
620 [self cancelAnimationAndTimers];
621 [[NSNotificationCenter defaultCenter] removeObserver:self];
623 [self removeTrackingAreaIfNecessary];
626 // This isn't tracked when not in presentation mode.
627 [browserController_ releaseBarVisibilityForOwner:self
631 // Call the main status resignation code to perform the associated cleanup,
632 // since we will no longer be receiving actual status resignation
634 [self hideActiveWindowUI];
636 // No more calls back up to the BWC.
637 browserController_ = nil;
640 - (void)showActiveWindowUI {
641 DCHECK_EQ(systemFullscreenMode_, base::mac::kFullScreenModeNormal);
642 if (systemFullscreenMode_ != base::mac::kFullScreenModeNormal)
645 if ([self shouldToggleMenuBar])
646 [self setSystemFullscreenModeTo:[self desiredSystemFullscreenMode]];
648 // TODO(rohitrao): Insert the Exit Fullscreen button. http://crbug.com/35956
651 - (void)hideActiveWindowUI {
652 if ([self shouldToggleMenuBar])
653 [self setSystemFullscreenModeTo:base::mac::kFullScreenModeNormal];
655 // TODO(rohitrao): Remove the Exit Fullscreen button. http://crbug.com/35956