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 "base/mac/sdk_forward_declarations.h"
12 #include "chrome/browser/fullscreen.h"
13 #import "chrome/browser/ui/cocoa/browser_window_controller.h"
14 #include "chrome/common/chrome_switches.h"
15 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSAnimation+Duration.h"
16 #import "ui/base/cocoa/nsview_additions.h"
18 NSString* const kWillEnterFullscreenNotification =
19 @"WillEnterFullscreenNotification";
20 NSString* const kWillLeaveFullscreenNotification =
21 @"WillLeaveFullscreenNotification";
25 // The activation zone for the main menu is 4 pixels high; if we make it any
26 // smaller, then the menu can be made to appear without the bar sliding down.
27 const CGFloat kDropdownActivationZoneHeight = 4;
28 const NSTimeInterval kDropdownAnimationDuration = 0.12;
29 const NSTimeInterval kMouseExitCheckDelay = 0.1;
30 // This show delay attempts to match the delay for the main menu.
31 const NSTimeInterval kDropdownShowDelay = 0.3;
32 const NSTimeInterval kDropdownHideDelay = 0.2;
34 // The amount by which the floating bar is offset downwards (to avoid the menu)
35 // in presentation mode. (We can't use |-[NSMenu menuBarHeight]| since it
36 // returns 0 when the menu bar is hidden.)
37 const CGFloat kFloatingBarVerticalOffset = 22;
39 OSStatus MenuBarRevealHandler(EventHandlerCallRef handler,
42 PresentationModeController* self =
43 static_cast<PresentationModeController*>(context);
44 CGFloat revealFraction = 0;
45 GetEventParameter(event,
46 FOUR_CHAR_CODE('rvlf'),
52 [self setMenuBarRevealProgress:revealFraction];
53 return CallNextEventHandler(handler, event);
58 // Helper class to manage animations for the dropdown bar. Calls
59 // [PresentationModeController changeToolbarFraction] once per
61 @interface DropdownAnimation : NSAnimation {
63 PresentationModeController* controller_;
64 CGFloat startFraction_;
68 @property(readonly, nonatomic) CGFloat startFraction;
69 @property(readonly, nonatomic) CGFloat endFraction;
71 // Designated initializer. Asks |controller| for the current shown fraction, so
72 // if the bar is already partially shown or partially hidden, the animation
73 // duration may be less than |fullDuration|.
74 - (id)initWithFraction:(CGFloat)fromFraction
75 fullDuration:(CGFloat)fullDuration
76 animationCurve:(NSAnimationCurve)animationCurve
77 controller:(PresentationModeController*)controller;
81 @implementation DropdownAnimation
83 @synthesize startFraction = startFraction_;
84 @synthesize endFraction = endFraction_;
86 - (id)initWithFraction:(CGFloat)toFraction
87 fullDuration:(CGFloat)fullDuration
88 animationCurve:(NSAnimationCurve)animationCurve
89 controller:(PresentationModeController*)controller {
90 // Calculate the effective duration, based on the current shown fraction.
92 CGFloat fromFraction = controller.toolbarFraction;
93 CGFloat effectiveDuration = fabs(fullDuration * (fromFraction - toFraction));
95 if ((self = [super gtm_initWithDuration:effectiveDuration
96 eventMask:NSLeftMouseDownMask
97 animationCurve:animationCurve])) {
98 startFraction_ = fromFraction;
99 endFraction_ = toFraction;
100 controller_ = controller;
105 // Called once per animation step. Overridden to change the floating bar's
106 // position based on the animation's progress.
107 - (void)setCurrentProgress:(NSAnimationProgress)progress {
109 startFraction_ + (progress * (endFraction_ - startFraction_));
110 [controller_ changeToolbarFraction:fraction];
116 @interface PresentationModeController (PrivateMethods)
118 // Updates the visibility of the menu bar and the dock.
119 - (void)updateMenuBarAndDockVisibility;
121 // Whether the current screen is expected to have a menu bar, regardless of
122 // current visibility of the menu bar.
123 - (BOOL)doesScreenHaveMenuBar;
125 // Returns YES if the window is on the primary screen.
126 - (BOOL)isWindowOnPrimaryScreen;
128 // Returns |kFullScreenModeHideAll| when the overlay is hidden and
129 // |kFullScreenModeHideDock| when the overlay is shown.
130 - (base::mac::FullScreenMode)desiredSystemFullscreenMode;
132 // Change the overlay to the given fraction, with or without animation. Only
133 // guaranteed to work properly with |fraction == 0| or |fraction == 1|. This
134 // performs the show/hide (animation) immediately. It does not touch the timers.
135 - (void)changeOverlayToFraction:(CGFloat)fraction
136 withAnimation:(BOOL)animate;
138 // Schedule the floating bar to be shown/hidden because of mouse position.
139 - (void)scheduleShowForMouse;
140 - (void)scheduleHideForMouse;
142 // Set up the tracking area used to activate the sliding bar or keep it active
143 // using with the rectangle in |trackingAreaBounds_|, or remove the tracking
144 // area if one was previously set up.
145 - (void)setupTrackingArea;
146 - (void)removeTrackingAreaIfNecessary;
148 // Returns YES if the mouse is currently in any current tracking rectangle, NO
150 - (BOOL)mouseInsideTrackingRect;
152 // The tracking area can "falsely" report exits when the menu slides down over
153 // it. In that case, we have to monitor for a "real" mouse exit on a timer.
154 // |-setupMouseExitCheck| schedules a check; |-cancelMouseExitCheck| cancels any
156 - (void)setupMouseExitCheck;
157 - (void)cancelMouseExitCheck;
159 // Called (after a delay) by |-setupMouseExitCheck|, to check whether the mouse
160 // has exited or not; if it hasn't, it will schedule another check.
161 - (void)checkForMouseExit;
163 // Start timers for showing/hiding the floating bar.
164 - (void)startShowTimer;
165 - (void)startHideTimer;
166 - (void)cancelShowTimer;
167 - (void)cancelHideTimer;
168 - (void)cancelAllTimers;
170 // Methods called when the show/hide timers fire. Do not call directly.
171 - (void)showTimerFire:(NSTimer*)timer;
172 - (void)hideTimerFire:(NSTimer*)timer;
174 // Stops any running animations, removes tracking areas, etc.
177 // Shows and hides the UI associated with this window being active (having main
178 // status). This includes hiding the menu bar. These functions are called when
179 // the window gains or loses main status as well as in |-cleanup|.
180 - (void)showActiveWindowUI;
181 - (void)hideActiveWindowUI;
183 // Whether the menu bar should be shown in immersive fullscreen for the screen
184 // that contains the window.
185 - (BOOL)shouldShowMenubarInImmersiveFullscreen;
190 @implementation PresentationModeController
192 @synthesize inPresentationMode = inPresentationMode_;
193 @synthesize slidingStyle = slidingStyle_;
194 @synthesize toolbarFraction = toolbarFraction_;
196 - (id)initWithBrowserController:(BrowserWindowController*)controller
197 style:(fullscreen_mac::SlidingStyle)style {
198 if ((self = [super init])) {
199 browserController_ = controller;
200 systemFullscreenMode_ = base::mac::kFullScreenModeNormal;
201 slidingStyle_ = style;
204 // Let the world know what we're up to.
205 [[NSNotificationCenter defaultCenter]
206 postNotificationName:kWillEnterFullscreenNotification
209 // Install the Carbon event handler for the undocumented menu bar show/hide
211 EventTypeSpec eventSpec = {kEventClassMenu, 2004};
212 InstallApplicationEventHandler(NewEventHandlerUPP(&MenuBarRevealHandler),
216 &menuBarTrackingHandler_);
221 RemoveEventHandler(menuBarTrackingHandler_);
222 DCHECK(!inPresentationMode_);
223 DCHECK(!trackingArea_);
227 - (void)enterPresentationModeForContentView:(NSView*)contentView
228 showDropdown:(BOOL)showDropdown {
229 DCHECK(!inPresentationMode_);
230 enteringPresentationMode_ = YES;
231 inPresentationMode_ = YES;
232 contentView_ = contentView;
233 [self changeToolbarFraction:(showDropdown ? 1 : 0)];
234 [self updateMenuBarAndDockVisibility];
236 // Register for notifications. Self is removed as an observer in |-cleanup|.
237 NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
238 NSWindow* window = [browserController_ window];
240 // Disable these notifications on Lion as they cause crashes.
241 // TODO(rohitrao): Figure out what happens if a fullscreen window changes
243 if (base::mac::IsOSSnowLeopard()) {
245 selector:@selector(windowDidChangeScreen:)
246 name:NSWindowDidChangeScreenNotification
250 selector:@selector(windowDidMove:)
251 name:NSWindowDidMoveNotification
256 selector:@selector(windowDidBecomeMain:)
257 name:NSWindowDidBecomeMainNotification
261 selector:@selector(windowDidResignMain:)
262 name:NSWindowDidResignMainNotification
265 enteringPresentationMode_ = NO;
268 - (void)exitPresentationMode {
269 [[NSNotificationCenter defaultCenter]
270 postNotificationName:kWillLeaveFullscreenNotification
272 DCHECK(inPresentationMode_);
273 inPresentationMode_ = NO;
278 - (void)windowDidChangeScreen:(NSNotification*)notification {
279 [browserController_ resizeFullscreenWindow];
282 - (void)windowDidMove:(NSNotification*)notification {
283 [browserController_ resizeFullscreenWindow];
286 - (void)windowDidBecomeMain:(NSNotification*)notification {
287 [self showActiveWindowUI];
290 - (void)windowDidResignMain:(NSNotification*)notification {
291 [self hideActiveWindowUI];
294 // On OSX 10.8+, the menu bar shows on the secondary screen in fullscreen.
295 // On OSX 10.7, fullscreen never fills the secondary screen.
296 // On OSX 10.6, the menu bar never shows on the secondary screen in fullscreen.
297 // See http://crbug.com/388906 for full details.
298 - (CGFloat)floatingBarVerticalOffset {
299 if (base::mac::IsOSMountainLionOrLater())
300 return kFloatingBarVerticalOffset;
301 return [self isWindowOnPrimaryScreen] ? kFloatingBarVerticalOffset : 0;
304 - (void)overlayFrameChanged:(NSRect)frame {
305 if (!inPresentationMode_)
308 // Make sure |trackingAreaBounds_| always reflects either the tracking area or
309 // the desired tracking area.
310 trackingAreaBounds_ = frame;
311 // The tracking area should always be at least the height of activation zone.
312 NSRect contentBounds = [contentView_ bounds];
313 trackingAreaBounds_.origin.y =
314 std::min(trackingAreaBounds_.origin.y,
315 NSMaxY(contentBounds) - kDropdownActivationZoneHeight);
316 trackingAreaBounds_.size.height =
317 NSMaxY(contentBounds) - trackingAreaBounds_.origin.y + 1;
319 // If an animation is currently running, do not set up a tracking area now.
320 // Instead, leave it to be created it in |-animationDidEnd:|.
321 if (currentAnimation_)
324 // If this is part of the initial setup, lock bar visibility if the mouse is
325 // within the tracking area bounds.
326 if (enteringPresentationMode_ && [self mouseInsideTrackingRect])
327 [browserController_ lockBarVisibilityForOwner:self
330 [self setupTrackingArea];
333 - (void)ensureOverlayShownWithAnimation:(BOOL)animate delay:(BOOL)delay {
334 if (!inPresentationMode_)
337 if (base::CommandLine::ForCurrentProcess()->HasSwitch(switches::kKioskMode))
340 if (self.slidingStyle == fullscreen_mac::OMNIBOX_TABS_PRESENT)
345 [self startShowTimer];
347 [self cancelAllTimers];
348 [self changeOverlayToFraction:1 withAnimation:YES];
352 [self cancelAllTimers];
353 [self changeOverlayToFraction:1 withAnimation:NO];
357 - (void)ensureOverlayHiddenWithAnimation:(BOOL)animate delay:(BOOL)delay {
358 if (!inPresentationMode_)
361 if (self.slidingStyle == fullscreen_mac::OMNIBOX_TABS_PRESENT)
366 [self startHideTimer];
368 [self cancelAllTimers];
369 [self changeOverlayToFraction:0 withAnimation:YES];
373 [self cancelAllTimers];
374 [self changeOverlayToFraction:0 withAnimation:NO];
378 - (void)cancelAnimationAndTimers {
379 [self cancelAllTimers];
380 [currentAnimation_ stopAnimation];
381 currentAnimation_.reset();
384 - (void)setSystemFullscreenModeTo:(base::mac::FullScreenMode)mode {
385 if (mode == systemFullscreenMode_)
387 if (systemFullscreenMode_ == base::mac::kFullScreenModeNormal)
388 base::mac::RequestFullScreen(mode);
389 else if (mode == base::mac::kFullScreenModeNormal)
390 base::mac::ReleaseFullScreen(systemFullscreenMode_);
392 base::mac::SwitchFullScreenModes(systemFullscreenMode_, mode);
393 systemFullscreenMode_ = mode;
396 - (void)changeToolbarFraction:(CGFloat)fraction {
397 toolbarFraction_ = fraction;
398 [browserController_ layoutSubviews];
400 // In AppKit fullscreen, moving the mouse to the top of the screen toggles
401 // menu visibility. Replicate the same effect for immersive fullscreen.
402 if ([browserController_ isInImmersiveFullscreen])
403 [self updateMenuBarAndDockVisibility];
406 // This method works, but is fragile.
408 // It gets used during view layout, which sometimes needs to be done at the
409 // beginning of an animation. As such, this method needs to reflect the
410 // menubarOffset expected at the end of the animation. This information is not
411 // readily available. (The layout logic needs a refactor).
413 // For AppKit Fullscreen, the menubar always starts hidden, and
414 // menubarFraction_ always starts at 0, so the logic happens to work. For
415 // Immersive Fullscreen, this class controls the visibility of the menu bar, so
416 // the logic is correct and not fragile.
417 - (CGFloat)menubarOffset {
418 if ([browserController_ isInAppKitFullscreen])
419 return -std::floor(menubarFraction_ * [self floatingBarVerticalOffset]);
421 return [self shouldShowMenubarInImmersiveFullscreen]
422 ? -[self floatingBarVerticalOffset]
426 // Used to activate the floating bar in presentation mode.
427 - (void)mouseEntered:(NSEvent*)event {
428 DCHECK(inPresentationMode_);
430 // Having gotten a mouse entered, we no longer need to do exit checks.
431 [self cancelMouseExitCheck];
433 NSTrackingArea* trackingArea = [event trackingArea];
434 if (trackingArea == trackingArea_) {
435 // The tracking area shouldn't be active during animation.
436 DCHECK(!currentAnimation_);
437 [self scheduleShowForMouse];
441 // Used to deactivate the floating bar in presentation mode.
442 - (void)mouseExited:(NSEvent*)event {
443 DCHECK(inPresentationMode_);
445 NSTrackingArea* trackingArea = [event trackingArea];
446 if (trackingArea == trackingArea_) {
447 // The tracking area shouldn't be active during animation.
448 DCHECK(!currentAnimation_);
450 // We can get a false mouse exit when the menu slides down, so if the mouse
451 // is still actually over the tracking area, we ignore the mouse exit, but
452 // we set up to check the mouse position again after a delay.
453 if ([self mouseInsideTrackingRect]) {
454 [self setupMouseExitCheck];
458 [self scheduleHideForMouse];
462 - (void)animationDidStop:(NSAnimation*)animation {
463 // Reset the |currentAnimation_| pointer now that the animation is over.
464 currentAnimation_.reset();
466 // Invariant says that the tracking area is not installed while animations are
467 // in progress. Ensure this is true.
468 DCHECK(!trackingArea_);
469 [self removeTrackingAreaIfNecessary]; // For paranoia.
471 // Don't automatically set up a new tracking area. When explicitly stopped,
472 // either another animation is going to start immediately or the state will be
473 // changed immediately.
476 - (void)animationDidEnd:(NSAnimation*)animation {
477 [self animationDidStop:animation];
479 // |trackingAreaBounds_| contains the correct tracking area bounds, including
480 // |any updates that may have come while the animation was running. Install a
481 // new tracking area with these bounds.
482 [self setupTrackingArea];
484 // TODO(viettrungluu): Better would be to check during the animation; doing it
485 // here means that the timing is slightly off.
486 if (![self mouseInsideTrackingRect])
487 [self scheduleHideForMouse];
490 - (void)setMenuBarRevealProgress:(CGFloat)progress {
491 menubarFraction_ = progress;
493 // If an animation is not running, then -layoutSubviews will not be called
494 // for each tick of the menu bar reveal. Do that manually.
495 // TODO(erikchen): The animation is janky. layoutSubviews need a refactor so
496 // that it calls setFrameOffset: instead of setFrame: if the frame's size has
498 if (!currentAnimation_.get())
499 [browserController_ layoutSubviews];
505 @implementation PresentationModeController (PrivateMethods)
507 - (void)updateMenuBarAndDockVisibility {
508 if (![[browserController_ window] isMainWindow] ||
509 ![browserController_ isInImmersiveFullscreen]) {
510 [self setSystemFullscreenModeTo:base::mac::kFullScreenModeNormal];
514 // The screen does not have a menu bar, so there's no need to hide it.
515 if (![self doesScreenHaveMenuBar]) {
516 [self setSystemFullscreenModeTo:base::mac::kFullScreenModeHideDock];
520 [self setSystemFullscreenModeTo:[self desiredSystemFullscreenMode]];
523 - (BOOL)doesScreenHaveMenuBar {
524 if (![[NSScreen class]
525 respondsToSelector:@selector(screensHaveSeparateSpaces)])
526 return [self isWindowOnPrimaryScreen];
528 BOOL eachScreenShouldHaveMenuBar = [NSScreen screensHaveSeparateSpaces];
529 return eachScreenShouldHaveMenuBar ?: [self isWindowOnPrimaryScreen];
532 - (BOOL)isWindowOnPrimaryScreen {
533 NSScreen* screen = [[browserController_ window] screen];
534 NSScreen* primaryScreen = [[NSScreen screens] objectAtIndex:0];
535 return (screen == primaryScreen);
538 - (base::mac::FullScreenMode)desiredSystemFullscreenMode {
539 if ([self shouldShowMenubarInImmersiveFullscreen])
540 return base::mac::kFullScreenModeHideDock;
541 return base::mac::kFullScreenModeHideAll;
544 - (void)changeOverlayToFraction:(CGFloat)fraction
545 withAnimation:(BOOL)animate {
546 // The non-animated case is really simple, so do it and return.
548 [currentAnimation_ stopAnimation];
549 [self changeToolbarFraction:fraction];
553 // If we're already animating to the given fraction, then there's nothing more
555 if (currentAnimation_ && [currentAnimation_ endFraction] == fraction)
558 // In all other cases, we want to cancel any running animation (which may be
559 // to show or to hide).
560 [currentAnimation_ stopAnimation];
562 // Create the animation and set it up.
563 currentAnimation_.reset(
564 [[DropdownAnimation alloc] initWithFraction:fraction
565 fullDuration:kDropdownAnimationDuration
566 animationCurve:NSAnimationEaseOut
568 DCHECK(currentAnimation_);
569 [currentAnimation_ setAnimationBlockingMode:NSAnimationNonblocking];
570 [currentAnimation_ setDelegate:self];
572 // If there is an existing tracking area, remove it. We do not track mouse
573 // movements during animations (see class comment in the header file).
574 [self removeTrackingAreaIfNecessary];
576 [currentAnimation_ startAnimation];
579 - (void)scheduleShowForMouse {
580 [browserController_ lockBarVisibilityForOwner:self
585 - (void)scheduleHideForMouse {
586 [browserController_ releaseBarVisibilityForOwner:self
591 - (void)setupTrackingArea {
593 // If the tracking rectangle is already |trackingAreaBounds_|, quit early.
594 NSRect oldRect = [trackingArea_ rect];
595 if (NSEqualRects(trackingAreaBounds_, oldRect))
598 // Otherwise, remove it.
599 [self removeTrackingAreaIfNecessary];
602 // Create and add a new tracking area for |frame|.
604 [[NSTrackingArea alloc] initWithRect:trackingAreaBounds_
605 options:NSTrackingMouseEnteredAndExited |
606 NSTrackingActiveInKeyWindow
609 DCHECK(contentView_);
610 [contentView_ addTrackingArea:trackingArea_];
613 - (void)removeTrackingAreaIfNecessary {
615 DCHECK(contentView_); // |contentView_| better be valid.
616 [contentView_ removeTrackingArea:trackingArea_];
617 trackingArea_.reset();
621 - (BOOL)mouseInsideTrackingRect {
622 NSWindow* window = [browserController_ window];
623 NSPoint mouseLoc = [window mouseLocationOutsideOfEventStream];
624 NSPoint mousePos = [contentView_ convertPoint:mouseLoc fromView:nil];
625 return NSMouseInRect(mousePos, trackingAreaBounds_, [contentView_ isFlipped]);
628 - (void)setupMouseExitCheck {
629 [self performSelector:@selector(checkForMouseExit)
631 afterDelay:kMouseExitCheckDelay];
634 - (void)cancelMouseExitCheck {
635 [NSObject cancelPreviousPerformRequestsWithTarget:self
636 selector:@selector(checkForMouseExit) object:nil];
639 - (void)checkForMouseExit {
640 if ([self mouseInsideTrackingRect])
641 [self setupMouseExitCheck];
643 [self scheduleHideForMouse];
646 - (void)startShowTimer {
647 // If there's already a show timer going, just keep it.
649 DCHECK([showTimer_ isValid]);
654 // Cancel the hide timer (if necessary) and set up the new show timer.
655 [self cancelHideTimer];
657 [[NSTimer scheduledTimerWithTimeInterval:kDropdownShowDelay
659 selector:@selector(showTimerFire:)
661 repeats:NO] retain]);
662 DCHECK([showTimer_ isValid]); // This also checks that |showTimer_ != nil|.
665 - (void)startHideTimer {
666 // If there's already a hide timer going, just keep it.
668 DCHECK([hideTimer_ isValid]);
673 // Cancel the show timer (if necessary) and set up the new hide timer.
674 [self cancelShowTimer];
676 [[NSTimer scheduledTimerWithTimeInterval:kDropdownHideDelay
678 selector:@selector(hideTimerFire:)
680 repeats:NO] retain]);
681 DCHECK([hideTimer_ isValid]); // This also checks that |hideTimer_ != nil|.
684 - (void)cancelShowTimer {
685 [showTimer_ invalidate];
689 - (void)cancelHideTimer {
690 [hideTimer_ invalidate];
694 - (void)cancelAllTimers {
695 [self cancelShowTimer];
696 [self cancelHideTimer];
699 - (void)showTimerFire:(NSTimer*)timer {
700 DCHECK_EQ(showTimer_, timer); // This better be our show timer.
701 [showTimer_ invalidate]; // Make sure it doesn't repeat.
702 showTimer_.reset(); // And get rid of it.
703 [self changeOverlayToFraction:1 withAnimation:YES];
706 - (void)hideTimerFire:(NSTimer*)timer {
707 DCHECK_EQ(hideTimer_, timer); // This better be our hide timer.
708 [hideTimer_ invalidate]; // Make sure it doesn't repeat.
709 hideTimer_.reset(); // And get rid of it.
710 [self changeOverlayToFraction:0 withAnimation:YES];
714 [self cancelMouseExitCheck];
715 [self cancelAnimationAndTimers];
716 [[NSNotificationCenter defaultCenter] removeObserver:self];
718 [self removeTrackingAreaIfNecessary];
721 // This isn't tracked when not in presentation mode.
722 [browserController_ releaseBarVisibilityForOwner:self
726 // Call the main status resignation code to perform the associated cleanup,
727 // since we will no longer be receiving actual status resignation
729 [self setSystemFullscreenModeTo:base::mac::kFullScreenModeNormal];
731 // No more calls back up to the BWC.
732 browserController_ = nil;
735 - (void)showActiveWindowUI {
736 [self updateMenuBarAndDockVisibility];
738 // TODO(rohitrao): Insert the Exit Fullscreen button. http://crbug.com/35956
741 - (void)hideActiveWindowUI {
742 [self updateMenuBarAndDockVisibility];
744 // TODO(rohitrao): Remove the Exit Fullscreen button. http://crbug.com/35956
747 - (BOOL)shouldShowMenubarInImmersiveFullscreen {
748 return [self doesScreenHaveMenuBar] && toolbarFraction_ > 0.99;