From http://www.jwz.org/xscreensaver/xscreensaver-5.31.tar.gz
[xscreensaver] / OSX / SaverRunner.m
1 /* xscreensaver, Copyright (c) 2006-2014 Jamie Zawinski <jwz@jwz.org>
2  *
3  * Permission to use, copy, modify, distribute, and sell this software and its
4  * documentation for any purpose is hereby granted without fee, provided that
5  * the above copyright notice appear in all copies and that both that
6  * copyright notice and this permission notice appear in supporting
7  * documentation.  No representations are made about the suitability of this
8  * software for any purpose.  It is provided "as is" without express or 
9  * implied warranty.
10  */
11
12 /* This program serves three purposes:
13
14    First, It is a test harness for screen savers.  When it launches, it
15    looks around for .saver bundles (in the current directory, and then in
16    the standard directories) and puts up a pair of windows that allow you
17    to select the saver to run.  This is less clicking than running them
18    through System Preferences.  This is the "SaverTester.app" program.
19
20    Second, it can be used to transform any screen saver into a standalone
21    program.  Just put one (and only one) .saver bundle into the app
22    bundle's Contents/Resources/ directory, and it will load and run that
23    saver at start-up (without the saver-selection menu or other chrome).
24    This is how the "Phosphor.app" and "Apple2.app" programs work.
25
26    Third, it is the scaffolding which turns a set of screen savers into
27    a single iPhone / iPad program.  In that case, all of the savers are
28    linked in to this executable, since iOS does not allow dynamic loading
29    of bundles that have executable code in them.  Bleh.
30  */
31
32 #import <TargetConditionals.h>
33 #import "SaverRunner.h"
34 #import "SaverListController.h"
35 #import "XScreenSaverGLView.h"
36 #import "yarandom.h"
37
38 #ifdef USE_IPHONE
39
40 @interface RotateyViewController : UINavigationController
41 {
42   BOOL allowRotation;
43 }
44 @end
45
46 @implementation RotateyViewController
47
48 /* This subclass exists so that we can ask that the SaverListController and
49    preferences panels be auto-rotated by the system.  Note that the 
50    XScreenSaverView is not auto-rotated because it is on a different UIWindow.
51  */
52
53 - (id)initWithRotation:(BOOL)rotatep
54 {
55   self = [super init];
56   allowRotation = rotatep;
57   return self;
58 }
59
60 - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)o
61 {
62   return allowRotation;                         /* Deprecated in iOS 6 */
63 }
64
65 - (BOOL)shouldAutorotate                        /* Added in iOS 6 */
66 {
67   return allowRotation;
68 }
69
70 - (NSUInteger)supportedInterfaceOrientations    /* Added in iOS 6 */
71 {
72   return UIInterfaceOrientationMaskAll;
73 }
74
75 @end
76
77 #endif // USE_IPHONE
78
79
80 @implementation SaverRunner
81
82
83 - (ScreenSaverView *) makeSaverView: (NSString *) module
84                            withSize: (NSSize) size
85 {
86   Class new_class = 0;
87
88 # ifndef USE_IPHONE
89
90   // Load the XScreenSaverView subclass and code from a ".saver" bundle.
91
92   NSString *name = [module stringByAppendingPathExtension:@"saver"];
93   NSString *path = [saverDir stringByAppendingPathComponent:name];
94
95   if (! [[NSFileManager defaultManager] fileExistsAtPath:path]) {
96     NSLog(@"bundle \"%@\" does not exist", path);
97     return 0;
98   }
99
100   NSLog(@"Loading %@", path);
101
102   // NSBundle *obundle = saverBundle;
103
104   saverBundle = [NSBundle bundleWithPath:path];
105   if (saverBundle)
106     new_class = [saverBundle principalClass];
107
108   // Not entirely unsurprisingly, this tends to break the world.
109   // if (obundle && obundle != saverBundle)
110   //  [obundle unload];
111
112 # else  // USE_IPHONE
113
114   // Determine whether to create an X11 view or an OpenGL view by
115   // looking for the "gl" tag in the xml file.  This is kind of awful.
116
117   NSString *path = [saverDir
118                      stringByAppendingPathComponent:
119                        [[[module lowercaseString]
120                           stringByReplacingOccurrencesOfString:@" "
121                           withString:@""]
122                          stringByAppendingPathExtension:@"xml"]];
123   NSData *xmld = [NSData dataWithContentsOfFile:path];
124   NSAssert (xmld, @"no XML: %@", path);
125   NSString *xml = [XScreenSaverView decompressXML:xmld];
126   Bool gl_p = (xml && [xml rangeOfString:@"gl=\"yes\""].length > 0);
127
128   new_class = (gl_p
129                ? [XScreenSaverGLView class]
130                : [XScreenSaverView class]);
131
132 # endif // USE_IPHONE
133
134   if (! new_class)
135     return 0;
136
137   NSRect rect;
138   rect.origin.x = rect.origin.y = 0;
139   rect.size.width  = size.width;
140   rect.size.height = size.height;
141
142   XScreenSaverView *instance =
143     [(XScreenSaverView *) [new_class alloc]
144                           initWithFrame:rect
145                           saverName:module
146                           isPreview:YES];
147   if (! instance) {
148     NSLog(@"Failed to instantiate %@ for \"%@\"", new_class, module);
149     return 0;
150   }
151
152
153   /* KLUGE: Inform the underlying program that we're in "standalone"
154      mode, e.g. running as "Phosphor.app" rather than "Phosphor.saver".
155      This is kind of horrible but I haven't thought of a more sensible
156      way to make this work.
157    */
158 # ifndef USE_IPHONE
159   if ([saverNames count] == 1) {
160     putenv (strdup ("XSCREENSAVER_STANDALONE=1"));
161   }
162 # endif
163
164   return (ScreenSaverView *) instance;
165 }
166
167
168 #ifndef USE_IPHONE
169
170 static ScreenSaverView *
171 find_saverView_child (NSView *v)
172 {
173   NSArray *kids = [v subviews];
174   int nkids = [kids count];
175   int i;
176   for (i = 0; i < nkids; i++) {
177     NSObject *kid = [kids objectAtIndex:i];
178     if ([kid isKindOfClass:[ScreenSaverView class]]) {
179       return (ScreenSaverView *) kid;
180     } else {
181       ScreenSaverView *sv = find_saverView_child ((NSView *) kid);
182       if (sv) return sv;
183     }
184   }
185   return 0;
186 }
187
188
189 static ScreenSaverView *
190 find_saverView (NSView *v)
191 {
192   while (1) {
193     NSView *p = [v superview];
194     if (p) v = p;
195     else break;
196   }
197   return find_saverView_child (v);
198 }
199
200
201 /* Changes the contents of the menubar menus to correspond to
202    the running saver.  Desktop only.
203  */
204 static void
205 relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
206 {
207   if ([v isKindOfClass:[NSMenu class]]) {
208     NSMenu *m = (NSMenu *)v;
209     [m setTitle: [[m title] stringByReplacingOccurrencesOfString:old_str
210                             withString:new_str]];
211     NSArray *kids = [m itemArray];
212     int nkids = [kids count];
213     int i;
214     for (i = 0; i < nkids; i++) {
215       relabel_menus ([kids objectAtIndex:i], old_str, new_str);
216     }
217   } else if ([v isKindOfClass:[NSMenuItem class]]) {
218     NSMenuItem *mi = (NSMenuItem *)v;
219     [mi setTitle: [[mi title] stringByReplacingOccurrencesOfString:old_str
220                               withString:new_str]];
221     NSMenu *m = [mi submenu];
222     if (m) relabel_menus (m, old_str, new_str);
223   }
224 }
225
226
227 - (void) openPreferences: (id) sender
228 {
229   ScreenSaverView *sv;
230   if ([sender isKindOfClass:[NSView class]]) {  // Sent from button
231     sv = find_saverView ((NSView *) sender);
232   } else {
233     int i;
234     NSWindow *w = 0;
235     for (i = [windows count]-1; i >= 0; i--) {  // Sent from menubar
236       w = [windows objectAtIndex:i];
237       if ([w isKeyWindow]) break;
238     }
239     sv = find_saverView ([w contentView]);
240   }
241
242   NSAssert (sv, @"no saver view");
243   if (!sv) return;
244   NSWindow *prefs = [sv configureSheet];
245
246   [NSApp beginSheet:prefs
247      modalForWindow:[sv window]
248       modalDelegate:self
249      didEndSelector:@selector(preferencesClosed:returnCode:contextInfo:)
250         contextInfo:nil];
251   int code = [NSApp runModalForWindow:prefs];
252   
253   /* Restart the animation if the "OK" button was hit, but not if "Cancel".
254      We have to restart *both* animations, because the xlockmore-style
255      ones will blow up if one re-inits but the other doesn't.
256    */
257   if (code != NSCancelButton) {
258     if ([sv isAnimating])
259       [sv stopAnimation];
260     [sv startAnimation];
261   }
262 }
263
264
265 - (void) preferencesClosed: (NSWindow *) sheet
266                 returnCode: (int) returnCode
267                contextInfo: (void  *) contextInfo
268 {
269   [NSApp stopModalWithCode:returnCode];
270 }
271
272 #else  // USE_IPHONE
273
274
275 - (UIImage *) screenshot
276 {
277   return saved_screenshot;
278 }
279
280 - (void) saveScreenshot
281 {
282   // Most of this is from:
283   // http://developer.apple.com/library/ios/#qa/qa1703/_index.html
284   // The rotation stuff is by me.
285
286   CGSize size = [[UIScreen mainScreen] bounds].size;
287
288   UIInterfaceOrientation orient =
289     [[window rootViewController] interfaceOrientation];
290   if (orient == UIInterfaceOrientationLandscapeLeft ||
291       orient == UIInterfaceOrientationLandscapeRight) {
292     // Rotate the shape of the canvas 90 degrees.
293     double s = size.width;
294     size.width = size.height;
295     size.height = s;
296   }
297
298
299   // Create a graphics context with the target size
300   // On iOS 4 and later, use UIGraphicsBeginImageContextWithOptions to
301   // take the scale into consideration
302   // On iOS prior to 4, fall back to use UIGraphicsBeginImageContext
303
304   if (UIGraphicsBeginImageContextWithOptions)
305     UIGraphicsBeginImageContextWithOptions (size, NO, 0);
306   else
307     UIGraphicsBeginImageContext (size);
308
309   CGContextRef ctx = UIGraphicsGetCurrentContext();
310
311
312   // Rotate the graphics context to match current hardware rotation.
313   //
314   switch (orient) {
315   case UIInterfaceOrientationPortraitUpsideDown:
316     CGContextTranslateCTM (ctx,  [window center].x,  [window center].y);
317     CGContextRotateCTM (ctx, M_PI);
318     CGContextTranslateCTM (ctx, -[window center].x, -[window center].y);
319     break;
320   case UIInterfaceOrientationLandscapeLeft:
321   case UIInterfaceOrientationLandscapeRight:
322     CGContextTranslateCTM (ctx,  
323                            ([window frame].size.height -
324                             [window frame].size.width) / 2,
325                            ([window frame].size.width -
326                             [window frame].size.height) / 2);
327     CGContextTranslateCTM (ctx,  [window center].x,  [window center].y);
328     CGContextRotateCTM (ctx, 
329                         (orient == UIInterfaceOrientationLandscapeLeft
330                          ?  M_PI/2
331                          : -M_PI/2));
332     CGContextTranslateCTM (ctx, -[window center].x, -[window center].y);
333     break;
334   default:
335     break;
336   }
337
338   // Iterate over every window from back to front
339   //
340   for (UIWindow *win in [[UIApplication sharedApplication] windows]) {
341     if (![win respondsToSelector:@selector(screen)] ||
342         [win screen] == [UIScreen mainScreen]) {
343
344       // -renderInContext: renders in the coordinate space of the layer,
345       // so we must first apply the layer's geometry to the graphics context
346       CGContextSaveGState (ctx);
347
348       // Center the context around the window's anchor point
349       CGContextTranslateCTM (ctx, [win center].x, [win center].y);
350
351       // Apply the window's transform about the anchor point
352       CGContextConcatCTM (ctx, [win transform]);
353
354       // Offset by the portion of the bounds left of and above anchor point
355       CGContextTranslateCTM (ctx,
356         -[win bounds].size.width  * [[win layer] anchorPoint].x,
357         -[win bounds].size.height * [[win layer] anchorPoint].y);
358
359       // Render the layer hierarchy to the current context
360       [[win layer] renderInContext:ctx];
361
362       // Restore the context
363       CGContextRestoreGState (ctx);
364     }
365   }
366
367   if (saved_screenshot)
368     [saved_screenshot release];
369   saved_screenshot = [UIGraphicsGetImageFromCurrentImageContext() retain];
370
371   UIGraphicsEndImageContext();
372 }
373
374
375 - (void) openPreferences: (NSString *) saver
376 {
377   [self loadSaver:saver launch:NO];
378   if (! saverView) return;
379
380   NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
381   [prefs setObject:saver forKey:@"selectedSaverName"];
382   [prefs synchronize];
383
384   [rotating_nav pushViewController: [saverView configureView]
385                       animated:YES];
386 }
387
388
389 #endif // USE_IPHONE
390
391
392
393 - (void)loadSaver:(NSString *)name launch:(BOOL)launch
394 {
395 # ifndef USE_IPHONE
396
397   if (saverName && [saverName isEqualToString: name]) {
398     if (launch)
399       for (NSWindow *win in windows) {
400         ScreenSaverView *sv = find_saverView ([win contentView]);
401         if (![sv isAnimating])
402           [sv startAnimation];
403       }
404     return;
405   }
406
407   saverName = name;
408
409   for (NSWindow *win in windows) {
410     NSView *cv = [win contentView];
411     NSString *old_title = [win title];
412     if (!old_title) old_title = @"XScreenSaver";
413     [win setTitle: name];
414     relabel_menus (menubar, old_title, name);
415
416     ScreenSaverView *old_view = find_saverView (cv);
417     NSView *sup = old_view ? [old_view superview] : cv;
418
419     if (old_view) {
420       if ([old_view isAnimating])
421         [old_view stopAnimation];
422       [old_view removeFromSuperview];
423     }
424
425     NSSize size = [cv frame].size;
426     ScreenSaverView *new_view = [self makeSaverView:name withSize: size];
427     NSAssert (new_view, @"unable to make a saver view");
428
429     [new_view setFrame: (old_view ? [old_view frame] : [cv frame])];
430     [sup addSubview: new_view];
431     [win makeFirstResponder:new_view];
432     [new_view setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
433     [new_view retain];
434     if (launch)
435       [new_view startAnimation];
436   }
437
438   NSUserDefaultsController *ctl =
439     [NSUserDefaultsController sharedUserDefaultsController];
440   [ctl save:self];
441
442 # else  // USE_IPHONE
443
444 #  if TARGET_IPHONE_SIMULATOR
445   NSLog (@"selecting saver \"%@\"", name);
446 #  endif
447
448   NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
449   [prefs setObject:name forKey:@"selectedSaverName"];
450   [prefs synchronize];
451
452   if (saverName && [saverName isEqualToString: name]) {
453     if ([saverView isAnimating])
454       return;
455     else
456       goto LAUNCH;
457   }
458
459   saverName = name;
460
461   if (saverView) {
462     if ([saverView isAnimating])
463       [saverView stopAnimation];
464     [saverView removeFromSuperview];
465     [backgroundView removeFromSuperview];
466     [[NSNotificationCenter defaultCenter] removeObserver:saverView];
467     [saverView release];
468   }
469
470   /* We can't just use [window bounds] because that is the *rotated* rectangle
471      and we need the *unrotated* rectangle, so that the view is always created
472      in portrait orientation.  Without that, the initial rotation event that
473      takes us from unknown->landscape will be out of step with reality.
474    */
475   UIScreen *screen = [UIScreen mainScreen];
476 # ifndef __IPHONE_8_0                           // iOS 7 SDK
477   NSSize size = [screen bounds].size;
478   int ss = [screen scale];
479 # else                                          // iOS 8 SDK
480   NSSize size = ([screen respondsToSelector:@selector(nativeBounds)]
481                  ? [screen nativeBounds].size   //  iOS 8
482                  : [screen bounds].size);       //  iOS 7
483   int ss = ([screen respondsToSelector:@selector(nativeScale)]
484             ? [screen nativeScale]              //  iOS 8
485             : [screen scale]);                  //  iOS 7
486 # endif                                         // iOS 8 SDK
487
488   size.width  /= ss;
489   size.height /= ss;
490   saverView = [self makeSaverView:name withSize:size];
491
492   if (! saverView) {
493     [[[UIAlertView alloc] initWithTitle: name
494                           message: @"Unable to load!"
495                           delegate: nil
496                           cancelButtonTitle: @"Bummer"
497                           otherButtonTitles: nil]
498      show];
499     return;
500   }
501
502   [[NSNotificationCenter defaultCenter]
503     addObserver:saverView
504     selector:@selector(didRotate:)
505     name:UIDeviceOrientationDidChangeNotification object:nil];
506
507  LAUNCH:
508
509   if (launch) {
510     [self saveScreenshot];
511     NSRect f = [saverWindow bounds];
512     [backgroundView setFrame:f];
513     [saverView setFrame:f];
514     [saverWindow addSubview: backgroundView];
515     [backgroundView addSubview: saverView];
516     [saverView setBackgroundColor:[NSColor blackColor]];
517
518     /* WTF! Without creating and keying this window, we get no events
519        delivered on the saverView/saverWindow!  Bad craziness.
520      */
521     {
522       UIWindow *dummy = [[UIWindow alloc] initWithFrame:CGRectMake(0,0,0,0)];
523       [dummy setRootViewController: nonrotating_nav];  // Must be this one.
524       [dummy setHidden:NO];  // required
525       [dummy setHidden:YES];
526       [dummy release];
527     }
528
529     [saverWindow setHidden:NO];
530     [saverWindow makeKeyAndVisible];
531     [saverView startAnimation];
532     [self aboutPanel:nil];
533
534     // Doing this makes savers cut back to the list instead of fading,
535     // even though [XScreenSaverView stopAndClose] does setHidden:NO first.
536     // [window setHidden:YES];
537   }
538 # endif // USE_IPHONE
539 }
540
541
542 - (void)loadSaver:(NSString *)name
543 {
544   [self loadSaver:name launch:YES];
545 }
546
547
548 - (void)aboutPanel:(id)sender
549 {
550 # ifndef USE_IPHONE
551
552   NSDictionary *bd = [saverBundle infoDictionary];
553   NSMutableDictionary *d = [NSMutableDictionary dictionaryWithCapacity:20];
554
555   [d setValue:[bd objectForKey:@"CFBundleName"] forKey:@"ApplicationName"];
556   [d setValue:[bd objectForKey:@"CFBundleVersion"] forKey:@"Version"];
557   [d setValue:[bd objectForKey:@"CFBundleShortVersionString"] 
558      forKey:@"ApplicationVersion"];
559   [d setValue:[bd objectForKey:@"NSHumanReadableCopyright"] forKey:@"Copy"];
560   [d setValue:[[NSAttributedString alloc]
561                 initWithString: (NSString *) 
562                   [bd objectForKey:@"CFBundleGetInfoString"]]
563      forKey:@"Credits"];
564
565   [[NSApplication sharedApplication]
566     orderFrontStandardAboutPanelWithOptions:d];
567 # else  // USE_IPHONE
568
569   if ([saverNames count] == 1)
570     return;
571
572   NSString *name = saverName;
573   NSString *year = [self makeDesc:saverName yearOnly:YES];
574
575
576   CGRect frame = [saverView frame];
577   CGFloat rot;
578   CGFloat pt1 = 24;
579   CGFloat pt2 = 14;
580   UIFont *font1 = [UIFont boldSystemFontOfSize:  pt1];
581   UIFont *font2 = [UIFont italicSystemFontOfSize:pt2];
582
583 # ifdef __IPHONE_7_0
584   CGSize s = CGSizeMake(frame.size.width, frame.size.height);
585   CGSize tsize1 = [[[NSAttributedString alloc]
586                      initWithString: name
587                      attributes:@{ NSFontAttributeName: font1 }]
588                     boundingRectWithSize: s
589                     options: NSStringDrawingUsesLineFragmentOrigin
590                     context: nil].size;
591   CGSize tsize2 = [[[NSAttributedString alloc]
592                      initWithString: name
593                      attributes:@{ NSFontAttributeName: font2 }]
594                     boundingRectWithSize: s
595                     options: NSStringDrawingUsesLineFragmentOrigin
596                     context: nil].size;
597 # else // iOS 6 or Cocoa
598   CGSize tsize1 = [name sizeWithFont:font1
599                    constrainedToSize:CGSizeMake(frame.size.width,
600                                                 frame.size.height)];
601   CGSize tsize2 = [year sizeWithFont:font2
602                    constrainedToSize:CGSizeMake(frame.size.width,
603                                                 frame.size.height)];
604 #endif
605
606   CGSize tsize = CGSizeMake (tsize1.width > tsize2.width ?
607                              tsize1.width : tsize2.width,
608                              tsize1.height + tsize2.height);
609
610   tsize.width  = ceilf(tsize.width);
611   tsize.height = ceilf(tsize.height);
612
613   // Don't know how to find inner margin of UITextView.
614   CGFloat margin = 10;
615   tsize.width  += margin * 4;
616   tsize.height += margin * 2;
617
618   if ([saverView frame].size.width >= 768)
619     tsize.height += pt1 * 3;  // extra bottom margin on iPad
620
621   frame = CGRectMake (0, 0, tsize.width, tsize.height);
622
623   UIInterfaceOrientation orient = [rotating_nav interfaceOrientation];
624
625   /* Get the text oriented properly, and move it to the bottom of the
626      screen, since many savers have action in the middle.
627    */
628   switch (orient) {
629   case UIDeviceOrientationLandscapeRight:     
630     rot = -M_PI/2;
631     frame.origin.x = ([saverView frame].size.width
632                       - (tsize.width - tsize.height) / 2
633                       - tsize.height);
634     frame.origin.y = ([saverView frame].size.height - tsize.height) / 2;
635     break;
636   case UIDeviceOrientationLandscapeLeft:
637     rot = M_PI/2;
638     frame.origin.x = -(tsize.width - tsize.height) / 2;
639     frame.origin.y = ([saverView frame].size.height - tsize.height) / 2;
640     break;
641   case UIDeviceOrientationPortraitUpsideDown: 
642     rot = M_PI;
643     frame.origin.x = ([saverView frame].size.width  - tsize.width) / 2;
644     frame.origin.y = 0;
645     break;
646   default:
647     rot = 0;
648     frame.origin.x = ([saverView frame].size.width  - tsize.width) / 2;
649     frame.origin.y =  [saverView frame].size.height - tsize.height;
650     break;
651   }
652
653   if (aboutBox)
654     [aboutBox removeFromSuperview];
655
656   aboutBox = [[UIView alloc] initWithFrame:frame];
657
658   aboutBox.transform = CGAffineTransformMakeRotation (rot);
659   aboutBox.backgroundColor = [UIColor clearColor];
660
661   /* There seems to be no easy way to stroke the font, so instead draw
662      it 5 times, 4 in black and 1 in yellow, offset by 1 pixel, and add
663      a black shadow to each.  (You'd think the shadow alone would be
664      enough, but there's no way to make it dark enough to be legible.)
665    */
666   for (int i = 0; i < 5; i++) {
667     UITextView *textview;
668     int off = 1;
669     frame.origin.x = frame.origin.y = 0;
670     switch (i) {
671       case 0: frame.origin.x = -off; break;
672       case 1: frame.origin.x =  off; break;
673       case 2: frame.origin.y = -off; break;
674       case 3: frame.origin.y =  off; break;
675     }
676
677     for (int j = 0; j < 2; j++) {
678
679       frame.origin.y = (j == 0 ? 0 : pt1);
680       textview = [[UITextView alloc] initWithFrame:frame];
681       textview.font = (j == 0 ? font1 : font2);
682       textview.text = (j == 0 ? name  : year);
683       textview.textAlignment = NSTextAlignmentCenter;
684       textview.showsHorizontalScrollIndicator = NO;
685       textview.showsVerticalScrollIndicator   = NO;
686       textview.scrollEnabled = NO;
687       textview.editable = NO;
688       textview.userInteractionEnabled = NO;
689       textview.backgroundColor = [UIColor clearColor];
690       textview.textColor = (i == 4 
691                             ? [UIColor yellowColor]
692                             : [UIColor blackColor]);
693
694       CALayer *textLayer = (CALayer *)
695         [textview.layer.sublayers objectAtIndex:0];
696       textLayer.shadowColor   = [UIColor blackColor].CGColor;
697       textLayer.shadowOffset  = CGSizeMake(0, 0);
698       textLayer.shadowOpacity = 1;
699       textLayer.shadowRadius  = 2;
700
701       [aboutBox addSubview:textview];
702     }
703   }
704
705   CABasicAnimation *anim = 
706     [CABasicAnimation animationWithKeyPath:@"opacity"];
707   anim.duration     = 0.3;
708   anim.repeatCount  = 1;
709   anim.autoreverses = NO;
710   anim.fromValue    = [NSNumber numberWithFloat:0.0];
711   anim.toValue      = [NSNumber numberWithFloat:1.0];
712   [aboutBox.layer addAnimation:anim forKey:@"animateOpacity"];
713
714   [backgroundView addSubview:aboutBox];
715
716   if (splashTimer)
717     [splashTimer invalidate];
718
719   splashTimer =
720     [NSTimer scheduledTimerWithTimeInterval: anim.duration + 2
721              target:self
722              selector:@selector(aboutOff)
723              userInfo:nil
724              repeats:NO];
725 # endif // USE_IPHONE
726 }
727
728
729 # ifdef USE_IPHONE
730 - (void)aboutOff
731 {
732   if (aboutBox) {
733     if (splashTimer) {
734       [splashTimer invalidate];
735       splashTimer = 0;
736     }
737     CABasicAnimation *anim = 
738       [CABasicAnimation animationWithKeyPath:@"opacity"];
739     anim.duration     = 0.3;
740     anim.repeatCount  = 1;
741     anim.autoreverses = NO;
742     anim.fromValue    = [NSNumber numberWithFloat: 1];
743     anim.toValue      = [NSNumber numberWithFloat: 0];
744     anim.delegate     = self;
745     aboutBox.layer.opacity = 0;
746     [aboutBox.layer addAnimation:anim forKey:@"animateOpacity"];
747   }
748 }
749 #endif // USE_IPHONE
750
751
752
753 - (void)selectedSaverDidChange:(NSDictionary *)change
754 {
755   NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
756   NSString *name = [prefs stringForKey:@"selectedSaverName"];
757
758   if (! name) return;
759
760   if (! [saverNames containsObject:name]) {
761     NSLog (@"saver \"%@\" does not exist", name);
762     return;
763   }
764
765   [self loadSaver: name];
766 }
767
768
769 - (NSArray *) listSaverBundleNamesInDir:(NSString *)dir
770 {
771 # ifndef USE_IPHONE
772   NSString *ext = @"saver";
773 # else
774   NSString *ext = @"xml";
775 # endif
776
777   NSArray *files = [[NSFileManager defaultManager]
778                      contentsOfDirectoryAtPath:dir error:nil];
779   if (! files) return 0;
780   NSMutableArray *result = [NSMutableArray arrayWithCapacity: [files count]+1];
781
782   for (NSString *p in files) {
783     if ([[p pathExtension] caseInsensitiveCompare: ext]) 
784       continue;
785
786     NSString *name = [[p lastPathComponent] stringByDeletingPathExtension];
787
788 # ifdef USE_IPHONE
789     // Get the saver name's capitalization right by reading the XML file.
790
791     p = [dir stringByAppendingPathComponent: p];
792     NSData *xmld = [NSData dataWithContentsOfFile:p];
793     NSAssert (xmld, @"no XML: %@", p);
794     NSString *xml = [XScreenSaverView decompressXML:xmld];
795     NSRange r = [xml rangeOfString:@"_label=\"" options:0];
796     NSAssert1 (r.length, @"no name in %@", p);
797     if (r.length) {
798       xml = [xml substringFromIndex: r.location + r.length];
799       r = [xml rangeOfString:@"\"" options:0];
800       if (r.length) name = [xml substringToIndex: r.location];
801     }
802
803 # endif // USE_IPHONE
804
805     NSAssert1 (name, @"no name in %@", p);
806     if (name) [result addObject: name];
807   }
808
809   if (! [result count])
810     result = 0;
811
812   return result;
813 }
814
815
816
817 - (NSArray *) listSaverBundleNames
818 {
819   NSMutableArray *dirs = [NSMutableArray arrayWithCapacity: 10];
820
821 # ifndef USE_IPHONE
822   // On MacOS, look in the "Contents/Resources/" and "Contents/PlugIns/"
823   // directories in the bundle.
824   [dirs addObject: [[[[NSBundle mainBundle] bundlePath]
825                       stringByAppendingPathComponent:@"Contents"]
826                      stringByAppendingPathComponent:@"Resources"]];
827   [dirs addObject: [[NSBundle mainBundle] builtInPlugInsPath]];
828
829   // Also look in the same directory as the executable.
830   [dirs addObject: [[[NSBundle mainBundle] bundlePath]
831                      stringByDeletingLastPathComponent]];
832
833   // Finally, look in standard MacOS screensaver directories.
834 //  [dirs addObject: @"~/Library/Screen Savers"];
835 //  [dirs addObject: @"/Library/Screen Savers"];
836 //  [dirs addObject: @"/System/Library/Screen Savers"];
837
838 # else  // USE_IPHONE
839
840   // On iOS, only look in the bundle's root directory.
841   [dirs addObject: [[NSBundle mainBundle] bundlePath]];
842
843 # endif // USE_IPHONE
844
845   int i;
846   for (i = 0; i < [dirs count]; i++) {
847     NSString *dir = [dirs objectAtIndex:i];
848     NSArray *names = [self listSaverBundleNamesInDir:dir];
849     if (! names) continue;
850     saverDir   = [dir retain];
851     saverNames = [names retain];
852     return names;
853   }
854
855   NSString *err = @"no .saver bundles found in: ";
856   for (i = 0; i < [dirs count]; i++) {
857     if (i) err = [err stringByAppendingString:@", "];
858     err = [err stringByAppendingString:[[dirs objectAtIndex:i] 
859                                          stringByAbbreviatingWithTildeInPath]];
860     err = [err stringByAppendingString:@"/"];
861   }
862   NSLog (@"%@", err);
863   return [NSArray array];
864 }
865
866
867 /* Create the popup menu of available saver names.
868  */
869 #ifndef USE_IPHONE
870
871 - (NSPopUpButton *) makeMenu
872 {
873   NSRect rect;
874   rect.origin.x = rect.origin.y = 0;
875   rect.size.width = 10;
876   rect.size.height = 10;
877   NSPopUpButton *popup = [[NSPopUpButton alloc] initWithFrame:rect
878                                                     pullsDown:NO];
879   int i;
880   float max_width = 0;
881   for (i = 0; i < [saverNames count]; i++) {
882     NSString *name = [saverNames objectAtIndex:i];
883     [popup addItemWithTitle:name];
884     [[popup itemWithTitle:name] setRepresentedObject:name];
885     [popup sizeToFit];
886     NSRect r = [popup frame];
887     if (r.size.width > max_width) max_width = r.size.width;
888   }
889
890   // Bind the menu to preferences, and trigger a callback when an item
891   // is selected.
892   //
893   NSString *key = @"values.selectedSaverName";
894   NSUserDefaultsController *prefs =
895     [NSUserDefaultsController sharedUserDefaultsController];
896   [prefs addObserver:self
897          forKeyPath:key
898             options:0
899             context:@selector(selectedSaverDidChange:)];
900   [popup   bind:@"selectedObject"
901        toObject:prefs
902     withKeyPath:key
903         options:nil];
904   [prefs setAppliesImmediately:YES];
905
906   NSRect r = [popup frame];
907   r.size.width = max_width;
908   [popup setFrame:r];
909   return popup;
910 }
911
912 #else  // USE_IPHONE
913
914 - (NSString *) makeDesc:(NSString *)saver
915                   yearOnly:(BOOL) yearp
916 {
917   NSString *desc = 0;
918   NSString *path = [saverDir stringByAppendingPathComponent:
919                                [[saver lowercaseString]
920                                  stringByReplacingOccurrencesOfString:@" "
921                                  withString:@""]];
922   NSRange r;
923
924   path = [path stringByAppendingPathExtension:@"xml"];
925   NSData *xmld = [NSData dataWithContentsOfFile:path];
926   if (! xmld) goto FAIL;
927   desc = [XScreenSaverView decompressXML:xmld];
928   if (! desc) goto FAIL;
929
930   r = [desc rangeOfString:@"<_description>"
931             options:NSCaseInsensitiveSearch];
932   if (r.length == 0) {
933     desc = 0;
934     goto FAIL;
935   }
936   desc = [desc substringFromIndex: r.location + r.length];
937   r = [desc rangeOfString:@"</_description>"
938             options:NSCaseInsensitiveSearch];
939   if (r.length > 0)
940     desc = [desc substringToIndex: r.location];
941
942   // Leading and trailing whitespace.
943   desc = [desc stringByTrimmingCharactersInSet:
944                  [NSCharacterSet whitespaceAndNewlineCharacterSet]];
945
946   // Let's see if we can find a year on the last line.
947   r = [desc rangeOfString:@"\n" options:NSBackwardsSearch];
948   NSString *year = 0;
949   for (NSString *word in
950          [[desc substringFromIndex:r.location + r.length]
951            componentsSeparatedByCharactersInSet:
952              [NSCharacterSet characterSetWithCharactersInString:
953                                @" \t\n-."]]) {
954     int n = [word doubleValue];
955     if (n > 1970 && n < 2100)
956       year = word;
957   }
958
959   // Delete everything after the first blank line.
960   //
961   r = [desc rangeOfString:@"\n\n" options:0];
962   if (r.length > 0)
963     desc = [desc substringToIndex: r.location];
964
965   // Unwrap lines and compress whitespace.
966   {
967     NSString *result = @"";
968     for (NSString *s in [desc componentsSeparatedByCharactersInSet:
969                           [NSCharacterSet whitespaceAndNewlineCharacterSet]]) {
970       if ([result length] == 0)
971         result = s;
972       else if ([s length] > 0)
973         result = [NSString stringWithFormat: @"%@ %@", result, s];
974       desc = result;
975     }
976   }
977
978   if (year)
979     desc = [year stringByAppendingString:
980                    [@": " stringByAppendingString: desc]];
981
982   if (yearp)
983     desc = year ? year : @"";
984
985 FAIL:
986   if (! desc) {
987     if ([saverNames count] > 1)
988       desc = @"Oops, this module appears to be incomplete.";
989     else
990       desc = @"";
991   }
992
993   return desc;
994 }
995
996 - (NSString *) makeDesc:(NSString *)saver
997 {
998   return [self makeDesc:saver yearOnly:NO];
999 }
1000
1001
1002
1003 /* Create a dictionary of one-line descriptions of every saver,
1004    for display on the UITableView.
1005  */
1006 - (NSDictionary *)makeDescTable
1007 {
1008   NSMutableDictionary *dict = 
1009     [NSMutableDictionary dictionaryWithCapacity:[saverNames count]];
1010   for (NSString *saver in saverNames) {
1011     [dict setObject:[self makeDesc:saver] forKey:saver];
1012   }
1013   return dict;
1014 }
1015
1016
1017 #endif // USE_IPHONE
1018
1019
1020
1021 /* This is called when the "selectedSaverName" pref changes, e.g.,
1022    when a menu selection is made.
1023  */
1024 - (void)observeValueForKeyPath:(NSString *)keyPath
1025                       ofObject:(id)object
1026                         change:(NSDictionary *)change
1027                        context:(void *)context
1028 {
1029   SEL dispatchSelector = (SEL)context;
1030   if (dispatchSelector != NULL) {
1031     [self performSelector:dispatchSelector withObject:change];
1032   } else {
1033     [super observeValueForKeyPath:keyPath
1034                          ofObject:object
1035                            change:change
1036                           context:context];
1037   }
1038 }
1039
1040
1041 # ifndef USE_IPHONE
1042
1043 /* Create the desktop window shell, possibly including a preferences button.
1044  */
1045 - (NSWindow *) makeWindow
1046 {
1047   NSRect rect;
1048   static int count = 0;
1049   Bool simple_p = ([saverNames count] == 1);
1050   NSButton *pb = 0;
1051   NSPopUpButton *menu = 0;
1052   NSBox *gbox = 0;
1053   NSBox *pbox = 0;
1054
1055   NSRect sv_rect;
1056   sv_rect.origin.x = sv_rect.origin.y = 0;
1057   sv_rect.size.width = 320;
1058   sv_rect.size.height = 240;
1059   ScreenSaverView *sv = [[ScreenSaverView alloc]  // dummy placeholder
1060                           initWithFrame:sv_rect
1061                           isPreview:YES];
1062
1063   // make a "Preferences" button
1064   //
1065   if (! simple_p) {
1066     rect.origin.x = 0;
1067     rect.origin.y = 0;
1068     rect.size.width = rect.size.height = 10;
1069     pb = [[NSButton alloc] initWithFrame:rect];
1070     [pb setTitle:@"Preferences"];
1071     [pb setBezelStyle:NSRoundedBezelStyle];
1072     [pb sizeToFit];
1073
1074     rect.origin.x = ([sv frame].size.width -
1075                      [pb frame].size.width) / 2;
1076     [pb setFrameOrigin:rect.origin];
1077   
1078     // grab the click
1079     //
1080     [pb setTarget:self];
1081     [pb setAction:@selector(openPreferences:)];
1082
1083     // Make a saver selection menu
1084     //
1085     menu = [self makeMenu];
1086     rect.origin.x = 2;
1087     rect.origin.y = 2;
1088     [menu setFrameOrigin:rect.origin];
1089
1090     // make a box to wrap the saverView
1091     //
1092     rect = [sv frame];
1093     rect.origin.x = 0;
1094     rect.origin.y = [pb frame].origin.y + [pb frame].size.height;
1095     gbox = [[NSBox alloc] initWithFrame:rect];
1096     rect.size.width = rect.size.height = 10;
1097     [gbox setContentViewMargins:rect.size];
1098     [gbox setTitlePosition:NSNoTitle];
1099     [gbox addSubview:sv];
1100     [gbox sizeToFit];
1101
1102     // make a box to wrap the other two boxes
1103     //
1104     rect.origin.x = rect.origin.y = 0;
1105     rect.size.width  = [gbox frame].size.width;
1106     rect.size.height = [gbox frame].size.height + [gbox frame].origin.y;
1107     pbox = [[NSBox alloc] initWithFrame:rect];
1108     [pbox setTitlePosition:NSNoTitle];
1109     [pbox setBorderType:NSNoBorder];
1110     [pbox addSubview:gbox];
1111     if (menu) [pbox addSubview:menu];
1112     if (pb)   [pbox addSubview:pb];
1113     [pbox sizeToFit];
1114
1115     [pb   setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin];
1116     [menu setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin];
1117     [gbox setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1118     [pbox setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1119   }
1120
1121   [sv     setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1122
1123
1124   // and make a window to hold that.
1125   //
1126   NSScreen *screen = [NSScreen mainScreen];
1127   rect = pbox ? [pbox frame] : [sv frame];
1128   rect.origin.x = ([screen frame].size.width  - rect.size.width)  / 2;
1129   rect.origin.y = ([screen frame].size.height - rect.size.height) / 2;
1130   
1131   rect.origin.x += rect.size.width * (count ? 0.55 : -0.55);
1132   
1133   NSWindow *win = [[NSWindow alloc]
1134                       initWithContentRect:rect
1135                                 styleMask:(NSTitledWindowMask |
1136                                            NSClosableWindowMask |
1137                                            NSMiniaturizableWindowMask |
1138                                            NSResizableWindowMask)
1139                                   backing:NSBackingStoreBuffered
1140                                     defer:YES
1141                                    screen:screen];
1142   [win setMinSize:[win frameRectForContentRect:rect].size];
1143   [[win contentView] addSubview: (pbox ? (NSView *) pbox : (NSView *) sv)];
1144
1145   [win makeKeyAndOrderFront:win];
1146   
1147   [sv startAnimation]; // this is the dummy saver
1148
1149   count++;
1150
1151   return win;
1152 }
1153
1154
1155 - (void) animTimer
1156 {
1157   for (NSWindow *win in windows) {
1158     ScreenSaverView *sv = find_saverView ([win contentView]);
1159     if ([sv isAnimating])
1160       [sv animateOneFrame];
1161   }
1162 }
1163
1164 # endif // !USE_IPHONE
1165
1166
1167 - (void)applicationDidFinishLaunching:
1168 # ifndef USE_IPHONE
1169     (NSNotification *) notif
1170 # else  // USE_IPHONE
1171     (UIApplication *) application
1172 # endif // USE_IPHONE
1173 {
1174   [self listSaverBundleNames];
1175
1176 # ifndef USE_IPHONE
1177   int window_count = ([saverNames count] <= 1 ? 1 : 2);
1178   NSMutableArray *a = [[NSMutableArray arrayWithCapacity: window_count+1]
1179                         retain];
1180   windows = a;
1181
1182   int i;
1183   // Create either one window (for standalone, e.g. Phosphor.app)
1184   // or two windows for SaverTester.app.
1185   for (i = 0; i < window_count; i++) {
1186     NSWindow *win = [self makeWindow];
1187     // Get the last-saved window position out of preferences.
1188     [win setFrameAutosaveName:
1189               [NSString stringWithFormat:@"XScreenSaverWindow%d", i]];
1190     [win setFrameUsingName:[win frameAutosaveName]];
1191     [a addObject: win];
1192     // This prevents clicks from being seen by savers.
1193     // [win setMovableByWindowBackground:YES];
1194   }
1195 # else  // USE_IPHONE
1196
1197 # undef ya_rand_init
1198   ya_rand_init (0);     // Now's a good time.
1199
1200   rotating_nav = [[[RotateyViewController alloc] initWithRotation:YES]
1201                          retain];
1202   [window setRootViewController: rotating_nav];
1203   [window setAutoresizesSubviews:YES];
1204   [window setAutoresizingMask: 
1205             (UIViewAutoresizingFlexibleWidth | 
1206              UIViewAutoresizingFlexibleHeight)];
1207
1208   nonrotating_nav = [[[RotateyViewController alloc] initWithRotation:NO]
1209                           retain];
1210   [nonrotating_nav setNavigationBarHidden:YES animated:NO];
1211
1212   /* We run the saver on a different UIWindow than the one the
1213      SaverListController and preferences panels run on, because that's
1214      the only way to make rotation work right.  We want the system to
1215      handle rotation of the UI stuff, but we want it to keep its hands
1216      off of rotation of the savers.  As of iOS 8, this seems to be the
1217      only way to accomplish that.
1218
1219      Also, we need to create saverWindow with a portrait rectangle, always.
1220      Note that [UIScreen bounds] returns rotated and scaled values.
1221   */
1222   UIScreen *screen = [UIScreen mainScreen];
1223 # ifndef __IPHONE_8_0                           // iOS 7 SDK
1224   NSRect frame = [screen bounds];
1225   int ss = [screen scale];
1226 # else                                          // iOS 8 SDK
1227   NSRect frame = ([screen respondsToSelector:@selector(nativeBounds)]
1228                  ? [screen nativeBounds]        //   iOS 8
1229                  : [screen bounds]);            //   iOS 7
1230   int ss = ([screen respondsToSelector:@selector(nativeScale)]
1231             ? [screen nativeScale]              //   iOS 8
1232             : [screen scale]);                  //   iOS 7
1233 # endif                                         // iOS 8 SDK
1234   frame.size.width  /= ss;
1235   frame.size.height /= ss;
1236   saverWindow = [[UIWindow alloc] initWithFrame:frame];
1237   [saverWindow setRootViewController: nonrotating_nav];
1238   [saverWindow setHidden:YES];
1239
1240   /* This view is the parent of the XScreenSaverView, and exists only
1241      so that there is a black background behind it.  Without this, when
1242      rotation is in progress, the scrolling-list window's corners show
1243      through in the corners.
1244   */
1245   backgroundView = [[[NSView class] alloc] initWithFrame:[saverWindow frame]];
1246   [backgroundView setBackgroundColor:[NSColor blackColor]];
1247
1248   SaverListController *menu = [[SaverListController alloc] 
1249                                 initWithNames:saverNames
1250                                 descriptions:[self makeDescTable]];
1251   [rotating_nav pushViewController:menu animated:YES];
1252   [menu becomeFirstResponder];
1253
1254   application.applicationSupportsShakeToEdit = YES;
1255
1256
1257 # endif // USE_IPHONE
1258
1259   NSString *forced = 0;
1260   /* In the XCode project, each .saver scheme sets this env var when
1261      launching SaverTester.app so that it knows which one we are
1262      currently debugging.  If this is set, it overrides the default
1263      selection in the popup menu.  If unset, that menu persists to
1264      whatever it was last time.
1265    */
1266   const char *f = getenv ("SELECTED_SAVER");
1267   if (f && *f)
1268     forced = [NSString stringWithCString:(char *)f
1269                        encoding:NSUTF8StringEncoding];
1270
1271   if (forced && ![saverNames containsObject:forced]) {
1272     NSLog(@"forced saver \"%@\" does not exist", forced);
1273     forced = 0;
1274   }
1275
1276   // If there's only one saver, run that.
1277   if (!forced && [saverNames count] == 1)
1278     forced = [saverNames objectAtIndex:0];
1279
1280   NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
1281
1282 # ifdef USE_IPHONE
1283   NSString *prev = [prefs stringForKey:@"selectedSaverName"];
1284
1285   if (forced)
1286     prev = forced;
1287
1288   // If nothing was selected (e.g., this is the first launch)
1289   // then scroll randomly instead of starting up at "A".
1290   //
1291   if (!prev)
1292     prev = [saverNames objectAtIndex: (random() % [saverNames count])];
1293
1294   if (prev)
1295     [menu scrollTo: prev];
1296 # endif // USE_IPHONE
1297
1298   if (forced)
1299     [prefs setObject:forced forKey:@"selectedSaverName"];
1300
1301 # ifdef USE_IPHONE
1302   /* Don't auto-launch the saver unless it was running last time.
1303      XScreenSaverView manages this, on crash_timer.
1304      Unless forced.
1305    */
1306   if (!forced && ![prefs boolForKey:@"wasRunning"])
1307     return;
1308 # endif
1309
1310   [self selectedSaverDidChange:nil];
1311 //  [NSTimer scheduledTimerWithTimeInterval: 0
1312 //           target:self
1313 //           selector:@selector(selectedSaverDidChange:)
1314 //           userInfo:nil
1315 //           repeats:NO];
1316
1317
1318
1319 # ifndef USE_IPHONE
1320   /* On 10.8 and earlier, [ScreenSaverView startAnimation] causes the
1321      ScreenSaverView to run its own timer calling animateOneFrame.
1322      On 10.9, that fails because the private class ScreenSaverModule
1323      is only initialized properly by ScreenSaverEngine, and in the
1324      context of SaverRunner, the null ScreenSaverEngine instance
1325      behaves as if [ScreenSaverEngine needsAnimationTimer] returned false.
1326      So, if it looks like this is the 10.9 version of ScreenSaverModule
1327      instead of the 10.8 version, we run our own timer here.  This sucks.
1328    */
1329   if (!anim_timer) {
1330     Class ssm = NSClassFromString (@"ScreenSaverModule");
1331     if (ssm && [ssm instancesRespondToSelector:
1332                       @selector(needsAnimationTimer)]) {
1333       NSWindow *win = [windows objectAtIndex:0];
1334       ScreenSaverView *sv = find_saverView ([win contentView]);
1335       anim_timer = [NSTimer scheduledTimerWithTimeInterval:
1336                               [sv animationTimeInterval]
1337                             target:self
1338                             selector:@selector(animTimer)
1339                             userInfo:nil
1340                             repeats:YES];
1341     }
1342   }
1343 # endif // !USE_IPHONE
1344 }
1345
1346
1347 #ifndef USE_IPHONE
1348
1349 /* When the window closes, exit (even if prefs still open.)
1350  */
1351 - (BOOL) applicationShouldTerminateAfterLastWindowClosed: (NSApplication *) n
1352 {
1353   return YES;
1354 }
1355
1356 # else // USE_IPHONE
1357
1358 - (void)applicationWillResignActive:(UIApplication *)app
1359 {
1360   [(XScreenSaverView *)view setScreenLocked:YES];
1361 }
1362
1363 - (void)applicationDidBecomeActive:(UIApplication *)app
1364 {
1365   [(XScreenSaverView *)view setScreenLocked:NO];
1366 }
1367
1368 - (void)applicationDidEnterBackground:(UIApplication *)application
1369 {
1370   [(XScreenSaverView *)view setScreenLocked:YES];
1371 }
1372
1373 #endif // USE_IPHONE
1374
1375
1376 @end