WebVideoFullscreenController.mm   [plain text]


/*
 * Copyright (C) 2009 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

#import "config.h"

#if ENABLE(VIDEO)

#import "WebVideoFullscreenController.h"

#import "WebVideoFullscreenHUDWindowController.h"
#import "WebWindowAnimation.h"
#import <Carbon/Carbon.h>
#import <QTKit/QTKit.h>
#import <WebCore/DisplaySleepDisabler.h>
#import <WebCore/HTMLMediaElement.h>
#import <WebCore/SoftLinking.h>
#import <objc/objc-runtime.h>
#import <wtf/UnusedParam.h>

#if USE(GSTREAMER)
#import <WebCore/GStreamerGWorld.h>
#endif

using namespace WebCore;

SOFT_LINK_FRAMEWORK(QTKit)
SOFT_LINK_CLASS(QTKit, QTMovieLayer)

SOFT_LINK_POINTER(QTKit, QTMovieRateDidChangeNotification, NSString *)

#define QTMovieRateDidChangeNotification getQTMovieRateDidChangeNotification()

@interface WebVideoFullscreenWindow : NSWindow
#ifndef BUILDING_ON_LEOPARD
<NSAnimationDelegate>
#endif
{
    SEL _controllerActionOnAnimationEnd;
    WebWindowScaleAnimation *_fullscreenAnimation; // (retain)
}
- (void)animateFromRect:(NSRect)startRect toRect:(NSRect)endRect withSubAnimation:(NSAnimation *)subAnimation controllerAction:(SEL)controllerAction;
@end

@interface WebVideoFullscreenController(HUDWindowControllerDelegate) <WebVideoFullscreenHUDWindowControllerDelegate>
- (void)requestExitFullscreenWithAnimation:(BOOL)animation;
- (void)updateMenuAndDockForFullscreen;
- (void)updatePowerAssertions;
@end

@interface NSWindow(IsOnActiveSpaceAdditionForTigerAndLeopard)
- (BOOL)isOnActiveSpace;
@end

@implementation WebVideoFullscreenController
- (id)init
{
    // Do not defer window creation, to make sure -windowNumber is created (needed by WebWindowScaleAnimation).
    NSWindow *window = [[WebVideoFullscreenWindow alloc] initWithContentRect:NSZeroRect styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO];
    self = [super initWithWindow:window];
    [window release];
    if (!self)
        return nil;
    [self windowDidLoad];
    return self;
    
}
- (void)dealloc
{
    ASSERT(!_backgroundFullscreenWindow);
    ASSERT(!_fadeAnimation);
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    [super dealloc];
}

- (WebVideoFullscreenWindow *)fullscreenWindow
{
    return (WebVideoFullscreenWindow *)[super window];
}

- (void)setupVideoOverlay:(QTMovieLayer*)layer
{
    WebVideoFullscreenWindow *window = [self fullscreenWindow];
#if USE(GSTREAMER)
    if (_mediaElement && _mediaElement->platformMedia().type == PlatformMedia::GStreamerGWorldType) {
        GStreamerGWorld* gstGworld = _mediaElement->platformMedia().media.gstreamerGWorld;
        if (gstGworld->enterFullscreen())
            [window setContentView:gstGworld->platformVideoWindow()->window()];
    }
#else
    [[window contentView] setLayer:layer];
    [[window contentView] setWantsLayer:YES];
    if (_mediaElement && _mediaElement->platformMedia().type == PlatformMedia::QTMovieType)
        [layer setMovie:_mediaElement->platformMedia().media.qtMovie];
#endif
}

- (void)windowDidLoad
{
    WebVideoFullscreenWindow *window = [self fullscreenWindow];
    [window setHasShadow:YES]; // This is nicer with a shadow.
    [window setLevel:NSPopUpMenuWindowLevel-1];

    QTMovieLayer *layer = [[getQTMovieLayerClass() alloc] init];
    [self setupVideoOverlay:layer];
    [layer release];

#if !USE(GSTREAMER)
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidResignActive:) name:NSApplicationDidResignActiveNotification object:NSApp];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidChangeScreenParameters:) name:NSApplicationDidChangeScreenParametersNotification object:NSApp];
#endif
}

- (HTMLMediaElement*)mediaElement
{
    return _mediaElement.get();
}

- (void)setMediaElement:(HTMLMediaElement*)mediaElement
{
    _mediaElement = mediaElement;
    if ([self isWindowLoaded]) {
        QTMovieLayer *movieLayer = (QTMovieLayer *)[[[self fullscreenWindow] contentView] layer];

        ASSERT(movieLayer && [movieLayer isKindOfClass:[getQTMovieLayerClass() class]]);
        [self setupVideoOverlay:movieLayer];
#if !USE(GSTREAMER)
        ASSERT([movieLayer movie]);
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(rateChanged:) 
                                                     name:QTMovieRateDidChangeNotification 
                                                   object:[movieLayer movie]];
#endif
    }
}

- (id <WebVideoFullscreenControllerDelegate>)delegate
{
    return _delegate;
}

- (void)setDelegate:(id <WebVideoFullscreenControllerDelegate>)delegate
{
    _delegate = delegate;
}

- (CGFloat)clearFadeAnimation
{
    [_fadeAnimation stopAnimation];
    CGFloat previousAlpha = [_fadeAnimation currentAlpha];
    [_fadeAnimation setWindow:nil];
    [_fadeAnimation release];
    _fadeAnimation = nil;
    return previousAlpha;
}

- (void)windowDidExitFullscreen
{
#if USE(GSTREAMER)
    if (_mediaElement && _mediaElement->platformMedia().type == PlatformMedia::GStreamerGWorldType)
        _mediaElement->platformMedia().media.gstreamerGWorld->exitFullscreen();
#endif
    [self clearFadeAnimation];
    [[self window] close];
    [self setWindow:nil];
    [self updateMenuAndDockForFullscreen];   
    [self updatePowerAssertions];
    [_hudController setDelegate:nil];
    [_hudController release];
    _hudController = nil;
    [_backgroundFullscreenWindow close];
    [_backgroundFullscreenWindow release];
    _backgroundFullscreenWindow = nil;
    
    [self autorelease]; // Associated -retain is in -exitFullscreen.
    _isEndingFullscreen = NO;
}

- (void)windowDidEnterFullscreen
{
    [self clearFadeAnimation];

    ASSERT(!_hudController);
    _hudController = [[WebVideoFullscreenHUDWindowController alloc] init];
    [_hudController setDelegate:self];

    [self updateMenuAndDockForFullscreen];
    [self updatePowerAssertions];
    [NSCursor setHiddenUntilMouseMoves:YES];
    
    // Give the HUD keyboard focus initially
    [_hudController fadeWindowIn];
}

- (NSRect)mediaElementRect
{
    return _mediaElement->screenRect();
}

- (void)applicationDidResignActive:(NSNotification*)notification
{   
    UNUSED_PARAM(notification);
    // Check to see if the fullscreenWindow is on the active space; this function is available
    // on 10.6 and later, so default to YES if the function is not available:
    NSWindow* fullscreenWindow = [self fullscreenWindow];
    BOOL isOnActiveSpace = ([fullscreenWindow respondsToSelector:@selector(isOnActiveSpace)] ? [fullscreenWindow isOnActiveSpace] : YES);

    // Replicate the QuickTime Player (X) behavior when losing active application status:
    // Is the fullscreen screen the main screen? (Note: this covers the case where only a 
    // single screen is available.)  Is the fullscreen screen on the current space? IFF so, 
    // then exit fullscreen mode.    
    if ([fullscreenWindow screen] == [[NSScreen screens] objectAtIndex:0] && isOnActiveSpace)
         [self requestExitFullscreenWithAnimation:NO];
}
         
         
// MARK: -
// MARK: Exposed Interface

static void constrainFrameToRatioOfFrame(NSRect *frameToConstrain, const NSRect *frame)
{
    // Keep a constrained aspect ratio for the destination window
    CGFloat originalRatio = frame->size.width / frame->size.height;
    CGFloat newRatio = frameToConstrain->size.width / frameToConstrain->size.height;
    if (newRatio > originalRatio) {
        CGFloat newWidth = originalRatio * frameToConstrain->size.height;
        CGFloat diff = frameToConstrain->size.width - newWidth;
        frameToConstrain->size.width = newWidth;
        frameToConstrain->origin.x += diff / 2;
    } else {
        CGFloat newHeight = frameToConstrain->size.width / originalRatio;
        CGFloat diff = frameToConstrain->size.height - newHeight;
        frameToConstrain->size.height = newHeight;
        frameToConstrain->origin.y += diff / 2;
    }    
}

static NSWindow *createBackgroundFullscreenWindow(NSRect frame, int level)
{
    NSWindow *window = [[NSWindow alloc] initWithContentRect:frame styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO];
    [window setOpaque:YES];
    [window setBackgroundColor:[NSColor blackColor]];
    [window setLevel:level];
    [window setReleasedWhenClosed:NO];
    return window;
}

- (void)setupFadeAnimationIfNeededAndFadeIn:(BOOL)fadeIn
{
    CGFloat initialAlpha = fadeIn ? 0 : 1;
    if (_fadeAnimation) {
        // Make sure we support queuing animation if the previous one isn't over yet
        initialAlpha = [self clearFadeAnimation];
    }
    if (!_forceDisableAnimation)
        _fadeAnimation = [[WebWindowFadeAnimation alloc] initWithDuration:0.2 window:_backgroundFullscreenWindow initialAlpha:initialAlpha finalAlpha:fadeIn ? 1 : 0];
}

- (void)enterFullscreen:(NSScreen *)screen
{
    if (!screen)
        screen = [NSScreen mainScreen];

    NSRect frame = [self mediaElementRect];
    NSRect endFrame = [screen frame];
    constrainFrameToRatioOfFrame(&endFrame, &frame);

    // Create a black window if needed
    if (!_backgroundFullscreenWindow)
        _backgroundFullscreenWindow = createBackgroundFullscreenWindow([screen frame], [[self window] level]-1);
    else
        [_backgroundFullscreenWindow setFrame:[screen frame] display:NO];

    [self setupFadeAnimationIfNeededAndFadeIn:YES];
    if (_forceDisableAnimation) {
        // This will disable scale animation
        frame = NSZeroRect;
    }
    [[self fullscreenWindow] animateFromRect:frame toRect:endFrame withSubAnimation:_fadeAnimation controllerAction:@selector(windowDidEnterFullscreen)];

    [_backgroundFullscreenWindow orderWindow:NSWindowBelow relativeTo:[[self fullscreenWindow] windowNumber]];
}

- (void)exitFullscreen
{
    if (_isEndingFullscreen)
        return;
    _isEndingFullscreen = YES;
    [_hudController closeWindow];

    NSRect endFrame = [self mediaElementRect];

    [self setupFadeAnimationIfNeededAndFadeIn:NO];
    if (_forceDisableAnimation) {
        // This will disable scale animation
        endFrame = NSZeroRect;
    }
    
    // We have to retain ourselves because we want to be alive for the end of the animation.
    // If our owner releases us we could crash if this is not the case.
    // Balanced in windowDidExitFullscreen
    [self retain];    
    
    [[self fullscreenWindow] animateFromRect:[[self window] frame] toRect:endFrame withSubAnimation:_fadeAnimation controllerAction:@selector(windowDidExitFullscreen)];
}

- (void)applicationDidChangeScreenParameters:(NSNotification*)notification
{
    UNUSED_PARAM(notification);
    // The user may have changed the main screen by moving the menu bar, or they may have changed
    // the Dock's size or location, or they may have changed the fullscreen screen's dimensions.  
    // Update our presentation parameters, and ensure that the full screen window occupies the 
    // entire screen:
    [self updateMenuAndDockForFullscreen];
    [[self window] setFrame:[[[self window] screen] frame] display:YES];
}

- (void)updateMenuAndDockForFullscreen
{
    // NSApplicationPresentationOptions is available on > 10.6 only:
#ifndef BUILDING_ON_LEOPARD
    NSApplicationPresentationOptions options = NSApplicationPresentationDefault;
    NSScreen* fullscreenScreen = [[self window] screen];

    if (!_isEndingFullscreen) {
        // Auto-hide the menu bar if the fullscreenScreen contains the menu bar:
        // NOTE: if the fullscreenScreen contains the menu bar but not the dock, we must still 
        // auto-hide the dock, or an exception will be thrown.
        if ([[NSScreen screens] objectAtIndex:0] == fullscreenScreen)
            options |= (NSApplicationPresentationAutoHideMenuBar | NSApplicationPresentationAutoHideDock);
        // Check if the current screen contains the dock by comparing the screen's frame to its
        // visibleFrame; if a dock is present, the visibleFrame will differ.  If the current screen
        // contains the dock, hide it.
        else if (!NSEqualRects([fullscreenScreen frame], [fullscreenScreen visibleFrame]))
            options |= NSApplicationPresentationAutoHideDock;
    }

    if ([NSApp respondsToSelector:@selector(setPresentationOptions:)])
        [NSApp setPresentationOptions:options];
    else
#endif
        SetSystemUIMode(_isEndingFullscreen ? kUIModeNormal : kUIModeAllHidden, 0);
}

- (void)updatePowerAssertions
{
    float rate = 0;
    if (_mediaElement && _mediaElement->platformMedia().type == PlatformMedia::QTMovieType)
        rate = [_mediaElement->platformMedia().media.qtMovie rate];
    
    if (rate && !_isEndingFullscreen) {
        if (!_displaySleepDisabler)
            _displaySleepDisabler = DisplaySleepDisabler::create("com.apple.WebCore - Fullscreen video");
    } else
        _displaySleepDisabler = nullptr;
}

// MARK: -
// MARK: Window callback

- (void)_requestExit
{
    if (_mediaElement)
        _mediaElement->exitFullscreen();
    _forceDisableAnimation = NO;
}

- (void)requestExitFullscreenWithAnimation:(BOOL)animation
{
    if (_isEndingFullscreen)
        return;

    _forceDisableAnimation = !animation;
    [self performSelector:@selector(_requestExit) withObject:nil afterDelay:0];

}

- (void)requestExitFullscreen
{
    [self requestExitFullscreenWithAnimation:YES];
}

- (void)fadeHUDIn
{
    [_hudController fadeWindowIn];
}

// MARK: -
// MARK: QTMovie callbacks

- (void)rateChanged:(NSNotification *)unusedNotification
{
    UNUSED_PARAM(unusedNotification);
    [_hudController updateRate];
    [self updatePowerAssertions];
}

@end

@implementation WebVideoFullscreenWindow

- (id)initWithContentRect:(NSRect)contentRect styleMask:(NSUInteger)aStyle backing:(NSBackingStoreType)bufferingType defer:(BOOL)flag
{
    UNUSED_PARAM(aStyle);
    self = [super initWithContentRect:contentRect styleMask:NSBorderlessWindowMask backing:bufferingType defer:flag];
    if (!self)
        return nil;
    [self setOpaque:NO];
    [self setBackgroundColor:[NSColor clearColor]];
    [self setIgnoresMouseEvents:NO];
    [self setAcceptsMouseMovedEvents:YES];
    return self;
}

- (void)dealloc
{
    ASSERT(!_fullscreenAnimation);
    [super dealloc];
}

- (BOOL)resignFirstResponder
{
    return NO;
}

- (BOOL)canBecomeKeyWindow
{
    return NO;
}

- (void)mouseDown:(NSEvent *)theEvent
{
    UNUSED_PARAM(theEvent);
}

- (void)cancelOperation:(id)sender
{
    UNUSED_PARAM(sender);
    [[self windowController] requestExitFullscreen];
}

- (void)animatedResizeDidEnd
{
    // Call our windowController.
    if (_controllerActionOnAnimationEnd)
        [[self windowController] performSelector:_controllerActionOnAnimationEnd];
    _controllerActionOnAnimationEnd = NULL;
}

//
// This function will animate a change of frame rectangle
// We support queuing animation, that means that we'll correctly
// interrupt the running animation, and queue the next one.
//
- (void)animateFromRect:(NSRect)startRect toRect:(NSRect)endRect withSubAnimation:(NSAnimation *)subAnimation controllerAction:(SEL)controllerAction
{
    _controllerActionOnAnimationEnd = controllerAction;

    BOOL wasAnimating = NO;
    if (_fullscreenAnimation) {
        wasAnimating = YES;

        // Interrupt any running animation.
        [_fullscreenAnimation stopAnimation];

        // Save the current rect to ensure a smooth transition.
        startRect = [_fullscreenAnimation currentFrame];
        [_fullscreenAnimation release];
        _fullscreenAnimation = nil;
    }
    
    if (NSIsEmptyRect(startRect) || NSIsEmptyRect(endRect)) {
        // Fakely end the subanimation.
        [subAnimation setCurrentProgress:1];
        // And remove the weak link to the window.
        [subAnimation stopAnimation];

        [self setFrame:endRect display:NO];
        [self makeKeyAndOrderFront:self];
        [self animatedResizeDidEnd];
        return;
    }

    if (!wasAnimating) {
        // We'll downscale the window during the animation based on the higher resolution rect
        BOOL higherResolutionIsEndRect = startRect.size.width < endRect.size.width && startRect.size.height < endRect.size.height;
        [self setFrame:higherResolutionIsEndRect ? endRect : startRect display:NO];        
    }
    
    ASSERT(!_fullscreenAnimation);
    _fullscreenAnimation = [[WebWindowScaleAnimation alloc] initWithHintedDuration:0.2 window:self initalFrame:startRect finalFrame:endRect];
    [_fullscreenAnimation setSubAnimation:subAnimation];
    [_fullscreenAnimation setDelegate:self];
    
    // Make sure the animation has scaled the window before showing it.
    [_fullscreenAnimation setCurrentProgress:0];
    [self makeKeyAndOrderFront:self];

    [_fullscreenAnimation startAnimation];
}

- (void)animationDidEnd:(NSAnimation *)animation
{
    if (![NSThread isMainThread]) {
        [self performSelectorOnMainThread:@selector(animationDidEnd:) withObject:animation waitUntilDone:NO];
        return;
    }
    if (animation != _fullscreenAnimation)
        return;

    // The animation is not really over and was interrupted
    // Don't send completion events.
    if ([animation currentProgress] < 1.0)
        return;

    // Ensure that animation (and subanimation) don't keep
    // the weak reference to the window ivar that may be destroyed from
    // now on.
    [_fullscreenAnimation setWindow:nil];

    [_fullscreenAnimation autorelease];
    _fullscreenAnimation = nil;

    [self animatedResizeDidEnd];
}

- (void)mouseMoved:(NSEvent *)theEvent
{
    UNUSED_PARAM(theEvent);
    [[self windowController] fadeHUDIn];
}

@end

#endif /* ENABLE(VIDEO) */