2009-10-15 Eric Carlson <eric.carlson@apple.com>
[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 - (double)maxVolume;
49 - (void)volumeChanged:(id)sender;
50 - (double)volume;
51 - (void)setVolume:(double)volume;
52
53 - (void)playingChanged:(id)sender;
54 - (BOOL)playing;
55 - (void)setPlaying:(BOOL)playing;
56
57 - (void)rewind:(id)sender;
58 - (void)fastForward:(id)sender;
59
60 - (NSString *)remainingTimeText;
61 - (NSString *)elapsedTimeText;
62
63 - (void)exitFullscreen:(id)sender;
64 @end
65
66
67 //
68 // HUD Window
69 //
70
71 @interface WebVideoFullscreenHUDWindow : NSWindow
72 @end
73
74 @implementation WebVideoFullscreenHUDWindow
75
76 - (id)initWithContentRect:(NSRect)contentRect styleMask:(NSUInteger)aStyle backing:(NSBackingStoreType)bufferingType defer:(BOOL)flag
77 {
78     UNUSED_PARAM(aStyle);
79     self = [super initWithContentRect:contentRect styleMask:NSBorderlessWindowMask backing:bufferingType defer:flag];
80     if (!self)
81         return nil;
82
83     [self setOpaque:NO];
84     [self setBackgroundColor:[NSColor clearColor]];
85     [self setLevel:NSPopUpMenuWindowLevel];
86     [self setAcceptsMouseMovedEvents:YES];
87     [self setIgnoresMouseEvents:NO];
88     [self setMovableByWindowBackground:YES];
89     [self setHidesOnDeactivate:YES];
90
91     return self;
92 }
93
94 - (BOOL)canBecomeKeyWindow
95 {
96     return YES;
97 }
98
99 - (void)cancelOperation:(id)sender
100 {
101     [[self windowController] exitFullscreen:self];
102 }
103
104 - (void)center
105 {
106     NSRect hudFrame = [self frame];
107     NSRect screenFrame = [[NSScreen mainScreen] frame];
108     [self setFrameTopLeftPoint:NSMakePoint(screenFrame.origin.x + (screenFrame.size.width - hudFrame.size.width) / 2,
109                                            screenFrame.origin.y + (screenFrame.size.height - hudFrame.size.height) / 6)];
110 }
111
112 - (void)keyDown:(NSEvent *)event
113 {
114     [super keyDown:event];
115     [[self windowController] fadeWindowIn];
116 }
117
118 @end
119
120 //
121 // HUD Window Controller
122 //
123
124 static const CGFloat windowHeight = 59;
125 static const CGFloat windowWidth = 438;
126
127 static const NSTimeInterval HUDWindowFadeOutDelay = 3;
128
129 @implementation WebVideoFullscreenHUDWindowController
130
131 - (id)init
132 {
133     NSWindow* window = [[WebVideoFullscreenHUDWindow alloc] initWithContentRect:NSMakeRect(0, 0, windowWidth, windowHeight)
134                             styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO];
135     self = [super initWithWindow:window];
136     [window setDelegate:self];
137     [window release];
138     if (!self)
139         return nil;
140     [self windowDidLoad];
141     return self;
142 }
143
144 - (void)dealloc
145 {
146     ASSERT(!_timelineUpdateTimer);
147 #if !defined(BUILDING_ON_TIGER)
148     ASSERT(!_area);
149 #endif
150     [_timeline release];
151     [_remainingTimeText release];
152     [_elapsedTimeText release];
153     [_volumeSlider release];
154     [_playButton release];
155     [super dealloc];
156 }
157
158 #if !defined(BUILDING_ON_TIGER)
159 - (void)setArea:(NSTrackingArea *)area
160 {
161     if (area == _area)
162         return;
163     [_area release];
164     _area = [area retain];
165 }
166 #endif
167
168 - (id<WebVideoFullscreenHUDWindowControllerDelegate>)delegate
169 {
170     return _delegate;
171 }
172      
173 - (void)setDelegate:(id<WebVideoFullscreenHUDWindowControllerDelegate>)delegate
174 {
175     _delegate = delegate;
176 }
177
178 - (void)scheduleTimeUpdate
179 {
180     [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(unscheduleTimeUpdate) object:self];
181
182     // First, update right away, then schedule future update
183     [self updateTime];
184
185     [_timelineUpdateTimer invalidate];
186     [_timelineUpdateTimer release];
187
188     // Note that this creates a retain cycle between the window and us.
189     _timelineUpdateTimer = [[NSTimer timerWithTimeInterval:0.25 target:self selector:@selector(updateTime) userInfo:nil repeats:YES] retain];
190 #if defined(BUILDING_ON_TIGER)
191     [[NSRunLoop currentRunLoop] addTimer:_timelineUpdateTimer forMode:(NSString*)kCFRunLoopCommonModes];
192 #else
193     [[NSRunLoop currentRunLoop] addTimer:_timelineUpdateTimer forMode:NSRunLoopCommonModes];
194 #endif
195 }
196
197 - (void)unscheduleTimeUpdate
198 {
199     [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(unscheduleTimeUpdate) object:nil];
200
201     [_timelineUpdateTimer invalidate];
202     [_timelineUpdateTimer release];
203     _timelineUpdateTimer = nil;
204 }
205
206 - (void)fadeWindowIn
207 {
208     NSWindow *window = [self window];
209     if (![window isVisible])
210         [window setAlphaValue:0];
211
212     [window makeKeyAndOrderFront:self];
213 #if defined(BUILDING_ON_TIGER)
214     [window setAlphaValue:1];
215 #else
216     [[window animator] setAlphaValue:1];
217 #endif
218     [self scheduleTimeUpdate];
219
220     [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(fadeWindowOut) object:nil];
221     if (!_mouseIsInHUD && [self playing])   // Don't fade out when paused.
222         [self performSelector:@selector(fadeWindowOut) withObject:nil afterDelay:HUDWindowFadeOutDelay];
223 }
224
225 - (void)fadeWindowOut
226 {
227     [NSCursor setHiddenUntilMouseMoves:YES];
228 #if defined(BUILDING_ON_TIGER)
229     [[self window] setAlphaValue:0];
230 #else
231     [[[self window] animator] setAlphaValue:0];
232 #endif
233     [self performSelector:@selector(unscheduleTimeUpdate) withObject:nil afterDelay:1];
234 }
235
236 - (void)closeWindow
237 {
238     [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(fadeWindowOut) object:nil];
239     [self unscheduleTimeUpdate];
240     NSWindow *window = [self window];
241 #if !defined(BUILDING_ON_TIGER)
242     [[window contentView] removeTrackingArea:_area];
243     [self setArea:nil];
244 #endif
245     [window close];
246     [window setDelegate:nil];
247     [self setWindow:nil];
248 }
249
250 #ifndef HAVE_MEDIA_CONTROL
251 enum {
252     WKMediaUIControlPlayPauseButton,
253     WKMediaUIControlRewindButton,
254     WKMediaUIControlFastForwardButton,
255     WKMediaUIControlExitFullscreenButton,
256     WKMediaUIControlVolumeDownButton,
257     WKMediaUIControlSlider,
258     WKMediaUIControlVolumeUpButton,
259     WKMediaUIControlTimeline
260 };
261 #endif
262
263 static NSControl *createControlWithMediaUIControlType(int controlType, NSRect frame)
264 {
265 #ifdef HAVE_MEDIA_CONTROL
266     NSControl *control = WKCreateMediaUIControl(controlType);
267     [control setFrame:frame];
268     return control;
269 #else
270     if (controlType == WKMediaUIControlSlider)
271         return [[NSSlider alloc] initWithFrame:frame];
272     return [[NSControl alloc] initWithFrame:frame];
273 #endif
274 }
275
276 static NSTextField *createTimeTextField(NSRect frame)
277 {
278     NSTextField *textField = [[NSTextField alloc] initWithFrame:frame];
279     [textField setTextColor:[NSColor whiteColor]];
280     [textField setBordered:NO];
281     [textField setFont:[NSFont systemFontOfSize:10]];
282     [textField setDrawsBackground:NO];
283     [textField setBezeled:NO];
284     [textField setEditable:NO];
285     [textField setSelectable:NO];
286     return textField;
287 }
288
289 - (void)windowDidLoad
290 {
291     static const CGFloat kMargin = 9;
292     static const CGFloat kMarginTop = 9;
293     static const CGFloat kButtonSize = 25;
294     static const CGFloat kButtonMiniSize = 16;
295
296     NSWindow *window = [self window];
297     ASSERT(window);
298
299 #ifdef HAVE_MEDIA_CONTROL
300     NSView *background = WKCreateMediaUIBackgroundView();
301 #else
302     NSView *background = [[NSView alloc] init];
303 #endif
304     [window setContentView:background];
305 #if !defined(BUILDING_ON_TIGER)
306     _area = [[NSTrackingArea alloc] initWithRect:[background bounds] options:NSTrackingMouseEnteredAndExited|NSTrackingActiveAlways owner:self userInfo:nil];
307     [background addTrackingArea:_area];
308 #endif
309     [background release];    
310
311     NSView *contentView = [[self window] contentView];
312
313     CGFloat top = windowHeight - kMarginTop;
314     CGFloat center = (windowWidth - kButtonSize) / 2;
315     _playButton = createControlWithMediaUIControlType(WKMediaUIControlPlayPauseButton, NSMakeRect(center, top - kButtonSize, kButtonSize, kButtonSize));
316     [_playButton setTarget:self];
317     [_playButton setAction:@selector(playingChanged:)];
318     [contentView addSubview:_playButton];
319
320     CGFloat closeToRight = windowWidth - 2 * kMargin - kButtonMiniSize;
321     NSControl *exitFullscreenButton = createControlWithMediaUIControlType(WKMediaUIControlExitFullscreenButton, NSMakeRect(closeToRight, top - kButtonSize / 2 - kButtonMiniSize / 2, kButtonMiniSize, kButtonMiniSize));
322     [exitFullscreenButton setAction:@selector(exitFullscreen:)];
323     [exitFullscreenButton setTarget:self];
324     [contentView addSubview:exitFullscreenButton];
325     [exitFullscreenButton release];
326     
327     CGFloat left = kMargin;
328     NSControl *volumeDownButton = createControlWithMediaUIControlType(WKMediaUIControlVolumeDownButton, NSMakeRect(left, top - kButtonSize / 2 - kButtonMiniSize / 2, kButtonMiniSize, kButtonMiniSize));
329     [contentView addSubview:volumeDownButton];
330     [volumeDownButton setTarget:self];
331     [volumeDownButton setAction:@selector(decrementVolume:)];
332     [volumeDownButton release];
333
334     static const int volumeSliderWidth = 50;
335
336     left = kMargin + kButtonMiniSize;
337     _volumeSlider = createControlWithMediaUIControlType(WKMediaUIControlSlider, NSMakeRect(left, top - kButtonSize / 2 - kButtonMiniSize / 2, volumeSliderWidth, kButtonMiniSize));
338     [_volumeSlider setValue:[NSNumber numberWithDouble:[self maxVolume]] forKey:@"maxValue"];
339     [_volumeSlider setTarget:self];
340     [_volumeSlider setAction:@selector(volumeChanged:)];
341     [contentView addSubview:_volumeSlider];
342
343     left = kMargin + kButtonMiniSize + volumeSliderWidth + kButtonMiniSize / 2;
344     NSControl *volumeUpButton = createControlWithMediaUIControlType(WKMediaUIControlVolumeUpButton, NSMakeRect(left, top - kButtonSize / 2 - kButtonMiniSize / 2, kButtonMiniSize, kButtonMiniSize));
345     [volumeUpButton setTarget:self];
346     [volumeUpButton setAction:@selector(incrementVolume:)];
347     [contentView addSubview:volumeUpButton];
348     [volumeUpButton release];
349     
350     static const int timeTextWidth = 50;
351     static const int sliderHeight = 13;
352     static const int sliderMarginFixup = 4;
353
354 #ifdef HAVE_MEDIA_CONTROL
355     _timeline = WKCreateMediaUIControl(WKMediaUIControlTimeline);
356 #else
357     _timeline = [[NSSlider alloc] init];
358 #endif
359     [_timeline setTarget:self];
360     [_timeline setAction:@selector(timelinePositionChanged:)];
361     [_timeline setFrame:NSMakeRect(kMargin + timeTextWidth + kMargin/2, kMargin - sliderMarginFixup, windowWidth - 2 * (kMargin - sliderMarginFixup) - kMargin * 2 - 2 * timeTextWidth, sliderHeight)];
362     [contentView addSubview:_timeline];
363
364     static const int timeTextHeight = 11;
365
366     _elapsedTimeText = createTimeTextField(NSMakeRect(kMargin, kMargin, timeTextWidth, timeTextHeight));
367     [contentView addSubview:_elapsedTimeText];
368
369     _remainingTimeText = createTimeTextField(NSMakeRect(windowWidth - kMargin - timeTextWidth, kMargin, timeTextWidth, timeTextHeight));
370     [contentView addSubview:_remainingTimeText];
371     
372     [window recalculateKeyViewLoop];
373     [window setInitialFirstResponder:_playButton];
374     [window center];
375 }
376                                 
377 /*
378  *  Bindings
379  *
380  */
381
382 - (void)updateVolume
383 {
384     [_volumeSlider setDoubleValue:[self volume]];
385 }
386
387 - (void)updateTime
388 {
389     [self updateVolume];
390
391     [_timeline setFloatValue:[self currentTime]];
392     [(NSSlider*)_timeline setMaxValue:[self duration]];
393
394     [_remainingTimeText setStringValue:[self remainingTimeText]];
395     [_elapsedTimeText setStringValue:[self elapsedTimeText]];
396 }
397
398 - (void)fastForward
399 {
400 }
401
402 - (void)timelinePositionChanged:(id)sender
403 {
404     [self setCurrentTime:[_timeline floatValue]];
405 }
406
407 - (float)currentTime
408 {
409     return [_delegate mediaElement] ? [_delegate mediaElement]->currentTime() : 0;
410 }
411
412 - (void)setCurrentTime:(float)currentTime
413 {
414     if (![_delegate mediaElement])
415         return;
416     WebCore::ExceptionCode e;
417     [_delegate mediaElement]->setCurrentTime(currentTime, e);
418 }
419
420 - (double)duration
421 {
422     return [_delegate mediaElement] ? [_delegate mediaElement]->duration() : 0;
423 }
424
425 - (double)maxVolume
426 {
427     // Set the volume slider resolution
428     return 100;
429 }
430
431 - (void)volumeChanged:(id)sender
432 {
433     [self setVolume:[_volumeSlider doubleValue]];
434 }
435
436 - (void)decrementVolume:(id)sender
437 {
438     if (![_delegate mediaElement])
439         return;
440
441     double volume = [self volume] - 10;
442     [self setVolume:max(volume, 0.)];
443 }
444
445 - (void)incrementVolume:(id)sender
446 {
447     if (![_delegate mediaElement])
448         return;
449
450     double volume = [self volume] + 10;
451     [self setVolume:min(volume, [self maxVolume])];
452 }
453
454 - (double)volume
455 {
456     return [_delegate mediaElement] ? [_delegate mediaElement]->volume() * [self maxVolume] : 0;
457 }
458
459 - (void)setVolume:(double)volume
460 {
461     if (![_delegate mediaElement])
462         return;
463     WebCore::ExceptionCode e;
464     if ([_delegate mediaElement]->muted())
465         [_delegate mediaElement]->setMuted(false);
466     [_delegate mediaElement]->setVolume(volume / [self maxVolume], e);
467 }
468
469 - (void)playingChanged:(id)sender
470 {
471     [self setPlaying:![self playing]];
472     
473     // Keep HUD visible when paused
474     if (![self playing])
475         [self fadeWindowIn];
476     else if (!_mouseIsInHUD) {
477         [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(fadeWindowOut) object:nil];
478         [self performSelector:@selector(fadeWindowOut) withObject:nil afterDelay:HUDWindowFadeOutDelay];
479     }
480 }
481
482 - (BOOL)playing
483 {
484     if (![_delegate mediaElement])
485         return false;
486     return ![_delegate mediaElement]->canPlay();
487 }
488
489 - (void)setPlaying:(BOOL)playing
490 {
491     if (![_delegate mediaElement])
492         return;
493
494     if (playing)
495         [_delegate mediaElement]->play();
496     else
497         [_delegate mediaElement]->pause();
498 }
499
500 static NSString *timeToString(double time)
501 {
502     if (!isfinite(time))
503         time = 0;
504     int seconds = (int)fabsf(time); 
505     int hours = seconds / (60 * 60);
506     int minutes = (seconds / 60) % 60;
507     seconds %= 60;
508     if (hours) {
509         if (hours > 9)
510             return [NSString stringWithFormat:@"%s%02d:%02d:%02d", (time < 0 ? "-" : ""), hours, minutes, seconds];
511         else
512             return [NSString stringWithFormat:@"%s%01d:%02d:%02d", (time < 0 ? "-" : ""), hours, minutes, seconds];
513     }
514     else
515         return [NSString stringWithFormat:@"%s%02d:%02d", (time < 0 ? "-" : ""), minutes, seconds];
516     
517 }
518
519 static NSString *stringToTimeTextAttributed(NSString *string, NSTextAlignment align)
520 {
521     NSShadow *blackShadow = [[NSShadow alloc] init];
522     [blackShadow setShadowColor:[NSColor blackColor]];
523     [blackShadow setShadowBlurRadius:0];
524     [blackShadow setShadowOffset:NSMakeSize(0, -1)];
525     NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init];
526     [style setAlignment:align];
527     NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:blackShadow, NSShadowAttributeName, style, NSParagraphStyleAttributeName, nil];
528     [style release];
529     [blackShadow release];
530
531     NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:string attributes:dict];
532     return [attrString autorelease];    
533 }
534
535 - (NSString *)remainingTimeText
536 {
537     if (![_delegate mediaElement])
538         return @"";
539
540     // Negative number
541     return stringToTimeTextAttributed(timeToString([_delegate mediaElement]->currentTime() - [_delegate mediaElement]->duration()), NSLeftTextAlignment);
542 }
543
544 - (NSString *)elapsedTimeText
545 {
546     if (![_delegate mediaElement])
547         return @"";
548
549     return stringToTimeTextAttributed(timeToString([_delegate mediaElement]->currentTime()), NSRightTextAlignment);
550 }
551
552 /*
553  *  Tracking area callbacks
554  *
555  */
556
557 - (void)mouseEntered:(NSEvent *)theEvent
558 {
559     // Make sure the HUD won't be hidden from now
560     _mouseIsInHUD = YES;
561     [self fadeWindowIn];
562 }
563
564 - (void)mouseExited:(NSEvent *)theEvent
565 {
566     _mouseIsInHUD = NO;
567     [self fadeWindowIn];
568 }
569
570 /*
571  *  Other Interface callbacks
572  *
573  */
574
575 - (void)rewind:(id)sender
576 {
577     if (![_delegate mediaElement])
578         return;
579     [_delegate mediaElement]->rewind(30);
580 }
581
582 - (void)fastForward:(id)sender
583 {
584     if (![_delegate mediaElement])
585         return;
586 }
587
588 - (void)exitFullscreen:(id)sender
589 {
590     [_delegate requestExitFullscreen]; 
591 }
592
593 /*
594  *  Window callback
595  *
596  */
597
598 - (void)windowDidExpose:(NSNotification *)notification
599 {
600     [self scheduleTimeUpdate];
601 }
602
603 - (void)windowDidClose:(NSNotification *)notification
604 {
605     [self unscheduleTimeUpdate];
606 }
607
608 @end
609
610 #endif