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