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