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