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