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