From http://www.jwz.org/xscreensaver/xscreensaver-5.27.tar.gz
[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     [[NSNotificationCenter defaultCenter] removeObserver:saverView];
449     [saverView release];
450   }
451
452   NSSize size = [window frame].size;
453   saverView = [self makeSaverView:name withSize: size];
454
455   if (! saverView) {
456     [[[UIAlertView alloc] initWithTitle: name
457                           message: @"Unable to load!"
458                           delegate: nil
459                           cancelButtonTitle: @"Bummer"
460                           otherButtonTitles: nil]
461      show];
462     return;
463   }
464
465   [saverView setFrame: [window frame]];
466   [[NSNotificationCenter defaultCenter]
467     addObserver:saverView
468     selector:@selector(didRotate:)
469     name:UIDeviceOrientationDidChangeNotification object:nil];
470
471  LAUNCH:
472   if (launch) {
473     [self saveScreenshot];
474     [window addSubview: backgroundView];
475     [backgroundView addSubview: saverView];
476     [saverView becomeFirstResponder];
477     [saverView startAnimation];
478     [self aboutPanel:nil];
479   }
480 # endif // USE_IPHONE
481 }
482
483
484 - (void)loadSaver:(NSString *)name
485 {
486   [self loadSaver:name launch:YES];
487 }
488
489
490 - (void)aboutPanel:(id)sender
491 {
492 # ifndef USE_IPHONE
493
494   NSDictionary *bd = [saverBundle infoDictionary];
495   NSMutableDictionary *d = [NSMutableDictionary dictionaryWithCapacity:20];
496
497   [d setValue:[bd objectForKey:@"CFBundleName"] forKey:@"ApplicationName"];
498   [d setValue:[bd objectForKey:@"CFBundleVersion"] forKey:@"Version"];
499   [d setValue:[bd objectForKey:@"CFBundleShortVersionString"] 
500      forKey:@"ApplicationVersion"];
501   [d setValue:[bd objectForKey:@"NSHumanReadableCopyright"] forKey:@"Copy"];
502   [d setValue:[[NSAttributedString alloc]
503                 initWithString: (NSString *) 
504                   [bd objectForKey:@"CFBundleGetInfoString"]]
505      forKey:@"Credits"];
506
507   [[NSApplication sharedApplication]
508     orderFrontStandardAboutPanelWithOptions:d];
509 # else  // USE_IPHONE
510
511   if ([saverNames count] == 1)
512     return;
513
514   NSString *name = saverName;
515   NSString *year = [self makeDesc:saverName yearOnly:YES];
516
517
518   CGRect frame = [saverView frame];
519   CGFloat rot;
520   CGFloat pt1 = 24;
521   CGFloat pt2 = 14;
522   UIFont *font1 = [UIFont boldSystemFontOfSize:  pt1];
523   UIFont *font2 = [UIFont italicSystemFontOfSize:pt2];
524   CGSize tsize1 = [name sizeWithFont:font1
525                    constrainedToSize:CGSizeMake(frame.size.width,
526                                                 frame.size.height)];
527   CGSize tsize2 = [year sizeWithFont:font2
528                    constrainedToSize:CGSizeMake(frame.size.width,
529                                                 frame.size.height)];
530   CGSize tsize = CGSizeMake (tsize1.width > tsize2.width ?
531                              tsize1.width : tsize2.width,
532                              tsize1.height + tsize2.height);
533
534   // Don't know how to find inner margin of UITextView.
535   CGFloat margin = 10;
536   tsize.width  += margin * 4;
537   tsize.height += margin * 2;
538
539   if ([saverView frame].size.width >= 768)
540     tsize.height += pt1 * 3;  // extra bottom margin on iPad
541
542   frame = CGRectMake (0, 0, tsize.width, tsize.height);
543
544   UIInterfaceOrientation orient =
545     // Why are both of these wrong when starting up rotated??
546     [[UIDevice currentDevice] orientation];
547     // [rootViewController interfaceOrientation];
548
549   /* Get the text oriented properly, and move it to the bottom of the
550      screen, since many savers have action in the middle.
551    */
552   switch (orient) {
553   case UIDeviceOrientationLandscapeRight:     
554     rot = -M_PI/2;
555     frame.origin.x = ([saverView frame].size.width
556                       - (tsize.width - tsize.height) / 2
557                       - tsize.height);
558     frame.origin.y = ([saverView frame].size.height - tsize.height) / 2;
559     break;
560   case UIDeviceOrientationLandscapeLeft:
561     rot = M_PI/2;
562     frame.origin.x = -(tsize.width - tsize.height) / 2;
563     frame.origin.y = ([saverView frame].size.height - tsize.height) / 2;
564     break;
565   case UIDeviceOrientationPortraitUpsideDown: 
566     rot = M_PI;
567     frame.origin.x = ([saverView frame].size.width  - tsize.width) / 2;
568     frame.origin.y = 0;
569     break;
570   default:
571     rot = 0;
572     frame.origin.x = ([saverView frame].size.width  - tsize.width) / 2;
573     frame.origin.y =  [saverView frame].size.height - tsize.height;
574     break;
575   }
576
577   if (aboutBox)
578     [aboutBox removeFromSuperview];
579
580   aboutBox = [[UIView alloc] initWithFrame:frame];
581
582   aboutBox.transform = CGAffineTransformMakeRotation (rot);
583   aboutBox.backgroundColor = [UIColor clearColor];
584
585   /* There seems to be no easy way to stroke the font, so instead draw
586      it 5 times, 4 in black and 1 in yellow, offset by 1 pixel, and add
587      a black shadow to each.  (You'd think the shadow alone would be
588      enough, but there's no way to make it dark enough to be legible.)
589    */
590   for (int i = 0; i < 5; i++) {
591     UITextView *textview;
592     int off = 1;
593     frame.origin.x = frame.origin.y = 0;
594     switch (i) {
595       case 0: frame.origin.x = -off; break;
596       case 1: frame.origin.x =  off; break;
597       case 2: frame.origin.y = -off; break;
598       case 3: frame.origin.y =  off; break;
599     }
600
601     for (int j = 0; j < 2; j++) {
602
603       frame.origin.y = (j == 0 ? 0 : pt1);
604       textview = [[UITextView alloc] initWithFrame:frame];
605       textview.font = (j == 0 ? font1 : font2);
606       textview.text = (j == 0 ? name  : year);
607       textview.textAlignment = UITextAlignmentCenter;
608       textview.showsHorizontalScrollIndicator = NO;
609       textview.showsVerticalScrollIndicator   = NO;
610       textview.scrollEnabled = NO;
611       textview.editable = NO;
612       textview.userInteractionEnabled = NO;
613       textview.backgroundColor = [UIColor clearColor];
614       textview.textColor = (i == 4 
615                             ? [UIColor yellowColor]
616                             : [UIColor blackColor]);
617
618       CALayer *textLayer = (CALayer *)
619         [textview.layer.sublayers objectAtIndex:0];
620       textLayer.shadowColor   = [UIColor blackColor].CGColor;
621       textLayer.shadowOffset  = CGSizeMake(0, 0);
622       textLayer.shadowOpacity = 1;
623       textLayer.shadowRadius  = 2;
624
625       [aboutBox addSubview:textview];
626     }
627   }
628
629   CABasicAnimation *anim = 
630     [CABasicAnimation animationWithKeyPath:@"opacity"];
631   anim.duration     = 0.3;
632   anim.repeatCount  = 1;
633   anim.autoreverses = NO;
634   anim.fromValue    = [NSNumber numberWithFloat:0.0];
635   anim.toValue      = [NSNumber numberWithFloat:1.0];
636   [aboutBox.layer addAnimation:anim forKey:@"animateOpacity"];
637
638   [backgroundView addSubview:aboutBox];
639
640   if (splashTimer)
641     [splashTimer invalidate];
642
643   splashTimer =
644     [NSTimer scheduledTimerWithTimeInterval: anim.duration + 2
645              target:self
646              selector:@selector(aboutOff)
647              userInfo:nil
648              repeats:NO];
649 # endif // USE_IPHONE
650 }
651
652
653 # ifdef USE_IPHONE
654 - (void)aboutOff
655 {
656   if (aboutBox) {
657     if (splashTimer) {
658       [splashTimer invalidate];
659       splashTimer = 0;
660     }
661     CABasicAnimation *anim = 
662       [CABasicAnimation animationWithKeyPath:@"opacity"];
663     anim.duration     = 0.3;
664     anim.repeatCount  = 1;
665     anim.autoreverses = NO;
666     anim.fromValue    = [NSNumber numberWithFloat: 1];
667     anim.toValue      = [NSNumber numberWithFloat: 0];
668     anim.delegate     = self;
669     aboutBox.layer.opacity = 0;
670     [aboutBox.layer addAnimation:anim forKey:@"animateOpacity"];
671   }
672 }
673 #endif // USE_IPHONE
674
675
676
677 - (void)selectedSaverDidChange:(NSDictionary *)change
678 {
679   NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
680   NSString *name = [prefs stringForKey:@"selectedSaverName"];
681
682   if (! name) return;
683
684   if (! [saverNames containsObject:name]) {
685     NSLog (@"saver \"%@\" does not exist", name);
686     return;
687   }
688
689   [self loadSaver: name];
690 }
691
692
693 - (NSArray *) listSaverBundleNamesInDir:(NSString *)dir
694 {
695 # ifndef USE_IPHONE
696   NSString *ext = @"saver";
697 # else
698   NSString *ext = @"xml";
699 # endif
700
701   NSArray *files = [[NSFileManager defaultManager]
702                      contentsOfDirectoryAtPath:dir error:nil];
703   if (! files) return 0;
704   NSMutableArray *result = [NSMutableArray arrayWithCapacity: [files count]+1];
705
706   for (NSString *p in files) {
707     if ([[p pathExtension] caseInsensitiveCompare: ext]) 
708       continue;
709
710     NSString *name = [[p lastPathComponent] stringByDeletingPathExtension];
711
712 # ifdef USE_IPHONE
713     // Get the saver name's capitalization right by reading the XML file.
714
715     p = [dir stringByAppendingPathComponent: p];
716     NSData *xmld = [NSData dataWithContentsOfFile:p];
717     NSAssert (xmld, @"no XML: %@", p);
718     NSString *xml = [XScreenSaverView decompressXML:xmld];
719     NSRange r = [xml rangeOfString:@"_label=\"" options:0];
720     NSAssert1 (r.length, @"no name in %@", p);
721     if (r.length) {
722       xml = [xml substringFromIndex: r.location + r.length];
723       r = [xml rangeOfString:@"\"" options:0];
724       if (r.length) name = [xml substringToIndex: r.location];
725     }
726
727 # endif // USE_IPHONE
728
729     NSAssert1 (name, @"no name in %@", p);
730     if (name) [result addObject: name];
731   }
732
733   if (! [result count])
734     result = 0;
735
736   return result;
737 }
738
739
740
741 - (NSArray *) listSaverBundleNames
742 {
743   NSMutableArray *dirs = [NSMutableArray arrayWithCapacity: 10];
744
745 # ifndef USE_IPHONE
746   // On MacOS, look in the "Contents/Resources/" and "Contents/PlugIns/"
747   // directories in the bundle.
748   [dirs addObject: [[[[NSBundle mainBundle] bundlePath]
749                       stringByAppendingPathComponent:@"Contents"]
750                      stringByAppendingPathComponent:@"Resources"]];
751   [dirs addObject: [[NSBundle mainBundle] builtInPlugInsPath]];
752
753   // Also look in the same directory as the executable.
754   [dirs addObject: [[[NSBundle mainBundle] bundlePath]
755                      stringByDeletingLastPathComponent]];
756
757   // Finally, look in standard MacOS screensaver directories.
758 //  [dirs addObject: @"~/Library/Screen Savers"];
759 //  [dirs addObject: @"/Library/Screen Savers"];
760 //  [dirs addObject: @"/System/Library/Screen Savers"];
761
762 # else  // USE_IPHONE
763
764   // On iOS, only look in the bundle's root directory.
765   [dirs addObject: [[NSBundle mainBundle] bundlePath]];
766
767 # endif // USE_IPHONE
768
769   int i;
770   for (i = 0; i < [dirs count]; i++) {
771     NSString *dir = [dirs objectAtIndex:i];
772     NSArray *names = [self listSaverBundleNamesInDir:dir];
773     if (! names) continue;
774     saverDir   = [dir retain];
775     saverNames = [names retain];
776     return names;
777   }
778
779   NSString *err = @"no .saver bundles found in: ";
780   for (i = 0; i < [dirs count]; i++) {
781     if (i) err = [err stringByAppendingString:@", "];
782     err = [err stringByAppendingString:[[dirs objectAtIndex:i] 
783                                          stringByAbbreviatingWithTildeInPath]];
784     err = [err stringByAppendingString:@"/"];
785   }
786   NSLog (@"%@", err);
787   return [NSArray array];
788 }
789
790
791 /* Create the popup menu of available saver names.
792  */
793 #ifndef USE_IPHONE
794
795 - (NSPopUpButton *) makeMenu
796 {
797   NSRect rect;
798   rect.origin.x = rect.origin.y = 0;
799   rect.size.width = 10;
800   rect.size.height = 10;
801   NSPopUpButton *popup = [[NSPopUpButton alloc] initWithFrame:rect
802                                                     pullsDown:NO];
803   int i;
804   float max_width = 0;
805   for (i = 0; i < [saverNames count]; i++) {
806     NSString *name = [saverNames objectAtIndex:i];
807     [popup addItemWithTitle:name];
808     [[popup itemWithTitle:name] setRepresentedObject:name];
809     [popup sizeToFit];
810     NSRect r = [popup frame];
811     if (r.size.width > max_width) max_width = r.size.width;
812   }
813
814   // Bind the menu to preferences, and trigger a callback when an item
815   // is selected.
816   //
817   NSString *key = @"values.selectedSaverName";
818   NSUserDefaultsController *prefs =
819     [NSUserDefaultsController sharedUserDefaultsController];
820   [prefs addObserver:self
821          forKeyPath:key
822             options:0
823             context:@selector(selectedSaverDidChange:)];
824   [popup   bind:@"selectedObject"
825        toObject:prefs
826     withKeyPath:key
827         options:nil];
828   [prefs setAppliesImmediately:YES];
829
830   NSRect r = [popup frame];
831   r.size.width = max_width;
832   [popup setFrame:r];
833   return popup;
834 }
835
836 #else  // USE_IPHONE
837
838 - (NSString *) makeDesc:(NSString *)saver
839                   yearOnly:(BOOL) yearp
840 {
841   NSString *desc = 0;
842   NSString *path = [saverDir stringByAppendingPathComponent:
843                                [[saver lowercaseString]
844                                  stringByReplacingOccurrencesOfString:@" "
845                                  withString:@""]];
846   NSRange r;
847
848   path = [path stringByAppendingPathExtension:@"xml"];
849   NSData *xmld = [NSData dataWithContentsOfFile:path];
850   if (! xmld) goto FAIL;
851   desc = [XScreenSaverView decompressXML:xmld];
852   if (! desc) goto FAIL;
853
854   r = [desc rangeOfString:@"<_description>"
855             options:NSCaseInsensitiveSearch];
856   if (r.length == 0) {
857     desc = 0;
858     goto FAIL;
859   }
860   desc = [desc substringFromIndex: r.location + r.length];
861   r = [desc rangeOfString:@"</_description>"
862             options:NSCaseInsensitiveSearch];
863   if (r.length > 0)
864     desc = [desc substringToIndex: r.location];
865
866   // Leading and trailing whitespace.
867   desc = [desc stringByTrimmingCharactersInSet:
868                  [NSCharacterSet whitespaceAndNewlineCharacterSet]];
869
870   // Let's see if we can find a year on the last line.
871   r = [desc rangeOfString:@"\n" options:NSBackwardsSearch];
872   NSString *year = 0;
873   for (NSString *word in
874          [[desc substringFromIndex:r.location + r.length]
875            componentsSeparatedByCharactersInSet:
876              [NSCharacterSet characterSetWithCharactersInString:
877                                @" \t\n-."]]) {
878     int n = [word doubleValue];
879     if (n > 1970 && n < 2100)
880       year = word;
881   }
882
883   // Delete everything after the first blank line.
884   r = [desc rangeOfString:@"\n\n" options:0];
885   if (r.length > 0)
886     desc = [desc substringToIndex: r.location];
887
888   // Truncate really long ones.
889   int max = 140;
890   if ([desc length] > max)
891     desc = [desc substringToIndex: max];
892
893   if (year)
894     desc = [year stringByAppendingString:
895                    [@": " stringByAppendingString: desc]];
896
897   if (yearp)
898     desc = year ? year : @"";
899
900 FAIL:
901   if (! desc) {
902     if ([saverNames count] > 1)
903       desc = @"Oops, this module appears to be incomplete.";
904     else
905       desc = @"";
906   }
907
908   return desc;
909 }
910
911 - (NSString *) makeDesc:(NSString *)saver
912 {
913   return [self makeDesc:saver yearOnly:NO];
914 }
915
916
917
918 /* Create a dictionary of one-line descriptions of every saver,
919    for display on the UITableView.
920  */
921 - (NSDictionary *)makeDescTable
922 {
923   NSMutableDictionary *dict = 
924     [NSMutableDictionary dictionaryWithCapacity:[saverNames count]];
925   for (NSString *saver in saverNames) {
926     [dict setObject:[self makeDesc:saver] forKey:saver];
927   }
928   return dict;
929 }
930
931
932 #endif // USE_IPHONE
933
934
935
936 /* This is called when the "selectedSaverName" pref changes, e.g.,
937    when a menu selection is made.
938  */
939 - (void)observeValueForKeyPath:(NSString *)keyPath
940                       ofObject:(id)object
941                         change:(NSDictionary *)change
942                        context:(void *)context
943 {
944   SEL dispatchSelector = (SEL)context;
945   if (dispatchSelector != NULL) {
946     [self performSelector:dispatchSelector withObject:change];
947   } else {
948     [super observeValueForKeyPath:keyPath
949                          ofObject:object
950                            change:change
951                           context:context];
952   }
953 }
954
955
956 # ifndef USE_IPHONE
957
958 /* Create the desktop window shell, possibly including a preferences button.
959  */
960 - (NSWindow *) makeWindow
961 {
962   NSRect rect;
963   static int count = 0;
964   Bool simple_p = ([saverNames count] == 1);
965   NSButton *pb = 0;
966   NSPopUpButton *menu = 0;
967   NSBox *gbox = 0;
968   NSBox *pbox = 0;
969
970   NSRect sv_rect;
971   sv_rect.origin.x = sv_rect.origin.y = 0;
972   sv_rect.size.width = 320;
973   sv_rect.size.height = 240;
974   ScreenSaverView *sv = [[ScreenSaverView alloc]  // dummy placeholder
975                           initWithFrame:sv_rect
976                           isPreview:YES];
977
978   // make a "Preferences" button
979   //
980   if (! simple_p) {
981     rect.origin.x = 0;
982     rect.origin.y = 0;
983     rect.size.width = rect.size.height = 10;
984     pb = [[NSButton alloc] initWithFrame:rect];
985     [pb setTitle:@"Preferences"];
986     [pb setBezelStyle:NSRoundedBezelStyle];
987     [pb sizeToFit];
988
989     rect.origin.x = ([sv frame].size.width -
990                      [pb frame].size.width) / 2;
991     [pb setFrameOrigin:rect.origin];
992   
993     // grab the click
994     //
995     [pb setTarget:self];
996     [pb setAction:@selector(openPreferences:)];
997
998     // Make a saver selection menu
999     //
1000     menu = [self makeMenu];
1001     rect.origin.x = 2;
1002     rect.origin.y = 2;
1003     [menu setFrameOrigin:rect.origin];
1004
1005     // make a box to wrap the saverView
1006     //
1007     rect = [sv frame];
1008     rect.origin.x = 0;
1009     rect.origin.y = [pb frame].origin.y + [pb frame].size.height;
1010     gbox = [[NSBox alloc] initWithFrame:rect];
1011     rect.size.width = rect.size.height = 10;
1012     [gbox setContentViewMargins:rect.size];
1013     [gbox setTitlePosition:NSNoTitle];
1014     [gbox addSubview:sv];
1015     [gbox sizeToFit];
1016
1017     // make a box to wrap the other two boxes
1018     //
1019     rect.origin.x = rect.origin.y = 0;
1020     rect.size.width  = [gbox frame].size.width;
1021     rect.size.height = [gbox frame].size.height + [gbox frame].origin.y;
1022     pbox = [[NSBox alloc] initWithFrame:rect];
1023     [pbox setTitlePosition:NSNoTitle];
1024     [pbox setBorderType:NSNoBorder];
1025     [pbox addSubview:gbox];
1026     if (menu) [pbox addSubview:menu];
1027     if (pb)   [pbox addSubview:pb];
1028     [pbox sizeToFit];
1029
1030     [pb   setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin];
1031     [menu setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin];
1032     [gbox setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1033     [pbox setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1034   }
1035
1036   [sv     setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1037
1038
1039   // and make a window to hold that.
1040   //
1041   NSScreen *screen = [NSScreen mainScreen];
1042   rect = pbox ? [pbox frame] : [sv frame];
1043   rect.origin.x = ([screen frame].size.width  - rect.size.width)  / 2;
1044   rect.origin.y = ([screen frame].size.height - rect.size.height) / 2;
1045   
1046   rect.origin.x += rect.size.width * (count ? 0.55 : -0.55);
1047   
1048   NSWindow *win = [[NSWindow alloc]
1049                       initWithContentRect:rect
1050                                 styleMask:(NSTitledWindowMask |
1051                                            NSClosableWindowMask |
1052                                            NSMiniaturizableWindowMask |
1053                                            NSResizableWindowMask)
1054                                   backing:NSBackingStoreBuffered
1055                                     defer:YES
1056                                    screen:screen];
1057   [win setMinSize:[win frameRectForContentRect:rect].size];
1058   [[win contentView] addSubview: (pbox ? (NSView *) pbox : (NSView *) sv)];
1059
1060   [win makeKeyAndOrderFront:win];
1061   
1062   [sv startAnimation]; // this is the dummy saver
1063
1064   count++;
1065
1066   return win;
1067 }
1068
1069
1070 - (void) animTimer
1071 {
1072   for (NSWindow *win in windows) {
1073     ScreenSaverView *sv = find_saverView ([win contentView]);
1074     if ([sv isAnimating])
1075       [sv animateOneFrame];
1076   }
1077 }
1078
1079 # endif // !USE_IPHONE
1080
1081
1082 - (void)applicationDidFinishLaunching:
1083 # ifndef USE_IPHONE
1084     (NSNotification *) notif
1085 # else  // USE_IPHONE
1086     (UIApplication *) application
1087 # endif // USE_IPHONE
1088 {
1089   [self listSaverBundleNames];
1090
1091 # ifndef USE_IPHONE
1092   int window_count = ([saverNames count] <= 1 ? 1 : 2);
1093   NSMutableArray *a = [[NSMutableArray arrayWithCapacity: window_count+1]
1094                         retain];
1095   windows = a;
1096
1097   int i;
1098   // Create either one window (for standalone, e.g. Phosphor.app)
1099   // or two windows for SaverTester.app.
1100   for (i = 0; i < window_count; i++) {
1101     NSWindow *win = [self makeWindow];
1102     // Get the last-saved window position out of preferences.
1103     [win setFrameAutosaveName:
1104               [NSString stringWithFormat:@"XScreenSaverWindow%d", i]];
1105     [win setFrameUsingName:[win frameAutosaveName]];
1106     [a addObject: win];
1107     // This prevents clicks from being seen by savers.
1108     // [win setMovableByWindowBackground:YES];
1109   }
1110 # else  // USE_IPHONE
1111
1112 # undef ya_rand_init
1113   ya_rand_init (0);     // Now's a good time.
1114
1115   rootViewController = [[[RotateyViewController alloc] init] retain];
1116   [window setRootViewController: rootViewController];
1117
1118   SaverListController *menu = [[SaverListController alloc] 
1119                                 initWithNames:saverNames
1120                                 descriptions:[self makeDescTable]];
1121   [rootViewController pushViewController:menu animated:YES];
1122   [menu becomeFirstResponder];
1123
1124   [window makeKeyAndVisible];
1125   [window setAutoresizesSubviews:YES];
1126   [window setAutoresizingMask: 
1127             (UIViewAutoresizingFlexibleWidth | 
1128              UIViewAutoresizingFlexibleHeight)];
1129
1130   application.applicationSupportsShakeToEdit = YES;
1131
1132 # endif // USE_IPHONE
1133
1134   NSString *forced = 0;
1135   /* In the XCode project, each .saver scheme sets this env var when
1136      launching SaverTester.app so that it knows which one we are
1137      currently debugging.  If this is set, it overrides the default
1138      selection in the popup menu.  If unset, that menu persists to
1139      whatever it was last time.
1140    */
1141   const char *f = getenv ("SELECTED_SAVER");
1142   if (f && *f)
1143     forced = [NSString stringWithCString:(char *)f
1144                        encoding:NSUTF8StringEncoding];
1145
1146   if (forced && ![saverNames containsObject:forced]) {
1147     NSLog(@"forced saver \"%@\" does not exist", forced);
1148     forced = 0;
1149   }
1150
1151   // If there's only one saver, run that.
1152   if (!forced && [saverNames count] == 1)
1153     forced = [saverNames objectAtIndex:0];
1154
1155   NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
1156
1157 # ifdef USE_IPHONE
1158   NSString *prev = [prefs stringForKey:@"selectedSaverName"];
1159
1160   if (forced)
1161     prev = forced;
1162
1163   // If nothing was selected (e.g., this is the first launch)
1164   // then scroll randomly instead of starting up at "A".
1165   //
1166   if (!prev)
1167     prev = [saverNames objectAtIndex: (random() % [saverNames count])];
1168
1169   if (prev)
1170     [menu scrollTo: prev];
1171 # endif // USE_IPHONE
1172
1173   if (forced)
1174     [prefs setObject:forced forKey:@"selectedSaverName"];
1175
1176 # ifdef USE_IPHONE
1177   /* Don't auto-launch the saver unless it was running last time.
1178      XScreenSaverView manages this, on crash_timer.
1179      Unless forced.
1180    */
1181   if (!forced && ![prefs boolForKey:@"wasRunning"])
1182     return;
1183 # endif
1184
1185   [self selectedSaverDidChange:nil];
1186
1187
1188 # ifndef USE_IPHONE
1189   /* On 10.8 and earlier, [ScreenSaverView startAnimation] causes the
1190      ScreenSaverView to run its own timer calling animateOneFrame.
1191      On 10.9, that fails because the private class ScreenSaverModule
1192      is only initialized properly by ScreenSaverEngine, and in the
1193      context of SaverRunner, the null ScreenSaverEngine instance
1194      behaves as if [ScreenSaverEngine needsAnimationTimer] returned false.
1195      So, if it looks like this is the 10.9 version of ScreenSaverModule
1196      instead of the 10.8 version, we run our own timer here.  This sucks.
1197    */
1198   if (!anim_timer) {
1199     Class ssm = NSClassFromString (@"ScreenSaverModule");
1200     if (ssm && [ssm instancesRespondToSelector:
1201                       @selector(needsAnimationTimer)]) {
1202       NSWindow *win = [windows objectAtIndex:0];
1203       ScreenSaverView *sv = find_saverView ([win contentView]);
1204       anim_timer = [NSTimer scheduledTimerWithTimeInterval:
1205                               [sv animationTimeInterval]
1206                             target:self
1207                             selector:@selector(animTimer)
1208                             userInfo:nil
1209                             repeats:YES];
1210     }
1211   }
1212 # endif // !USE_IPHONE
1213 }
1214
1215
1216 #ifndef USE_IPHONE
1217
1218 /* When the window closes, exit (even if prefs still open.)
1219  */
1220 - (BOOL) applicationShouldTerminateAfterLastWindowClosed: (NSApplication *) n
1221 {
1222   return YES;
1223 }
1224
1225 # else // USE_IPHONE
1226
1227 - (void)applicationWillResignActive:(UIApplication *)app
1228 {
1229   [(XScreenSaverView *)view setScreenLocked:YES];
1230 }
1231
1232 - (void)applicationDidBecomeActive:(UIApplication *)app
1233 {
1234   [(XScreenSaverView *)view setScreenLocked:NO];
1235 }
1236
1237 - (void)applicationDidEnterBackground:(UIApplication *)application
1238 {
1239   [(XScreenSaverView *)view setScreenLocked:YES];
1240 }
1241
1242 #endif // USE_IPHONE
1243
1244
1245 @end