diff -r 000000000000 -r 4f2f89ce4247 WebKit/mac/WebView/WebVideoFullscreenHUDWindowController.mm --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebKit/mac/WebView/WebVideoFullscreenHUDWindowController.mm Fri Sep 17 09:02:29 2010 +0300 @@ -0,0 +1,677 @@ +/* + * 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 +#import +#import + +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) + +- (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]; + [self setHidesOnDeactivate: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 )delegate +{ + return _delegate; +} + +- (void)setDelegate:(id )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()); +} + +#pragma 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]; +} + +#pragma mark NSWindowDelegate + +- (void)windowDidExpose:(NSNotification *)notification +{ + [self scheduleTimeUpdate]; +} + +- (void)windowDidClose:(NSNotification *)notification +{ + [self unscheduleTimeUpdate]; +} + +@end + +#endif