Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(216)

Side by Side Diff: chrome/browser/ui/cocoa/fullscreen_controller.mm

Issue 7566016: Fullscreen support for Lion. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src/
Patch Set: '' Created 9 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
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
13 NSString* const kWillEnterFullscreenNotification =
14 @"WillEnterFullscreenNotification";
15 NSString* const kWillLeaveFullscreenNotification =
16 @"WillLeaveFullscreenNotification";
17
18 namespace {
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.
21 const CGFloat kDropdownActivationZoneHeight = 4;
22 const NSTimeInterval kDropdownAnimationDuration = 0.12;
23 const NSTimeInterval kMouseExitCheckDelay = 0.1;
24 // This show delay attempts to match the delay for the main menu.
25 const NSTimeInterval kDropdownShowDelay = 0.3;
26 const 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.)
31 const 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
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698