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