<http://webkit.org/b/91015> Remove BUILDING_ON / TARGETING macros in favor of system...
[WebKit-https.git] / Source / WebCore / platform / mac / WebVideoFullscreenController.mm
1 /*
2  * Copyright (C) 2009 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #import "config.h"
27
28 #if ENABLE(VIDEO)
29
30 #import "WebVideoFullscreenController.h"
31
32 #import "WebVideoFullscreenHUDWindowController.h"
33 #import "WebWindowAnimation.h"
34 #import <Carbon/Carbon.h>
35 #import <QTKit/QTKit.h>
36 #import <WebCore/DisplaySleepDisabler.h>
37 #import <WebCore/HTMLMediaElement.h>
38 #import <WebCore/SoftLinking.h>
39 #import <objc/objc-runtime.h>
40 #import <wtf/UnusedParam.h>
41
42 #if USE(GSTREAMER)
43 #import <WebCore/GStreamerGWorld.h>
44 #endif
45
46 using namespace WebCore;
47
48 SOFT_LINK_FRAMEWORK(QTKit)
49 SOFT_LINK_CLASS(QTKit, QTMovieLayer)
50
51 SOFT_LINK_POINTER(QTKit, QTMovieRateDidChangeNotification, NSString *)
52
53 #define QTMovieRateDidChangeNotification getQTMovieRateDidChangeNotification()
54
55 @interface WebVideoFullscreenWindow : NSWindow
56 #if __MAC_OS_X_VERSION_MIN_REQUIRED >= 1060
57 <NSAnimationDelegate>
58 #endif
59 {
60     SEL _controllerActionOnAnimationEnd;
61     WebWindowScaleAnimation *_fullscreenAnimation; // (retain)
62 }
63 - (void)animateFromRect:(NSRect)startRect toRect:(NSRect)endRect withSubAnimation:(NSAnimation *)subAnimation controllerAction:(SEL)controllerAction;
64 @end
65
66 @interface WebVideoFullscreenController(HUDWindowControllerDelegate) <WebVideoFullscreenHUDWindowControllerDelegate>
67 - (void)requestExitFullscreenWithAnimation:(BOOL)animation;
68 - (void)updateMenuAndDockForFullscreen;
69 - (void)updatePowerAssertions;
70 @end
71
72 @interface NSWindow(IsOnActiveSpaceAdditionForTigerAndLeopard)
73 - (BOOL)isOnActiveSpace;
74 @end
75
76 @implementation WebVideoFullscreenController
77 - (id)init
78 {
79     // Do not defer window creation, to make sure -windowNumber is created (needed by WebWindowScaleAnimation).
80     NSWindow *window = [[WebVideoFullscreenWindow alloc] initWithContentRect:NSZeroRect styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO];
81     self = [super initWithWindow:window];
82     [window release];
83     if (!self)
84         return nil;
85     [self windowDidLoad];
86     return self;
87     
88 }
89 - (void)dealloc
90 {
91     ASSERT(!_backgroundFullscreenWindow);
92     ASSERT(!_fadeAnimation);
93     [[NSNotificationCenter defaultCenter] removeObserver:self];
94     [super dealloc];
95 }
96
97 - (WebVideoFullscreenWindow *)fullscreenWindow
98 {
99     return (WebVideoFullscreenWindow *)[super window];
100 }
101
102 - (void)setupVideoOverlay:(QTMovieLayer*)layer
103 {
104     WebVideoFullscreenWindow *window = [self fullscreenWindow];
105 #if USE(GSTREAMER)
106     if (_mediaElement && _mediaElement->platformMedia().type == PlatformMedia::GStreamerGWorldType) {
107         GStreamerGWorld* gstGworld = _mediaElement->platformMedia().media.gstreamerGWorld;
108         if (gstGworld->enterFullscreen())
109             [window setContentView:gstGworld->platformVideoWindow()->window()];
110     }
111 #else
112     [[window contentView] setLayer:layer];
113     [[window contentView] setWantsLayer:YES];
114     if (_mediaElement && _mediaElement->platformMedia().type == PlatformMedia::QTMovieType)
115         [layer setMovie:_mediaElement->platformMedia().media.qtMovie];
116 #endif
117 }
118
119 - (void)windowDidLoad
120 {
121     WebVideoFullscreenWindow *window = [self fullscreenWindow];
122     [window setHasShadow:YES]; // This is nicer with a shadow.
123     [window setLevel:NSPopUpMenuWindowLevel-1];
124
125     QTMovieLayer *layer = [[getQTMovieLayerClass() alloc] init];
126     [self setupVideoOverlay:layer];
127     [layer release];
128
129 #if !USE(GSTREAMER)
130     [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidResignActive:) name:NSApplicationDidResignActiveNotification object:NSApp];
131     [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidChangeScreenParameters:) name:NSApplicationDidChangeScreenParametersNotification object:NSApp];
132 #endif
133 }
134
135 - (HTMLMediaElement*)mediaElement
136 {
137     return _mediaElement.get();
138 }
139
140 - (void)setMediaElement:(HTMLMediaElement*)mediaElement
141 {
142     _mediaElement = mediaElement;
143     if ([self isWindowLoaded]) {
144         QTMovieLayer *movieLayer = (QTMovieLayer *)[[[self fullscreenWindow] contentView] layer];
145
146         ASSERT(movieLayer && [movieLayer isKindOfClass:[getQTMovieLayerClass() class]]);
147         [self setupVideoOverlay:movieLayer];
148 #if !USE(GSTREAMER)
149         ASSERT([movieLayer movie]);
150         [[NSNotificationCenter defaultCenter] addObserver:self
151                                                  selector:@selector(rateChanged:) 
152                                                      name:QTMovieRateDidChangeNotification 
153                                                    object:[movieLayer movie]];
154 #endif
155     }
156 }
157
158 - (id <WebVideoFullscreenControllerDelegate>)delegate
159 {
160     return _delegate;
161 }
162
163 - (void)setDelegate:(id <WebVideoFullscreenControllerDelegate>)delegate
164 {
165     _delegate = delegate;
166 }
167
168 - (CGFloat)clearFadeAnimation
169 {
170     [_fadeAnimation stopAnimation];
171     CGFloat previousAlpha = [_fadeAnimation currentAlpha];
172     [_fadeAnimation setWindow:nil];
173     [_fadeAnimation release];
174     _fadeAnimation = nil;
175     return previousAlpha;
176 }
177
178 - (void)windowDidExitFullscreen
179 {
180 #if USE(GSTREAMER)
181     if (_mediaElement && _mediaElement->platformMedia().type == PlatformMedia::GStreamerGWorldType)
182         _mediaElement->platformMedia().media.gstreamerGWorld->exitFullscreen();
183 #endif
184     [self clearFadeAnimation];
185     [[self window] close];
186     [self setWindow:nil];
187     [self updateMenuAndDockForFullscreen];   
188     [self updatePowerAssertions];
189     [_hudController setDelegate:nil];
190     [_hudController release];
191     _hudController = nil;
192     [_backgroundFullscreenWindow close];
193     [_backgroundFullscreenWindow release];
194     _backgroundFullscreenWindow = nil;
195     
196     [self autorelease]; // Associated -retain is in -exitFullscreen.
197     _isEndingFullscreen = NO;
198 }
199
200 - (void)windowDidEnterFullscreen
201 {
202     [self clearFadeAnimation];
203
204     ASSERT(!_hudController);
205     _hudController = [[WebVideoFullscreenHUDWindowController alloc] init];
206     [_hudController setDelegate:self];
207
208     [self updateMenuAndDockForFullscreen];
209     [self updatePowerAssertions];
210     [NSCursor setHiddenUntilMouseMoves:YES];
211     
212     // Give the HUD keyboard focus initially
213     [_hudController fadeWindowIn];
214 }
215
216 - (NSRect)mediaElementRect
217 {
218     return _mediaElement->screenRect();
219 }
220
221 - (void)applicationDidResignActive:(NSNotification*)notification
222 {   
223     UNUSED_PARAM(notification);
224     // Check to see if the fullscreenWindow is on the active space; this function is available
225     // on 10.6 and later, so default to YES if the function is not available:
226     NSWindow* fullscreenWindow = [self fullscreenWindow];
227     BOOL isOnActiveSpace = ([fullscreenWindow respondsToSelector:@selector(isOnActiveSpace)] ? [fullscreenWindow isOnActiveSpace] : YES);
228
229     // Replicate the QuickTime Player (X) behavior when losing active application status:
230     // Is the fullscreen screen the main screen? (Note: this covers the case where only a 
231     // single screen is available.)  Is the fullscreen screen on the current space? IFF so, 
232     // then exit fullscreen mode.    
233     if ([fullscreenWindow screen] == [[NSScreen screens] objectAtIndex:0] && isOnActiveSpace)
234          [self requestExitFullscreenWithAnimation:NO];
235 }
236          
237          
238 // MARK: -
239 // MARK: Exposed Interface
240
241 static void constrainFrameToRatioOfFrame(NSRect *frameToConstrain, const NSRect *frame)
242 {
243     // Keep a constrained aspect ratio for the destination window
244     CGFloat originalRatio = frame->size.width / frame->size.height;
245     CGFloat newRatio = frameToConstrain->size.width / frameToConstrain->size.height;
246     if (newRatio > originalRatio) {
247         CGFloat newWidth = originalRatio * frameToConstrain->size.height;
248         CGFloat diff = frameToConstrain->size.width - newWidth;
249         frameToConstrain->size.width = newWidth;
250         frameToConstrain->origin.x += diff / 2;
251     } else {
252         CGFloat newHeight = frameToConstrain->size.width / originalRatio;
253         CGFloat diff = frameToConstrain->size.height - newHeight;
254         frameToConstrain->size.height = newHeight;
255         frameToConstrain->origin.y += diff / 2;
256     }    
257 }
258
259 static NSWindow *createBackgroundFullscreenWindow(NSRect frame, int level)
260 {
261     NSWindow *window = [[NSWindow alloc] initWithContentRect:frame styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO];
262     [window setOpaque:YES];
263     [window setBackgroundColor:[NSColor blackColor]];
264     [window setLevel:level];
265     [window setReleasedWhenClosed:NO];
266     return window;
267 }
268
269 - (void)setupFadeAnimationIfNeededAndFadeIn:(BOOL)fadeIn
270 {
271     CGFloat initialAlpha = fadeIn ? 0 : 1;
272     if (_fadeAnimation) {
273         // Make sure we support queuing animation if the previous one isn't over yet
274         initialAlpha = [self clearFadeAnimation];
275     }
276     if (!_forceDisableAnimation)
277         _fadeAnimation = [[WebWindowFadeAnimation alloc] initWithDuration:0.2 window:_backgroundFullscreenWindow initialAlpha:initialAlpha finalAlpha:fadeIn ? 1 : 0];
278 }
279
280 - (void)enterFullscreen:(NSScreen *)screen
281 {
282     if (!screen)
283         screen = [NSScreen mainScreen];
284
285     NSRect frame = [self mediaElementRect];
286     NSRect endFrame = [screen frame];
287     constrainFrameToRatioOfFrame(&endFrame, &frame);
288
289     // Create a black window if needed
290     if (!_backgroundFullscreenWindow)
291         _backgroundFullscreenWindow = createBackgroundFullscreenWindow([screen frame], [[self window] level]-1);
292     else
293         [_backgroundFullscreenWindow setFrame:[screen frame] display:NO];
294
295     [self setupFadeAnimationIfNeededAndFadeIn:YES];
296     if (_forceDisableAnimation) {
297         // This will disable scale animation
298         frame = NSZeroRect;
299     }
300     [[self fullscreenWindow] animateFromRect:frame toRect:endFrame withSubAnimation:_fadeAnimation controllerAction:@selector(windowDidEnterFullscreen)];
301
302     [_backgroundFullscreenWindow orderWindow:NSWindowBelow relativeTo:[[self fullscreenWindow] windowNumber]];
303 }
304
305 - (void)exitFullscreen
306 {
307     if (_isEndingFullscreen)
308         return;
309     _isEndingFullscreen = YES;
310     [_hudController closeWindow];
311
312     NSRect endFrame = [self mediaElementRect];
313
314     [self setupFadeAnimationIfNeededAndFadeIn:NO];
315     if (_forceDisableAnimation) {
316         // This will disable scale animation
317         endFrame = NSZeroRect;
318     }
319     
320     // We have to retain ourselves because we want to be alive for the end of the animation.
321     // If our owner releases us we could crash if this is not the case.
322     // Balanced in windowDidExitFullscreen
323     [self retain];    
324     
325     [[self fullscreenWindow] animateFromRect:[[self window] frame] toRect:endFrame withSubAnimation:_fadeAnimation controllerAction:@selector(windowDidExitFullscreen)];
326 }
327
328 - (void)applicationDidChangeScreenParameters:(NSNotification*)notification
329 {
330     UNUSED_PARAM(notification);
331     // The user may have changed the main screen by moving the menu bar, or they may have changed
332     // the Dock's size or location, or they may have changed the fullscreen screen's dimensions.  
333     // Update our presentation parameters, and ensure that the full screen window occupies the 
334     // entire screen:
335     [self updateMenuAndDockForFullscreen];
336     [[self window] setFrame:[[[self window] screen] frame] display:YES];
337 }
338
339 - (void)updateMenuAndDockForFullscreen
340 {
341     // NSApplicationPresentationOptions is available on > 10.6 only:
342 #if __MAC_OS_X_VERSION_MIN_REQUIRED >= 1060
343     NSApplicationPresentationOptions options = NSApplicationPresentationDefault;
344     NSScreen* fullscreenScreen = [[self window] screen];
345
346     if (!_isEndingFullscreen) {
347         // Auto-hide the menu bar if the fullscreenScreen contains the menu bar:
348         // NOTE: if the fullscreenScreen contains the menu bar but not the dock, we must still 
349         // auto-hide the dock, or an exception will be thrown.
350         if ([[NSScreen screens] objectAtIndex:0] == fullscreenScreen)
351             options |= (NSApplicationPresentationAutoHideMenuBar | NSApplicationPresentationAutoHideDock);
352         // Check if the current screen contains the dock by comparing the screen's frame to its
353         // visibleFrame; if a dock is present, the visibleFrame will differ.  If the current screen
354         // contains the dock, hide it.
355         else if (!NSEqualRects([fullscreenScreen frame], [fullscreenScreen visibleFrame]))
356             options |= NSApplicationPresentationAutoHideDock;
357     }
358
359     if ([NSApp respondsToSelector:@selector(setPresentationOptions:)])
360         [NSApp setPresentationOptions:options];
361     else
362 #endif
363         SetSystemUIMode(_isEndingFullscreen ? kUIModeNormal : kUIModeAllHidden, 0);
364 }
365
366 - (void)updatePowerAssertions
367 {
368     float rate = 0;
369     if (_mediaElement && _mediaElement->platformMedia().type == PlatformMedia::QTMovieType)
370         rate = [_mediaElement->platformMedia().media.qtMovie rate];
371     
372     if (rate && !_isEndingFullscreen) {
373         if (!_displaySleepDisabler)
374             _displaySleepDisabler = DisplaySleepDisabler::create("com.apple.WebCore - Fullscreen video");
375     } else
376         _displaySleepDisabler = nullptr;
377 }
378
379 // MARK: -
380 // MARK: Window callback
381
382 - (void)_requestExit
383 {
384     if (_mediaElement)
385         _mediaElement->exitFullscreen();
386     _forceDisableAnimation = NO;
387 }
388
389 - (void)requestExitFullscreenWithAnimation:(BOOL)animation
390 {
391     if (_isEndingFullscreen)
392         return;
393
394     _forceDisableAnimation = !animation;
395     [self performSelector:@selector(_requestExit) withObject:nil afterDelay:0];
396
397 }
398
399 - (void)requestExitFullscreen
400 {
401     [self requestExitFullscreenWithAnimation:YES];
402 }
403
404 - (void)fadeHUDIn
405 {
406     [_hudController fadeWindowIn];
407 }
408
409 // MARK: -
410 // MARK: QTMovie callbacks
411
412 - (void)rateChanged:(NSNotification *)unusedNotification
413 {
414     UNUSED_PARAM(unusedNotification);
415     [_hudController updateRate];
416     [self updatePowerAssertions];
417 }
418
419 @end
420
421 @implementation WebVideoFullscreenWindow
422
423 - (id)initWithContentRect:(NSRect)contentRect styleMask:(NSUInteger)aStyle backing:(NSBackingStoreType)bufferingType defer:(BOOL)flag
424 {
425     UNUSED_PARAM(aStyle);
426     self = [super initWithContentRect:contentRect styleMask:NSBorderlessWindowMask backing:bufferingType defer:flag];
427     if (!self)
428         return nil;
429     [self setOpaque:NO];
430     [self setBackgroundColor:[NSColor clearColor]];
431     [self setIgnoresMouseEvents:NO];
432     [self setAcceptsMouseMovedEvents:YES];
433     return self;
434 }
435
436 - (void)dealloc
437 {
438     ASSERT(!_fullscreenAnimation);
439     [super dealloc];
440 }
441
442 - (BOOL)resignFirstResponder
443 {
444     return NO;
445 }
446
447 - (BOOL)canBecomeKeyWindow
448 {
449     return NO;
450 }
451
452 - (void)mouseDown:(NSEvent *)theEvent
453 {
454     UNUSED_PARAM(theEvent);
455 }
456
457 - (void)cancelOperation:(id)sender
458 {
459     UNUSED_PARAM(sender);
460     [[self windowController] requestExitFullscreen];
461 }
462
463 - (void)animatedResizeDidEnd
464 {
465     // Call our windowController.
466     if (_controllerActionOnAnimationEnd)
467         [[self windowController] performSelector:_controllerActionOnAnimationEnd];
468     _controllerActionOnAnimationEnd = NULL;
469 }
470
471 //
472 // This function will animate a change of frame rectangle
473 // We support queuing animation, that means that we'll correctly
474 // interrupt the running animation, and queue the next one.
475 //
476 - (void)animateFromRect:(NSRect)startRect toRect:(NSRect)endRect withSubAnimation:(NSAnimation *)subAnimation controllerAction:(SEL)controllerAction
477 {
478     _controllerActionOnAnimationEnd = controllerAction;
479
480     BOOL wasAnimating = NO;
481     if (_fullscreenAnimation) {
482         wasAnimating = YES;
483
484         // Interrupt any running animation.
485         [_fullscreenAnimation stopAnimation];
486
487         // Save the current rect to ensure a smooth transition.
488         startRect = [_fullscreenAnimation currentFrame];
489         [_fullscreenAnimation release];
490         _fullscreenAnimation = nil;
491     }
492     
493     if (NSIsEmptyRect(startRect) || NSIsEmptyRect(endRect)) {
494         // Fakely end the subanimation.
495         [subAnimation setCurrentProgress:1];
496         // And remove the weak link to the window.
497         [subAnimation stopAnimation];
498
499         [self setFrame:endRect display:NO];
500         [self makeKeyAndOrderFront:self];
501         [self animatedResizeDidEnd];
502         return;
503     }
504
505     if (!wasAnimating) {
506         // We'll downscale the window during the animation based on the higher resolution rect
507         BOOL higherResolutionIsEndRect = startRect.size.width < endRect.size.width && startRect.size.height < endRect.size.height;
508         [self setFrame:higherResolutionIsEndRect ? endRect : startRect display:NO];        
509     }
510     
511     ASSERT(!_fullscreenAnimation);
512     _fullscreenAnimation = [[WebWindowScaleAnimation alloc] initWithHintedDuration:0.2 window:self initalFrame:startRect finalFrame:endRect];
513     [_fullscreenAnimation setSubAnimation:subAnimation];
514     [_fullscreenAnimation setDelegate:self];
515     
516     // Make sure the animation has scaled the window before showing it.
517     [_fullscreenAnimation setCurrentProgress:0];
518     [self makeKeyAndOrderFront:self];
519
520     [_fullscreenAnimation startAnimation];
521 }
522
523 - (void)animationDidEnd:(NSAnimation *)animation
524 {
525     if (![NSThread isMainThread]) {
526         [self performSelectorOnMainThread:@selector(animationDidEnd:) withObject:animation waitUntilDone:NO];
527         return;
528     }
529     if (animation != _fullscreenAnimation)
530         return;
531
532     // The animation is not really over and was interrupted
533     // Don't send completion events.
534     if ([animation currentProgress] < 1.0)
535         return;
536
537     // Ensure that animation (and subanimation) don't keep
538     // the weak reference to the window ivar that may be destroyed from
539     // now on.
540     [_fullscreenAnimation setWindow:nil];
541
542     [_fullscreenAnimation autorelease];
543     _fullscreenAnimation = nil;
544
545     [self animatedResizeDidEnd];
546 }
547
548 - (void)mouseMoved:(NSEvent *)theEvent
549 {
550     UNUSED_PARAM(theEvent);
551     [[self windowController] fadeHUDIn];
552 }
553
554 @end
555
556 #endif /* ENABLE(VIDEO) */