Add a minor text member to ui::MenuModel.
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / presentation_mode_controller.mm
blob0448db2efeb818d725e850ebaabc940224e55cd7
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 #include "chrome/common/chrome_switches.h"
14 #import "third_party/GTM/AppKit/GTMNSAnimation+Duration.h"
16 NSString* const kWillEnterFullscreenNotification =
17     @"WillEnterFullscreenNotification";
18 NSString* const kWillLeaveFullscreenNotification =
19     @"WillLeaveFullscreenNotification";
21 namespace {
22 // The activation zone for the main menu is 4 pixels high; if we make it any
23 // smaller, then the menu can be made to appear without the bar sliding down.
24 const CGFloat kDropdownActivationZoneHeight = 4;
25 const NSTimeInterval kDropdownAnimationDuration = 0.12;
26 const NSTimeInterval kMouseExitCheckDelay = 0.1;
27 // This show delay attempts to match the delay for the main menu.
28 const NSTimeInterval kDropdownShowDelay = 0.3;
29 const NSTimeInterval kDropdownHideDelay = 0.2;
31 // The amount by which the floating bar is offset downwards (to avoid the menu)
32 // in presentation mode. (We can't use |-[NSMenu menuBarHeight]| since it
33 // returns 0 when the menu bar is hidden.)
34 const CGFloat kFloatingBarVerticalOffset = 22;
36 }  // end namespace
39 // Helper class to manage animations for the dropdown bar.  Calls
40 // [PresentationModeController changeFloatingBarShownFraction] once per
41 // animation step.
42 @interface DropdownAnimation : NSAnimation {
43  @private
44   PresentationModeController* controller_;
45   CGFloat startFraction_;
46   CGFloat endFraction_;
49 @property(readonly, nonatomic) CGFloat startFraction;
50 @property(readonly, nonatomic) CGFloat endFraction;
52 // Designated initializer.  Asks |controller| for the current shown fraction, so
53 // if the bar is already partially shown or partially hidden, the animation
54 // duration may be less than |fullDuration|.
55 - (id)initWithFraction:(CGFloat)fromFraction
56           fullDuration:(CGFloat)fullDuration
57         animationCurve:(NSAnimationCurve)animationCurve
58             controller:(PresentationModeController*)controller;
60 @end
62 @implementation DropdownAnimation
64 @synthesize startFraction = startFraction_;
65 @synthesize endFraction = endFraction_;
67 - (id)initWithFraction:(CGFloat)toFraction
68           fullDuration:(CGFloat)fullDuration
69         animationCurve:(NSAnimationCurve)animationCurve
70             controller:(PresentationModeController*)controller {
71   // Calculate the effective duration, based on the current shown fraction.
72   DCHECK(controller);
73   CGFloat fromFraction = [controller floatingBarShownFraction];
74   CGFloat effectiveDuration = fabs(fullDuration * (fromFraction - toFraction));
76   if ((self = [super gtm_initWithDuration:effectiveDuration
77                                 eventMask:NSLeftMouseDownMask
78                            animationCurve:animationCurve])) {
79     startFraction_ = fromFraction;
80     endFraction_ = toFraction;
81     controller_ = controller;
82   }
83   return self;
86 // Called once per animation step.  Overridden to change the floating bar's
87 // position based on the animation's progress.
88 - (void)setCurrentProgress:(NSAnimationProgress)progress {
89   CGFloat fraction =
90       startFraction_ + (progress * (endFraction_ - startFraction_));
91   [controller_ changeFloatingBarShownFraction:fraction];
94 @end
97 @interface PresentationModeController (PrivateMethods)
99 // Returns YES if the window is on the primary screen.
100 - (BOOL)isWindowOnPrimaryScreen;
102 // Returns YES if it is ok to show and hide the menu bar in response to the
103 // overlay opening and closing.  Will return NO if the window is not main or not
104 // on the primary monitor.
105 - (BOOL)shouldToggleMenuBar;
107 // Returns |kFullScreenModeHideAll| when the overlay is hidden and
108 // |kFullScreenModeHideDock| when the overlay is shown.
109 - (base::mac::FullScreenMode)desiredSystemFullscreenMode;
111 // Change the overlay to the given fraction, with or without animation. Only
112 // guaranteed to work properly with |fraction == 0| or |fraction == 1|. This
113 // performs the show/hide (animation) immediately. It does not touch the timers.
114 - (void)changeOverlayToFraction:(CGFloat)fraction
115                   withAnimation:(BOOL)animate;
117 // Schedule the floating bar to be shown/hidden because of mouse position.
118 - (void)scheduleShowForMouse;
119 - (void)scheduleHideForMouse;
121 // Set up the tracking area used to activate the sliding bar or keep it active
122 // using with the rectangle in |trackingAreaBounds_|, or remove the tracking
123 // area if one was previously set up.
124 - (void)setupTrackingArea;
125 - (void)removeTrackingAreaIfNecessary;
127 // Returns YES if the mouse is currently in any current tracking rectangle, NO
128 // otherwise.
129 - (BOOL)mouseInsideTrackingRect;
131 // The tracking area can "falsely" report exits when the menu slides down over
132 // it. In that case, we have to monitor for a "real" mouse exit on a timer.
133 // |-setupMouseExitCheck| schedules a check; |-cancelMouseExitCheck| cancels any
134 // scheduled check.
135 - (void)setupMouseExitCheck;
136 - (void)cancelMouseExitCheck;
138 // Called (after a delay) by |-setupMouseExitCheck|, to check whether the mouse
139 // has exited or not; if it hasn't, it will schedule another check.
140 - (void)checkForMouseExit;
142 // Start timers for showing/hiding the floating bar.
143 - (void)startShowTimer;
144 - (void)startHideTimer;
145 - (void)cancelShowTimer;
146 - (void)cancelHideTimer;
147 - (void)cancelAllTimers;
149 // Methods called when the show/hide timers fire. Do not call directly.
150 - (void)showTimerFire:(NSTimer*)timer;
151 - (void)hideTimerFire:(NSTimer*)timer;
153 // Stops any running animations, removes tracking areas, etc.
154 - (void)cleanup;
156 // Shows and hides the UI associated with this window being active (having main
157 // status).  This includes hiding the menu bar.  These functions are called when
158 // the window gains or loses main status as well as in |-cleanup|.
159 - (void)showActiveWindowUI;
160 - (void)hideActiveWindowUI;
162 @end
165 @implementation PresentationModeController
167 @synthesize inPresentationMode = inPresentationMode_;
169 - (id)initWithBrowserController:(BrowserWindowController*)controller {
170   if ((self = [super init])) {
171     browserController_ = controller;
172     systemFullscreenMode_ = base::mac::kFullScreenModeNormal;
173   }
175   // Let the world know what we're up to.
176   [[NSNotificationCenter defaultCenter]
177     postNotificationName:kWillEnterFullscreenNotification
178                   object:nil];
180   return self;
183 - (void)dealloc {
184   DCHECK(!inPresentationMode_);
185   DCHECK(!trackingArea_);
186   [super dealloc];
189 - (void)enterPresentationModeForContentView:(NSView*)contentView
190                                showDropdown:(BOOL)showDropdown {
191   DCHECK(!inPresentationMode_);
192   enteringPresentationMode_ = YES;
193   inPresentationMode_ = YES;
194   contentView_ = contentView;
195   [self changeFloatingBarShownFraction:(showDropdown ? 1 : 0)];
197   // Register for notifications.  Self is removed as an observer in |-cleanup|.
198   NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
199   NSWindow* window = [browserController_ window];
201   // Disable these notifications on Lion as they cause crashes.
202   // TODO(rohitrao): Figure out what happens if a fullscreen window changes
203   // monitors on Lion.
204   if (base::mac::IsOSSnowLeopard()) {
205     [nc addObserver:self
206            selector:@selector(windowDidChangeScreen:)
207                name:NSWindowDidChangeScreenNotification
208              object:window];
210     [nc addObserver:self
211            selector:@selector(windowDidMove:)
212                name:NSWindowDidMoveNotification
213              object:window];
214   }
216   [nc addObserver:self
217          selector:@selector(windowDidBecomeMain:)
218              name:NSWindowDidBecomeMainNotification
219            object:window];
221   [nc addObserver:self
222          selector:@selector(windowDidResignMain:)
223              name:NSWindowDidResignMainNotification
224            object:window];
226   enteringPresentationMode_ = NO;
229 - (void)exitPresentationMode {
230   [[NSNotificationCenter defaultCenter]
231     postNotificationName:kWillLeaveFullscreenNotification
232                   object:nil];
233   DCHECK(inPresentationMode_);
234   inPresentationMode_ = NO;
235   [self cleanup];
238 - (void)windowDidChangeScreen:(NSNotification*)notification {
239   [browserController_ resizeFullscreenWindow];
242 - (void)windowDidMove:(NSNotification*)notification {
243   [browserController_ resizeFullscreenWindow];
246 - (void)windowDidBecomeMain:(NSNotification*)notification {
247   [self showActiveWindowUI];
250 - (void)windowDidResignMain:(NSNotification*)notification {
251   [self hideActiveWindowUI];
254 - (CGFloat)floatingBarVerticalOffset {
255   return [self isWindowOnPrimaryScreen] ? kFloatingBarVerticalOffset : 0;
258 - (void)overlayFrameChanged:(NSRect)frame {
259   if (!inPresentationMode_)
260     return;
262   // Make sure |trackingAreaBounds_| always reflects either the tracking area or
263   // the desired tracking area.
264   trackingAreaBounds_ = frame;
265   // The tracking area should always be at least the height of activation zone.
266   NSRect contentBounds = [contentView_ bounds];
267   trackingAreaBounds_.origin.y =
268       std::min(trackingAreaBounds_.origin.y,
269                NSMaxY(contentBounds) - kDropdownActivationZoneHeight);
270   trackingAreaBounds_.size.height =
271       NSMaxY(contentBounds) - trackingAreaBounds_.origin.y + 1;
273   // If an animation is currently running, do not set up a tracking area now.
274   // Instead, leave it to be created it in |-animationDidEnd:|.
275   if (currentAnimation_)
276     return;
278   // If this is part of the initial setup, lock bar visibility if the mouse is
279   // within the tracking area bounds.
280   if (enteringPresentationMode_ && [self mouseInsideTrackingRect])
281     [browserController_ lockBarVisibilityForOwner:self
282                                     withAnimation:NO
283                                             delay:NO];
284   [self setupTrackingArea];
287 - (void)ensureOverlayShownWithAnimation:(BOOL)animate delay:(BOOL)delay {
288   if (!inPresentationMode_)
289     return;
291   if (CommandLine::ForCurrentProcess()->HasSwitch(switches::kKioskMode))
292     return;
294   if (animate) {
295     if (delay) {
296       [self startShowTimer];
297     } else {
298       [self cancelAllTimers];
299       [self changeOverlayToFraction:1 withAnimation:YES];
300     }
301   } else {
302     DCHECK(!delay);
303     [self cancelAllTimers];
304     [self changeOverlayToFraction:1 withAnimation:NO];
305   }
308 - (void)ensureOverlayHiddenWithAnimation:(BOOL)animate delay:(BOOL)delay {
309   if (!inPresentationMode_)
310     return;
312   if (animate) {
313     if (delay) {
314       [self startHideTimer];
315     } else {
316       [self cancelAllTimers];
317       [self changeOverlayToFraction:0 withAnimation:YES];
318     }
319   } else {
320     DCHECK(!delay);
321     [self cancelAllTimers];
322     [self changeOverlayToFraction:0 withAnimation:NO];
323   }
326 - (void)cancelAnimationAndTimers {
327   [self cancelAllTimers];
328   [currentAnimation_ stopAnimation];
329   currentAnimation_.reset();
332 - (CGFloat)floatingBarShownFraction {
333   return [browserController_ floatingBarShownFraction];
336 - (void)changeFloatingBarShownFraction:(CGFloat)fraction {
337   [browserController_ setFloatingBarShownFraction:fraction];
339   base::mac::FullScreenMode desiredMode = [self desiredSystemFullscreenMode];
340   if (desiredMode != systemFullscreenMode_ && [self shouldToggleMenuBar]) {
341     if (systemFullscreenMode_ == base::mac::kFullScreenModeNormal)
342       base::mac::RequestFullScreen(desiredMode);
343     else
344       base::mac::SwitchFullScreenModes(systemFullscreenMode_, desiredMode);
345     systemFullscreenMode_ = desiredMode;
346   }
349 // Used to activate the floating bar in presentation mode.
350 - (void)mouseEntered:(NSEvent*)event {
351   DCHECK(inPresentationMode_);
353   // Having gotten a mouse entered, we no longer need to do exit checks.
354   [self cancelMouseExitCheck];
356   NSTrackingArea* trackingArea = [event trackingArea];
357   if (trackingArea == trackingArea_) {
358     // The tracking area shouldn't be active during animation.
359     DCHECK(!currentAnimation_);
360     [self scheduleShowForMouse];
361   }
364 // Used to deactivate the floating bar in presentation mode.
365 - (void)mouseExited:(NSEvent*)event {
366   DCHECK(inPresentationMode_);
368   NSTrackingArea* trackingArea = [event trackingArea];
369   if (trackingArea == trackingArea_) {
370     // The tracking area shouldn't be active during animation.
371     DCHECK(!currentAnimation_);
373     // We can get a false mouse exit when the menu slides down, so if the mouse
374     // is still actually over the tracking area, we ignore the mouse exit, but
375     // we set up to check the mouse position again after a delay.
376     if ([self mouseInsideTrackingRect]) {
377       [self setupMouseExitCheck];
378       return;
379     }
381     [self scheduleHideForMouse];
382   }
385 - (void)animationDidStop:(NSAnimation*)animation {
386   // Reset the |currentAnimation_| pointer now that the animation is over.
387   currentAnimation_.reset();
389   // Invariant says that the tracking area is not installed while animations are
390   // in progress. Ensure this is true.
391   DCHECK(!trackingArea_);
392   [self removeTrackingAreaIfNecessary];  // For paranoia.
394   // Don't automatically set up a new tracking area. When explicitly stopped,
395   // either another animation is going to start immediately or the state will be
396   // changed immediately.
399 - (void)animationDidEnd:(NSAnimation*)animation {
400   [self animationDidStop:animation];
402   // |trackingAreaBounds_| contains the correct tracking area bounds, including
403   // |any updates that may have come while the animation was running. Install a
404   // new tracking area with these bounds.
405   [self setupTrackingArea];
407   // TODO(viettrungluu): Better would be to check during the animation; doing it
408   // here means that the timing is slightly off.
409   if (![self mouseInsideTrackingRect])
410     [self scheduleHideForMouse];
413 @end
416 @implementation PresentationModeController (PrivateMethods)
418 - (BOOL)isWindowOnPrimaryScreen {
419   NSScreen* screen = [[browserController_ window] screen];
420   NSScreen* primaryScreen = [[NSScreen screens] objectAtIndex:0];
421   return (screen == primaryScreen);
424 - (BOOL)shouldToggleMenuBar {
425   return !chrome::mac::SupportsSystemFullscreen() &&
426          [self isWindowOnPrimaryScreen] &&
427          [[browserController_ window] isMainWindow];
430 - (base::mac::FullScreenMode)desiredSystemFullscreenMode {
431   if ([browserController_ floatingBarShownFraction] >= 1.0)
432     return base::mac::kFullScreenModeHideDock;
433   return base::mac::kFullScreenModeHideAll;
436 - (void)changeOverlayToFraction:(CGFloat)fraction
437                   withAnimation:(BOOL)animate {
438   // The non-animated case is really simple, so do it and return.
439   if (!animate) {
440     [currentAnimation_ stopAnimation];
441     [self changeFloatingBarShownFraction:fraction];
442     return;
443   }
445   // If we're already animating to the given fraction, then there's nothing more
446   // to do.
447   if (currentAnimation_ && [currentAnimation_ endFraction] == fraction)
448     return;
450   // In all other cases, we want to cancel any running animation (which may be
451   // to show or to hide).
452   [currentAnimation_ stopAnimation];
454   // Now, if it happens to already be in the right state, there's nothing more
455   // to do.
456   if ([browserController_ floatingBarShownFraction] == fraction)
457     return;
459   // Create the animation and set it up.
460   currentAnimation_.reset(
461       [[DropdownAnimation alloc] initWithFraction:fraction
462                                      fullDuration:kDropdownAnimationDuration
463                                    animationCurve:NSAnimationEaseOut
464                                        controller:self]);
465   DCHECK(currentAnimation_);
466   [currentAnimation_ setAnimationBlockingMode:NSAnimationNonblocking];
467   [currentAnimation_ setDelegate:self];
469   // If there is an existing tracking area, remove it. We do not track mouse
470   // movements during animations (see class comment in the header file).
471   [self removeTrackingAreaIfNecessary];
473   [currentAnimation_ startAnimation];
476 - (void)scheduleShowForMouse {
477   [browserController_ lockBarVisibilityForOwner:self
478                                   withAnimation:YES
479                                           delay:YES];
482 - (void)scheduleHideForMouse {
483   [browserController_ releaseBarVisibilityForOwner:self
484                                      withAnimation:YES
485                                              delay:YES];
488 - (void)setupTrackingArea {
489   if (trackingArea_) {
490     // If the tracking rectangle is already |trackingAreaBounds_|, quit early.
491     NSRect oldRect = [trackingArea_ rect];
492     if (NSEqualRects(trackingAreaBounds_, oldRect))
493       return;
495     // Otherwise, remove it.
496     [self removeTrackingAreaIfNecessary];
497   }
499   // Create and add a new tracking area for |frame|.
500   trackingArea_.reset(
501       [[NSTrackingArea alloc] initWithRect:trackingAreaBounds_
502                                    options:NSTrackingMouseEnteredAndExited |
503                                            NSTrackingActiveInKeyWindow
504                                      owner:self
505                                   userInfo:nil]);
506   DCHECK(contentView_);
507   [contentView_ addTrackingArea:trackingArea_];
510 - (void)removeTrackingAreaIfNecessary {
511   if (trackingArea_) {
512     DCHECK(contentView_);  // |contentView_| better be valid.
513     [contentView_ removeTrackingArea:trackingArea_];
514     trackingArea_.reset();
515   }
518 - (BOOL)mouseInsideTrackingRect {
519   NSWindow* window = [browserController_ window];
520   NSPoint mouseLoc = [window mouseLocationOutsideOfEventStream];
521   NSPoint mousePos = [contentView_ convertPoint:mouseLoc fromView:nil];
522   return NSMouseInRect(mousePos, trackingAreaBounds_, [contentView_ isFlipped]);
525 - (void)setupMouseExitCheck {
526   [self performSelector:@selector(checkForMouseExit)
527              withObject:nil
528              afterDelay:kMouseExitCheckDelay];
531 - (void)cancelMouseExitCheck {
532   [NSObject cancelPreviousPerformRequestsWithTarget:self
533       selector:@selector(checkForMouseExit) object:nil];
536 - (void)checkForMouseExit {
537   if ([self mouseInsideTrackingRect])
538     [self setupMouseExitCheck];
539   else
540     [self scheduleHideForMouse];
543 - (void)startShowTimer {
544   // If there's already a show timer going, just keep it.
545   if (showTimer_) {
546     DCHECK([showTimer_ isValid]);
547     DCHECK(!hideTimer_);
548     return;
549   }
551   // Cancel the hide timer (if necessary) and set up the new show timer.
552   [self cancelHideTimer];
553   showTimer_.reset(
554       [[NSTimer scheduledTimerWithTimeInterval:kDropdownShowDelay
555                                         target:self
556                                       selector:@selector(showTimerFire:)
557                                       userInfo:nil
558                                        repeats:NO] retain]);
559   DCHECK([showTimer_ isValid]);  // This also checks that |showTimer_ != nil|.
562 - (void)startHideTimer {
563   // If there's already a hide timer going, just keep it.
564   if (hideTimer_) {
565     DCHECK([hideTimer_ isValid]);
566     DCHECK(!showTimer_);
567     return;
568   }
570   // Cancel the show timer (if necessary) and set up the new hide timer.
571   [self cancelShowTimer];
572   hideTimer_.reset(
573       [[NSTimer scheduledTimerWithTimeInterval:kDropdownHideDelay
574                                         target:self
575                                       selector:@selector(hideTimerFire:)
576                                       userInfo:nil
577                                        repeats:NO] retain]);
578   DCHECK([hideTimer_ isValid]);  // This also checks that |hideTimer_ != nil|.
581 - (void)cancelShowTimer {
582   [showTimer_ invalidate];
583   showTimer_.reset();
586 - (void)cancelHideTimer {
587   [hideTimer_ invalidate];
588   hideTimer_.reset();
591 - (void)cancelAllTimers {
592   [self cancelShowTimer];
593   [self cancelHideTimer];
596 - (void)showTimerFire:(NSTimer*)timer {
597   DCHECK_EQ(showTimer_, timer);  // This better be our show timer.
598   [showTimer_ invalidate];       // Make sure it doesn't repeat.
599   showTimer_.reset();            // And get rid of it.
600   [self changeOverlayToFraction:1 withAnimation:YES];
603 - (void)hideTimerFire:(NSTimer*)timer {
604   DCHECK_EQ(hideTimer_, timer);  // This better be our hide timer.
605   [hideTimer_ invalidate];       // Make sure it doesn't repeat.
606   hideTimer_.reset();            // And get rid of it.
607   [self changeOverlayToFraction:0 withAnimation:YES];
610 - (void)cleanup {
611   [self cancelMouseExitCheck];
612   [self cancelAnimationAndTimers];
613   [[NSNotificationCenter defaultCenter] removeObserver:self];
615   [self removeTrackingAreaIfNecessary];
616   contentView_ = nil;
618   // This isn't tracked when not in presentation mode.
619   [browserController_ releaseBarVisibilityForOwner:self
620                                      withAnimation:NO
621                                              delay:NO];
623   // Call the main status resignation code to perform the associated cleanup,
624   // since we will no longer be receiving actual status resignation
625   // notifications.
626   [self hideActiveWindowUI];
628   // No more calls back up to the BWC.
629   browserController_ = nil;
632 - (void)showActiveWindowUI {
633   DCHECK_EQ(systemFullscreenMode_, base::mac::kFullScreenModeNormal);
634   if (systemFullscreenMode_ != base::mac::kFullScreenModeNormal)
635     return;
637   if ([self shouldToggleMenuBar]) {
638     base::mac::FullScreenMode desiredMode = [self desiredSystemFullscreenMode];
639     base::mac::RequestFullScreen(desiredMode);
640     systemFullscreenMode_ = desiredMode;
641   }
643   // TODO(rohitrao): Insert the Exit Fullscreen button.  http://crbug.com/35956
646 - (void)hideActiveWindowUI {
647   if (systemFullscreenMode_ != base::mac::kFullScreenModeNormal) {
648     base::mac::ReleaseFullScreen(systemFullscreenMode_);
649     systemFullscreenMode_ = base::mac::kFullScreenModeNormal;
650   }
652   // TODO(rohitrao): Remove the Exit Fullscreen button.  http://crbug.com/35956
655 @end