/* * Copyright (C) 2009, 2010, 2011 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(FULLSCREEN_API) #import "WKFullScreenWindowController.h" #import "LayerTreeContext.h" #import "WKAPICast.h" #import "WKViewInternal.h" #import "WebFullScreenManagerProxy.h" #import "WebPageProxy.h" #import <Carbon/Carbon.h> // For SetSystemUIMode() #import <IOKit/pwr_mgt/IOPMLib.h> // For IOPMAssertionCreate() #import <QuartzCore/QuartzCore.h> #import <WebCore/FloatRect.h> #import <WebCore/IntRect.h> #import <WebKitSystemInterface.h> static const NSTimeInterval tickleTimerInterval = 1.0; using namespace WebKit; using namespace WebCore; #if defined(BUILDING_ON_LEOPARD) @interface CATransaction(SnowLeopardConvenienceFunctions) + (void)setDisableActions:(BOOL)flag; + (void)setAnimationDuration:(CFTimeInterval)dur; @end @implementation CATransaction(SnowLeopardConvenienceFunctions) + (void)setDisableActions:(BOOL)flag { [self setValue:[NSNumber numberWithBool:flag] forKey:kCATransactionDisableActions]; } + (void)setAnimationDuration:(CFTimeInterval)dur { [self setValue:[NSNumber numberWithDouble:dur] forKey:kCATransactionAnimationDuration]; } @end #endif @interface WKFullScreenWindow : NSWindow { NSView* _animationView; CALayer* _backgroundLayer; } - (CALayer*)backgroundLayer; - (NSView*)animationView; @end @interface WKFullScreenWindowController(Private) - (void)_requestExitFullScreenWithAnimation:(BOOL)animation; - (void)_updateMenuAndDockForFullScreen; - (void)_updatePowerAssertions; - (WKFullScreenWindow *)_fullScreenWindow; - (CFTimeInterval)_animationDuration; - (void)_swapView:(NSView*)view with:(NSView*)otherView; - (WebFullScreenManagerProxy*)_manager; @end @interface NSWindow(IsOnActiveSpaceAdditionForTigerAndLeopard) - (BOOL)isOnActiveSpace; @end @implementation WKFullScreenWindowController #pragma mark - #pragma mark Initialization - (id)init { NSWindow *window = [[WKFullScreenWindow 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 { [self setWebView:nil]; [NSObject cancelPreviousPerformRequestsWithTarget:self]; [[NSNotificationCenter defaultCenter] removeObserver:self]; [super dealloc]; } - (void)windowDidLoad { [super windowDidLoad]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidResignActive:) name:NSApplicationDidResignActiveNotification object:NSApp]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidChangeScreenParameters:) name:NSApplicationDidChangeScreenParametersNotification object:NSApp]; } #pragma mark - #pragma mark Accessors - (WKView*)webView { return _webView; } - (void)setWebView:(WKView *)webView { [webView retain]; [_webView release]; _webView = webView; } #pragma mark - #pragma mark Notifications - (void)applicationDidResignActive:(NSNotification*)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]; } - (void)applicationDidChangeScreenParameters:(NSNotification*)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]; NSWindow* window = [self window]; [window setFrame:[[window screen] frame] display:YES]; } #pragma mark - #pragma mark Exposed Interface - (void)enterFullScreen:(NSScreen *)screen { if (_isFullScreen) return; _isFullScreen = YES; _isAnimating = YES; NSDisableScreenUpdates(); if (!screen) screen = [NSScreen mainScreen]; NSRect screenFrame = [screen frame]; NSRect webViewFrame = [_webView convertRectToBase:[_webView frame]]; webViewFrame.origin = [[_webView window] convertBaseToScreen:webViewFrame.origin]; // In the case of a multi-monitor setup where the webView straddles two // monitors, we must create a window large enough to contain the destination // frame and the initial frame. NSRect windowFrame = NSUnionRect(screenFrame, webViewFrame); [[self window] setFrame:windowFrame display:YES]; CALayer* backgroundLayer = [[self _fullScreenWindow] backgroundLayer]; NSRect backgroundFrame = {[[self window] convertScreenToBase:screenFrame.origin], screenFrame.size}; backgroundFrame = [[[self window] contentView] convertRectFromBase:backgroundFrame]; [CATransaction begin]; [CATransaction setDisableActions:YES]; [backgroundLayer setFrame:NSRectToCGRect(backgroundFrame)]; [CATransaction commit]; CFTimeInterval duration = [self _animationDuration]; [self _manager]->willEnterFullScreen(); [self _manager]->beginEnterFullScreenAnimation(duration); } - (void)beganEnterFullScreenAnimation { [self _updateMenuAndDockForFullScreen]; [self _updatePowerAssertions]; // In a previous incarnation, the NSWindow attached to this controller may have // been on a different screen. Temporarily change the collectionBehavior of the window: NSWindow* fullScreenWindow = [self window]; NSWindowCollectionBehavior behavior = [fullScreenWindow collectionBehavior]; [fullScreenWindow setCollectionBehavior:NSWindowCollectionBehaviorCanJoinAllSpaces]; [fullScreenWindow makeKeyAndOrderFront:self]; [fullScreenWindow setCollectionBehavior:behavior]; // Start the opacity animation. We can use implicit animations here because we don't care when // the animation finishes. [CATransaction begin]; [CATransaction setAnimationDuration:[self _animationDuration]]; [[[self _fullScreenWindow] backgroundLayer] setOpacity:1]; [CATransaction commit]; NSEnableScreenUpdates(); _isAnimating = YES; } - (void)finishedEnterFullScreenAnimation:(bool)completed { NSDisableScreenUpdates(); if (completed) { // Swap the webView placeholder into place. if (!_webViewPlaceholder) _webViewPlaceholder.adoptNS([[NSView alloc] init]); [self _swapView:_webView with:_webViewPlaceholder.get()]; // Then insert the WebView into the full screen window NSView* animationView = [[self _fullScreenWindow] animationView]; [animationView addSubview:_webView positioned:NSWindowBelow relativeTo:_layerHostingView.get()]; [_webView setFrame:[animationView bounds]]; // FIXME: In Barolo, orderIn will animate, which is not what we want. Find a way // to work around this behavior. //[[_webViewPlaceholder.get() window] orderOut:self]; [[self window] makeKeyAndOrderFront:self]; } [self _manager]->didEnterFullScreen(); NSEnableScreenUpdates(); _isAnimating = NO; } - (void)exitFullScreen { if (!_isFullScreen) return; _isFullScreen = NO; _isAnimating = YES; NSDisableScreenUpdates(); [self _manager]->willExitFullScreen(); [self _manager]->beginExitFullScreenAnimation([self _animationDuration]); } - (void)beganExitFullScreenAnimation { [self _updateMenuAndDockForFullScreen]; [self _updatePowerAssertions]; // The user may have moved the fullScreen window in Spaces, so temporarily change // the collectionBehavior of the webView's window: NSWindow* webWindow = [[self webView] window]; NSWindowCollectionBehavior behavior = [webWindow collectionBehavior]; [webWindow setCollectionBehavior:NSWindowCollectionBehaviorCanJoinAllSpaces]; [webWindow orderWindow:NSWindowBelow relativeTo:[[self window] windowNumber]]; [webWindow setCollectionBehavior:behavior]; // Swap the webView back into its original position: if ([_webView window] == [self window]) [self _swapView:_webViewPlaceholder.get() with:_webView]; [CATransaction begin]; [CATransaction setAnimationDuration:[self _animationDuration]]; [[[self _fullScreenWindow] backgroundLayer] setOpacity:0]; [CATransaction commit]; NSEnableScreenUpdates(); _isAnimating = YES; } - (void)finishedExitFullScreenAnimation:(bool)completed { NSDisableScreenUpdates(); if (completed) { [self _updateMenuAndDockForFullScreen]; [self _updatePowerAssertions]; [NSCursor setHiddenUntilMouseMoves:YES]; [[self window] orderOut:self]; [[_webView window] makeKeyAndOrderFront:self]; } [self _manager]->didExitFullScreen(); NSEnableScreenUpdates(); _isAnimating = NO; } - (void)enterAcceleratedCompositingMode:(const WebKit::LayerTreeContext&)layerTreeContext { if (_layerHostingView) return; ASSERT(!layerTreeContext.isEmpty()); // Create an NSView that will host our layer tree. _layerHostingView.adoptNS([[NSView alloc] initWithFrame:[[self window] frame]]); [_layerHostingView.get() setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; [CATransaction begin]; [CATransaction setDisableActions:YES]; WKFullScreenWindow* window = [self _fullScreenWindow]; [[window animationView] addSubview:_layerHostingView.get()]; // Create a root layer that will back the NSView. RetainPtr<CALayer> rootLayer(AdoptNS, [[CALayer alloc] init]); #ifndef NDEBUG [rootLayer.get() setName:@"Hosting root layer"]; #endif CALayer *renderLayer = WKMakeRenderLayer(layerTreeContext.contextID); [rootLayer.get() addSublayer:renderLayer]; [_layerHostingView.get() setLayer:rootLayer.get()]; [_layerHostingView.get() setWantsLayer:YES]; [[window backgroundLayer] setHidden:NO]; [CATransaction commit]; } - (void)exitAcceleratedCompositingMode { if (!_layerHostingView) return; [CATransaction begin]; [CATransaction setDisableActions:YES]; [_layerHostingView.get() removeFromSuperview]; [_layerHostingView.get() setLayer:nil]; [_layerHostingView.get() setWantsLayer:NO]; [[[self _fullScreenWindow] backgroundLayer] setHidden:YES]; [CATransaction commit]; _layerHostingView = 0; } - (WebCore::IntRect)getFullScreenRect { return enclosingIntRect([[self window] frame]); } #pragma mark - #pragma mark Internal Interface - (void)_updateMenuAndDockForFullScreen { // NSApplicationPresentationOptions is available on > 10.6 only: #if !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) NSApplicationPresentationOptions options = NSApplicationPresentationDefault; NSScreen* fullScreenScreen = [[self window] screen]; if (_isFullScreen) { // 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(_isFullScreen ? kUIModeNormal : kUIModeAllHidden, 0); } #if !defined(BUILDING_ON_TIGER) // IOPMAssertionCreateWithName not defined on < 10.5 - (void)_disableIdleDisplaySleep { if (_idleDisplaySleepAssertion == kIOPMNullAssertionID) #if defined(BUILDING_ON_LEOPARD) // IOPMAssertionCreateWithName is not defined in the 10.5 SDK IOPMAssertionCreate(kIOPMAssertionTypeNoDisplaySleep, kIOPMAssertionLevelOn, &_idleDisplaySleepAssertion); #else // IOPMAssertionCreate is depreciated in > 10.5 IOPMAssertionCreateWithName(kIOPMAssertionTypeNoDisplaySleep, kIOPMAssertionLevelOn, CFSTR("WebKit playing a video fullScreen."), &_idleDisplaySleepAssertion); #endif } - (void)_enableIdleDisplaySleep { if (_idleDisplaySleepAssertion != kIOPMNullAssertionID) { IOPMAssertionRelease(_idleDisplaySleepAssertion); _idleDisplaySleepAssertion = kIOPMNullAssertionID; } } - (void)_disableIdleSystemSleep { if (_idleSystemSleepAssertion == kIOPMNullAssertionID) #if defined(BUILDING_ON_LEOPARD) // IOPMAssertionCreateWithName is not defined in the 10.5 SDK IOPMAssertionCreate(kIOPMAssertionTypeNoIdleSleep, kIOPMAssertionLevelOn, &_idleSystemSleepAssertion); #else // IOPMAssertionCreate is depreciated in > 10.5 IOPMAssertionCreateWithName(kIOPMAssertionTypeNoIdleSleep, kIOPMAssertionLevelOn, CFSTR("WebKit playing a video fullScreen."), &_idleSystemSleepAssertion); #endif } - (void)_enableIdleSystemSleep { if (_idleSystemSleepAssertion != kIOPMNullAssertionID) { IOPMAssertionRelease(_idleSystemSleepAssertion); _idleSystemSleepAssertion = kIOPMNullAssertionID; } } - (void)_enableTickleTimer { [_tickleTimer invalidate]; [_tickleTimer release]; _tickleTimer = [[NSTimer scheduledTimerWithTimeInterval:tickleTimerInterval target:self selector:@selector(_tickleTimerFired) userInfo:nil repeats:YES] retain]; } - (void)_disableTickleTimer { [_tickleTimer invalidate]; [_tickleTimer release]; _tickleTimer = nil; } - (void)_tickleTimerFired { UpdateSystemActivity(OverallAct); } #endif - (void)_updatePowerAssertions { #if !defined(BUILDING_ON_TIGER) if (_isPlaying && _isFullScreen) { [self _disableIdleSystemSleep]; [self _disableIdleDisplaySleep]; [self _enableTickleTimer]; } else { [self _enableIdleSystemSleep]; [self _enableIdleDisplaySleep]; [self _disableTickleTimer]; } #endif } - (WebFullScreenManagerProxy*)_manager { WebPageProxy* webPage = toImpl([_webView pageRef]); if (!webPage) return 0; return webPage->fullScreenManager(); } - (void)_requestExit { [self exitFullScreen]; _forceDisableAnimation = NO; } - (void)_requestExitFullScreenWithAnimation:(BOOL)animation { _forceDisableAnimation = !animation; [self performSelector:@selector(_requestExit) withObject:nil afterDelay:0]; } - (void)_swapView:(NSView*)view with:(NSView*)otherView { [otherView setFrame:[view frame]]; [otherView setAutoresizingMask:[view autoresizingMask]]; [otherView removeFromSuperview]; [[view superview] replaceSubview:view with:otherView]; } #pragma mark - #pragma mark Utility Functions - (WKFullScreenWindow *)_fullScreenWindow { ASSERT([[self window] isKindOfClass:[WKFullScreenWindow class]]); return (WKFullScreenWindow *)[self window]; } - (CFTimeInterval)_animationDuration { static const CFTimeInterval defaultDuration = 0.5; CFTimeInterval duration = defaultDuration; #if !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) NSUInteger modifierFlags = [NSEvent modifierFlags]; #else NSUInteger modifierFlags = [[NSApp currentEvent] modifierFlags]; #endif if ((modifierFlags & NSControlKeyMask) == NSControlKeyMask) duration *= 2; if ((modifierFlags & NSShiftKeyMask) == NSShiftKeyMask) duration *= 10; if (_forceDisableAnimation) { // This will disable scale animation duration = 0; } return duration; } @end #pragma mark - @implementation WKFullScreenWindow - (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]; [self setReleasedWhenClosed:NO]; [self setHasShadow:YES]; #if !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) [self setMovable:NO]; #else [self setMovableByWindowBackground:NO]; #endif NSView* contentView = [self contentView]; _animationView = [[NSView alloc] initWithFrame:[contentView bounds]]; CALayer* contentLayer = [[CALayer alloc] init]; [_animationView setLayer:contentLayer]; [_animationView setWantsLayer:YES]; [_animationView setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable]; [contentView addSubview:_animationView]; _backgroundLayer = [[CALayer alloc] init]; [contentLayer addSublayer:_backgroundLayer]; [_backgroundLayer setBackgroundColor:CGColorGetConstantColor(kCGColorBlack)]; [_backgroundLayer setOpacity:0]; return self; } - (void)dealloc { [_animationView release]; [_backgroundLayer release]; [super dealloc]; } - (BOOL)canBecomeKeyWindow { return YES; } - (void)keyDown:(NSEvent *)theEvent { if ([[theEvent charactersIgnoringModifiers] isEqual:@"\e"]) // Esacpe key-code [self cancelOperation:self]; else [super keyDown:theEvent]; } - (void)cancelOperation:(id)sender { UNUSED_PARAM(sender); [[self windowController] _requestExitFullScreenWithAnimation:YES]; } - (CALayer*)backgroundLayer { return _backgroundLayer; } - (NSView*)animationView { return _animationView; } @end #endif