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