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