394f0b281e04f4aeffdda2ded937f8385c3d67df
[WebKit-https.git] / WebKit / mac / WebView / WebVideoFullscreenHUDWindowController.mm
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 COMPUTER, INC. ``AS IS'' AND ANY
14  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE COMPUTER, INC. OR
17  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21  * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
24  */
25
26 #if ENABLE(VIDEO)
27
28 #import "WebVideoFullscreenHUDWindowController.h"
29
30 #import <QTKit/QTKit.h>
31 #import "WebKitSystemInterface.h"
32 #import "WebTypesInternal.h"
33 #import <wtf/RetainPtr.h>
34 #import <limits>
35
36 using namespace std;
37
38 #define HAVE_MEDIA_CONTROL (!defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD))
39
40 @interface WebVideoFullscreenHUDWindowController (Private) <NSWindowDelegate>
41
42 - (void)updateTime;
43 - (void)timelinePositionChanged:(id)sender;
44 - (float)currentTime;
45 - (void)setCurrentTime:(float)currentTime;
46 - (double)duration;
47
48 - (void)volumeChanged:(id)sender;
49 - (double)maxVolume;
50 - (double)volume;
51 - (void)setVolume:(double)volume;
52 - (void)decrementVolume;
53 - (void)incrementVolume;
54
55 - (void)togglePlaying:(id)sender;
56 - (BOOL)playing;
57 - (void)setPlaying:(BOOL)playing;
58
59 - (void)rewind:(id)sender;
60 - (void)fastForward:(id)sender;
61
62 - (NSString *)remainingTimeText;
63 - (NSString *)elapsedTimeText;
64
65 - (void)exitFullscreen:(id)sender;
66 @end
67
68
69 //
70 // HUD Window
71 //
72
73 @interface WebVideoFullscreenHUDWindow : NSWindow
74 @end
75
76 @implementation WebVideoFullscreenHUDWindow
77
78 - (id)initWithContentRect:(NSRect)contentRect styleMask:(NSUInteger)aStyle backing:(NSBackingStoreType)bufferingType defer:(BOOL)flag
79 {
80     UNUSED_PARAM(aStyle);
81     self = [super initWithContentRect:contentRect styleMask:NSBorderlessWindowMask backing:bufferingType defer:flag];
82     if (!self)
83         return nil;
84
85     [self setOpaque:NO];
86     [self setBackgroundColor:[NSColor clearColor]];
87     [self setLevel:NSPopUpMenuWindowLevel];
88     [self setAcceptsMouseMovedEvents:YES];
89     [self setIgnoresMouseEvents:NO];
90     [self setMovableByWindowBackground:YES];
91     [self setHidesOnDeactivate:YES];
92
93     return self;
94 }
95
96 - (BOOL)canBecomeKeyWindow
97 {
98     return YES;
99 }
100
101 - (void)cancelOperation:(id)sender
102 {
103     [[self windowController] exitFullscreen:self];
104 }
105
106 - (void)center
107 {
108     NSRect hudFrame = [self frame];
109     NSRect screenFrame = [[NSScreen mainScreen] frame];
110     [self setFrameTopLeftPoint:NSMakePoint(screenFrame.origin.x + (screenFrame.size.width - hudFrame.size.width) / 2,
111                                            screenFrame.origin.y + (screenFrame.size.height - hudFrame.size.height) / 6)];
112 }
113
114 - (void)keyDown:(NSEvent *)event
115 {
116     [super keyDown:event];
117     [[self windowController] fadeWindowIn];
118 }
119
120 - (BOOL)resignFirstResponder
121 {
122     return NO;
123 }
124
125 - (BOOL)performKeyEquivalent:(NSEvent *)event
126 {
127     // Block all command key events while the fullscreen window is up.
128     if ([event type] != NSKeyDown)
129         return NO;
130     
131     if (!([event modifierFlags] & NSCommandKeyMask))
132         return NO;
133     
134     return YES;
135 }
136
137 @end
138
139 //
140 // HUD Window Controller
141 //
142
143 static const CGFloat windowHeight = 59;
144 static const CGFloat windowWidth = 438;
145
146 static const NSTimeInterval HUDWindowFadeOutDelay = 3;
147
148 @implementation WebVideoFullscreenHUDWindowController
149
150 - (id)init
151 {
152     NSWindow* window = [[WebVideoFullscreenHUDWindow alloc] initWithContentRect:NSMakeRect(0, 0, windowWidth, windowHeight)
153                             styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO];
154     self = [super initWithWindow:window];
155     [window setDelegate:self];
156     [window release];
157     if (!self)
158         return nil;
159     [self windowDidLoad];
160     return self;
161 }
162
163 - (void)dealloc
164 {
165     ASSERT(!_timelineUpdateTimer);
166 #if !defined(BUILDING_ON_TIGER)
167     ASSERT(!_area);
168 #endif
169     [_timeline release];
170     [_remainingTimeText release];
171     [_elapsedTimeText release];
172     [_volumeSlider release];
173     [_playButton release];
174     [super dealloc];
175 }
176
177 #if !defined(BUILDING_ON_TIGER)
178 - (void)setArea:(NSTrackingArea *)area
179 {
180     if (area == _area)
181         return;
182     [_area release];
183     _area = [area retain];
184 }
185 #endif
186
187 - (void)keyDown:(NSEvent *)event
188 {
189     NSString *charactersIgnoringModifiers = [event charactersIgnoringModifiers];
190     if ([charactersIgnoringModifiers length] == 1) {
191         switch ([charactersIgnoringModifiers characterAtIndex:0]) {
192             case ' ':
193                 [self togglePlaying:nil];
194                 return;
195             case NSUpArrowFunctionKey:
196                 if ([event modifierFlags] & NSAlternateKeyMask)
197                     [self setVolume:[self maxVolume]];
198                 else
199                     [self incrementVolume];
200                 return;
201             case NSDownArrowFunctionKey:
202                 if ([event modifierFlags] & NSAlternateKeyMask)
203                     [self setVolume:0];
204                 else
205                     [self decrementVolume];
206                 return;
207             default:
208                 break;
209         }
210     }
211
212     [super keyDown:event];
213 }
214
215 - (id<WebVideoFullscreenHUDWindowControllerDelegate>)delegate
216 {
217     return _delegate;
218 }
219
220 - (void)setDelegate:(id<WebVideoFullscreenHUDWindowControllerDelegate>)delegate
221 {
222     _delegate = delegate;
223 }
224
225 - (void)scheduleTimeUpdate
226 {
227     [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(unscheduleTimeUpdate) object:self];
228
229     // First, update right away, then schedule future update
230     [self updateTime];
231     [self updateRate];
232
233     [_timelineUpdateTimer invalidate];
234     [_timelineUpdateTimer release];
235
236     // Note that this creates a retain cycle between the window and us.
237     _timelineUpdateTimer = [[NSTimer timerWithTimeInterval:0.25 target:self selector:@selector(updateTime) userInfo:nil repeats:YES] retain];
238 #if defined(BUILDING_ON_TIGER)
239     [[NSRunLoop currentRunLoop] addTimer:_timelineUpdateTimer forMode:(NSString*)kCFRunLoopCommonModes];
240 #else
241     [[NSRunLoop currentRunLoop] addTimer:_timelineUpdateTimer forMode:NSRunLoopCommonModes];
242 #endif
243 }
244
245 - (void)unscheduleTimeUpdate
246 {
247     [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(unscheduleTimeUpdate) object:nil];
248
249     [_timelineUpdateTimer invalidate];
250     [_timelineUpdateTimer release];
251     _timelineUpdateTimer = nil;
252 }
253
254 - (void)fadeWindowIn
255 {
256     NSWindow *window = [self window];
257     if (![window isVisible])
258         [window setAlphaValue:0];
259
260     [window makeKeyAndOrderFront:self];
261 #if defined(BUILDING_ON_TIGER)
262     [window setAlphaValue:1];
263 #else
264     [[window animator] setAlphaValue:1];
265 #endif
266     [self scheduleTimeUpdate];
267
268     [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(fadeWindowOut) object:nil];
269     if (!_mouseIsInHUD && [self playing])   // Don't fade out when paused.
270         [self performSelector:@selector(fadeWindowOut) withObject:nil afterDelay:HUDWindowFadeOutDelay];
271 }
272
273 - (void)fadeWindowOut
274 {
275     [NSCursor setHiddenUntilMouseMoves:YES];
276 #if defined(BUILDING_ON_TIGER)
277     [[self window] setAlphaValue:0];
278 #else
279     [[[self window] animator] setAlphaValue:0];
280 #endif
281     [self performSelector:@selector(unscheduleTimeUpdate) withObject:nil afterDelay:1];
282 }
283
284 - (void)closeWindow
285 {
286     [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(fadeWindowOut) object:nil];
287     [self unscheduleTimeUpdate];
288     NSWindow *window = [self window];
289 #if !defined(BUILDING_ON_TIGER)
290     [[window contentView] removeTrackingArea:_area];
291     [self setArea:nil];
292 #endif
293     [window close];
294     [window setDelegate:nil];
295     [self setWindow:nil];
296 }
297
298 #ifndef HAVE_MEDIA_CONTROL
299 enum {
300     WKMediaUIControlPlayPauseButton,
301     WKMediaUIControlRewindButton,
302     WKMediaUIControlFastForwardButton,
303     WKMediaUIControlExitFullscreenButton,
304     WKMediaUIControlVolumeDownButton,
305     WKMediaUIControlSlider,
306     WKMediaUIControlVolumeUpButton,
307     WKMediaUIControlTimeline
308 };
309 #endif
310
311 static NSControl *createControlWithMediaUIControlType(int controlType, NSRect frame)
312 {
313 #ifdef HAVE_MEDIA_CONTROL
314     NSControl *control = WKCreateMediaUIControl(controlType);
315     [control setFrame:frame];
316     return control;
317 #else
318     if (controlType == WKMediaUIControlSlider)
319         return [[NSSlider alloc] initWithFrame:frame];
320     return [[NSControl alloc] initWithFrame:frame];
321 #endif
322 }
323
324 static NSTextField *createTimeTextField(NSRect frame)
325 {
326     NSTextField *textField = [[NSTextField alloc] initWithFrame:frame];
327     [textField setTextColor:[NSColor whiteColor]];
328     [textField setBordered:NO];
329     [textField setFont:[NSFont systemFontOfSize:10]];
330     [textField setDrawsBackground:NO];
331     [textField setBezeled:NO];
332     [textField setEditable:NO];
333     [textField setSelectable:NO];
334     return textField;
335 }
336
337 - (void)windowDidLoad
338 {
339     static const CGFloat kMargin = 9;
340     static const CGFloat kMarginTop = 9;
341     static const CGFloat kButtonSize = 25;
342     static const CGFloat kButtonMiniSize = 16;
343
344     NSWindow *window = [self window];
345     ASSERT(window);
346
347 #ifdef HAVE_MEDIA_CONTROL
348     NSView *background = WKCreateMediaUIBackgroundView();
349 #else
350     NSView *background = [[NSView alloc] init];
351 #endif
352     [window setContentView:background];
353 #if !defined(BUILDING_ON_TIGER)
354     _area = [[NSTrackingArea alloc] initWithRect:[background bounds] options:NSTrackingMouseEnteredAndExited|NSTrackingActiveAlways owner:self userInfo:nil];
355     [background addTrackingArea:_area];
356 #endif
357     [background release];    
358
359     NSView *contentView = [[self window] contentView];
360
361     CGFloat top = windowHeight - kMarginTop;
362     CGFloat center = (windowWidth - kButtonSize) / 2;
363     _playButton = createControlWithMediaUIControlType(WKMediaUIControlPlayPauseButton, NSMakeRect(center, top - kButtonSize, kButtonSize, kButtonSize));
364     [_playButton setTarget:self];
365     [_playButton setAction:@selector(togglePlaying:)];
366     [contentView addSubview:_playButton];
367
368     CGFloat closeToRight = windowWidth - 2 * kMargin - kButtonMiniSize;
369     NSControl *exitFullscreenButton = createControlWithMediaUIControlType(WKMediaUIControlExitFullscreenButton, NSMakeRect(closeToRight, top - kButtonSize / 2 - kButtonMiniSize / 2, kButtonMiniSize, kButtonMiniSize));
370     [exitFullscreenButton setAction:@selector(exitFullscreen:)];
371     [exitFullscreenButton setTarget:self];
372     [contentView addSubview:exitFullscreenButton];
373     [exitFullscreenButton release];
374     
375     CGFloat left = kMargin;
376     NSControl *volumeDownButton = createControlWithMediaUIControlType(WKMediaUIControlVolumeDownButton, NSMakeRect(left, top - kButtonSize / 2 - kButtonMiniSize / 2, kButtonMiniSize, kButtonMiniSize));
377     [contentView addSubview:volumeDownButton];
378     [volumeDownButton setTarget:self];
379     [volumeDownButton setAction:@selector(setVolumeToZero:)];
380     [volumeDownButton release];
381
382     static const int volumeSliderWidth = 50;
383
384     left = kMargin + kButtonMiniSize;
385     _volumeSlider = createControlWithMediaUIControlType(WKMediaUIControlSlider, NSMakeRect(left, top - kButtonSize / 2 - kButtonMiniSize / 2, volumeSliderWidth, kButtonMiniSize));
386     [_volumeSlider setValue:[NSNumber numberWithDouble:[self maxVolume]] forKey:@"maxValue"];
387     [_volumeSlider setTarget:self];
388     [_volumeSlider setAction:@selector(volumeChanged:)];
389     [contentView addSubview:_volumeSlider];
390
391     left = kMargin + kButtonMiniSize + volumeSliderWidth + kButtonMiniSize / 2;
392     NSControl *volumeUpButton = createControlWithMediaUIControlType(WKMediaUIControlVolumeUpButton, NSMakeRect(left, top - kButtonSize / 2 - kButtonMiniSize / 2, kButtonMiniSize, kButtonMiniSize));
393     [volumeUpButton setTarget:self];
394     [volumeUpButton setAction:@selector(setVolumeToMaximum:)];
395     [contentView addSubview:volumeUpButton];
396     [volumeUpButton release];
397     
398     static const int timeTextWidth = 50;
399     static const int sliderHeight = 13;
400     static const int sliderMarginFixup = 4;
401
402 #ifdef HAVE_MEDIA_CONTROL
403     _timeline = WKCreateMediaUIControl(WKMediaUIControlTimeline);
404 #else
405     _timeline = [[NSSlider alloc] init];
406 #endif
407     [_timeline setTarget:self];
408     [_timeline setAction:@selector(timelinePositionChanged:)];
409     [_timeline setFrame:NSMakeRect(kMargin + timeTextWidth + kMargin/2, kMargin - sliderMarginFixup, windowWidth - 2 * (kMargin - sliderMarginFixup) - kMargin * 2 - 2 * timeTextWidth, sliderHeight)];
410     [contentView addSubview:_timeline];
411
412     static const int timeTextHeight = 11;
413
414     _elapsedTimeText = createTimeTextField(NSMakeRect(kMargin, kMargin, timeTextWidth, timeTextHeight));
415     [contentView addSubview:_elapsedTimeText];
416
417     _remainingTimeText = createTimeTextField(NSMakeRect(windowWidth - kMargin - timeTextWidth, kMargin, timeTextWidth, timeTextHeight));
418     [contentView addSubview:_remainingTimeText];
419     
420     [window recalculateKeyViewLoop];
421     [window setInitialFirstResponder:_playButton];
422     [window center];
423 }
424                                 
425 /*
426  *  Bindings
427  *
428  */
429
430 - (void)updateVolume
431 {
432     [_volumeSlider setDoubleValue:[self volume]];
433 }
434
435 - (void)updateTime
436 {
437     [self updateVolume];
438
439     [_timeline setFloatValue:[self currentTime]];
440     [(NSSlider*)_timeline setMaxValue:[self duration]];
441
442     [_remainingTimeText setStringValue:[self remainingTimeText]];
443     [_elapsedTimeText setStringValue:[self elapsedTimeText]];
444 }
445
446 - (void)fastForward
447 {
448 }
449
450 - (void)timelinePositionChanged:(id)sender
451 {
452     [self setCurrentTime:[_timeline floatValue]];
453 }
454
455 - (float)currentTime
456 {
457     return [_delegate mediaElement] ? [_delegate mediaElement]->currentTime() : 0;
458 }
459
460 - (void)setCurrentTime:(float)currentTime
461 {
462     if (![_delegate mediaElement])
463         return;
464     WebCore::ExceptionCode e;
465     [_delegate mediaElement]->setCurrentTime(currentTime, e);
466     [self updateTime];
467 }
468
469 - (double)duration
470 {
471     return [_delegate mediaElement] ? [_delegate mediaElement]->duration() : 0;
472 }
473
474 - (double)maxVolume
475 {
476     // Set the volume slider resolution
477     return 100;
478 }
479
480 - (void)volumeChanged:(id)sender
481 {
482     [self setVolume:[_volumeSlider doubleValue]];
483 }
484
485 - (void)setVolumeToZero:(id)sender
486 {
487     [self setVolume:0];
488 }
489
490 - (void)setVolumeToMaximum:(id)sender
491 {
492     [self setVolume:[self maxVolume]];
493 }
494
495 - (void)decrementVolume
496 {
497     if (![_delegate mediaElement])
498         return;
499
500     double volume = [self volume] - 10;
501     [self setVolume:max(volume, 0.)];
502 }
503
504 - (void)incrementVolume
505 {
506     if (![_delegate mediaElement])
507         return;
508
509     double volume = [self volume] + 10;
510     [self setVolume:min(volume, [self maxVolume])];
511 }
512
513 - (double)volume
514 {
515     return [_delegate mediaElement] ? [_delegate mediaElement]->volume() * [self maxVolume] : 0;
516 }
517
518 - (void)setVolume:(double)volume
519 {
520     if (![_delegate mediaElement])
521         return;
522     WebCore::ExceptionCode e;
523     if ([_delegate mediaElement]->muted())
524         [_delegate mediaElement]->setMuted(false);
525     [_delegate mediaElement]->setVolume(volume / [self maxVolume], e);
526     [self updateVolume];
527 }
528
529 - (void)updateRate
530 {
531     [_playButton setIntValue:[self playing]];
532 }
533
534 - (void)togglePlaying:(id)sender
535 {
536     BOOL nowPlaying = [self playing];
537     [self setPlaying:!nowPlaying];
538
539     // Keep HUD visible when paused
540     if (!nowPlaying)
541         [self fadeWindowIn];
542     else if (!_mouseIsInHUD) {
543         [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(fadeWindowOut) object:nil];
544         [self performSelector:@selector(fadeWindowOut) withObject:nil afterDelay:HUDWindowFadeOutDelay];
545     }
546 }
547
548 - (BOOL)playing
549 {
550     if (![_delegate mediaElement])
551         return false;
552     return ![_delegate mediaElement]->canPlay();
553 }
554
555 - (void)setPlaying:(BOOL)playing
556 {
557     if (![_delegate mediaElement])
558         return;
559
560     if (playing)
561         [_delegate mediaElement]->play();
562     else
563         [_delegate mediaElement]->pause();
564 }
565
566 static NSString *timeToString(double time)
567 {
568     if (!isfinite(time))
569         time = 0;
570     int seconds = (int)fabsf(time); 
571     int hours = seconds / (60 * 60);
572     int minutes = (seconds / 60) % 60;
573     seconds %= 60;
574     if (hours) {
575         if (hours > 9)
576             return [NSString stringWithFormat:@"%s%02d:%02d:%02d", (time < 0 ? "-" : ""), hours, minutes, seconds];
577         else
578             return [NSString stringWithFormat:@"%s%01d:%02d:%02d", (time < 0 ? "-" : ""), hours, minutes, seconds];
579     }
580     else
581         return [NSString stringWithFormat:@"%s%02d:%02d", (time < 0 ? "-" : ""), minutes, seconds];
582     
583 }
584
585 static NSString *stringToTimeTextAttributed(NSString *string, NSTextAlignment align)
586 {
587     NSShadow *blackShadow = [[NSShadow alloc] init];
588     [blackShadow setShadowColor:[NSColor blackColor]];
589     [blackShadow setShadowBlurRadius:0];
590     [blackShadow setShadowOffset:NSMakeSize(0, -1)];
591     NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init];
592     [style setAlignment:align];
593     NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:blackShadow, NSShadowAttributeName, style, NSParagraphStyleAttributeName, nil];
594     [style release];
595     [blackShadow release];
596
597     NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:string attributes:dict];
598     return [attrString autorelease];    
599 }
600
601 - (NSString *)remainingTimeText
602 {
603     if (![_delegate mediaElement])
604         return @"";
605
606     // Negative number
607     return stringToTimeTextAttributed(timeToString([_delegate mediaElement]->currentTime() - [_delegate mediaElement]->duration()), NSLeftTextAlignment);
608 }
609
610 - (NSString *)elapsedTimeText
611 {
612     if (![_delegate mediaElement])
613         return @"";
614
615     return stringToTimeTextAttributed(timeToString([_delegate mediaElement]->currentTime()), NSRightTextAlignment);
616 }
617
618 /*
619  *  Tracking area callbacks
620  *
621  */
622
623 - (void)mouseEntered:(NSEvent *)theEvent
624 {
625     // Make sure the HUD won't be hidden from now
626     _mouseIsInHUD = YES;
627     [self fadeWindowIn];
628 }
629
630 - (void)mouseExited:(NSEvent *)theEvent
631 {
632     _mouseIsInHUD = NO;
633     [self fadeWindowIn];
634 }
635
636 /*
637  *  Other Interface callbacks
638  *
639  */
640
641 - (void)rewind:(id)sender
642 {
643     if (![_delegate mediaElement])
644         return;
645     [_delegate mediaElement]->rewind(30);
646 }
647
648 - (void)fastForward:(id)sender
649 {
650     if (![_delegate mediaElement])
651         return;
652 }
653
654 - (void)exitFullscreen:(id)sender
655 {
656     if (_isEndingFullscreen)
657         return;
658     _isEndingFullscreen = YES;
659     [_delegate requestExitFullscreen]; 
660 }
661
662 /*
663  *  Window callback
664  *
665  */
666
667 - (void)windowDidExpose:(NSNotification *)notification
668 {
669     [self scheduleTimeUpdate];
670 }
671
672 - (void)windowDidClose:(NSNotification *)notification
673 {
674     [self unscheduleTimeUpdate];
675 }
676
677 @end
678
679 #endif