/* * Copyright (C) 2009 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. */ #if ENABLE(VIDEO) #import "WebVideoFullscreenHUDWindowController.h" #import "WebKitSystemInterface.h" #import "WebTypesInternal.h" #import <JavaScriptCore/RetainPtr.h> #import <JavaScriptCore/UnusedParam.h> #import <WebCore/HTMLMediaElement.h> using namespace WebCore; using namespace std; static inline CGFloat webkit_CGFloor(CGFloat value) { if (sizeof(value) == sizeof(float)) return floorf(value); return floor(value); } #define HAVE_MEDIA_CONTROL (!defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD)) @interface WebVideoFullscreenHUDWindowController (Private) <NSWindowDelegate> - (void)updateTime; - (void)timelinePositionChanged:(id)sender; - (float)currentTime; - (void)setCurrentTime:(float)currentTime; - (double)duration; - (void)volumeChanged:(id)sender; - (double)maxVolume; - (double)volume; - (void)setVolume:(double)volume; - (void)decrementVolume; - (void)incrementVolume; - (void)updatePlayButton; - (void)togglePlaying:(id)sender; - (BOOL)playing; - (void)setPlaying:(BOOL)playing; - (void)rewind:(id)sender; - (void)fastForward:(id)sender; - (NSString *)remainingTimeText; - (NSString *)elapsedTimeText; - (void)exitFullscreen:(id)sender; @end @interface WebVideoFullscreenHUDWindow : NSWindow @end @implementation WebVideoFullscreenHUDWindow - (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 setLevel:NSPopUpMenuWindowLevel]; [self setAcceptsMouseMovedEvents:YES]; [self setIgnoresMouseEvents:NO]; [self setMovableByWindowBackground:YES]; return self; } - (BOOL)canBecomeKeyWindow { return YES; } - (void)cancelOperation:(id)sender { [[self windowController] exitFullscreen:self]; } - (void)center { NSRect hudFrame = [self frame]; NSRect screenFrame = [[NSScreen mainScreen] frame]; [self setFrameTopLeftPoint:NSMakePoint(screenFrame.origin.x + (screenFrame.size.width - hudFrame.size.width) / 2, screenFrame.origin.y + (screenFrame.size.height - hudFrame.size.height) / 6)]; } - (void)keyDown:(NSEvent *)event { [super keyDown:event]; [[self windowController] fadeWindowIn]; } - (BOOL)resignFirstResponder { return NO; } - (BOOL)performKeyEquivalent:(NSEvent *)event { // Block all command key events while the fullscreen window is up. if ([event type] != NSKeyDown) return NO; if (!([event modifierFlags] & NSCommandKeyMask)) return NO; return YES; } @end static const CGFloat windowHeight = 59; static const CGFloat windowWidth = 438; static const NSTimeInterval HUDWindowFadeOutDelay = 3; @implementation WebVideoFullscreenHUDWindowController - (id)init { NSWindow *window = [[WebVideoFullscreenHUDWindow alloc] initWithContentRect:NSMakeRect(0, 0, windowWidth, windowHeight) styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO]; self = [super initWithWindow:window]; [window setDelegate:self]; [window release]; if (!self) return nil; [self windowDidLoad]; return self; } - (void)dealloc { ASSERT(!_timelineUpdateTimer); #if !defined(BUILDING_ON_TIGER) ASSERT(!_area); #endif ASSERT(!_isScrubbing); [_timeline release]; [_remainingTimeText release]; [_elapsedTimeText release]; [_volumeSlider release]; [_playButton release]; [super dealloc]; } #if !defined(BUILDING_ON_TIGER) - (void)setArea:(NSTrackingArea *)area { if (area == _area) return; [_area release]; _area = [area retain]; } #endif - (void)keyDown:(NSEvent *)event { NSString *charactersIgnoringModifiers = [event charactersIgnoringModifiers]; if ([charactersIgnoringModifiers length] == 1) { switch ([charactersIgnoringModifiers characterAtIndex:0]) { case ' ': [self togglePlaying:nil]; return; case NSUpArrowFunctionKey: if ([event modifierFlags] & NSAlternateKeyMask) [self setVolume:[self maxVolume]]; else [self incrementVolume]; return; case NSDownArrowFunctionKey: if ([event modifierFlags] & NSAlternateKeyMask) [self setVolume:0]; else [self decrementVolume]; return; default: break; } } [super keyDown:event]; } - (id <WebVideoFullscreenHUDWindowControllerDelegate>)delegate { return _delegate; } - (void)setDelegate:(id <WebVideoFullscreenHUDWindowControllerDelegate>)delegate { _delegate = delegate; } - (void)scheduleTimeUpdate { [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(unscheduleTimeUpdate) object:self]; // First, update right away, then schedule future update [self updateTime]; [self updatePlayButton]; [_timelineUpdateTimer invalidate]; [_timelineUpdateTimer release]; // Note that this creates a retain cycle between the window and us. _timelineUpdateTimer = [[NSTimer timerWithTimeInterval:0.25 target:self selector:@selector(updateTime) userInfo:nil repeats:YES] retain]; #if defined(BUILDING_ON_TIGER) [[NSRunLoop currentRunLoop] addTimer:_timelineUpdateTimer forMode:(NSString *)kCFRunLoopCommonModes]; #else [[NSRunLoop currentRunLoop] addTimer:_timelineUpdateTimer forMode:NSRunLoopCommonModes]; #endif } - (void)unscheduleTimeUpdate { [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(unscheduleTimeUpdate) object:nil]; [_timelineUpdateTimer invalidate]; [_timelineUpdateTimer release]; _timelineUpdateTimer = nil; } - (void)fadeWindowIn { NSWindow *window = [self window]; if (![window isVisible]) [window setAlphaValue:0]; [window makeKeyAndOrderFront:self]; #if defined(BUILDING_ON_TIGER) [window setAlphaValue:1]; #else [[window animator] setAlphaValue:1]; #endif [self scheduleTimeUpdate]; [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(fadeWindowOut) object:nil]; if (!_mouseIsInHUD && [self playing]) // Don't fade out when paused. [self performSelector:@selector(fadeWindowOut) withObject:nil afterDelay:HUDWindowFadeOutDelay]; } - (void)fadeWindowOut { [NSCursor setHiddenUntilMouseMoves:YES]; #if defined(BUILDING_ON_TIGER) [[self window] setAlphaValue:0]; #else [[[self window] animator] setAlphaValue:0]; #endif [self performSelector:@selector(unscheduleTimeUpdate) withObject:nil afterDelay:1]; } - (void)closeWindow { [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(fadeWindowOut) object:nil]; [self unscheduleTimeUpdate]; NSWindow *window = [self window]; #if !defined(BUILDING_ON_TIGER) [[window contentView] removeTrackingArea:_area]; [self setArea:nil]; #endif [window close]; [window setDelegate:nil]; [self setWindow:nil]; } #ifndef HAVE_MEDIA_CONTROL enum { WKMediaUIControlPlayPauseButton, WKMediaUIControlRewindButton, WKMediaUIControlFastForwardButton, WKMediaUIControlExitFullscreenButton, WKMediaUIControlVolumeDownButton, WKMediaUIControlSlider, WKMediaUIControlVolumeUpButton, WKMediaUIControlTimeline }; #endif static NSControl *createControlWithMediaUIControlType(int controlType, NSRect frame) { #ifdef HAVE_MEDIA_CONTROL NSControl *control = WKCreateMediaUIControl(controlType); [control setFrame:frame]; return control; #else if (controlType == WKMediaUIControlSlider) return [[NSSlider alloc] initWithFrame:frame]; return [[NSControl alloc] initWithFrame:frame]; #endif } static NSTextField *createTimeTextField(NSRect frame) { NSTextField *textField = [[NSTextField alloc] initWithFrame:frame]; [textField setTextColor:[NSColor whiteColor]]; [textField setBordered:NO]; [textField setFont:[NSFont boldSystemFontOfSize:10]]; [textField setDrawsBackground:NO]; [textField setBezeled:NO]; [textField setEditable:NO]; [textField setSelectable:NO]; return textField; } - (void)windowDidLoad { static const CGFloat horizontalMargin = 10; static const CGFloat playButtonWidth = 41; static const CGFloat playButtonHeight = 35; static const CGFloat playButtonTopMargin = 4; static const CGFloat volumeSliderWidth = 50; static const CGFloat volumeSliderHeight = 13; static const CGFloat volumeButtonWidth = 18; static const CGFloat volumeButtonHeight = 16; static const CGFloat volumeUpButtonLeftMargin = 4; static const CGFloat volumeControlsTopMargin = 13; static const CGFloat exitFullscreenButtonWidth = 25; static const CGFloat exitFullscreenButtonHeight = 21; static const CGFloat exitFullscreenButtonTopMargin = 11; static const CGFloat timelineWidth = 315; static const CGFloat timelineHeight = 14; static const CGFloat timelineBottomMargin = 7; static const CGFloat timeTextFieldWidth = 54; static const CGFloat timeTextFieldHeight = 13; static const CGFloat timeTextFieldHorizontalMargin = 7; NSWindow *window = [self window]; ASSERT(window); #ifdef HAVE_MEDIA_CONTROL NSView *background = WKCreateMediaUIBackgroundView(); #else NSView *background = [[NSView alloc] init]; #endif [window setContentView:background]; #if !defined(BUILDING_ON_TIGER) _area = [[NSTrackingArea alloc] initWithRect:[background bounds] options:NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways owner:self userInfo:nil]; [background addTrackingArea:_area]; #endif [background release]; NSView *contentView = [window contentView]; CGFloat center = webkit_CGFloor((windowWidth - playButtonWidth) / 2); _playButton = (NSButton *)createControlWithMediaUIControlType(WKMediaUIControlPlayPauseButton, NSMakeRect(center, windowHeight - playButtonTopMargin - playButtonHeight, playButtonWidth, playButtonHeight)); ASSERT([_playButton isKindOfClass:[NSButton class]]); [_playButton setTarget:self]; [_playButton setAction:@selector(togglePlaying:)]; [contentView addSubview:_playButton]; CGFloat closeToRight = windowWidth - horizontalMargin - exitFullscreenButtonWidth; NSControl *exitFullscreenButton = createControlWithMediaUIControlType(WKMediaUIControlExitFullscreenButton, NSMakeRect(closeToRight, windowHeight - exitFullscreenButtonTopMargin - exitFullscreenButtonHeight, exitFullscreenButtonWidth, exitFullscreenButtonHeight)); [exitFullscreenButton setAction:@selector(exitFullscreen:)]; [exitFullscreenButton setTarget:self]; [contentView addSubview:exitFullscreenButton]; [exitFullscreenButton release]; CGFloat volumeControlsBottom = windowHeight - volumeControlsTopMargin - volumeButtonHeight; CGFloat left = horizontalMargin; NSControl *volumeDownButton = createControlWithMediaUIControlType(WKMediaUIControlVolumeDownButton, NSMakeRect(left, volumeControlsBottom, volumeButtonWidth, volumeButtonHeight)); [contentView addSubview:volumeDownButton]; [volumeDownButton setTarget:self]; [volumeDownButton setAction:@selector(setVolumeToZero:)]; [volumeDownButton release]; left += volumeButtonWidth; _volumeSlider = createControlWithMediaUIControlType(WKMediaUIControlSlider, NSMakeRect(left, volumeControlsBottom + webkit_CGFloor((volumeButtonHeight - volumeSliderHeight) / 2), volumeSliderWidth, volumeSliderHeight)); [_volumeSlider setValue:[NSNumber numberWithDouble:[self maxVolume]] forKey:@"maxValue"]; [_volumeSlider setTarget:self]; [_volumeSlider setAction:@selector(volumeChanged:)]; [contentView addSubview:_volumeSlider]; left += volumeSliderWidth + volumeUpButtonLeftMargin; NSControl *volumeUpButton = createControlWithMediaUIControlType(WKMediaUIControlVolumeUpButton, NSMakeRect(left, volumeControlsBottom, volumeButtonWidth, volumeButtonHeight)); [volumeUpButton setTarget:self]; [volumeUpButton setAction:@selector(setVolumeToMaximum:)]; [contentView addSubview:volumeUpButton]; [volumeUpButton release]; #ifdef HAVE_MEDIA_CONTROL _timeline = WKCreateMediaUIControl(WKMediaUIControlTimeline); #else _timeline = [[NSSlider alloc] init]; #endif [_timeline setTarget:self]; [_timeline setAction:@selector(timelinePositionChanged:)]; [_timeline setFrame:NSMakeRect(webkit_CGFloor((windowWidth - timelineWidth) / 2), timelineBottomMargin, timelineWidth, timelineHeight)]; [contentView addSubview:_timeline]; _elapsedTimeText = createTimeTextField(NSMakeRect(timeTextFieldHorizontalMargin, timelineBottomMargin, timeTextFieldWidth, timeTextFieldHeight)); [_elapsedTimeText setAlignment:NSLeftTextAlignment]; [contentView addSubview:_elapsedTimeText]; _remainingTimeText = createTimeTextField(NSMakeRect(windowWidth - timeTextFieldHorizontalMargin - timeTextFieldWidth, timelineBottomMargin, timeTextFieldWidth, timeTextFieldHeight)); [_remainingTimeText setAlignment:NSRightTextAlignment]; [contentView addSubview:_remainingTimeText]; [window recalculateKeyViewLoop]; [window setInitialFirstResponder:_playButton]; [window center]; } - (void)updateVolume { [_volumeSlider setDoubleValue:[self volume]]; } - (void)updateTime { [self updateVolume]; [_timeline setFloatValue:[self currentTime]]; [_timeline setValue:[NSNumber numberWithDouble:[self duration]] forKey:@"maxValue"]; [_remainingTimeText setStringValue:[self remainingTimeText]]; [_elapsedTimeText setStringValue:[self elapsedTimeText]]; } - (void)endScrubbing { ASSERT(_isScrubbing); _isScrubbing = NO; if (HTMLMediaElement* mediaElement = [_delegate mediaElement]) mediaElement->endScrubbing(); } - (void)timelinePositionChanged:(id)sender { [self setCurrentTime:[_timeline floatValue]]; if (!_isScrubbing) { _isScrubbing = YES; if (HTMLMediaElement* mediaElement = [_delegate mediaElement]) mediaElement->beginScrubbing(); static NSArray *endScrubbingModes = [[NSArray alloc] initWithObjects:NSDefaultRunLoopMode, NSModalPanelRunLoopMode, nil]; // Schedule -endScrubbing for when leaving mouse tracking mode. [[NSRunLoop currentRunLoop] performSelector:@selector(endScrubbing) target:self argument:nil order:0 modes:endScrubbingModes]; } } - (float)currentTime { return [_delegate mediaElement] ? [_delegate mediaElement]->currentTime() : 0; } - (void)setCurrentTime:(float)currentTime { if (![_delegate mediaElement]) return; WebCore::ExceptionCode e; [_delegate mediaElement]->setCurrentTime(currentTime, e); [self updateTime]; } - (double)duration { return [_delegate mediaElement] ? [_delegate mediaElement]->duration() : 0; } - (double)maxVolume { // Set the volume slider resolution return 100; } - (void)volumeChanged:(id)sender { [self setVolume:[_volumeSlider doubleValue]]; } - (void)setVolumeToZero:(id)sender { [self setVolume:0]; } - (void)setVolumeToMaximum:(id)sender { [self setVolume:[self maxVolume]]; } - (void)decrementVolume { if (![_delegate mediaElement]) return; double volume = [self volume] - 10; [self setVolume:max(volume, 0.)]; } - (void)incrementVolume { if (![_delegate mediaElement]) return; double volume = [self volume] + 10; [self setVolume:min(volume, [self maxVolume])]; } - (double)volume { return [_delegate mediaElement] ? [_delegate mediaElement]->volume() * [self maxVolume] : 0; } - (void)setVolume:(double)volume { if (![_delegate mediaElement]) return; WebCore::ExceptionCode e; if ([_delegate mediaElement]->muted()) [_delegate mediaElement]->setMuted(false); [_delegate mediaElement]->setVolume(volume / [self maxVolume], e); [self updateVolume]; } - (void)updatePlayButton { [_playButton setIntValue:[self playing]]; } - (void)updateRate { BOOL playing = [self playing]; // Keep the HUD visible when paused. if (!playing) [self fadeWindowIn]; else if (!_mouseIsInHUD) { [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(fadeWindowOut) object:nil]; [self performSelector:@selector(fadeWindowOut) withObject:nil afterDelay:HUDWindowFadeOutDelay]; } [self updatePlayButton]; } - (void)togglePlaying:(id)sender { [self setPlaying:![self playing]]; } - (BOOL)playing { HTMLMediaElement* mediaElement = [_delegate mediaElement]; if (!mediaElement) return NO; return !mediaElement->canPlay(); } - (void)setPlaying:(BOOL)playing { HTMLMediaElement* mediaElement = [_delegate mediaElement]; if (!mediaElement) return; if (playing) mediaElement->play(mediaElement->processingUserGesture()); else mediaElement->pause(mediaElement->processingUserGesture()); } static NSString *timeToString(double time) { ASSERT_ARG(time, time >= 0); if (!isfinite(time)) time = 0; int seconds = fabs(time); int hours = seconds / (60 * 60); int minutes = (seconds / 60) % 60; seconds %= 60; if (hours) return [NSString stringWithFormat:@"%d:%02d:%02d", hours, minutes, seconds]; return [NSString stringWithFormat:@"%02d:%02d", minutes, seconds]; } - (NSString *)remainingTimeText { HTMLMediaElement* mediaElement = [_delegate mediaElement]; if (!mediaElement) return @""; return [@"-" stringByAppendingString:timeToString(mediaElement->duration() - mediaElement->currentTime())]; } - (NSString *)elapsedTimeText { if (![_delegate mediaElement]) return @""; return timeToString([_delegate mediaElement]->currentTime()); } // MARK: NSResponder - (void)mouseEntered:(NSEvent *)theEvent { // Make sure the HUD won't be hidden from now _mouseIsInHUD = YES; [self fadeWindowIn]; } - (void)mouseExited:(NSEvent *)theEvent { _mouseIsInHUD = NO; [self fadeWindowIn]; } - (void)rewind:(id)sender { if (![_delegate mediaElement]) return; [_delegate mediaElement]->rewind(30); } - (void)fastForward:(id)sender { if (![_delegate mediaElement]) return; } - (void)exitFullscreen:(id)sender { if (_isEndingFullscreen) return; _isEndingFullscreen = YES; [_delegate requestExitFullscreen]; } // MARK: NSWindowDelegate - (void)windowDidExpose:(NSNotification *)notification { [self scheduleTimeUpdate]; } - (void)windowDidClose:(NSNotification *)notification { [self unscheduleTimeUpdate]; } @end #endif