[Metrics] Make MetricsStateManager take a callback param to check if UMA is enabled.
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / presentation_mode_controller.mm
blob649145b4ec927e714b36f443aefc7d1a9605f6ef
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"
7 #include <algorithm>
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";
22 namespace {
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;
37 }  // end namespace
40 // Helper class to manage animations for the dropdown bar.  Calls
41 // [PresentationModeController changeFloatingBarShownFraction] once per
42 // animation step.
43 @interface DropdownAnimation : NSAnimation {
44  @private
45   PresentationModeController* controller_;
46   CGFloat startFraction_;
47   CGFloat endFraction_;
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;
61 @end
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.
73   DCHECK(controller);
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;
83   }
84   return self;
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 {
90   CGFloat fraction =
91       startFraction_ + (progress * (endFraction_ - startFraction_));
92   [controller_ changeFloatingBarShownFraction:fraction];
95 @end
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
129 // otherwise.
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
135 // scheduled check.
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.
155 - (void)cleanup;
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;
163 @end
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;
174   }
176   // Let the world know what we're up to.
177   [[NSNotificationCenter defaultCenter]
178     postNotificationName:kWillEnterFullscreenNotification
179                   object:nil];
181   return self;
184 - (void)dealloc {
185   DCHECK(!inPresentationMode_);
186   DCHECK(!trackingArea_);
187   [super dealloc];
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
204   // monitors on Lion.
205   if (base::mac::IsOSSnowLeopard()) {
206     [nc addObserver:self
207            selector:@selector(windowDidChangeScreen:)
208                name:NSWindowDidChangeScreenNotification
209              object:window];
211     [nc addObserver:self
212            selector:@selector(windowDidMove:)
213                name:NSWindowDidMoveNotification
214              object:window];
215   }
217   [nc addObserver:self
218          selector:@selector(windowDidBecomeMain:)
219              name:NSWindowDidBecomeMainNotification
220            object:window];
222   [nc addObserver:self
223          selector:@selector(windowDidResignMain:)
224              name:NSWindowDidResignMainNotification
225            object:window];
227   enteringPresentationMode_ = NO;
230 - (void)exitPresentationMode {
231   [[NSNotificationCenter defaultCenter]
232     postNotificationName:kWillLeaveFullscreenNotification
233                   object:nil];
234   DCHECK(inPresentationMode_);
235   inPresentationMode_ = NO;
237   [self cleanup];
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_)
262     return;
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_)
278     return;
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
284                                     withAnimation:NO
285                                             delay:NO];
286   [self setupTrackingArea];
289 - (void)ensureOverlayShownWithAnimation:(BOOL)animate delay:(BOOL)delay {
290   if (!inPresentationMode_)
291     return;
293   if (CommandLine::ForCurrentProcess()->HasSwitch(switches::kKioskMode))
294     return;
296   if (animate) {
297     if (delay) {
298       [self startShowTimer];
299     } else {
300       [self cancelAllTimers];
301       [self changeOverlayToFraction:1 withAnimation:YES];
302     }
303   } else {
304     DCHECK(!delay);
305     [self cancelAllTimers];
306     [self changeOverlayToFraction:1 withAnimation:NO];
307   }
310 - (void)ensureOverlayHiddenWithAnimation:(BOOL)animate delay:(BOOL)delay {
311   if (!inPresentationMode_)
312     return;
314   if (animate) {
315     if (delay) {
316       [self startHideTimer];
317     } else {
318       [self cancelAllTimers];
319       [self changeOverlayToFraction:0 withAnimation:YES];
320     }
321   } else {
322     DCHECK(!delay);
323     [self cancelAllTimers];
324     [self changeOverlayToFraction:0 withAnimation:NO];
325   }
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_)
340     return;
341   if (systemFullscreenMode_ == base::mac::kFullScreenModeNormal)
342     base::mac::RequestFullScreen(mode);
343   else if (mode == base::mac::kFullScreenModeNormal)
344     base::mac::ReleaseFullScreen(systemFullscreenMode_);
345   else
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];
369   }
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];
386       return;
387     }
389     [self scheduleHideForMouse];
390   }
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];
421 @end
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.
447   if (!animate) {
448     [currentAnimation_ stopAnimation];
449     [self changeFloatingBarShownFraction:fraction];
450     return;
451   }
453   // If we're already animating to the given fraction, then there's nothing more
454   // to do.
455   if (currentAnimation_ && [currentAnimation_ endFraction] == fraction)
456     return;
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
463   // to do.
464   if ([browserController_ floatingBarShownFraction] == fraction)
465     return;
467   // Create the animation and set it up.
468   currentAnimation_.reset(
469       [[DropdownAnimation alloc] initWithFraction:fraction
470                                      fullDuration:kDropdownAnimationDuration
471                                    animationCurve:NSAnimationEaseOut
472                                        controller:self]);
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
486                                   withAnimation:YES
487                                           delay:YES];
490 - (void)scheduleHideForMouse {
491   [browserController_ releaseBarVisibilityForOwner:self
492                                      withAnimation:YES
493                                              delay:YES];
496 - (void)setupTrackingArea {
497   if (trackingArea_) {
498     // If the tracking rectangle is already |trackingAreaBounds_|, quit early.
499     NSRect oldRect = [trackingArea_ rect];
500     if (NSEqualRects(trackingAreaBounds_, oldRect))
501       return;
503     // Otherwise, remove it.
504     [self removeTrackingAreaIfNecessary];
505   }
507   // Create and add a new tracking area for |frame|.
508   trackingArea_.reset(
509       [[NSTrackingArea alloc] initWithRect:trackingAreaBounds_
510                                    options:NSTrackingMouseEnteredAndExited |
511                                            NSTrackingActiveInKeyWindow
512                                      owner:self
513                                   userInfo:nil]);
514   DCHECK(contentView_);
515   [contentView_ addTrackingArea:trackingArea_];
518 - (void)removeTrackingAreaIfNecessary {
519   if (trackingArea_) {
520     DCHECK(contentView_);  // |contentView_| better be valid.
521     [contentView_ removeTrackingArea:trackingArea_];
522     trackingArea_.reset();
523   }
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)
535              withObject:nil
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];
547   else
548     [self scheduleHideForMouse];
551 - (void)startShowTimer {
552   // If there's already a show timer going, just keep it.
553   if (showTimer_) {
554     DCHECK([showTimer_ isValid]);
555     DCHECK(!hideTimer_);
556     return;
557   }
559   // Cancel the hide timer (if necessary) and set up the new show timer.
560   [self cancelHideTimer];
561   showTimer_.reset(
562       [[NSTimer scheduledTimerWithTimeInterval:kDropdownShowDelay
563                                         target:self
564                                       selector:@selector(showTimerFire:)
565                                       userInfo:nil
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.
572   if (hideTimer_) {
573     DCHECK([hideTimer_ isValid]);
574     DCHECK(!showTimer_);
575     return;
576   }
578   // Cancel the show timer (if necessary) and set up the new hide timer.
579   [self cancelShowTimer];
580   hideTimer_.reset(
581       [[NSTimer scheduledTimerWithTimeInterval:kDropdownHideDelay
582                                         target:self
583                                       selector:@selector(hideTimerFire:)
584                                       userInfo:nil
585                                        repeats:NO] retain]);
586   DCHECK([hideTimer_ isValid]);  // This also checks that |hideTimer_ != nil|.
589 - (void)cancelShowTimer {
590   [showTimer_ invalidate];
591   showTimer_.reset();
594 - (void)cancelHideTimer {
595   [hideTimer_ invalidate];
596   hideTimer_.reset();
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];
618 - (void)cleanup {
619   [self cancelMouseExitCheck];
620   [self cancelAnimationAndTimers];
621   [[NSNotificationCenter defaultCenter] removeObserver:self];
623   [self removeTrackingAreaIfNecessary];
624   contentView_ = nil;
626   // This isn't tracked when not in presentation mode.
627   [browserController_ releaseBarVisibilityForOwner:self
628                                      withAnimation:NO
629                                              delay:NO];
631   // Call the main status resignation code to perform the associated cleanup,
632   // since we will no longer be receiving actual status resignation
633   // notifications.
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)
643     return;
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
658 @end