• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.
4
5#import "chrome/browser/ui/cocoa/presentation_mode_controller.h"
6
7#include <algorithm>
8
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"
16
17NSString* const kWillEnterFullscreenNotification =
18    @"WillEnterFullscreenNotification";
19NSString* const kWillLeaveFullscreenNotification =
20    @"WillLeaveFullscreenNotification";
21
22namespace {
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.
25const CGFloat kDropdownActivationZoneHeight = 4;
26const NSTimeInterval kDropdownAnimationDuration = 0.12;
27const NSTimeInterval kMouseExitCheckDelay = 0.1;
28// This show delay attempts to match the delay for the main menu.
29const NSTimeInterval kDropdownShowDelay = 0.3;
30const NSTimeInterval kDropdownHideDelay = 0.2;
31
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.)
35const CGFloat kFloatingBarVerticalOffset = 22;
36
37}  // end namespace
38
39
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_;
48}
49
50@property(readonly, nonatomic) CGFloat startFraction;
51@property(readonly, nonatomic) CGFloat endFraction;
52
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;
60
61@end
62
63@implementation DropdownAnimation
64
65@synthesize startFraction = startFraction_;
66@synthesize endFraction = endFraction_;
67
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));
76
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;
85}
86
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];
93}
94
95@end
96
97
98@interface PresentationModeController (PrivateMethods)
99
100// Returns YES if the window is on the primary screen.
101- (BOOL)isWindowOnPrimaryScreen;
102
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;
107
108// Returns |kFullScreenModeHideAll| when the overlay is hidden and
109// |kFullScreenModeHideDock| when the overlay is shown.
110- (base::mac::FullScreenMode)desiredSystemFullscreenMode;
111
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;
117
118// Schedule the floating bar to be shown/hidden because of mouse position.
119- (void)scheduleShowForMouse;
120- (void)scheduleHideForMouse;
121
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;
127
128// Returns YES if the mouse is currently in any current tracking rectangle, NO
129// otherwise.
130- (BOOL)mouseInsideTrackingRect;
131
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;
138
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;
142
143// Start timers for showing/hiding the floating bar.
144- (void)startShowTimer;
145- (void)startHideTimer;
146- (void)cancelShowTimer;
147- (void)cancelHideTimer;
148- (void)cancelAllTimers;
149
150// Methods called when the show/hide timers fire. Do not call directly.
151- (void)showTimerFire:(NSTimer*)timer;
152- (void)hideTimerFire:(NSTimer*)timer;
153
154// Stops any running animations, removes tracking areas, etc.
155- (void)cleanup;
156
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;
162
163@end
164
165
166@implementation PresentationModeController
167
168@synthesize inPresentationMode = inPresentationMode_;
169
170- (id)initWithBrowserController:(BrowserWindowController*)controller {
171  if ((self = [super init])) {
172    browserController_ = controller;
173    systemFullscreenMode_ = base::mac::kFullScreenModeNormal;
174  }
175
176  // Let the world know what we're up to.
177  [[NSNotificationCenter defaultCenter]
178    postNotificationName:kWillEnterFullscreenNotification
179                  object:nil];
180
181  return self;
182}
183
184- (void)dealloc {
185  DCHECK(!inPresentationMode_);
186  DCHECK(!trackingArea_);
187  [super dealloc];
188}
189
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)];
197
198  // Register for notifications.  Self is removed as an observer in |-cleanup|.
199  NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
200  NSWindow* window = [browserController_ window];
201
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];
210
211    [nc addObserver:self
212           selector:@selector(windowDidMove:)
213               name:NSWindowDidMoveNotification
214             object:window];
215  }
216
217  [nc addObserver:self
218         selector:@selector(windowDidBecomeMain:)
219             name:NSWindowDidBecomeMainNotification
220           object:window];
221
222  [nc addObserver:self
223         selector:@selector(windowDidResignMain:)
224             name:NSWindowDidResignMainNotification
225           object:window];
226
227  enteringPresentationMode_ = NO;
228}
229
230- (void)exitPresentationMode {
231  [[NSNotificationCenter defaultCenter]
232    postNotificationName:kWillLeaveFullscreenNotification
233                  object:nil];
234  DCHECK(inPresentationMode_);
235  inPresentationMode_ = NO;
236
237  [self cleanup];
238}
239
240- (void)windowDidChangeScreen:(NSNotification*)notification {
241  [browserController_ resizeFullscreenWindow];
242}
243
244- (void)windowDidMove:(NSNotification*)notification {
245  [browserController_ resizeFullscreenWindow];
246}
247
248- (void)windowDidBecomeMain:(NSNotification*)notification {
249  [self showActiveWindowUI];
250}
251
252- (void)windowDidResignMain:(NSNotification*)notification {
253  [self hideActiveWindowUI];
254}
255
256- (CGFloat)floatingBarVerticalOffset {
257  return [self isWindowOnPrimaryScreen] ? kFloatingBarVerticalOffset : 0;
258}
259
260- (void)overlayFrameChanged:(NSRect)frame {
261  if (!inPresentationMode_)
262    return;
263
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;
274
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;
279
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];
287}
288
289- (void)ensureOverlayShownWithAnimation:(BOOL)animate delay:(BOOL)delay {
290  if (!inPresentationMode_)
291    return;
292
293  if (CommandLine::ForCurrentProcess()->HasSwitch(switches::kKioskMode))
294    return;
295
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  }
308}
309
310- (void)ensureOverlayHiddenWithAnimation:(BOOL)animate delay:(BOOL)delay {
311  if (!inPresentationMode_)
312    return;
313
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  }
326}
327
328- (void)cancelAnimationAndTimers {
329  [self cancelAllTimers];
330  [currentAnimation_ stopAnimation];
331  currentAnimation_.reset();
332}
333
334- (CGFloat)floatingBarShownFraction {
335  return [browserController_ floatingBarShownFraction];
336}
337
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;
348}
349
350- (void)changeFloatingBarShownFraction:(CGFloat)fraction {
351  [browserController_ setFloatingBarShownFraction:fraction];
352
353  if ([self shouldToggleMenuBar])
354    [self setSystemFullscreenModeTo:[self desiredSystemFullscreenMode]];
355}
356
357// Used to activate the floating bar in presentation mode.
358- (void)mouseEntered:(NSEvent*)event {
359  DCHECK(inPresentationMode_);
360
361  // Having gotten a mouse entered, we no longer need to do exit checks.
362  [self cancelMouseExitCheck];
363
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  }
370}
371
372// Used to deactivate the floating bar in presentation mode.
373- (void)mouseExited:(NSEvent*)event {
374  DCHECK(inPresentationMode_);
375
376  NSTrackingArea* trackingArea = [event trackingArea];
377  if (trackingArea == trackingArea_) {
378    // The tracking area shouldn't be active during animation.
379    DCHECK(!currentAnimation_);
380
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    }
388
389    [self scheduleHideForMouse];
390  }
391}
392
393- (void)animationDidStop:(NSAnimation*)animation {
394  // Reset the |currentAnimation_| pointer now that the animation is over.
395  currentAnimation_.reset();
396
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.
401
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.
405}
406
407- (void)animationDidEnd:(NSAnimation*)animation {
408  [self animationDidStop:animation];
409
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];
414
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];
419}
420
421@end
422
423
424@implementation PresentationModeController (PrivateMethods)
425
426- (BOOL)isWindowOnPrimaryScreen {
427  NSScreen* screen = [[browserController_ window] screen];
428  NSScreen* primaryScreen = [[NSScreen screens] objectAtIndex:0];
429  return (screen == primaryScreen);
430}
431
432- (BOOL)shouldToggleMenuBar {
433  return [browserController_ isInImmersiveFullscreen] &&
434         [self isWindowOnPrimaryScreen] &&
435         [[browserController_ window] isMainWindow];
436}
437
438- (base::mac::FullScreenMode)desiredSystemFullscreenMode {
439  if ([browserController_ floatingBarShownFraction] >= 1.0)
440    return base::mac::kFullScreenModeHideDock;
441  return base::mac::kFullScreenModeHideAll;
442}
443
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  }
452
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;
457
458  // In all other cases, we want to cancel any running animation (which may be
459  // to show or to hide).
460  [currentAnimation_ stopAnimation];
461
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;
466
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];
476
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];
480
481  [currentAnimation_ startAnimation];
482}
483
484- (void)scheduleShowForMouse {
485  [browserController_ lockBarVisibilityForOwner:self
486                                  withAnimation:YES
487                                          delay:YES];
488}
489
490- (void)scheduleHideForMouse {
491  [browserController_ releaseBarVisibilityForOwner:self
492                                     withAnimation:YES
493                                             delay:YES];
494}
495
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;
502
503    // Otherwise, remove it.
504    [self removeTrackingAreaIfNecessary];
505  }
506
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_];
516}
517
518- (void)removeTrackingAreaIfNecessary {
519  if (trackingArea_) {
520    DCHECK(contentView_);  // |contentView_| better be valid.
521    [contentView_ removeTrackingArea:trackingArea_];
522    trackingArea_.reset();
523  }
524}
525
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]);
531}
532
533- (void)setupMouseExitCheck {
534  [self performSelector:@selector(checkForMouseExit)
535             withObject:nil
536             afterDelay:kMouseExitCheckDelay];
537}
538
539- (void)cancelMouseExitCheck {
540  [NSObject cancelPreviousPerformRequestsWithTarget:self
541      selector:@selector(checkForMouseExit) object:nil];
542}
543
544- (void)checkForMouseExit {
545  if ([self mouseInsideTrackingRect])
546    [self setupMouseExitCheck];
547  else
548    [self scheduleHideForMouse];
549}
550
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  }
558
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|.
568}
569
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  }
577
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|.
587}
588
589- (void)cancelShowTimer {
590  [showTimer_ invalidate];
591  showTimer_.reset();
592}
593
594- (void)cancelHideTimer {
595  [hideTimer_ invalidate];
596  hideTimer_.reset();
597}
598
599- (void)cancelAllTimers {
600  [self cancelShowTimer];
601  [self cancelHideTimer];
602}
603
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];
609}
610
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];
616}
617
618- (void)cleanup {
619  [self cancelMouseExitCheck];
620  [self cancelAnimationAndTimers];
621  [[NSNotificationCenter defaultCenter] removeObserver:self];
622
623  [self removeTrackingAreaIfNecessary];
624  contentView_ = nil;
625
626  // This isn't tracked when not in presentation mode.
627  [browserController_ releaseBarVisibilityForOwner:self
628                                     withAnimation:NO
629                                             delay:NO];
630
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];
635
636  // No more calls back up to the BWC.
637  browserController_ = nil;
638}
639
640- (void)showActiveWindowUI {
641  DCHECK_EQ(systemFullscreenMode_, base::mac::kFullScreenModeNormal);
642  if (systemFullscreenMode_ != base::mac::kFullScreenModeNormal)
643    return;
644
645  if ([self shouldToggleMenuBar])
646    [self setSystemFullscreenModeTo:[self desiredSystemFullscreenMode]];
647
648  // TODO(rohitrao): Insert the Exit Fullscreen button.  http://crbug.com/35956
649}
650
651- (void)hideActiveWindowUI {
652  if ([self shouldToggleMenuBar])
653    [self setSystemFullscreenModeTo:base::mac::kFullScreenModeNormal];
654
655  // TODO(rohitrao): Remove the Exit Fullscreen button.  http://crbug.com/35956
656}
657
658@end
659