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 // When in presentation mode, the window's background will need to appear
203 // above the window contents. For this to happen, the window will need its
204 // own layer. This comes with a performance penalty when not fullscreen, so
205 // do not enable this by default.
206 [[window contentView] cr_setWantsLayer:YES withSquashing:NO];
208 // Disable these notifications on Lion as they cause crashes.
209 // TODO(rohitrao): Figure out what happens if a fullscreen window changes
211 if (base::mac::IsOSSnowLeopard()) {
213 selector:@selector(windowDidChangeScreen:)
214 name:NSWindowDidChangeScreenNotification
218 selector:@selector(windowDidMove:)
219 name:NSWindowDidMoveNotification
224 selector:@selector(windowDidBecomeMain:)
225 name:NSWindowDidBecomeMainNotification
229 selector:@selector(windowDidResignMain:)
230 name:NSWindowDidResignMainNotification
233 enteringPresentationMode_ = NO;
236 - (void)exitPresentationMode {
237 [[NSNotificationCenter defaultCenter]
238 postNotificationName:kWillLeaveFullscreenNotification
240 DCHECK(inPresentationMode_);
241 inPresentationMode_ = NO;
243 // Remove the layer that was created.
244 NSWindow* window = [browserController_ window];
245 [[window contentView] cr_setWantsLayer:NO withSquashing:NO];
250 - (void)windowDidChangeScreen:(NSNotification*)notification {
251 [browserController_ resizeFullscreenWindow];
254 - (void)windowDidMove:(NSNotification*)notification {
255 [browserController_ resizeFullscreenWindow];
258 - (void)windowDidBecomeMain:(NSNotification*)notification {
259 [self showActiveWindowUI];
262 - (void)windowDidResignMain:(NSNotification*)notification {
263 [self hideActiveWindowUI];
266 - (CGFloat)floatingBarVerticalOffset {
267 return [self isWindowOnPrimaryScreen] ? kFloatingBarVerticalOffset : 0;
270 - (void)overlayFrameChanged:(NSRect)frame {
271 if (!inPresentationMode_)
274 // Make sure |trackingAreaBounds_| always reflects either the tracking area or
275 // the desired tracking area.
276 trackingAreaBounds_ = frame;
277 // The tracking area should always be at least the height of activation zone.
278 NSRect contentBounds = [contentView_ bounds];
279 trackingAreaBounds_.origin.y =
280 std::min(trackingAreaBounds_.origin.y,
281 NSMaxY(contentBounds) - kDropdownActivationZoneHeight);
282 trackingAreaBounds_.size.height =
283 NSMaxY(contentBounds) - trackingAreaBounds_.origin.y + 1;
285 // If an animation is currently running, do not set up a tracking area now.
286 // Instead, leave it to be created it in |-animationDidEnd:|.
287 if (currentAnimation_)
290 // If this is part of the initial setup, lock bar visibility if the mouse is
291 // within the tracking area bounds.
292 if (enteringPresentationMode_ && [self mouseInsideTrackingRect])
293 [browserController_ lockBarVisibilityForOwner:self
296 [self setupTrackingArea];
299 - (void)ensureOverlayShownWithAnimation:(BOOL)animate delay:(BOOL)delay {
300 if (!inPresentationMode_)
303 if (CommandLine::ForCurrentProcess()->HasSwitch(switches::kKioskMode))
308 [self startShowTimer];
310 [self cancelAllTimers];
311 [self changeOverlayToFraction:1 withAnimation:YES];
315 [self cancelAllTimers];
316 [self changeOverlayToFraction:1 withAnimation:NO];
320 - (void)ensureOverlayHiddenWithAnimation:(BOOL)animate delay:(BOOL)delay {
321 if (!inPresentationMode_)
326 [self startHideTimer];
328 [self cancelAllTimers];
329 [self changeOverlayToFraction:0 withAnimation:YES];
333 [self cancelAllTimers];
334 [self changeOverlayToFraction:0 withAnimation:NO];
338 - (void)cancelAnimationAndTimers {
339 [self cancelAllTimers];
340 [currentAnimation_ stopAnimation];
341 currentAnimation_.reset();
344 - (CGFloat)floatingBarShownFraction {
345 return [browserController_ floatingBarShownFraction];
348 - (void)changeFloatingBarShownFraction:(CGFloat)fraction {
349 [browserController_ setFloatingBarShownFraction:fraction];
351 base::mac::FullScreenMode desiredMode = [self desiredSystemFullscreenMode];
352 if (desiredMode != systemFullscreenMode_ && [self shouldToggleMenuBar]) {
353 if (systemFullscreenMode_ == base::mac::kFullScreenModeNormal)
354 base::mac::RequestFullScreen(desiredMode);
356 base::mac::SwitchFullScreenModes(systemFullscreenMode_, desiredMode);
357 systemFullscreenMode_ = desiredMode;
361 // Used to activate the floating bar in presentation mode.
362 - (void)mouseEntered:(NSEvent*)event {
363 DCHECK(inPresentationMode_);
365 // Having gotten a mouse entered, we no longer need to do exit checks.
366 [self cancelMouseExitCheck];
368 NSTrackingArea* trackingArea = [event trackingArea];
369 if (trackingArea == trackingArea_) {
370 // The tracking area shouldn't be active during animation.
371 DCHECK(!currentAnimation_);
372 [self scheduleShowForMouse];
376 // Used to deactivate the floating bar in presentation mode.
377 - (void)mouseExited:(NSEvent*)event {
378 DCHECK(inPresentationMode_);
380 NSTrackingArea* trackingArea = [event trackingArea];
381 if (trackingArea == trackingArea_) {
382 // The tracking area shouldn't be active during animation.
383 DCHECK(!currentAnimation_);
385 // We can get a false mouse exit when the menu slides down, so if the mouse
386 // is still actually over the tracking area, we ignore the mouse exit, but
387 // we set up to check the mouse position again after a delay.
388 if ([self mouseInsideTrackingRect]) {
389 [self setupMouseExitCheck];
393 [self scheduleHideForMouse];
397 - (void)animationDidStop:(NSAnimation*)animation {
398 // Reset the |currentAnimation_| pointer now that the animation is over.
399 currentAnimation_.reset();
401 // Invariant says that the tracking area is not installed while animations are
402 // in progress. Ensure this is true.
403 DCHECK(!trackingArea_);
404 [self removeTrackingAreaIfNecessary]; // For paranoia.
406 // Don't automatically set up a new tracking area. When explicitly stopped,
407 // either another animation is going to start immediately or the state will be
408 // changed immediately.
411 - (void)animationDidEnd:(NSAnimation*)animation {
412 [self animationDidStop:animation];
414 // |trackingAreaBounds_| contains the correct tracking area bounds, including
415 // |any updates that may have come while the animation was running. Install a
416 // new tracking area with these bounds.
417 [self setupTrackingArea];
419 // TODO(viettrungluu): Better would be to check during the animation; doing it
420 // here means that the timing is slightly off.
421 if (![self mouseInsideTrackingRect])
422 [self scheduleHideForMouse];
428 @implementation PresentationModeController (PrivateMethods)
430 - (BOOL)isWindowOnPrimaryScreen {
431 NSScreen* screen = [[browserController_ window] screen];
432 NSScreen* primaryScreen = [[NSScreen screens] objectAtIndex:0];
433 return (screen == primaryScreen);
436 - (BOOL)shouldToggleMenuBar {
437 return !chrome::mac::SupportsSystemFullscreen() &&
438 [self isWindowOnPrimaryScreen] &&
439 [[browserController_ window] isMainWindow];
442 - (base::mac::FullScreenMode)desiredSystemFullscreenMode {
443 if ([browserController_ floatingBarShownFraction] >= 1.0)
444 return base::mac::kFullScreenModeHideDock;
445 return base::mac::kFullScreenModeHideAll;
448 - (void)changeOverlayToFraction:(CGFloat)fraction
449 withAnimation:(BOOL)animate {
450 // The non-animated case is really simple, so do it and return.
452 [currentAnimation_ stopAnimation];
453 [self changeFloatingBarShownFraction:fraction];
457 // If we're already animating to the given fraction, then there's nothing more
459 if (currentAnimation_ && [currentAnimation_ endFraction] == fraction)
462 // In all other cases, we want to cancel any running animation (which may be
463 // to show or to hide).
464 [currentAnimation_ stopAnimation];
466 // Now, if it happens to already be in the right state, there's nothing more
468 if ([browserController_ floatingBarShownFraction] == fraction)
471 // Create the animation and set it up.
472 currentAnimation_.reset(
473 [[DropdownAnimation alloc] initWithFraction:fraction
474 fullDuration:kDropdownAnimationDuration
475 animationCurve:NSAnimationEaseOut
477 DCHECK(currentAnimation_);
478 [currentAnimation_ setAnimationBlockingMode:NSAnimationNonblocking];
479 [currentAnimation_ setDelegate:self];
481 // If there is an existing tracking area, remove it. We do not track mouse
482 // movements during animations (see class comment in the header file).
483 [self removeTrackingAreaIfNecessary];
485 [currentAnimation_ startAnimation];
488 - (void)scheduleShowForMouse {
489 [browserController_ lockBarVisibilityForOwner:self
494 - (void)scheduleHideForMouse {
495 [browserController_ releaseBarVisibilityForOwner:self
500 - (void)setupTrackingArea {
502 // If the tracking rectangle is already |trackingAreaBounds_|, quit early.
503 NSRect oldRect = [trackingArea_ rect];
504 if (NSEqualRects(trackingAreaBounds_, oldRect))
507 // Otherwise, remove it.
508 [self removeTrackingAreaIfNecessary];
511 // Create and add a new tracking area for |frame|.
513 [[NSTrackingArea alloc] initWithRect:trackingAreaBounds_
514 options:NSTrackingMouseEnteredAndExited |
515 NSTrackingActiveInKeyWindow
518 DCHECK(contentView_);
519 [contentView_ addTrackingArea:trackingArea_];
522 - (void)removeTrackingAreaIfNecessary {
524 DCHECK(contentView_); // |contentView_| better be valid.
525 [contentView_ removeTrackingArea:trackingArea_];
526 trackingArea_.reset();
530 - (BOOL)mouseInsideTrackingRect {
531 NSWindow* window = [browserController_ window];
532 NSPoint mouseLoc = [window mouseLocationOutsideOfEventStream];
533 NSPoint mousePos = [contentView_ convertPoint:mouseLoc fromView:nil];
534 return NSMouseInRect(mousePos, trackingAreaBounds_, [contentView_ isFlipped]);
537 - (void)setupMouseExitCheck {
538 [self performSelector:@selector(checkForMouseExit)
540 afterDelay:kMouseExitCheckDelay];
543 - (void)cancelMouseExitCheck {
544 [NSObject cancelPreviousPerformRequestsWithTarget:self
545 selector:@selector(checkForMouseExit) object:nil];
548 - (void)checkForMouseExit {
549 if ([self mouseInsideTrackingRect])
550 [self setupMouseExitCheck];
552 [self scheduleHideForMouse];
555 - (void)startShowTimer {
556 // If there's already a show timer going, just keep it.
558 DCHECK([showTimer_ isValid]);
563 // Cancel the hide timer (if necessary) and set up the new show timer.
564 [self cancelHideTimer];
566 [[NSTimer scheduledTimerWithTimeInterval:kDropdownShowDelay
568 selector:@selector(showTimerFire:)
570 repeats:NO] retain]);
571 DCHECK([showTimer_ isValid]); // This also checks that |showTimer_ != nil|.
574 - (void)startHideTimer {
575 // If there's already a hide timer going, just keep it.
577 DCHECK([hideTimer_ isValid]);
582 // Cancel the show timer (if necessary) and set up the new hide timer.
583 [self cancelShowTimer];
585 [[NSTimer scheduledTimerWithTimeInterval:kDropdownHideDelay
587 selector:@selector(hideTimerFire:)
589 repeats:NO] retain]);
590 DCHECK([hideTimer_ isValid]); // This also checks that |hideTimer_ != nil|.
593 - (void)cancelShowTimer {
594 [showTimer_ invalidate];
598 - (void)cancelHideTimer {
599 [hideTimer_ invalidate];
603 - (void)cancelAllTimers {
604 [self cancelShowTimer];
605 [self cancelHideTimer];
608 - (void)showTimerFire:(NSTimer*)timer {
609 DCHECK_EQ(showTimer_, timer); // This better be our show timer.
610 [showTimer_ invalidate]; // Make sure it doesn't repeat.
611 showTimer_.reset(); // And get rid of it.
612 [self changeOverlayToFraction:1 withAnimation:YES];
615 - (void)hideTimerFire:(NSTimer*)timer {
616 DCHECK_EQ(hideTimer_, timer); // This better be our hide timer.
617 [hideTimer_ invalidate]; // Make sure it doesn't repeat.
618 hideTimer_.reset(); // And get rid of it.
619 [self changeOverlayToFraction:0 withAnimation:YES];
623 [self cancelMouseExitCheck];
624 [self cancelAnimationAndTimers];
625 [[NSNotificationCenter defaultCenter] removeObserver:self];
627 [self removeTrackingAreaIfNecessary];
630 // This isn't tracked when not in presentation mode.
631 [browserController_ releaseBarVisibilityForOwner:self
635 // Call the main status resignation code to perform the associated cleanup,
636 // since we will no longer be receiving actual status resignation
638 [self hideActiveWindowUI];
640 // No more calls back up to the BWC.
641 browserController_ = nil;
644 - (void)showActiveWindowUI {
645 DCHECK_EQ(systemFullscreenMode_, base::mac::kFullScreenModeNormal);
646 if (systemFullscreenMode_ != base::mac::kFullScreenModeNormal)
649 if ([self shouldToggleMenuBar]) {
650 base::mac::FullScreenMode desiredMode = [self desiredSystemFullscreenMode];
651 base::mac::RequestFullScreen(desiredMode);
652 systemFullscreenMode_ = desiredMode;
655 // TODO(rohitrao): Insert the Exit Fullscreen button. http://crbug.com/35956
658 - (void)hideActiveWindowUI {
659 if (systemFullscreenMode_ != base::mac::kFullScreenModeNormal) {
660 base::mac::ReleaseFullScreen(systemFullscreenMode_);
661 systemFullscreenMode_ = base::mac::kFullScreenModeNormal;
664 // TODO(rohitrao): Remove the Exit Fullscreen button. http://crbug.com/35956