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