blob: 0a5aca46e42b08f6659d1f7cea7ca87a0e46d3e5 [file] [log] [blame]
/*
* 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