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 #if ENABLE(VIDEO) 27 28 #import "WebVideoFullscreenController.h" 29 30 #import "WebTypesInternal.h" 31 #import "WebVideoFullscreenHUDWindowController.h" 32 #import "WebWindowAnimation.h" 33 #import <QTKit/QTKit.h> 34 #import <WebCore/HTMLMediaElement.h> 35 #import <WebCore/SoftLinking.h> 36 #import <objc/objc-runtime.h> 37 #import <wtf/UnusedParam.h> 38 39 SOFT_LINK_FRAMEWORK(QTKit) 40 SOFT_LINK_CLASS(QTKit, QTMovieView) 41 42 SOFT_LINK_POINTER(QTKit, QTMovieRateDidChangeNotification, NSString *) 43 44 #define QTMovieRateDidChangeNotification getQTMovieRateDidChangeNotification() 45 46 @interface WebVideoFullscreenWindow : NSWindow 47 #if !defined(BUILDING_ON_LEOPARD) && !defined(BUILDING_ON_TIGER) 48 <NSAnimationDelegate> 49 #endif 50 { 51 SEL _controllerActionOnAnimationEnd; 52 WebWindowScaleAnimation *_fullscreenAnimation; // (retain) 53 } 54 - (void)animateFromRect:(NSRect)startRect toRect:(NSRect)endRect withSubAnimation:(NSAnimation *)subAnimation controllerAction:(SEL)controllerAction; 55 @end 56 57 @interface WebVideoFullscreenController(HUDWindowControllerDelegate) <WebVideoFullscreenHUDWindowControllerDelegate> 58 @end 59 60 @implementation WebVideoFullscreenController 61 - (id)init 62 { 63 // Do not defer window creation, to make sure -windowNumber is created (needed by WebWindowScaleAnimation). 64 NSWindow *window = [[WebVideoFullscreenWindow alloc] initWithContentRect:NSZeroRect styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO]; 65 self = [super initWithWindow:window]; 66 [window release]; 67 if (!self) 68 return nil; 69 [self windowDidLoad]; 70 return self; 71 72 } 73 - (void)dealloc 74 { 75 ASSERT(!_backgroundFullscreenWindow); 76 ASSERT(!_fadeAnimation); 77 [[NSNotificationCenter defaultCenter] removeObserver:self]; 78 [super dealloc]; 79 } 80 81 - (WebVideoFullscreenWindow *)fullscreenWindow 82 { 83 return (WebVideoFullscreenWindow *)[super window]; 84 } 85 86 - (void)windowDidLoad 87 { 88 WebVideoFullscreenWindow *window = [self fullscreenWindow]; 89 QTMovieView *view = [[getQTMovieViewClass() alloc] init]; 90 [view setFillColor:[NSColor clearColor]]; 91 [window setContentView:view]; 92 [view setControllerVisible:NO]; 93 [view setPreservesAspectRatio:YES]; 94 if (_mediaElement) 95 [view setMovie:_mediaElement->platformMedia().qtMovie]; 96 [window setHasShadow:YES]; // This is nicer with a shadow. 97 [window setLevel:NSPopUpMenuWindowLevel-1]; 98 [view release]; 99 } 100 101 - (WebCore::HTMLMediaElement*)mediaElement; 102 { 103 return _mediaElement.get(); 104 } 105 106 - (void)setMediaElement:(WebCore::HTMLMediaElement*)mediaElement; 107 { 108 _mediaElement = mediaElement; 109 if ([self isWindowLoaded]) { 110 QTMovieView *movieView = (QTMovieView *)[[self fullscreenWindow] contentView]; 111 QTMovie *movie = _mediaElement->platformMedia().qtMovie; 112 113 ASSERT(movieView && [movieView isKindOfClass:[getQTMovieViewClass() class]]); 114 ASSERT(movie); 115 [movieView setMovie:movie]; 116 [[NSNotificationCenter defaultCenter] addObserver:self 117 selector:@selector(rateChanged:) 118 name:QTMovieRateDidChangeNotification 119 object:movie]; 120 } 121 } 122 123 - (id <WebVideoFullscreenControllerDelegate>)delegate 124 { 125 return _delegate; 126 } 127 128 - (void)setDelegate:(id <WebVideoFullscreenControllerDelegate>)delegate; 129 { 130 _delegate = delegate; 131 } 132 133 - (CGFloat)clearFadeAnimation 134 { 135 [_fadeAnimation stopAnimation]; 136 CGFloat previousAlpha = [_fadeAnimation currentAlpha]; 137 [_fadeAnimation setWindow:nil]; 138 [_fadeAnimation release]; 139 _fadeAnimation = nil; 140 return previousAlpha; 141 } 142 143 - (void)windowDidExitFullscreen 144 { 145 [self clearFadeAnimation]; 146 [[self window] close]; 147 [self setWindow:nil]; 148 SetSystemUIMode(_savedUIMode, _savedUIOptions); 149 [_hudController setDelegate:nil]; 150 [_hudController release]; 151 _hudController = nil; 152 [_backgroundFullscreenWindow close]; 153 [_backgroundFullscreenWindow release]; 154 _backgroundFullscreenWindow = nil; 155 156 [self autorelease]; // Associated -retain is in -exitFullscreen. 157 _isEndingFullscreen = NO; 158 } 159 160 - (void)windowDidEnterFullscreen 161 { 162 [self clearFadeAnimation]; 163 164 ASSERT(!_hudController); 165 _hudController = [[WebVideoFullscreenHUDWindowController alloc] init]; 166 [_hudController setDelegate:self]; 167 168 GetSystemUIMode(&_savedUIMode, &_savedUIOptions); 169 SetSystemUIMode(kUIModeAllSuppressed , 0); 170 [NSCursor setHiddenUntilMouseMoves:YES]; 171 172 // Give the HUD keyboard focus initially 173 [_hudController fadeWindowIn]; 174 } 175 176 - (NSRect)mediaElementRect 177 { 178 return _mediaElement->screenRect(); 179 } 180 181 #pragma mark - 182 #pragma mark Exposed Interface 183 184 static void constrainFrameToRatioOfFrame(NSRect *frameToConstrain, const NSRect *frame) 185 { 186 // Keep a constrained aspect ratio for the destination window 187 double originalRatio = frame->size.width / frame->size.height; 188 double newRatio = frameToConstrain->size.width / frameToConstrain->size.height; 189 if (newRatio > originalRatio) { 190 double newWidth = originalRatio * frameToConstrain->size.height; 191 double diff = frameToConstrain->size.width - newWidth; 192 frameToConstrain->size.width = newWidth; 193 frameToConstrain->origin.x += diff / 2; 194 } else { 195 double newHeight = frameToConstrain->size.width / originalRatio; 196 double diff = frameToConstrain->size.height - newHeight; 197 frameToConstrain->size.height = newHeight; 198 frameToConstrain->origin.y += diff / 2; 199 } 200 } 201 202 static NSWindow *createBackgroundFullscreenWindow(NSRect frame, int level) 203 { 204 NSWindow *window = [[NSWindow alloc] initWithContentRect:frame styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO]; 205 [window setOpaque:YES]; 206 [window setBackgroundColor:[NSColor blackColor]]; 207 [window setLevel:level]; 208 [window setHidesOnDeactivate:YES]; 209 [window setReleasedWhenClosed:NO]; 210 return window; 211 } 212 213 - (void)setupFadeAnimationIfNeededAndFadeIn:(BOOL)fadeIn 214 { 215 CGFloat initialAlpha = fadeIn ? 0 : 1; 216 if (_fadeAnimation) { 217 // Make sure we support queuing animation if the previous one isn't over yet 218 initialAlpha = [self clearFadeAnimation]; 219 } 220 if (!_forceDisableAnimation) 221 _fadeAnimation = [[WebWindowFadeAnimation alloc] initWithDuration:0.2 window:_backgroundFullscreenWindow initialAlpha:initialAlpha finalAlpha:fadeIn ? 1 : 0]; 222 } 223 224 - (void)enterFullscreen:(NSScreen *)screen; 225 { 226 if (!screen) 227 screen = [NSScreen mainScreen]; 228 229 NSRect frame = [self mediaElementRect]; 230 NSRect endFrame = [screen frame]; 231 constrainFrameToRatioOfFrame(&endFrame, &frame); 232 233 // Create a black window if needed 234 if (!_backgroundFullscreenWindow) 235 _backgroundFullscreenWindow = createBackgroundFullscreenWindow([screen frame], [[self window] level]-1); 236 else 237 [_backgroundFullscreenWindow setFrame:[screen frame] display:NO]; 238 239 [self setupFadeAnimationIfNeededAndFadeIn:YES]; 240 if (_forceDisableAnimation) { 241 // This will disable scale animation 242 frame = NSZeroRect; 243 } 244 [[self fullscreenWindow] animateFromRect:frame toRect:endFrame withSubAnimation:_fadeAnimation controllerAction:@selector(windowDidEnterFullscreen)]; 245 246 [_backgroundFullscreenWindow orderWindow:NSWindowBelow relativeTo:[[self fullscreenWindow] windowNumber]]; 247 } 248 249 - (void)exitFullscreen 250 { 251 if (_isEndingFullscreen) 252 return; 253 _isEndingFullscreen = YES; 254 [_hudController closeWindow]; 255 256 NSRect endFrame = [self mediaElementRect]; 257 258 [self setupFadeAnimationIfNeededAndFadeIn:NO]; 259 if (_forceDisableAnimation) { 260 // This will disable scale animation 261 endFrame = NSZeroRect; 262 } 263 264 // We have to retain ourselves because we want to be alive for the end of the animation. 265 // If our owner releases us we could crash if this is not the case. 266 // Balanced in windowDidExitFullscreen 267 [self retain]; 268 269 [[self fullscreenWindow] animateFromRect:[[self window] frame] toRect:endFrame withSubAnimation:_fadeAnimation controllerAction:@selector(windowDidExitFullscreen)]; 270 } 271 272 #pragma mark - 273 #pragma mark Window callback 274 275 - (void)_requestExit 276 { 277 if (_mediaElement) 278 _mediaElement->exitFullscreen(); 279 _forceDisableAnimation = NO; 280 } 281 282 - (void)requestExitFullscreenWithAnimation:(BOOL)animation 283 { 284 if (_isEndingFullscreen) 285 return; 286 287 _forceDisableAnimation = !animation; 288 [self performSelector:@selector(_requestExit) withObject:nil afterDelay:0]; 289 290 } 291 292 - (void)requestExitFullscreen 293 { 294 [self requestExitFullscreenWithAnimation:YES]; 295 } 296 297 - (void)fadeHUDIn 298 { 299 [_hudController fadeWindowIn]; 300 } 301 302 #pragma mark - 303 #pragma mark QTMovie callbacks 304 305 - (void)rateChanged:(NSNotification *)unusedNotification 306 { 307 UNUSED_PARAM(unusedNotification); 308 [_hudController updateRate]; 309 } 310 311 @end 312 313 @implementation WebVideoFullscreenWindow 314 315 - (id)initWithContentRect:(NSRect)contentRect styleMask:(NSUInteger)aStyle backing:(NSBackingStoreType)bufferingType defer:(BOOL)flag 316 { 317 UNUSED_PARAM(aStyle); 318 self = [super initWithContentRect:contentRect styleMask:NSBorderlessWindowMask backing:bufferingType defer:flag]; 319 if (!self) 320 return nil; 321 [self setOpaque:NO]; 322 [self setBackgroundColor:[NSColor clearColor]]; 323 [self setHidesOnDeactivate:YES]; 324 [self setIgnoresMouseEvents:NO]; 325 [self setAcceptsMouseMovedEvents:YES]; 326 return self; 327 } 328 329 - (void)dealloc 330 { 331 ASSERT(!_fullscreenAnimation); 332 [super dealloc]; 333 } 334 335 - (BOOL)resignFirstResponder 336 { 337 return NO; 338 } 339 340 - (BOOL)canBecomeKeyWindow 341 { 342 return NO; 343 } 344 345 - (void)mouseDown:(NSEvent *)theEvent 346 { 347 UNUSED_PARAM(theEvent); 348 } 349 350 - (void)cancelOperation:(id)sender 351 { 352 UNUSED_PARAM(sender); 353 [[self windowController] requestExitFullscreen]; 354 } 355 356 - (void)animatedResizeDidEnd 357 { 358 // Call our windowController. 359 if (_controllerActionOnAnimationEnd) 360 [[self windowController] performSelector:_controllerActionOnAnimationEnd]; 361 _controllerActionOnAnimationEnd = NULL; 362 } 363 364 // 365 // This function will animate a change of frame rectangle 366 // We support queuing animation, that means that we'll correctly 367 // interrupt the running animation, and queue the next one. 368 // 369 - (void)animateFromRect:(NSRect)startRect toRect:(NSRect)endRect withSubAnimation:(NSAnimation *)subAnimation controllerAction:(SEL)controllerAction 370 { 371 _controllerActionOnAnimationEnd = controllerAction; 372 373 BOOL wasAnimating = NO; 374 if (_fullscreenAnimation) { 375 wasAnimating = YES; 376 377 // Interrupt any running animation. 378 [_fullscreenAnimation stopAnimation]; 379 380 // Save the current rect to ensure a smooth transition. 381 startRect = [_fullscreenAnimation currentFrame]; 382 [_fullscreenAnimation release]; 383 _fullscreenAnimation = nil; 384 } 385 386 if (NSIsEmptyRect(startRect) || NSIsEmptyRect(endRect)) { 387 // Fakely end the subanimation. 388 [subAnimation setCurrentProgress:1.0]; 389 // And remove the weak link to the window. 390 [subAnimation stopAnimation]; 391 392 [self setFrame:endRect display:NO]; 393 [self makeKeyAndOrderFront:self]; 394 [self animatedResizeDidEnd]; 395 return; 396 } 397 398 if (!wasAnimating) { 399 // We'll downscale the window during the animation based on the higher resolution rect 400 BOOL higherResolutionIsEndRect = startRect.size.width < endRect.size.width && startRect.size.height < endRect.size.height; 401 [self setFrame:higherResolutionIsEndRect ? endRect : startRect display:NO]; 402 } 403 404 ASSERT(!_fullscreenAnimation); 405 _fullscreenAnimation = [[WebWindowScaleAnimation alloc] initWithHintedDuration:0.2 window:self initalFrame:startRect finalFrame:endRect]; 406 [_fullscreenAnimation setSubAnimation:subAnimation]; 407 [_fullscreenAnimation setDelegate:self]; 408 409 // Make sure the animation has scaled the window before showing it. 410 [_fullscreenAnimation setCurrentProgress:0]; 411 [self makeKeyAndOrderFront:self]; 412 413 [_fullscreenAnimation startAnimation]; 414 } 415 416 - (void)animationDidEnd:(NSAnimation *)animation 417 { 418 #if !defined(BUILDING_ON_TIGER) // Animations are never threaded on Tiger. 419 if (![NSThread isMainThread]) { 420 [self performSelectorOnMainThread:@selector(animationDidEnd:) withObject:animation waitUntilDone:NO]; 421 return; 422 } 423 #endif 424 if (animation != _fullscreenAnimation) 425 return; 426 427 // The animation is not really over and was interrupted 428 // Don't send completion events. 429 if ([animation currentProgress] < 1.0) 430 return; 431 432 // Ensure that animation (and subanimation) don't keep 433 // the weak reference to the window ivar that may be destroyed from 434 // now on. 435 [_fullscreenAnimation setWindow:nil]; 436 437 [_fullscreenAnimation autorelease]; 438 _fullscreenAnimation = nil; 439 440 [self animatedResizeDidEnd]; 441 } 442 443 - (void)mouseMoved:(NSEvent *)theEvent 444 { 445 [[self windowController] fadeHUDIn]; 446 } 447 448 - (void)resignKeyWindow 449 { 450 [super resignKeyWindow]; 451 [[self windowController] requestExitFullscreenWithAnimation:NO]; 452 } 453 454 @end 455 456 #endif /* ENABLE(VIDEO) */ 457