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'' AND ANY 14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 * DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY 17 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 20 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 */ 24 25#import "config.h" 26 27#if ENABLE(VIDEO) 28 29#import "WebVideoFullscreenHUDWindowController.h" 30 31#import "ExceptionCodePlaceholder.h" 32#import "FloatConversion.h" 33#import <WebCoreSystemInterface.h> 34#import <WebCore/HTMLMediaElement.h> 35#import <wtf/RetainPtr.h> 36 37using namespace WebCore; 38using namespace std; 39 40static inline CGFloat webkit_CGFloor(CGFloat value) 41{ 42 if (sizeof(value) == sizeof(float)) 43 return floorf(value); 44 return floor(value); 45} 46 47@interface WebVideoFullscreenHUDWindowController (Private) <NSWindowDelegate> 48 49- (void)updateTime; 50- (void)timelinePositionChanged:(id)sender; 51- (float)currentTime; 52- (void)setCurrentTime:(float)currentTime; 53- (double)duration; 54 55- (void)volumeChanged:(id)sender; 56- (float)maxVolume; 57- (float)volume; 58- (void)setVolume:(float)volume; 59- (void)decrementVolume; 60- (void)incrementVolume; 61 62- (void)updatePlayButton; 63- (void)togglePlaying:(id)sender; 64- (BOOL)playing; 65- (void)setPlaying:(BOOL)playing; 66 67- (void)rewind:(id)sender; 68- (void)fastForward:(id)sender; 69 70- (NSString *)remainingTimeText; 71- (NSString *)elapsedTimeText; 72 73- (void)exitFullscreen:(id)sender; 74@end 75 76@interface WebVideoFullscreenHUDWindow : NSWindow 77@end 78 79@implementation WebVideoFullscreenHUDWindow 80 81- (id)initWithContentRect:(NSRect)contentRect styleMask:(NSUInteger)aStyle backing:(NSBackingStoreType)bufferingType defer:(BOOL)flag 82{ 83 UNUSED_PARAM(aStyle); 84 self = [super initWithContentRect:contentRect styleMask:NSBorderlessWindowMask backing:bufferingType defer:flag]; 85 if (!self) 86 return nil; 87 88 [self setOpaque:NO]; 89 [self setBackgroundColor:[NSColor clearColor]]; 90 [self setLevel:NSPopUpMenuWindowLevel]; 91 [self setAcceptsMouseMovedEvents:YES]; 92 [self setIgnoresMouseEvents:NO]; 93 [self setMovableByWindowBackground:YES]; 94 95 return self; 96} 97 98- (BOOL)canBecomeKeyWindow 99{ 100 return YES; 101} 102 103- (void)cancelOperation:(id)sender 104{ 105 UNUSED_PARAM(sender); 106 [[self windowController] exitFullscreen:self]; 107} 108 109- (void)center 110{ 111 NSRect hudFrame = [self frame]; 112 NSRect screenFrame = [[NSScreen mainScreen] frame]; 113 [self setFrameTopLeftPoint:NSMakePoint(screenFrame.origin.x + (screenFrame.size.width - hudFrame.size.width) / 2, 114 screenFrame.origin.y + (screenFrame.size.height - hudFrame.size.height) / 6)]; 115} 116 117- (void)keyDown:(NSEvent *)event 118{ 119 [super keyDown:event]; 120 [[self windowController] fadeWindowIn]; 121} 122 123- (BOOL)resignFirstResponder 124{ 125 return NO; 126} 127 128- (BOOL)performKeyEquivalent:(NSEvent *)event 129{ 130 // Block all command key events while the fullscreen window is up. 131 if ([event type] != NSKeyDown) 132 return NO; 133 134 if (!([event modifierFlags] & NSCommandKeyMask)) 135 return NO; 136 137 return YES; 138} 139 140@end 141 142static const CGFloat windowHeight = 59; 143static const CGFloat windowWidth = 438; 144 145static const NSTimeInterval HUDWindowFadeOutDelay = 3; 146 147@implementation WebVideoFullscreenHUDWindowController 148 149- (id)init 150{ 151 NSWindow *window = [[WebVideoFullscreenHUDWindow alloc] initWithContentRect:NSMakeRect(0, 0, windowWidth, windowHeight) 152 styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO]; 153 self = [super initWithWindow:window]; 154 [window setDelegate:self]; 155 [window release]; 156 if (!self) 157 return nil; 158 [self windowDidLoad]; 159 return self; 160} 161 162- (void)dealloc 163{ 164 ASSERT(!_timelineUpdateTimer); 165 ASSERT(!_area); 166 ASSERT(!_isScrubbing); 167 [_timeline release]; 168 [_remainingTimeText release]; 169 [_elapsedTimeText release]; 170 [_volumeSlider release]; 171 [_playButton release]; 172 [super dealloc]; 173} 174 175- (void)setArea:(NSTrackingArea *)area 176{ 177 if (area == _area) 178 return; 179 [_area release]; 180 _area = [area retain]; 181} 182 183- (void)keyDown:(NSEvent *)event 184{ 185 NSString *charactersIgnoringModifiers = [event charactersIgnoringModifiers]; 186 if ([charactersIgnoringModifiers length] == 1) { 187 switch ([charactersIgnoringModifiers characterAtIndex:0]) { 188 case ' ': 189 [self togglePlaying:nil]; 190 return; 191 case NSUpArrowFunctionKey: 192 if ([event modifierFlags] & NSAlternateKeyMask) 193 [self setVolume:[self maxVolume]]; 194 else 195 [self incrementVolume]; 196 return; 197 case NSDownArrowFunctionKey: 198 if ([event modifierFlags] & NSAlternateKeyMask) 199 [self setVolume:0]; 200 else 201 [self decrementVolume]; 202 return; 203 default: 204 break; 205 } 206 } 207 208 [super keyDown:event]; 209} 210 211- (id <WebVideoFullscreenHUDWindowControllerDelegate>)delegate 212{ 213 return _delegate; 214} 215 216- (void)setDelegate:(id <WebVideoFullscreenHUDWindowControllerDelegate>)delegate 217{ 218 _delegate = delegate; 219} 220 221- (void)scheduleTimeUpdate 222{ 223 [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(unscheduleTimeUpdate) object:self]; 224 225 // First, update right away, then schedule future update 226 [self updateTime]; 227 [self updatePlayButton]; 228 229 [_timelineUpdateTimer invalidate]; 230 [_timelineUpdateTimer release]; 231 232 // Note that this creates a retain cycle between the window and us. 233 _timelineUpdateTimer = [[NSTimer timerWithTimeInterval:0.25 target:self selector:@selector(updateTime) userInfo:nil repeats:YES] retain]; 234 [[NSRunLoop currentRunLoop] addTimer:_timelineUpdateTimer forMode:NSRunLoopCommonModes]; 235} 236 237- (void)unscheduleTimeUpdate 238{ 239 [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(unscheduleTimeUpdate) object:nil]; 240 241 [_timelineUpdateTimer invalidate]; 242 [_timelineUpdateTimer release]; 243 _timelineUpdateTimer = nil; 244} 245 246- (void)fadeWindowIn 247{ 248 NSWindow *window = [self window]; 249 if (![window isVisible]) 250 [window setAlphaValue:0]; 251 252 [window makeKeyAndOrderFront:self]; 253 [[window animator] setAlphaValue:1]; 254 [self scheduleTimeUpdate]; 255 256 [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(fadeWindowOut) object:nil]; 257 if (!_mouseIsInHUD && [self playing]) // Don't fade out when paused. 258 [self performSelector:@selector(fadeWindowOut) withObject:nil afterDelay:HUDWindowFadeOutDelay]; 259} 260 261- (void)fadeWindowOut 262{ 263 [NSCursor setHiddenUntilMouseMoves:YES]; 264 [[[self window] animator] setAlphaValue:0]; 265 [self performSelector:@selector(unscheduleTimeUpdate) withObject:nil afterDelay:1]; 266} 267 268- (void)closeWindow 269{ 270 [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(fadeWindowOut) object:nil]; 271 [self unscheduleTimeUpdate]; 272 NSWindow *window = [self window]; 273 [[window contentView] removeTrackingArea:_area]; 274 [self setArea:nil]; 275 [window close]; 276 [window setDelegate:nil]; 277 [self setWindow:nil]; 278} 279 280static NSControl *createControlWithMediaUIControlType(int controlType, NSRect frame) 281{ 282 NSControl *control = wkCreateMediaUIControl(controlType); 283 [control setFrame:frame]; 284 return control; 285} 286 287static NSTextField *createTimeTextField(NSRect frame) 288{ 289 NSTextField *textField = [[NSTextField alloc] initWithFrame:frame]; 290 [textField setTextColor:[NSColor whiteColor]]; 291 [textField setBordered:NO]; 292 [textField setFont:[NSFont boldSystemFontOfSize:10]]; 293 [textField setDrawsBackground:NO]; 294 [textField setBezeled:NO]; 295 [textField setEditable:NO]; 296 [textField setSelectable:NO]; 297 return textField; 298} 299 300- (void)windowDidLoad 301{ 302 static const CGFloat horizontalMargin = 10; 303 static const CGFloat playButtonWidth = 41; 304 static const CGFloat playButtonHeight = 35; 305 static const CGFloat playButtonTopMargin = 4; 306 static const CGFloat volumeSliderWidth = 50; 307 static const CGFloat volumeSliderHeight = 13; 308 static const CGFloat volumeButtonWidth = 18; 309 static const CGFloat volumeButtonHeight = 16; 310 static const CGFloat volumeUpButtonLeftMargin = 4; 311 static const CGFloat volumeControlsTopMargin = 13; 312 static const CGFloat exitFullscreenButtonWidth = 25; 313 static const CGFloat exitFullscreenButtonHeight = 21; 314 static const CGFloat exitFullscreenButtonTopMargin = 11; 315 static const CGFloat timelineWidth = 315; 316 static const CGFloat timelineHeight = 14; 317 static const CGFloat timelineBottomMargin = 7; 318 static const CGFloat timeTextFieldWidth = 54; 319 static const CGFloat timeTextFieldHeight = 13; 320 static const CGFloat timeTextFieldHorizontalMargin = 7; 321 322 NSWindow *window = [self window]; 323 ASSERT(window); 324 325 NSView *background = wkCreateMediaUIBackgroundView(); 326 327 [window setContentView:background]; 328 _area = [[NSTrackingArea alloc] initWithRect:[background bounds] options:NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways owner:self userInfo:nil]; 329 [background addTrackingArea:_area]; 330 [background release]; 331 332 NSView *contentView = [window contentView]; 333 334 CGFloat center = webkit_CGFloor((windowWidth - playButtonWidth) / 2); 335 _playButton = (NSButton *)createControlWithMediaUIControlType(wkMediaUIControlPlayPauseButton, NSMakeRect(center, windowHeight - playButtonTopMargin - playButtonHeight, playButtonWidth, playButtonHeight)); 336 ASSERT([_playButton isKindOfClass:[NSButton class]]); 337 [_playButton setTarget:self]; 338 [_playButton setAction:@selector(togglePlaying:)]; 339 [contentView addSubview:_playButton]; 340 341 CGFloat closeToRight = windowWidth - horizontalMargin - exitFullscreenButtonWidth; 342 NSControl *exitFullscreenButton = createControlWithMediaUIControlType(wkMediaUIControlExitFullscreenButton, NSMakeRect(closeToRight, windowHeight - exitFullscreenButtonTopMargin - exitFullscreenButtonHeight, exitFullscreenButtonWidth, exitFullscreenButtonHeight)); 343 [exitFullscreenButton setAction:@selector(exitFullscreen:)]; 344 [exitFullscreenButton setTarget:self]; 345 [contentView addSubview:exitFullscreenButton]; 346 [exitFullscreenButton release]; 347 348 CGFloat volumeControlsBottom = windowHeight - volumeControlsTopMargin - volumeButtonHeight; 349 CGFloat left = horizontalMargin; 350 NSControl *volumeDownButton = createControlWithMediaUIControlType(wkMediaUIControlVolumeDownButton, NSMakeRect(left, volumeControlsBottom, volumeButtonWidth, volumeButtonHeight)); 351 [contentView addSubview:volumeDownButton]; 352 [volumeDownButton setTarget:self]; 353 [volumeDownButton setAction:@selector(setVolumeToZero:)]; 354 [volumeDownButton release]; 355 356 left += volumeButtonWidth; 357 _volumeSlider = createControlWithMediaUIControlType(wkMediaUIControlSlider, NSMakeRect(left, volumeControlsBottom + webkit_CGFloor((volumeButtonHeight - volumeSliderHeight) / 2), volumeSliderWidth, volumeSliderHeight)); 358 [_volumeSlider setValue:[NSNumber numberWithDouble:[self maxVolume]] forKey:@"maxValue"]; 359 [_volumeSlider setTarget:self]; 360 [_volumeSlider setAction:@selector(volumeChanged:)]; 361 [contentView addSubview:_volumeSlider]; 362 363 left += volumeSliderWidth + volumeUpButtonLeftMargin; 364 NSControl *volumeUpButton = createControlWithMediaUIControlType(wkMediaUIControlVolumeUpButton, NSMakeRect(left, volumeControlsBottom, volumeButtonWidth, volumeButtonHeight)); 365 [volumeUpButton setTarget:self]; 366 [volumeUpButton setAction:@selector(setVolumeToMaximum:)]; 367 [contentView addSubview:volumeUpButton]; 368 [volumeUpButton release]; 369 370 _timeline = wkCreateMediaUIControl(wkMediaUIControlTimeline); 371 372 [_timeline setTarget:self]; 373 [_timeline setAction:@selector(timelinePositionChanged:)]; 374 [_timeline setFrame:NSMakeRect(webkit_CGFloor((windowWidth - timelineWidth) / 2), timelineBottomMargin, timelineWidth, timelineHeight)]; 375 [contentView addSubview:_timeline]; 376 377 _elapsedTimeText = createTimeTextField(NSMakeRect(timeTextFieldHorizontalMargin, timelineBottomMargin, timeTextFieldWidth, timeTextFieldHeight)); 378 [_elapsedTimeText setAlignment:NSLeftTextAlignment]; 379 [contentView addSubview:_elapsedTimeText]; 380 381 _remainingTimeText = createTimeTextField(NSMakeRect(windowWidth - timeTextFieldHorizontalMargin - timeTextFieldWidth, timelineBottomMargin, timeTextFieldWidth, timeTextFieldHeight)); 382 [_remainingTimeText setAlignment:NSRightTextAlignment]; 383 [contentView addSubview:_remainingTimeText]; 384 385 [window recalculateKeyViewLoop]; 386 [window setInitialFirstResponder:_playButton]; 387 [window center]; 388} 389 390- (void)updateVolume 391{ 392 [_volumeSlider setFloatValue:[self volume]]; 393} 394 395- (void)updateTime 396{ 397 [self updateVolume]; 398 399 [_timeline setFloatValue:[self currentTime]]; 400 [_timeline setValue:[NSNumber numberWithDouble:[self duration]] forKey:@"maxValue"]; 401 402 [_remainingTimeText setStringValue:[self remainingTimeText]]; 403 [_elapsedTimeText setStringValue:[self elapsedTimeText]]; 404} 405 406- (void)endScrubbing 407{ 408 ASSERT(_isScrubbing); 409 _isScrubbing = NO; 410 if (HTMLMediaElement* mediaElement = [_delegate mediaElement]) 411 mediaElement->endScrubbing(); 412} 413 414- (void)timelinePositionChanged:(id)sender 415{ 416 UNUSED_PARAM(sender); 417 [self setCurrentTime:[_timeline floatValue]]; 418 if (!_isScrubbing) { 419 _isScrubbing = YES; 420 if (HTMLMediaElement* mediaElement = [_delegate mediaElement]) 421 mediaElement->beginScrubbing(); 422 static NSArray *endScrubbingModes = [[NSArray alloc] initWithObjects:NSDefaultRunLoopMode, NSModalPanelRunLoopMode, nil]; 423 // Schedule -endScrubbing for when leaving mouse tracking mode. 424 [[NSRunLoop currentRunLoop] performSelector:@selector(endScrubbing) target:self argument:nil order:0 modes:endScrubbingModes]; 425 } 426} 427 428- (float)currentTime 429{ 430 return [_delegate mediaElement] ? [_delegate mediaElement]->currentTime() : 0; 431} 432 433- (void)setCurrentTime:(float)currentTime 434{ 435 if (![_delegate mediaElement]) 436 return; 437 [_delegate mediaElement]->setCurrentTime(currentTime, IGNORE_EXCEPTION); 438 [self updateTime]; 439} 440 441- (double)duration 442{ 443 return [_delegate mediaElement] ? [_delegate mediaElement]->duration() : 0; 444} 445 446- (float)maxVolume 447{ 448 // Set the volume slider resolution 449 return 100; 450} 451 452- (void)volumeChanged:(id)sender 453{ 454 UNUSED_PARAM(sender); 455 [self setVolume:[_volumeSlider floatValue]]; 456} 457 458- (void)setVolumeToZero:(id)sender 459{ 460 UNUSED_PARAM(sender); 461 [self setVolume:0]; 462} 463 464- (void)setVolumeToMaximum:(id)sender 465{ 466 UNUSED_PARAM(sender); 467 [self setVolume:[self maxVolume]]; 468} 469 470- (void)decrementVolume 471{ 472 if (![_delegate mediaElement]) 473 return; 474 475 float volume = [self volume] - 10; 476 [self setVolume:MAX(volume, 0)]; 477} 478 479- (void)incrementVolume 480{ 481 if (![_delegate mediaElement]) 482 return; 483 484 float volume = [self volume] + 10; 485 [self setVolume:min(volume, [self maxVolume])]; 486} 487 488- (float)volume 489{ 490 return [_delegate mediaElement] ? [_delegate mediaElement]->volume() * [self maxVolume] : 0; 491} 492 493- (void)setVolume:(float)volume 494{ 495 if (![_delegate mediaElement]) 496 return; 497 if ([_delegate mediaElement]->muted()) 498 [_delegate mediaElement]->setMuted(false); 499 [_delegate mediaElement]->setVolume(volume / [self maxVolume], IGNORE_EXCEPTION); 500 [self updateVolume]; 501} 502 503- (void)updatePlayButton 504{ 505 [_playButton setIntValue:[self playing]]; 506} 507 508- (void)updateRate 509{ 510 BOOL playing = [self playing]; 511 512 // Keep the HUD visible when paused. 513 if (!playing) 514 [self fadeWindowIn]; 515 else if (!_mouseIsInHUD) { 516 [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(fadeWindowOut) object:nil]; 517 [self performSelector:@selector(fadeWindowOut) withObject:nil afterDelay:HUDWindowFadeOutDelay]; 518 } 519 [self updatePlayButton]; 520} 521 522- (void)togglePlaying:(id)sender 523{ 524 UNUSED_PARAM(sender); 525 [self setPlaying:![self playing]]; 526} 527 528- (BOOL)playing 529{ 530 HTMLMediaElement* mediaElement = [_delegate mediaElement]; 531 if (!mediaElement) 532 return NO; 533 534 return !mediaElement->canPlay(); 535} 536 537- (void)setPlaying:(BOOL)playing 538{ 539 HTMLMediaElement* mediaElement = [_delegate mediaElement]; 540 541 if (!mediaElement) 542 return; 543 544 if (playing) 545 mediaElement->play(); 546 else 547 mediaElement->pause(); 548} 549 550static NSString *timeToString(double time) 551{ 552 ASSERT_ARG(time, time >= 0); 553 554 if (!std::isfinite(time)) 555 time = 0; 556 557 int seconds = narrowPrecisionToFloat(abs(time)); 558 int hours = seconds / (60 * 60); 559 int minutes = (seconds / 60) % 60; 560 seconds %= 60; 561 562 if (hours) 563 return [NSString stringWithFormat:@"%d:%02d:%02d", hours, minutes, seconds]; 564 565 return [NSString stringWithFormat:@"%02d:%02d", minutes, seconds]; 566} 567 568- (NSString *)remainingTimeText 569{ 570 HTMLMediaElement* mediaElement = [_delegate mediaElement]; 571 if (!mediaElement) 572 return @""; 573 574 return [@"-" stringByAppendingString:timeToString(mediaElement->duration() - mediaElement->currentTime())]; 575} 576 577- (NSString *)elapsedTimeText 578{ 579 if (![_delegate mediaElement]) 580 return @""; 581 582 return timeToString([_delegate mediaElement]->currentTime()); 583} 584 585// MARK: NSResponder 586 587- (void)mouseEntered:(NSEvent *)theEvent 588{ 589 UNUSED_PARAM(theEvent); 590 // Make sure the HUD won't be hidden from now 591 _mouseIsInHUD = YES; 592 [self fadeWindowIn]; 593} 594 595- (void)mouseExited:(NSEvent *)theEvent 596{ 597 UNUSED_PARAM(theEvent); 598 _mouseIsInHUD = NO; 599 [self fadeWindowIn]; 600} 601 602- (void)rewind:(id)sender 603{ 604 UNUSED_PARAM(sender); 605 if (![_delegate mediaElement]) 606 return; 607 [_delegate mediaElement]->rewind(30); 608} 609 610- (void)fastForward:(id)sender 611{ 612 UNUSED_PARAM(sender); 613 if (![_delegate mediaElement]) 614 return; 615} 616 617- (void)exitFullscreen:(id)sender 618{ 619 UNUSED_PARAM(sender); 620 if (_isEndingFullscreen) 621 return; 622 _isEndingFullscreen = YES; 623 [_delegate requestExitFullscreen]; 624} 625 626// MARK: NSWindowDelegate 627 628- (void)windowDidExpose:(NSNotification *)notification 629{ 630 UNUSED_PARAM(notification); 631 [self scheduleTimeUpdate]; 632} 633 634- (void)windowDidClose:(NSNotification *)notification 635{ 636 UNUSED_PARAM(notification); 637 [self unscheduleTimeUpdate]; 638} 639 640@end 641 642#endif 643