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