NaCl: Update revision in DEPS, r12770 -> r12773
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / presentation_mode_controller.mm
blob79e3cc190ece1aebcf2ff0b5620a52d80610913a
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   // 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
210   // monitors on Lion.
211   if (base::mac::IsOSSnowLeopard()) {
212     [nc addObserver:self
213            selector:@selector(windowDidChangeScreen:)
214                name:NSWindowDidChangeScreenNotification
215              object:window];
217     [nc addObserver:self
218            selector:@selector(windowDidMove:)
219                name:NSWindowDidMoveNotification
220              object:window];
221   }
223   [nc addObserver:self
224          selector:@selector(windowDidBecomeMain:)
225              name:NSWindowDidBecomeMainNotification
226            object:window];
228   [nc addObserver:self
229          selector:@selector(windowDidResignMain:)
230              name:NSWindowDidResignMainNotification
231            object:window];
233   enteringPresentationMode_ = NO;
236 - (void)exitPresentationMode {
237   [[NSNotificationCenter defaultCenter]
238     postNotificationName:kWillLeaveFullscreenNotification
239                   object:nil];
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];
247   [self cleanup];
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_)
272     return;
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_)
288     return;
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
294                                     withAnimation:NO
295                                             delay:NO];
296   [self setupTrackingArea];
299 - (void)ensureOverlayShownWithAnimation:(BOOL)animate delay:(BOOL)delay {
300   if (!inPresentationMode_)
301     return;
303   if (CommandLine::ForCurrentProcess()->HasSwitch(switches::kKioskMode))
304     return;
306   if (animate) {
307     if (delay) {
308       [self startShowTimer];
309     } else {
310       [self cancelAllTimers];
311       [self changeOverlayToFraction:1 withAnimation:YES];
312     }
313   } else {
314     DCHECK(!delay);
315     [self cancelAllTimers];
316     [self changeOverlayToFraction:1 withAnimation:NO];
317   }
320 - (void)ensureOverlayHiddenWithAnimation:(BOOL)animate delay:(BOOL)delay {
321   if (!inPresentationMode_)
322     return;
324   if (animate) {
325     if (delay) {
326       [self startHideTimer];
327     } else {
328       [self cancelAllTimers];
329       [self changeOverlayToFraction:0 withAnimation:YES];
330     }
331   } else {
332     DCHECK(!delay);
333     [self cancelAllTimers];
334     [self changeOverlayToFraction:0 withAnimation:NO];
335   }
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);
355     else
356       base::mac::SwitchFullScreenModes(systemFullscreenMode_, desiredMode);
357     systemFullscreenMode_ = desiredMode;
358   }
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];
373   }
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];
390       return;
391     }
393     [self scheduleHideForMouse];
394   }
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];
425 @end
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.
451   if (!animate) {
452     [currentAnimation_ stopAnimation];
453     [self changeFloatingBarShownFraction:fraction];
454     return;
455   }
457   // If we're already animating to the given fraction, then there's nothing more
458   // to do.
459   if (currentAnimation_ && [currentAnimation_ endFraction] == fraction)
460     return;
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
467   // to do.
468   if ([browserController_ floatingBarShownFraction] == fraction)
469     return;
471   // Create the animation and set it up.
472   currentAnimation_.reset(
473       [[DropdownAnimation alloc] initWithFraction:fraction
474                                      fullDuration:kDropdownAnimationDuration
475                                    animationCurve:NSAnimationEaseOut
476                                        controller:self]);
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
490                                   withAnimation:YES
491                                           delay:YES];
494 - (void)scheduleHideForMouse {
495   [browserController_ releaseBarVisibilityForOwner:self
496                                      withAnimation:YES
497                                              delay:YES];
500 - (void)setupTrackingArea {
501   if (trackingArea_) {
502     // If the tracking rectangle is already |trackingAreaBounds_|, quit early.
503     NSRect oldRect = [trackingArea_ rect];
504     if (NSEqualRects(trackingAreaBounds_, oldRect))
505       return;
507     // Otherwise, remove it.
508     [self removeTrackingAreaIfNecessary];
509   }
511   // Create and add a new tracking area for |frame|.
512   trackingArea_.reset(
513       [[NSTrackingArea alloc] initWithRect:trackingAreaBounds_
514                                    options:NSTrackingMouseEnteredAndExited |
515                                            NSTrackingActiveInKeyWindow
516                                      owner:self
517                                   userInfo:nil]);
518   DCHECK(contentView_);
519   [contentView_ addTrackingArea:trackingArea_];
522 - (void)removeTrackingAreaIfNecessary {
523   if (trackingArea_) {
524     DCHECK(contentView_);  // |contentView_| better be valid.
525     [contentView_ removeTrackingArea:trackingArea_];
526     trackingArea_.reset();
527   }
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)
539              withObject:nil
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];
551   else
552     [self scheduleHideForMouse];
555 - (void)startShowTimer {
556   // If there's already a show timer going, just keep it.
557   if (showTimer_) {
558     DCHECK([showTimer_ isValid]);
559     DCHECK(!hideTimer_);
560     return;
561   }
563   // Cancel the hide timer (if necessary) and set up the new show timer.
564   [self cancelHideTimer];
565   showTimer_.reset(
566       [[NSTimer scheduledTimerWithTimeInterval:kDropdownShowDelay
567                                         target:self
568                                       selector:@selector(showTimerFire:)
569                                       userInfo:nil
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.
576   if (hideTimer_) {
577     DCHECK([hideTimer_ isValid]);
578     DCHECK(!showTimer_);
579     return;
580   }
582   // Cancel the show timer (if necessary) and set up the new hide timer.
583   [self cancelShowTimer];
584   hideTimer_.reset(
585       [[NSTimer scheduledTimerWithTimeInterval:kDropdownHideDelay
586                                         target:self
587                                       selector:@selector(hideTimerFire:)
588                                       userInfo:nil
589                                        repeats:NO] retain]);
590   DCHECK([hideTimer_ isValid]);  // This also checks that |hideTimer_ != nil|.
593 - (void)cancelShowTimer {
594   [showTimer_ invalidate];
595   showTimer_.reset();
598 - (void)cancelHideTimer {
599   [hideTimer_ invalidate];
600   hideTimer_.reset();
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];
622 - (void)cleanup {
623   [self cancelMouseExitCheck];
624   [self cancelAnimationAndTimers];
625   [[NSNotificationCenter defaultCenter] removeObserver:self];
627   [self removeTrackingAreaIfNecessary];
628   contentView_ = nil;
630   // This isn't tracked when not in presentation mode.
631   [browserController_ releaseBarVisibilityForOwner:self
632                                      withAnimation:NO
633                                              delay:NO];
635   // Call the main status resignation code to perform the associated cleanup,
636   // since we will no longer be receiving actual status resignation
637   // notifications.
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)
647     return;
649   if ([self shouldToggleMenuBar]) {
650     base::mac::FullScreenMode desiredMode = [self desiredSystemFullscreenMode];
651     base::mac::RequestFullScreen(desiredMode);
652     systemFullscreenMode_ = desiredMode;
653   }
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;
662   }
664   // TODO(rohitrao): Remove the Exit Fullscreen button.  http://crbug.com/35956
667 @end