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#if ENABLE(VIDEO) 26 27#import "WebVideoFullscreenHUDWindowController.h" 28 29#import "WebKitSystemInterface.h" 30#import "WebTypesInternal.h" 31#import <JavaScriptCore/RetainPtr.h> 32#import <JavaScriptCore/UnusedParam.h> 33#import <WebCore/HTMLMediaElement.h> 34 35using namespace WebCore; 36using namespace std; 37 38static inline CGFloat webkit_CGFloor(CGFloat value) 39{ 40 if (sizeof(value) == sizeof(float)) 41 return floorf(value); 42 return floor(value); 43} 44 45#define HAVE_MEDIA_CONTROL (!defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD)) 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- (double)maxVolume; 57- (double)volume; 58- (void)setVolume:(double)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 [self setHidesOnDeactivate:YES]; 95 96 return self; 97} 98 99- (BOOL)canBecomeKeyWindow 100{ 101 return YES; 102} 103 104- (void)cancelOperation:(id)sender 105{ 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#if !defined(BUILDING_ON_TIGER) 166 ASSERT(!_area); 167#endif 168 ASSERT(!_isScrubbing); 169 [_timeline release]; 170 [_remainingTimeText release]; 171 [_elapsedTimeText release]; 172 [_volumeSlider release]; 173 [_playButton release]; 174 [super dealloc]; 175} 176 177#if !defined(BUILDING_ON_TIGER) 178- (void)setArea:(NSTrackingArea *)area 179{ 180 if (area == _area) 181 return; 182 [_area release]; 183 _area = [area retain]; 184} 185#endif 186 187- (void)keyDown:(NSEvent *)event 188{ 189 NSString *charactersIgnoringModifiers = [event charactersIgnoringModifiers]; 190 if ([charactersIgnoringModifiers length] == 1) { 191 switch ([charactersIgnoringModifiers characterAtIndex:0]) { 192 case ' ': 193 [self togglePlaying:nil]; 194 return; 195 case NSUpArrowFunctionKey: 196 if ([event modifierFlags] & NSAlternateKeyMask) 197 [self setVolume:[self maxVolume]]; 198 else 199 [self incrementVolume]; 200 return; 201 case NSDownArrowFunctionKey: 202 if ([event modifierFlags] & NSAlternateKeyMask) 203 [self setVolume:0]; 204 else 205 [self decrementVolume]; 206 return; 207 default: 208 break; 209 } 210 } 211 212 [super keyDown:event]; 213} 214 215- (id <WebVideoFullscreenHUDWindowControllerDelegate>)delegate 216{ 217 return _delegate; 218} 219 220- (void)setDelegate:(id <WebVideoFullscreenHUDWindowControllerDelegate>)delegate 221{ 222 _delegate = delegate; 223} 224 225- (void)scheduleTimeUpdate 226{ 227 [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(unscheduleTimeUpdate) object:self]; 228 229 // First, update right away, then schedule future update 230 [self updateTime]; 231 [self updatePlayButton]; 232 233 [_timelineUpdateTimer invalidate]; 234 [_timelineUpdateTimer release]; 235 236 // Note that this creates a retain cycle between the window and us. 237 _timelineUpdateTimer = [[NSTimer timerWithTimeInterval:0.25 target:self selector:@selector(updateTime) userInfo:nil repeats:YES] retain]; 238#if defined(BUILDING_ON_TIGER) 239 [[NSRunLoop currentRunLoop] addTimer:_timelineUpdateTimer forMode:(NSString *)kCFRunLoopCommonModes]; 240#else 241 [[NSRunLoop currentRunLoop] addTimer:_timelineUpdateTimer forMode:NSRunLoopCommonModes]; 242#endif 243} 244 245- (void)unscheduleTimeUpdate 246{ 247 [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(unscheduleTimeUpdate) object:nil]; 248 249 [_timelineUpdateTimer invalidate]; 250 [_timelineUpdateTimer release]; 251 _timelineUpdateTimer = nil; 252} 253 254- (void)fadeWindowIn 255{ 256 NSWindow *window = [self window]; 257 if (![window isVisible]) 258 [window setAlphaValue:0]; 259 260 [window makeKeyAndOrderFront:self]; 261#if defined(BUILDING_ON_TIGER) 262 [window setAlphaValue:1]; 263#else 264 [[window animator] setAlphaValue:1]; 265#endif 266 [self scheduleTimeUpdate]; 267 268 [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(fadeWindowOut) object:nil]; 269 if (!_mouseIsInHUD && [self playing]) // Don't fade out when paused. 270 [self performSelector:@selector(fadeWindowOut) withObject:nil afterDelay:HUDWindowFadeOutDelay]; 271} 272 273- (void)fadeWindowOut 274{ 275 [NSCursor setHiddenUntilMouseMoves:YES]; 276#if defined(BUILDING_ON_TIGER) 277 [[self window] setAlphaValue:0]; 278#else 279 [[[self window] animator] setAlphaValue:0]; 280#endif 281 [self performSelector:@selector(unscheduleTimeUpdate) withObject:nil afterDelay:1]; 282} 283 284- (void)closeWindow 285{ 286 [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(fadeWindowOut) object:nil]; 287 [self unscheduleTimeUpdate]; 288 NSWindow *window = [self window]; 289#if !defined(BUILDING_ON_TIGER) 290 [[window contentView] removeTrackingArea:_area]; 291 [self setArea:nil]; 292#endif 293 [window close]; 294 [window setDelegate:nil]; 295 [self setWindow:nil]; 296} 297 298#ifndef HAVE_MEDIA_CONTROL 299enum { 300 WKMediaUIControlPlayPauseButton, 301 WKMediaUIControlRewindButton, 302 WKMediaUIControlFastForwardButton, 303 WKMediaUIControlExitFullscreenButton, 304 WKMediaUIControlVolumeDownButton, 305 WKMediaUIControlSlider, 306 WKMediaUIControlVolumeUpButton, 307 WKMediaUIControlTimeline 308}; 309#endif 310 311static NSControl *createControlWithMediaUIControlType(int controlType, NSRect frame) 312{ 313#ifdef HAVE_MEDIA_CONTROL 314 NSControl *control = WKCreateMediaUIControl(controlType); 315 [control setFrame:frame]; 316 return control; 317#else 318 if (controlType == WKMediaUIControlSlider) 319 return [[NSSlider alloc] initWithFrame:frame]; 320 return [[NSControl alloc] initWithFrame:frame]; 321#endif 322} 323 324static NSTextField *createTimeTextField(NSRect frame) 325{ 326 NSTextField *textField = [[NSTextField alloc] initWithFrame:frame]; 327 [textField setTextColor:[NSColor whiteColor]]; 328 [textField setBordered:NO]; 329 [textField setFont:[NSFont boldSystemFontOfSize:10]]; 330 [textField setDrawsBackground:NO]; 331 [textField setBezeled:NO]; 332 [textField setEditable:NO]; 333 [textField setSelectable:NO]; 334 return textField; 335} 336 337- (void)windowDidLoad 338{ 339 static const CGFloat horizontalMargin = 10; 340 static const CGFloat playButtonWidth = 41; 341 static const CGFloat playButtonHeight = 35; 342 static const CGFloat playButtonTopMargin = 4; 343 static const CGFloat volumeSliderWidth = 50; 344 static const CGFloat volumeSliderHeight = 13; 345 static const CGFloat volumeButtonWidth = 18; 346 static const CGFloat volumeButtonHeight = 16; 347 static const CGFloat volumeUpButtonLeftMargin = 4; 348 static const CGFloat volumeControlsTopMargin = 13; 349 static const CGFloat exitFullScreenButtonWidth = 25; 350 static const CGFloat exitFullScreenButtonHeight = 21; 351 static const CGFloat exitFullScreenButtonTopMargin = 11; 352 static const CGFloat timelineWidth = 315; 353 static const CGFloat timelineHeight = 14; 354 static const CGFloat timelineBottomMargin = 7; 355 static const CGFloat timeTextFieldWidth = 54; 356 static const CGFloat timeTextFieldHeight = 13; 357 static const CGFloat timeTextFieldHorizontalMargin = 7; 358 359 NSWindow *window = [self window]; 360 ASSERT(window); 361 362#ifdef HAVE_MEDIA_CONTROL 363 NSView *background = WKCreateMediaUIBackgroundView(); 364#else 365 NSView *background = [[NSView alloc] init]; 366#endif 367 [window setContentView:background]; 368#if !defined(BUILDING_ON_TIGER) 369 _area = [[NSTrackingArea alloc] initWithRect:[background bounds] options:NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways owner:self userInfo:nil]; 370 [background addTrackingArea:_area]; 371#endif 372 [background release]; 373 374 NSView *contentView = [window contentView]; 375 376 CGFloat center = webkit_CGFloor((windowWidth - playButtonWidth) / 2); 377 _playButton = (NSButton *)createControlWithMediaUIControlType(WKMediaUIControlPlayPauseButton, NSMakeRect(center, windowHeight - playButtonTopMargin - playButtonHeight, playButtonWidth, playButtonHeight)); 378 ASSERT([_playButton isKindOfClass:[NSButton class]]); 379 [_playButton setTarget:self]; 380 [_playButton setAction:@selector(togglePlaying:)]; 381 [contentView addSubview:_playButton]; 382 383 CGFloat closeToRight = windowWidth - horizontalMargin - exitFullScreenButtonWidth; 384 NSControl *exitFullscreenButton = createControlWithMediaUIControlType(WKMediaUIControlExitFullscreenButton, NSMakeRect(closeToRight, windowHeight - exitFullScreenButtonTopMargin - exitFullScreenButtonHeight, exitFullScreenButtonWidth, exitFullScreenButtonHeight)); 385 [exitFullscreenButton setAction:@selector(exitFullscreen:)]; 386 [exitFullscreenButton setTarget:self]; 387 [contentView addSubview:exitFullscreenButton]; 388 [exitFullscreenButton release]; 389 390 CGFloat volumeControlsBottom = windowHeight - volumeControlsTopMargin - volumeButtonHeight; 391 CGFloat left = horizontalMargin; 392 NSControl *volumeDownButton = createControlWithMediaUIControlType(WKMediaUIControlVolumeDownButton, NSMakeRect(left, volumeControlsBottom, volumeButtonWidth, volumeButtonHeight)); 393 [contentView addSubview:volumeDownButton]; 394 [volumeDownButton setTarget:self]; 395 [volumeDownButton setAction:@selector(setVolumeToZero:)]; 396 [volumeDownButton release]; 397 398 left += volumeButtonWidth; 399 _volumeSlider = createControlWithMediaUIControlType(WKMediaUIControlSlider, NSMakeRect(left, volumeControlsBottom + webkit_CGFloor((volumeButtonHeight - volumeSliderHeight) / 2), volumeSliderWidth, volumeSliderHeight)); 400 [_volumeSlider setValue:[NSNumber numberWithDouble:[self maxVolume]] forKey:@"maxValue"]; 401 [_volumeSlider setTarget:self]; 402 [_volumeSlider setAction:@selector(volumeChanged:)]; 403 [contentView addSubview:_volumeSlider]; 404 405 left += volumeSliderWidth + volumeUpButtonLeftMargin; 406 NSControl *volumeUpButton = createControlWithMediaUIControlType(WKMediaUIControlVolumeUpButton, NSMakeRect(left, volumeControlsBottom, volumeButtonWidth, volumeButtonHeight)); 407 [volumeUpButton setTarget:self]; 408 [volumeUpButton setAction:@selector(setVolumeToMaximum:)]; 409 [contentView addSubview:volumeUpButton]; 410 [volumeUpButton release]; 411 412#ifdef HAVE_MEDIA_CONTROL 413 _timeline = WKCreateMediaUIControl(WKMediaUIControlTimeline); 414#else 415 _timeline = [[NSSlider alloc] init]; 416#endif 417 [_timeline setTarget:self]; 418 [_timeline setAction:@selector(timelinePositionChanged:)]; 419 [_timeline setFrame:NSMakeRect(webkit_CGFloor((windowWidth - timelineWidth) / 2), timelineBottomMargin, timelineWidth, timelineHeight)]; 420 [contentView addSubview:_timeline]; 421 422 _elapsedTimeText = createTimeTextField(NSMakeRect(timeTextFieldHorizontalMargin, timelineBottomMargin, timeTextFieldWidth, timeTextFieldHeight)); 423 [_elapsedTimeText setAlignment:NSLeftTextAlignment]; 424 [contentView addSubview:_elapsedTimeText]; 425 426 _remainingTimeText = createTimeTextField(NSMakeRect(windowWidth - timeTextFieldHorizontalMargin - timeTextFieldWidth, timelineBottomMargin, timeTextFieldWidth, timeTextFieldHeight)); 427 [_remainingTimeText setAlignment:NSRightTextAlignment]; 428 [contentView addSubview:_remainingTimeText]; 429 430 [window recalculateKeyViewLoop]; 431 [window setInitialFirstResponder:_playButton]; 432 [window center]; 433} 434 435- (void)updateVolume 436{ 437 [_volumeSlider setDoubleValue:[self volume]]; 438} 439 440- (void)updateTime 441{ 442 [self updateVolume]; 443 444 [_timeline setFloatValue:[self currentTime]]; 445 [_timeline setValue:[NSNumber numberWithDouble:[self duration]] forKey:@"maxValue"]; 446 447 [_remainingTimeText setStringValue:[self remainingTimeText]]; 448 [_elapsedTimeText setStringValue:[self elapsedTimeText]]; 449} 450 451- (void)endScrubbing 452{ 453 ASSERT(_isScrubbing); 454 _isScrubbing = NO; 455 if (HTMLMediaElement* mediaElement = [_delegate mediaElement]) 456 mediaElement->endScrubbing(); 457} 458 459- (void)timelinePositionChanged:(id)sender 460{ 461 [self setCurrentTime:[_timeline floatValue]]; 462 if (!_isScrubbing) { 463 _isScrubbing = YES; 464 if (HTMLMediaElement* mediaElement = [_delegate mediaElement]) 465 mediaElement->beginScrubbing(); 466 static NSArray *endScrubbingModes = [[NSArray alloc] initWithObjects:NSDefaultRunLoopMode, NSModalPanelRunLoopMode, nil]; 467 // Schedule -endScrubbing for when leaving mouse tracking mode. 468 [[NSRunLoop currentRunLoop] performSelector:@selector(endScrubbing) target:self argument:nil order:0 modes:endScrubbingModes]; 469 } 470} 471 472- (float)currentTime 473{ 474 return [_delegate mediaElement] ? [_delegate mediaElement]->currentTime() : 0; 475} 476 477- (void)setCurrentTime:(float)currentTime 478{ 479 if (![_delegate mediaElement]) 480 return; 481 WebCore::ExceptionCode e; 482 [_delegate mediaElement]->setCurrentTime(currentTime, e); 483 [self updateTime]; 484} 485 486- (double)duration 487{ 488 return [_delegate mediaElement] ? [_delegate mediaElement]->duration() : 0; 489} 490 491- (double)maxVolume 492{ 493 // Set the volume slider resolution 494 return 100; 495} 496 497- (void)volumeChanged:(id)sender 498{ 499 [self setVolume:[_volumeSlider doubleValue]]; 500} 501 502- (void)setVolumeToZero:(id)sender 503{ 504 [self setVolume:0]; 505} 506 507- (void)setVolumeToMaximum:(id)sender 508{ 509 [self setVolume:[self maxVolume]]; 510} 511 512- (void)decrementVolume 513{ 514 if (![_delegate mediaElement]) 515 return; 516 517 double volume = [self volume] - 10; 518 [self setVolume:max(volume, 0.)]; 519} 520 521- (void)incrementVolume 522{ 523 if (![_delegate mediaElement]) 524 return; 525 526 double volume = [self volume] + 10; 527 [self setVolume:min(volume, [self maxVolume])]; 528} 529 530- (double)volume 531{ 532 return [_delegate mediaElement] ? [_delegate mediaElement]->volume() * [self maxVolume] : 0; 533} 534 535- (void)setVolume:(double)volume 536{ 537 if (![_delegate mediaElement]) 538 return; 539 WebCore::ExceptionCode e; 540 if ([_delegate mediaElement]->muted()) 541 [_delegate mediaElement]->setMuted(false); 542 [_delegate mediaElement]->setVolume(volume / [self maxVolume], e); 543 [self updateVolume]; 544} 545 546- (void)updatePlayButton 547{ 548 [_playButton setIntValue:[self playing]]; 549} 550 551- (void)updateRate 552{ 553 BOOL playing = [self playing]; 554 555 // Keep the HUD visible when paused. 556 if (!playing) 557 [self fadeWindowIn]; 558 else if (!_mouseIsInHUD) { 559 [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(fadeWindowOut) object:nil]; 560 [self performSelector:@selector(fadeWindowOut) withObject:nil afterDelay:HUDWindowFadeOutDelay]; 561 } 562 [self updatePlayButton]; 563} 564 565- (void)togglePlaying:(id)sender 566{ 567 [self setPlaying:![self playing]]; 568} 569 570- (BOOL)playing 571{ 572 HTMLMediaElement* mediaElement = [_delegate mediaElement]; 573 if (!mediaElement) 574 return NO; 575 576 return !mediaElement->canPlay(); 577} 578 579- (void)setPlaying:(BOOL)playing 580{ 581 HTMLMediaElement* mediaElement = [_delegate mediaElement]; 582 583 if (!mediaElement) 584 return; 585 586 if (playing) 587 mediaElement->play(mediaElement->processingUserGesture()); 588 else 589 mediaElement->pause(mediaElement->processingUserGesture()); 590} 591 592static NSString *timeToString(double time) 593{ 594 ASSERT_ARG(time, time >= 0); 595 596 if (!isfinite(time)) 597 time = 0; 598 599 int seconds = fabs(time); 600 int hours = seconds / (60 * 60); 601 int minutes = (seconds / 60) % 60; 602 seconds %= 60; 603 604 if (hours) 605 return [NSString stringWithFormat:@"%d:%02d:%02d", hours, minutes, seconds]; 606 607 return [NSString stringWithFormat:@"%02d:%02d", minutes, seconds]; 608} 609 610- (NSString *)remainingTimeText 611{ 612 HTMLMediaElement* mediaElement = [_delegate mediaElement]; 613 if (!mediaElement) 614 return @""; 615 616 return [@"-" stringByAppendingString:timeToString(mediaElement->duration() - mediaElement->currentTime())]; 617} 618 619- (NSString *)elapsedTimeText 620{ 621 if (![_delegate mediaElement]) 622 return @""; 623 624 return timeToString([_delegate mediaElement]->currentTime()); 625} 626 627#pragma mark NSResponder 628 629- (void)mouseEntered:(NSEvent *)theEvent 630{ 631 // Make sure the HUD won't be hidden from now 632 _mouseIsInHUD = YES; 633 [self fadeWindowIn]; 634} 635 636- (void)mouseExited:(NSEvent *)theEvent 637{ 638 _mouseIsInHUD = NO; 639 [self fadeWindowIn]; 640} 641 642- (void)rewind:(id)sender 643{ 644 if (![_delegate mediaElement]) 645 return; 646 [_delegate mediaElement]->rewind(30); 647} 648 649- (void)fastForward:(id)sender 650{ 651 if (![_delegate mediaElement]) 652 return; 653} 654 655- (void)exitFullscreen:(id)sender 656{ 657 if (_isEndingFullscreen) 658 return; 659 _isEndingFullscreen = YES; 660 [_delegate requestExitFullscreen]; 661} 662 663#pragma mark NSWindowDelegate 664 665- (void)windowDidExpose:(NSNotification *)notification 666{ 667 [self scheduleTimeUpdate]; 668} 669 670- (void)windowDidClose:(NSNotification *)notification 671{ 672 [self unscheduleTimeUpdate]; 673} 674 675@end 676 677#endif 678