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