From http://www.jwz.org/xscreensaver/xscreensaver-5.16.tar.gz
[xscreensaver] / OSX / SaverRunner.m
1 /* xscreensaver, Copyright (c) 2006-2012 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/PlugIns/ 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 "SaverRunner.h"
33 #import "SaverListController.h"
34 #import "XScreenSaverGLView.h"
35 #import <TargetConditionals.h>
36
37 #ifdef USE_IPHONE
38
39 @interface RotateyViewController : UINavigationController
40 @end
41
42 @implementation RotateyViewController
43 - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)o
44 {
45   return YES;
46 }
47 @end
48
49 #endif // USE_IPHONE
50
51
52 @implementation SaverRunner
53
54
55 - (ScreenSaverView *) makeSaverView: (NSString *) module
56                            withSize: (NSSize) size
57 {
58   Class new_class = 0;
59
60 # ifndef USE_IPHONE
61
62   // Load the XScreenSaverView subclass and code from a ".saver" bundle.
63
64   NSString *name = [module stringByAppendingPathExtension:@"saver"];
65   NSString *path = [saverDir stringByAppendingPathComponent:name];
66
67   if (! [[NSFileManager defaultManager] fileExistsAtPath:path]) {
68     NSLog(@"bundle \"%@\" does not exist", path);
69     return 0;
70   }
71
72   NSLog(@"Loading %@", path);
73
74   // NSBundle *obundle = saverBundle;
75
76   saverBundle = [NSBundle bundleWithPath:path];
77   if (saverBundle)
78     new_class = [saverBundle principalClass];
79
80   // Not entirely unsurprisingly, this tends to break the world.
81   // if (obundle && obundle != saverBundle)
82   //  [obundle unload];
83
84 # else  // USE_IPHONE
85
86   // Determine whether to create an X11 view or an OpenGL view by
87   // looking for the "gl" tag in the xml file.  This is kind of awful.
88
89   NSString *path = [saverDir
90                      stringByAppendingPathComponent:
91                        [[[module lowercaseString]
92                           stringByReplacingOccurrencesOfString:@" "
93                           withString:@""]
94                          stringByAppendingPathExtension:@"xml"]];
95   NSString *xml = [NSString stringWithContentsOfFile:path
96                             encoding:NSISOLatin1StringEncoding
97                             error:nil];
98   NSAssert (xml, @"no XML: %@", path);
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) return 0;
121
122
123   /* KLUGE: Inform the underlying program that we're in "standalone"
124      mode, e.g. running as "Phosphor.app" rather than "Phosphor.saver".
125      This is kind of horrible but I haven't thought of a more sensible
126      way to make this work.
127    */
128 # ifndef USE_IPHONE
129   if ([saverNames count] == 1) {
130     putenv (strdup ("XSCREENSAVER_STANDALONE=1"));
131   }
132 # endif
133
134   return (ScreenSaverView *) instance;
135 }
136
137
138 #ifndef USE_IPHONE
139
140 static ScreenSaverView *
141 find_saverView_child (NSView *v)
142 {
143   NSArray *kids = [v subviews];
144   int nkids = [kids count];
145   int i;
146   for (i = 0; i < nkids; i++) {
147     NSObject *kid = [kids objectAtIndex:i];
148     if ([kid isKindOfClass:[ScreenSaverView class]]) {
149       return (ScreenSaverView *) kid;
150     } else {
151       ScreenSaverView *sv = find_saverView_child ((NSView *) kid);
152       if (sv) return sv;
153     }
154   }
155   return 0;
156 }
157
158
159 static ScreenSaverView *
160 find_saverView (NSView *v)
161 {
162   while (1) {
163     NSView *p = [v superview];
164     if (p) v = p;
165     else break;
166   }
167   return find_saverView_child (v);
168 }
169
170
171 /* Changes the contents of the menubar menus to correspond to
172    the running saver.  Desktop only.
173  */
174 static void
175 relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
176 {
177   if ([v isKindOfClass:[NSMenu class]]) {
178     NSMenu *m = (NSMenu *)v;
179     [m setTitle: [[m title] stringByReplacingOccurrencesOfString:old_str
180                             withString:new_str]];
181     NSArray *kids = [m itemArray];
182     int nkids = [kids count];
183     int i;
184     for (i = 0; i < nkids; i++) {
185       relabel_menus ([kids objectAtIndex:i], old_str, new_str);
186     }
187   } else if ([v isKindOfClass:[NSMenuItem class]]) {
188     NSMenuItem *mi = (NSMenuItem *)v;
189     [mi setTitle: [[mi title] stringByReplacingOccurrencesOfString:old_str
190                               withString:new_str]];
191     NSMenu *m = [mi submenu];
192     if (m) relabel_menus (m, old_str, new_str);
193   }
194 }
195
196
197 - (void) openPreferences: (id) sender
198 {
199   ScreenSaverView *sv;
200   if ([sender isKindOfClass:[NSView class]]) {  // Sent from button
201     sv = find_saverView ((NSView *) sender);
202   } else {
203     int i;
204     NSWindow *w = 0;
205     for (i = [windows count]-1; i >= 0; i--) {  // Sent from menubar
206       w = [windows objectAtIndex:i];
207       if ([w isKeyWindow]) break;
208     }
209     sv = find_saverView ([w contentView]);
210   }
211
212   NSAssert (sv, @"no saver view");
213   NSWindow *prefs = [sv configureSheet];
214
215   [NSApp beginSheet:prefs
216      modalForWindow:[sv window]
217       modalDelegate:self
218      didEndSelector:@selector(preferencesClosed:returnCode:contextInfo:)
219         contextInfo:nil];
220   int code = [NSApp runModalForWindow:prefs];
221   
222   /* Restart the animation if the "OK" button was hit, but not if "Cancel".
223      We have to restart *both* animations, because the xlockmore-style
224      ones will blow up if one re-inits but the other doesn't.
225    */
226   if (code != NSCancelButton) {
227     if ([sv isAnimating])
228       [sv stopAnimation];
229     [sv startAnimation];
230   }
231 }
232
233
234 - (void) preferencesClosed: (NSWindow *) sheet
235                 returnCode: (int) returnCode
236                contextInfo: (void  *) contextInfo
237 {
238   [NSApp stopModalWithCode:returnCode];
239 }
240
241 #else  // USE_IPHONE
242
243
244 - (UIImage *) screenshot
245 {
246   return saved_screenshot;
247 }
248
249 - (void) saveScreenshot
250 {
251   // Most of this is from:
252   // http://developer.apple.com/library/ios/#qa/qa1703/_index.html
253   // The rotation stuff is by me.
254
255   CGSize size = [[UIScreen mainScreen] bounds].size;
256
257   UIInterfaceOrientation orient =
258     [[window rootViewController] interfaceOrientation];
259   if (orient == UIInterfaceOrientationLandscapeLeft ||
260       orient == UIInterfaceOrientationLandscapeRight) {
261     // Rotate the shape of the canvas 90 degrees.
262     double s = size.width;
263     size.width = size.height;
264     size.height = s;
265   }
266
267
268   // Create a graphics context with the target size
269   // On iOS 4 and later, use UIGraphicsBeginImageContextWithOptions to
270   // take the scale into consideration
271   // On iOS prior to 4, fall back to use UIGraphicsBeginImageContext
272
273   if (UIGraphicsBeginImageContextWithOptions)
274     UIGraphicsBeginImageContextWithOptions (size, NO, 0);
275   else
276     UIGraphicsBeginImageContext (size);
277
278   CGContextRef ctx = UIGraphicsGetCurrentContext();
279
280
281   // Rotate the graphics context to match current hardware rotation.
282   //
283   switch (orient) {
284   case UIInterfaceOrientationPortraitUpsideDown:
285     CGContextTranslateCTM (ctx,  [window center].x,  [window center].y);
286     CGContextRotateCTM (ctx, M_PI);
287     CGContextTranslateCTM (ctx, -[window center].x, -[window center].y);
288     break;
289   case UIInterfaceOrientationLandscapeLeft:
290   case UIInterfaceOrientationLandscapeRight:
291     CGContextTranslateCTM (ctx,  
292                            ([window frame].size.height -
293                             [window frame].size.width) / 2,
294                            ([window frame].size.width -
295                             [window frame].size.height) / 2);
296     CGContextTranslateCTM (ctx,  [window center].x,  [window center].y);
297     CGContextRotateCTM (ctx, 
298                         (orient == UIInterfaceOrientationLandscapeLeft
299                          ?  M_PI/2
300                          : -M_PI/2));
301     CGContextTranslateCTM (ctx, -[window center].x, -[window center].y);
302     break;
303   default:
304     break;
305   }
306
307   // Iterate over every window from back to front
308   //
309   for (UIWindow *win in [[UIApplication sharedApplication] windows]) {
310     if (![win respondsToSelector:@selector(screen)] ||
311         [win screen] == [UIScreen mainScreen]) {
312
313       // -renderInContext: renders in the coordinate space of the layer,
314       // so we must first apply the layer's geometry to the graphics context
315       CGContextSaveGState (ctx);
316
317       // Center the context around the window's anchor point
318       CGContextTranslateCTM (ctx, [win center].x, [win center].y);
319
320       // Apply the window's transform about the anchor point
321       CGContextConcatCTM (ctx, [win transform]);
322
323       // Offset by the portion of the bounds left of and above anchor point
324       CGContextTranslateCTM (ctx,
325         -[win bounds].size.width  * [[win layer] anchorPoint].x,
326         -[win bounds].size.height * [[win layer] anchorPoint].y);
327
328       // Render the layer hierarchy to the current context
329       [[win layer] renderInContext:ctx];
330
331       // Restore the context
332       CGContextRestoreGState (ctx);
333     }
334   }
335
336   if (saved_screenshot)
337     [saved_screenshot release];
338   saved_screenshot = [UIGraphicsGetImageFromCurrentImageContext() retain];
339
340   UIGraphicsEndImageContext();
341 }
342
343
344 - (void) openPreferences: (id) sender
345 {
346   NSString *saver = [listController selected];
347   if (! saver) return;
348
349   [self loadSaver:saver launch:NO];
350   if (! saverView) return;
351
352   [rootViewController pushViewController: [saverView configureView]
353                       animated:YES];
354 }
355
356
357 - (void) loadSaverMenu: (id) sender
358 {
359   NSString *saver = [listController selected];
360   NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
361   if (saver) {
362     [prefs setObject:saver forKey:@"selectedSaverName"];
363   } else {
364     [prefs removeObjectForKey:@"selectedSaverName"];
365   }
366   [self saveScreenshot];
367   [self selectedSaverDidChange:nil];
368 }
369
370 #endif // USE_IPHONE
371
372
373
374 - (void)loadSaver:(NSString *)name launch:(BOOL)launch
375 {
376   // NSLog (@"selecting saver \"%@\"", name);
377
378 # ifndef USE_IPHONE
379
380   if (saverName && [saverName isEqualToString: name]) {
381     if (launch)
382       for (NSWindow *win in windows) {
383         ScreenSaverView *sv = find_saverView ([win contentView]);
384         if (![sv isAnimating])
385           [sv startAnimation];
386       }
387     return;
388   }
389
390   saverName = name;
391
392   for (NSWindow *win in windows) {
393     NSView *cv = [win contentView];
394     NSString *old_title = [win title];
395     if (!old_title) old_title = @"XScreenSaver";
396     [win setTitle: name];
397     relabel_menus (menubar, old_title, name);
398
399     ScreenSaverView *old_view = find_saverView (cv);
400     NSView *sup = old_view ? [old_view superview] : cv;
401
402     if (old_view) {
403       if ([old_view isAnimating])
404         [old_view stopAnimation];
405       [old_view removeFromSuperview];
406     }
407
408     NSSize size = [cv frame].size;
409     ScreenSaverView *new_view = [self makeSaverView:name withSize: size];
410     NSAssert (new_view, @"unable to make a saver view");
411
412     [new_view setFrame: (old_view ? [old_view frame] : [cv frame])];
413     [sup addSubview: new_view];
414     [win makeFirstResponder:new_view];
415     [new_view setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
416     [new_view retain];
417     if (launch)
418       [new_view startAnimation];
419   }
420
421   NSUserDefaultsController *ctl =
422     [NSUserDefaultsController sharedUserDefaultsController];
423   [ctl save:self];
424
425 # else  // USE_IPHONE
426
427   if (saverName && [saverName isEqualToString: name]) {
428     if (launch && ![saverView isAnimating]) {
429       [window addSubview: saverView];
430       [saverView startAnimation];
431     }
432     return;
433   }
434
435   saverName = name;
436
437   if (saverView) {
438     if ([saverView isAnimating])
439       [saverView stopAnimation];
440     [saverView removeFromSuperview];
441   }
442
443   NSSize size = [window frame].size;
444   saverView = [self makeSaverView:name withSize: size];
445
446   if (! saverView) {
447     [[[UIAlertView alloc] initWithTitle: name
448                           message: @"Unable to load!"
449                           delegate: nil
450                           cancelButtonTitle: @"Bummer"
451                           otherButtonTitles: nil]
452      show];
453     return;
454   }
455
456   [saverView setFrame: [window frame]];
457   [saverView retain];
458   [[NSNotificationCenter defaultCenter]
459     addObserver:saverView
460     selector:@selector(didRotate:)
461     name:UIDeviceOrientationDidChangeNotification object:nil];
462
463   if (launch) {
464     [window addSubview: saverView];
465     [saverView startAnimation];
466   }
467 # endif // USE_IPHONE
468 }
469
470
471 - (void)loadSaver:(NSString *)name
472 {
473   [self loadSaver:name launch:YES];
474 }
475
476
477 - (void)aboutPanel:(id)sender
478 {
479 # ifndef USE_IPHONE
480   NSDictionary *bd = [saverBundle infoDictionary];
481   NSMutableDictionary *d = [NSMutableDictionary dictionaryWithCapacity:20];
482
483   [d setValue:[bd objectForKey:@"CFBundleName"] forKey:@"ApplicationName"];
484   [d setValue:[bd objectForKey:@"CFBundleVersion"] forKey:@"Version"];
485   [d setValue:[bd objectForKey:@"CFBundleShortVersionString"] 
486      forKey:@"ApplicationVersion"];
487   [d setValue:[bd objectForKey:@"NSHumanReadableCopyright"] forKey:@"Copy"];
488   [d setValue:[[NSAttributedString alloc]
489                 initWithString: (NSString *) 
490                   [bd objectForKey:@"CFBundleGetInfoString"]]
491      forKey:@"Credits"];
492
493   [[NSApplication sharedApplication]
494     orderFrontStandardAboutPanelWithOptions:d];
495
496 # else  // USE_IPHONE
497
498   NSDictionary *bd = [[NSBundle mainBundle] infoDictionary];
499   NSString *body = [bd objectForKey:@"CFBundleGetInfoString"];
500
501   body = [body stringByReplacingOccurrencesOfString:@", " withString:@",\n"];
502   body = [body stringByAppendingString:
503                @"\n\n"
504                "Double-tap to run.\n\n"
505                "Double-tap again to\n"
506                "return to this list."];
507
508   [[[UIAlertView alloc] initWithTitle: @""
509                         message: body
510                         delegate: nil
511                         cancelButtonTitle: @"OK"
512                         otherButtonTitles: nil]
513     show];
514
515 # endif // USE_IPHONE
516 }
517
518
519
520 - (void)selectedSaverDidChange:(NSDictionary *)change
521 {
522   NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
523   NSString *name = [prefs stringForKey:@"selectedSaverName"];
524
525   if (! name) return;
526
527   if (! [saverNames containsObject:name]) {
528     NSLog (@"saver \"%@\" does not exist", name);
529     return;
530   }
531
532   [self loadSaver: name];
533 }
534
535
536 - (NSArray *) listSaverBundleNamesInDir:(NSString *)dir
537 {
538 # ifndef USE_IPHONE
539   NSString *ext = @"saver";
540 # else
541   NSString *ext = @"xml";
542 # endif
543
544   NSArray *files = [[NSFileManager defaultManager]
545                      contentsOfDirectoryAtPath:dir error:nil];
546   if (! files) return 0;
547   NSMutableArray *result = [NSMutableArray arrayWithCapacity: [files count]+1];
548
549   for (NSString *p in files) {
550     if ([[p pathExtension] caseInsensitiveCompare: ext]) 
551       continue;
552
553 # ifndef USE_IPHONE
554     NSString *name = [[p lastPathComponent] stringByDeletingPathExtension];
555 # else  // !USE_IPHONE
556
557     // Get the saver name's capitalization right by reading the XML file.
558
559     p = [dir stringByAppendingPathComponent: p];
560     NSString *name = [NSString stringWithContentsOfFile:p
561                                encoding:NSISOLatin1StringEncoding
562                                error:nil];
563     NSRange r = [name rangeOfString:@"_label=\"" options:0];
564     name = [name substringFromIndex: r.location + r.length];
565     r = [name rangeOfString:@"\"" options:0];
566     name = [name substringToIndex: r.location];
567
568     NSAssert1 (name, @"no name in %@", p);
569
570 # endif // !USE_IPHONE
571
572     [result addObject: name];
573   }
574
575   return result;
576 }
577
578
579
580 - (NSArray *) listSaverBundleNames
581 {
582   NSMutableArray *dirs = [NSMutableArray arrayWithCapacity: 10];
583
584 # ifndef USE_IPHONE
585   // On MacOS, look in the "Contents/PlugIns/" directory in the bundle.
586   [dirs addObject: [[NSBundle mainBundle] builtInPlugInsPath]];
587
588   // Also look in the same directory as the executable.
589   [dirs addObject: [[[NSBundle mainBundle] bundlePath]
590                      stringByDeletingLastPathComponent]];
591
592   // Finally, look in standard MacOS screensaver directories.
593   [dirs addObject: @"~/Library/Screen Savers"];
594   [dirs addObject: @"/Library/Screen Savers"];
595   [dirs addObject: @"/System/Library/Screen Savers"];
596
597 # else
598   // On iOS, just look in the bundle's root directory.
599   [dirs addObject: [[NSBundle mainBundle] bundlePath]];
600 # endif
601
602   int i;
603   for (i = 0; i < [dirs count]; i++) {
604     NSString *dir = [dirs objectAtIndex:i];
605     NSArray *names = [self listSaverBundleNamesInDir:dir];
606     if (! names) continue;
607     saverDir   = [dir retain];
608     saverNames = [names retain];
609     return names;
610   }
611
612   NSString *err = @"no .saver bundles found in: ";
613   for (i = 0; i < [dirs count]; i++) {
614     if (i) err = [err stringByAppendingString:@", "];
615     err = [err stringByAppendingString:[[dirs objectAtIndex:i] 
616                                          stringByAbbreviatingWithTildeInPath]];
617     err = [err stringByAppendingString:@"/"];
618   }
619   NSLog (@"%@", err);
620   return [NSArray array];
621 }
622
623
624 /* Create the popup menu of available saver names.
625  */
626 #ifndef USE_IPHONE
627
628 - (NSPopUpButton *) makeMenu
629 {
630   NSRect rect;
631   rect.origin.x = rect.origin.y = 0;
632   rect.size.width = 10;
633   rect.size.height = 10;
634   NSPopUpButton *popup = [[NSPopUpButton alloc] initWithFrame:rect
635                                                     pullsDown:NO];
636   int i;
637   float max_width = 0;
638   for (i = 0; i < [saverNames count]; i++) {
639     NSString *name = [saverNames objectAtIndex:i];
640     [popup addItemWithTitle:name];
641     [[popup itemWithTitle:name] setRepresentedObject:name];
642     [popup sizeToFit];
643     NSRect r = [popup frame];
644     if (r.size.width > max_width) max_width = r.size.width;
645   }
646
647   // Bind the menu to preferences, and trigger a callback when an item
648   // is selected.
649   //
650   NSString *key = @"values.selectedSaverName";
651   NSUserDefaultsController *prefs =
652     [NSUserDefaultsController sharedUserDefaultsController];
653   [prefs addObserver:self
654          forKeyPath:key
655             options:0
656             context:@selector(selectedSaverDidChange:)];
657   [popup   bind:@"selectedObject"
658        toObject:prefs
659     withKeyPath:key
660         options:nil];
661   [prefs setAppliesImmediately:YES];
662
663   NSRect r = [popup frame];
664   r.size.width = max_width;
665   [popup setFrame:r];
666   return popup;
667 }
668
669 #else  // USE_IPHONE
670
671 /* Create a dictionary of one-line descriptions of every saver,
672    for display on the UITableView.
673  */
674 - (NSDictionary *)makeDescTable
675 {
676   NSMutableDictionary *dict = 
677     [NSMutableDictionary dictionaryWithCapacity:[saverNames count]];
678
679   for (NSString *saver in saverNames) {
680     NSString *desc = 0;
681     NSString *path = [saverDir stringByAppendingPathComponent:
682                                  [[saver lowercaseString]
683                                    stringByReplacingOccurrencesOfString:@" "
684                                    withString:@""]];
685     NSRange r;
686
687     path = [path stringByAppendingPathExtension:@"xml"];
688     desc = [NSString stringWithContentsOfFile:path
689                      encoding:NSISOLatin1StringEncoding
690                      error:nil];
691     if (! desc) goto FAIL;
692
693     r = [desc rangeOfString:@"<_description>"
694               options:NSCaseInsensitiveSearch];
695     if (r.length == 0) {
696       desc = 0;
697       goto FAIL;
698     }
699     desc = [desc substringFromIndex: r.location + r.length];
700     r = [desc rangeOfString:@"</_description>"
701               options:NSCaseInsensitiveSearch];
702     if (r.length > 0)
703       desc = [desc substringToIndex: r.location];
704
705     // Leading and trailing whitespace.
706     desc = [desc stringByTrimmingCharactersInSet:
707                    [NSCharacterSet whitespaceAndNewlineCharacterSet]];
708
709     // Let's see if we can find a year on the last line.
710     r = [desc rangeOfString:@"\n" options:NSBackwardsSearch];
711     NSString *year = 0;
712     for (NSString *word in
713            [[desc substringFromIndex:r.location + r.length]
714              componentsSeparatedByCharactersInSet:
715                [NSCharacterSet characterSetWithCharactersInString:
716                                  @" \t\n-."]]) {
717       int n = [word doubleValue];
718       if (n > 1970 && n < 2100)
719         year = word;
720     }
721
722     // Delete everything after the first blank line.
723     r = [desc rangeOfString:@"\n\n" options:0];
724     if (r.length > 0)
725       desc = [desc substringToIndex: r.location];
726
727     // Truncate really long ones.
728     int max = 140;
729     if ([desc length] > max)
730       desc = [desc substringToIndex: max];
731
732     if (year)
733       desc = [year stringByAppendingString:
734                      [@": " stringByAppendingString: desc]];
735
736   FAIL:
737     if (! desc) {
738       desc = @"Oops, this module appears to be incomplete.";
739       // NSLog(@"broken saver: %@", path);
740     }
741
742     [dict setObject:desc forKey:saver];
743   }
744
745   return dict;
746 }
747
748
749 - (UIViewController *) makeMenu
750 {
751   listController = [[[SaverListController alloc] 
752                       initWithNames:saverNames
753                       descriptions:[self makeDescTable]]
754                      retain];
755   UIBarButtonItem *run  = [[[UIBarButtonItem alloc]
756                              initWithTitle:@"Run"
757                              style: UIBarButtonItemStylePlain
758                              target: self
759                              action: @selector(loadSaverMenu:)]
760                             autorelease];
761   UIBarButtonItem *about = [[[UIBarButtonItem alloc]
762                              initWithTitle: @"About"
763                              style: UIBarButtonItemStylePlain
764                              target: self
765                              action: @selector(aboutPanel:)]
766                             autorelease];
767   UIBarButtonItem *pref = [[[UIBarButtonItem alloc]
768                              initWithTitle:@"Settings"
769                              style: UIBarButtonItemStylePlain
770                              target: self
771                              action: @selector(openPreferences:)]
772                             autorelease];
773   NSArray *a = [NSArray arrayWithObjects: pref, about, nil];
774
775   [run   setEnabled:NO];
776   [about setEnabled:YES];
777   [pref  setEnabled:NO];
778   listController.navigationItem.leftBarButtonItem  = run;
779   listController.navigationItem.rightBarButtonItems = a;
780
781   return listController;
782 }
783
784 #endif // USE_IPHONE
785
786
787
788 /* This is called when the "selectedSaverName" pref changes, e.g.,
789    when a menu selection is made.
790  */
791 - (void)observeValueForKeyPath:(NSString *)keyPath
792                       ofObject:(id)object
793                         change:(NSDictionary *)change
794                        context:(void *)context
795 {
796   SEL dispatchSelector = (SEL)context;
797   if (dispatchSelector != NULL) {
798     [self performSelector:dispatchSelector withObject:change];
799   } else {
800     [super observeValueForKeyPath:keyPath
801                          ofObject:object
802                            change:change
803                           context:context];
804   }
805 }
806
807
808 # ifndef USE_IPHONE
809
810 /* Create the desktop window shell, possibly including a preferences button.
811  */
812 - (NSWindow *) makeWindow
813 {
814   NSRect rect;
815   static int count = 0;
816   Bool simple_p = ([saverNames count] == 1);
817   NSButton *pb = 0;
818   NSPopUpButton *menu = 0;
819   NSBox *gbox = 0;
820   NSBox *pbox = 0;
821
822   NSRect sv_rect;
823   sv_rect.origin.x = sv_rect.origin.y = 0;
824   sv_rect.size.width = 320;
825   sv_rect.size.height = 240;
826   ScreenSaverView *sv = [[ScreenSaverView alloc]  // dummy placeholder
827                           initWithFrame:sv_rect
828                           isPreview:YES];
829
830   // make a "Preferences" button
831   //
832   if (! simple_p) {
833     rect.origin.x = 0;
834     rect.origin.y = 0;
835     rect.size.width = rect.size.height = 10;
836     pb = [[NSButton alloc] initWithFrame:rect];
837     [pb setTitle:@"Preferences"];
838     [pb setBezelStyle:NSRoundedBezelStyle];
839     [pb sizeToFit];
840
841     rect.origin.x = ([sv frame].size.width -
842                      [pb frame].size.width) / 2;
843     [pb setFrameOrigin:rect.origin];
844   
845     // grab the click
846     //
847     [pb setTarget:self];
848     [pb setAction:@selector(openPreferences:)];
849
850     // Make a saver selection menu
851     //
852     menu = [self makeMenu];
853     rect.origin.x = 2;
854     rect.origin.y = 2;
855     [menu setFrameOrigin:rect.origin];
856
857     // make a box to wrap the saverView
858     //
859     rect = [sv frame];
860     rect.origin.x = 0;
861     rect.origin.y = [pb frame].origin.y + [pb frame].size.height;
862     gbox = [[NSBox alloc] initWithFrame:rect];
863     rect.size.width = rect.size.height = 10;
864     [gbox setContentViewMargins:rect.size];
865     [gbox setTitlePosition:NSNoTitle];
866     [gbox addSubview:sv];
867     [gbox sizeToFit];
868
869     // make a box to wrap the other two boxes
870     //
871     rect.origin.x = rect.origin.y = 0;
872     rect.size.width  = [gbox frame].size.width;
873     rect.size.height = [gbox frame].size.height + [gbox frame].origin.y;
874     pbox = [[NSBox alloc] initWithFrame:rect];
875     [pbox setTitlePosition:NSNoTitle];
876     [pbox setBorderType:NSNoBorder];
877     [pbox addSubview:gbox];
878     if (menu) [pbox addSubview:menu];
879     if (pb)   [pbox addSubview:pb];
880     [pbox sizeToFit];
881
882     [pb   setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin];
883     [menu setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin];
884     [gbox setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
885     [pbox setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
886   }
887
888   [sv     setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
889
890
891   // and make a window to hold that.
892   //
893   NSScreen *screen = [NSScreen mainScreen];
894   rect = pbox ? [pbox frame] : [sv frame];
895   rect.origin.x = ([screen frame].size.width  - rect.size.width)  / 2;
896   rect.origin.y = ([screen frame].size.height - rect.size.height) / 2;
897   
898   rect.origin.x += rect.size.width * (count ? 0.55 : -0.55);
899   
900   NSWindow *win = [[NSWindow alloc]
901                       initWithContentRect:rect
902                                 styleMask:(NSTitledWindowMask |
903                                            NSClosableWindowMask |
904                                            NSMiniaturizableWindowMask |
905                                            NSResizableWindowMask)
906                                   backing:NSBackingStoreBuffered
907                                     defer:YES
908                                    screen:screen];
909   [win setMinSize:[win frameRectForContentRect:rect].size];
910   [[win contentView] addSubview: (pbox ? (NSView *) pbox : (NSView *) sv)];
911
912   [win makeKeyAndOrderFront:win];
913   
914   [sv startAnimation]; // this is the dummy saver
915
916   count++;
917
918   return win;
919 }
920
921 # endif // !USE_IPHONE
922
923
924 - (void)applicationDidFinishLaunching:
925 # ifndef USE_IPHONE
926     (NSNotification *) notif
927 # else  // USE_IPHONE
928     (UIApplication *) application
929 # endif // USE_IPHONE
930 {
931   [self listSaverBundleNames];
932
933 # ifndef USE_IPHONE
934   int window_count = ([saverNames count] <= 1 ? 1 : 2);
935   NSMutableArray *a = [[NSMutableArray arrayWithCapacity: window_count+1]
936                         retain];
937   windows = a;
938
939   int i;
940   // Create either one window (for standalone, e.g. Phosphor.app)
941   // or two windows for SaverTester.app.
942   for (i = 0; i < window_count; i++) {
943     NSWindow *win = [self makeWindow];
944     // Get the last-saved window position out of preferences.
945     [win setFrameAutosaveName:
946               [NSString stringWithFormat:@"XScreenSaverWindow%d", i]];
947     [win setFrameUsingName:[win frameAutosaveName]];
948     [a addObject: win];
949   }
950 # else  // USE_IPHONE
951
952 # undef ya_rand_init
953   ya_rand_init (0);     // Now's a good time.
954
955   rootViewController = [[[RotateyViewController alloc] init] retain];
956   [window setRootViewController: rootViewController];
957   [rootViewController pushViewController:[self makeMenu]
958                       animated:YES];
959
960   [window makeKeyAndVisible];
961   [window setAutoresizesSubviews:YES];
962   [window setAutoresizingMask: 
963             (UIViewAutoresizingFlexibleWidth | 
964              UIViewAutoresizingFlexibleHeight)];
965
966   // Has to be after the list window is up, or we get black.
967   [self saveScreenshot];
968
969 # endif // USE_IPHONE
970
971   NSString *forced = 0;
972   /* In the XCode project, each .saver scheme sets this env var when
973      launching SaverTester.app so that it knows which one we are
974      currently debugging.  If this is set, it overrides the default
975      selection in the popup menu.  If unset, that menu persists to
976      whatever it was last time.
977    */
978   const char *f = getenv ("SELECTED_SAVER");
979   if (f && *f)
980     forced = [NSString stringWithCString:(char *)f
981                        encoding:NSUTF8StringEncoding];
982
983   if (forced && ![saverNames containsObject:forced]) {
984     NSLog(@"forced saver \"%@\" does not exist", forced);
985     forced = 0;
986   }
987
988   // If there's only one saver, run that.
989   if (!forced && [saverNames count] == 1)
990     forced = [saverNames objectAtIndex:0];
991
992   NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
993
994 # ifdef USE_IPHONE
995   NSString *prev = [prefs stringForKey:@"selectedSaverName"];
996   if (forced)
997     prev = forced;
998
999   // If nothing was selected (e.g., this is the first launch)
1000   // then scroll randomly instead of starting up at "A".
1001   //
1002   if (!prev)
1003     prev = [saverNames objectAtIndex: (random() % [saverNames count])];
1004
1005   if (prev)
1006     [listController scrollTo: prev];
1007 # endif // USE_IPHONE
1008
1009   if (forced)
1010     [prefs setObject:forced forKey:@"selectedSaverName"];
1011
1012 # ifdef USE_IPHONE
1013   /* Don't auto-launch the saver unless it was running last time.
1014      XScreenSaverView manages this, on crash_timer.
1015    */
1016   if (! [prefs boolForKey:@"wasRunning"])
1017     return;
1018 # endif
1019
1020   [self selectedSaverDidChange:nil];
1021 }
1022
1023
1024 #ifndef USE_IPHONE
1025
1026 /* When the window closes, exit (even if prefs still open.)
1027 */
1028 - (BOOL) applicationShouldTerminateAfterLastWindowClosed: (NSApplication *) n
1029 {
1030   return YES;
1031 }
1032
1033 # else // USE_IPHONE
1034
1035 - (void)applicationWillResignActive:(UIApplication *)app
1036 {
1037   [(XScreenSaverView *)view setScreenLocked:YES];
1038 }
1039
1040 - (void)applicationDidBecomeActive:(UIApplication *)app
1041 {
1042   [(XScreenSaverView *)view setScreenLocked:NO];
1043 }
1044
1045 - (void)applicationDidEnterBackground:(UIApplication *)application
1046 {
1047   [(XScreenSaverView *)view setScreenLocked:YES];
1048 }
1049
1050 #endif // USE_IPHONE
1051
1052
1053 @end