/* * Copyright (C) 2009-2017 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. */ #import "config.h" #import "WebVideoFullscreenHUDWindowController.h" #if ENABLE(VIDEO) #import "FloatConversion.h" #import #import #import #import SOFT_LINK_FRAMEWORK(QTKit) SOFT_LINK_CLASS(QTKit, QTHUDBackgroundView) SOFT_LINK_CLASS(QTKit, QTHUDButton) SOFT_LINK_CLASS(QTKit, QTHUDSlider) SOFT_LINK_CLASS(QTKit, QTHUDTimeline) #define QTHUDBackgroundView getQTHUDBackgroundViewClass() #define QTHUDButton getQTHUDButtonClass() #define QTHUDSlider getQTHUDSliderClass() #define QTHUDTimeline getQTHUDTimelineClass() namespace WebCore { enum class MediaUIControl { Timeline, Slider, PlayPauseButton, ExitFullscreenButton, RewindButton, FastForwardButton, VolumeUpButton, VolumeDownButton, }; } using WebCore::MediaUIControl; using WebCore::HTMLVideoElement; using WebCore::narrowPrecisionToFloat; @interface WebVideoFullscreenHUDWindowController (Private) - (void)updateTime; - (void)timelinePositionChanged:(id)sender; - (float)currentTime; - (void)setCurrentTime:(float)currentTime; - (double)duration; - (void)volumeChanged:(id)sender; - (float)maxVolume; - (float)volume; - (void)setVolume:(float)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:NSWindowStyleMaskBorderless 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 { UNUSED_PARAM(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] != NSEventTypeKeyDown) return NO; if (!([event modifierFlags] & NSEventModifierFlagCommand)) 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:NSWindowStyleMaskBorderless 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); ASSERT(!_area); ASSERT(!_isScrubbing); [_timeline release]; [_remainingTimeText release]; [_elapsedTimeText release]; [_volumeSlider release]; [_playButton release]; [super dealloc]; } - (void)setArea:(NSTrackingArea *)area { if (area == _area) return; [_area release]; _area = [area retain]; } - (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] & NSEventModifierFlagOption) [self setVolume:[self maxVolume]]; else [self incrementVolume]; return; case NSDownArrowFunctionKey: if ([event modifierFlags] & NSEventModifierFlagOption) [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]; [[NSRunLoop currentRunLoop] addTimer:_timelineUpdateTimer forMode:NSRunLoopCommonModes]; } - (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]; [[window animator] setAlphaValue:1]; [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]; [[[self window] animator] setAlphaValue:0]; [self performSelector:@selector(unscheduleTimeUpdate) withObject:nil afterDelay:1]; } - (void)closeWindow { [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(fadeWindowOut) object:nil]; [self unscheduleTimeUpdate]; NSWindow *window = [self window]; [[window contentView] removeTrackingArea:_area]; [self setArea:nil]; [window close]; [window setDelegate:nil]; [self setWindow:nil]; } static NSControl *createMediaUIControl(MediaUIControl controlType) { switch (controlType) { case MediaUIControl::Timeline: { NSSlider *slider = [[QTHUDTimeline alloc] init]; [[slider cell] setContinuous:YES]; return slider; } case MediaUIControl::Slider: { NSButton *slider = [[QTHUDSlider alloc] init]; [[slider cell] setContinuous:YES]; return slider; } case MediaUIControl::PlayPauseButton: { NSButton *button = [[QTHUDButton alloc] init]; [button setImage:[NSImage imageNamed:@"NSPlayTemplate"]]; [button setAlternateImage:[NSImage imageNamed:@"NSPauseQTPrivateTemplate"]]; [[button cell] setShowsStateBy:NSContentsCellMask]; [button setBordered:NO]; return button; } case MediaUIControl::ExitFullscreenButton: { NSButton *button = [[QTHUDButton alloc] init]; [button setImage:[NSImage imageNamed:@"NSExitFullScreenTemplate"]]; [button setBordered:NO]; return button; } case MediaUIControl::RewindButton: { NSButton *button = [[QTHUDButton alloc] init]; [button setImage:[NSImage imageNamed:@"NSRewindTemplate"]]; [button setBordered:NO]; return button; } case MediaUIControl::FastForwardButton: { NSButton *button = [[QTHUDButton alloc] init]; [button setImage:[NSImage imageNamed:@"NSFastForwardTemplate"]]; [button setBordered:NO]; return button; } case MediaUIControl::VolumeUpButton: { NSButton *button = [[QTHUDButton alloc] init]; [button setImage:[NSImage imageNamed:@"NSAudioOutputVolumeHighTemplate"]]; [button setBordered:NO]; return button; } case MediaUIControl::VolumeDownButton: { NSButton *button = [[QTHUDButton alloc] init]; [button setImage:[NSImage imageNamed:@"NSAudioOutputVolumeLowTemplate"]]; [button setBordered:NO]; return button; } } ASSERT_NOT_REACHED(); return nil; } static NSControl *createControlWithMediaUIControlType(MediaUIControl controlType, NSRect frame) { NSControl *control = createMediaUIControl(controlType); control.frame = frame; return control; } 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; } static NSView *createMediaUIBackgroundView() { id view = [[QTHUDBackgroundView alloc] init]; const CGFloat quickTimePlayerHUDHeight = 59; const CGFloat quickTimePlayerHUDContentBorderPosition = 38; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" [view setContentBorderPosition:quickTimePlayerHUDContentBorderPosition / quickTimePlayerHUDHeight]; #pragma clang diagnostic pop return view; } - (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); NSView *background = createMediaUIBackgroundView(); [window setContentView:background]; _area = [[NSTrackingArea alloc] initWithRect:[background bounds] options:NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways owner:self userInfo:nil]; [background addTrackingArea:_area]; [background release]; NSView *contentView = [window contentView]; CGFloat center = CGFloor((windowWidth - playButtonWidth) / 2); _playButton = (NSButton *)createControlWithMediaUIControlType(MediaUIControl::PlayPauseButton, 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(MediaUIControl::ExitFullscreenButton, 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(MediaUIControl::VolumeDownButton, NSMakeRect(left, volumeControlsBottom, volumeButtonWidth, volumeButtonHeight)); [contentView addSubview:volumeDownButton]; [volumeDownButton setTarget:self]; [volumeDownButton setAction:@selector(setVolumeToZero:)]; [volumeDownButton release]; left += volumeButtonWidth; _volumeSlider = createControlWithMediaUIControlType(MediaUIControl::Slider, NSMakeRect(left, volumeControlsBottom + 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(MediaUIControl::VolumeUpButton, NSMakeRect(left, volumeControlsBottom, volumeButtonWidth, volumeButtonHeight)); [volumeUpButton setTarget:self]; [volumeUpButton setAction:@selector(setVolumeToMaximum:)]; [contentView addSubview:volumeUpButton]; [volumeUpButton release]; _timeline = createMediaUIControl(MediaUIControl::Timeline); [_timeline setTarget:self]; [_timeline setAction:@selector(timelinePositionChanged:)]; [_timeline setFrame:NSMakeRect(CGFloor((windowWidth - timelineWidth) / 2), timelineBottomMargin, timelineWidth, timelineHeight)]; [contentView addSubview:_timeline]; _elapsedTimeText = createTimeTextField(NSMakeRect(timeTextFieldHorizontalMargin, timelineBottomMargin, timeTextFieldWidth, timeTextFieldHeight)); [_elapsedTimeText setAlignment:NSTextAlignmentLeft]; [contentView addSubview:_elapsedTimeText]; _remainingTimeText = createTimeTextField(NSMakeRect(windowWidth - timeTextFieldHorizontalMargin - timeTextFieldWidth, timelineBottomMargin, timeTextFieldWidth, timeTextFieldHeight)); [_remainingTimeText setAlignment:NSTextAlignmentRight]; [contentView addSubview:_remainingTimeText]; [window recalculateKeyViewLoop]; [window setInitialFirstResponder:_playButton]; [window center]; } - (void)updateVolume { [_volumeSlider setFloatValue:[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 (HTMLVideoElement* videoElement = [_delegate videoElement]) videoElement->endScrubbing(); } - (void)timelinePositionChanged:(id)sender { UNUSED_PARAM(sender); [self setCurrentTime:[_timeline floatValue]]; if (!_isScrubbing) { _isScrubbing = YES; if (HTMLVideoElement* videoElement = [_delegate videoElement]) videoElement->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 videoElement] ? [_delegate videoElement]->currentTime() : 0; } - (void)setCurrentTime:(float)currentTime { if (![_delegate videoElement]) return; [_delegate videoElement]->setCurrentTime(currentTime); [self updateTime]; } - (double)duration { return [_delegate videoElement] ? [_delegate videoElement]->duration() : 0; } - (float)maxVolume { // Set the volume slider resolution return 100; } - (void)volumeChanged:(id)sender { UNUSED_PARAM(sender); [self setVolume:[_volumeSlider floatValue]]; } - (void)setVolumeToZero:(id)sender { UNUSED_PARAM(sender); [self setVolume:0]; } - (void)setVolumeToMaximum:(id)sender { UNUSED_PARAM(sender); [self setVolume:[self maxVolume]]; } - (void)decrementVolume { if (![_delegate videoElement]) return; float volume = [self volume] - 10; [self setVolume:MAX(volume, 0)]; } - (void)incrementVolume { if (![_delegate videoElement]) return; float volume = [self volume] + 10; [self setVolume:std::min(volume, [self maxVolume])]; } - (float)volume { return [_delegate videoElement] ? [_delegate videoElement]->volume() * [self maxVolume] : 0; } - (void)setVolume:(float)volume { if (![_delegate videoElement]) return; if ([_delegate videoElement]->muted()) [_delegate videoElement]->setMuted(false); [_delegate videoElement]->setVolume(volume / [self maxVolume]); [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 { UNUSED_PARAM(sender); [self setPlaying:![self playing]]; } - (BOOL)playing { HTMLVideoElement* videoElement = [_delegate videoElement]; if (!videoElement) return NO; return !videoElement->canPlay(); } - (void)setPlaying:(BOOL)playing { HTMLVideoElement* videoElement = [_delegate videoElement]; if (!videoElement) return; if (playing) videoElement->play(); else videoElement->pause(); } static NSString *timeToString(double time) { ASSERT_ARG(time, time >= 0); if (!std::isfinite(time)) time = 0; int seconds = narrowPrecisionToFloat(std::abs(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 { HTMLVideoElement* videoElement = [_delegate videoElement]; if (!videoElement) return @""; double remainingTime = 0; if (std::isfinite(videoElement->duration()) && std::isfinite(videoElement->currentTime())) remainingTime = videoElement->duration() - videoElement->currentTime(); return [@"-" stringByAppendingString:timeToString(remainingTime)]; } - (NSString *)elapsedTimeText { if (![_delegate videoElement]) return @""; return timeToString([_delegate videoElement]->currentTime()); } // MARK: NSResponder - (void)mouseEntered:(NSEvent *)theEvent { UNUSED_PARAM(theEvent); // Make sure the HUD won't be hidden from now _mouseIsInHUD = YES; [self fadeWindowIn]; } - (void)mouseExited:(NSEvent *)theEvent { UNUSED_PARAM(theEvent); _mouseIsInHUD = NO; [self fadeWindowIn]; } - (void)rewind:(id)sender { UNUSED_PARAM(sender); if (![_delegate videoElement]) return; [_delegate videoElement]->rewind(30); } - (void)fastForward:(id)sender { UNUSED_PARAM(sender); if (![_delegate videoElement]) return; } - (void)exitFullscreen:(id)sender { UNUSED_PARAM(sender); if (_isEndingFullscreen) return; _isEndingFullscreen = YES; [_delegate requestExitFullscreen]; } // MARK: NSWindowDelegate - (void)windowDidExpose:(NSNotification *)notification { UNUSED_PARAM(notification); [self scheduleTimeUpdate]; } - (void)windowDidClose:(NSNotification *)notification { UNUSED_PARAM(notification); [self unscheduleTimeUpdate]; } @end #endif