From http://www.jwz.org/xscreensaver/xscreensaver-5.17.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: (NSString *) saver
345 {
346   [self loadSaver:saver launch:NO];
347   if (! saverView) return;
348
349   NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
350   [prefs setObject:saver forKey:@"selectedSaverName"];
351   [prefs synchronize];
352
353   [rootViewController pushViewController: [saverView configureView]
354                       animated:YES];
355 }
356
357
358 #endif // USE_IPHONE
359
360
361
362 - (void)loadSaver:(NSString *)name launch:(BOOL)launch
363 {
364   // NSLog (@"selecting saver \"%@\"", name);
365
366 # ifndef USE_IPHONE
367
368   if (saverName && [saverName isEqualToString: name]) {
369     if (launch)
370       for (NSWindow *win in windows) {
371         ScreenSaverView *sv = find_saverView ([win contentView]);
372         if (![sv isAnimating])
373           [sv startAnimation];
374       }
375     return;
376   }
377
378   saverName = name;
379
380   for (NSWindow *win in windows) {
381     NSView *cv = [win contentView];
382     NSString *old_title = [win title];
383     if (!old_title) old_title = @"XScreenSaver";
384     [win setTitle: name];
385     relabel_menus (menubar, old_title, name);
386
387     ScreenSaverView *old_view = find_saverView (cv);
388     NSView *sup = old_view ? [old_view superview] : cv;
389
390     if (old_view) {
391       if ([old_view isAnimating])
392         [old_view stopAnimation];
393       [old_view removeFromSuperview];
394     }
395
396     NSSize size = [cv frame].size;
397     ScreenSaverView *new_view = [self makeSaverView:name withSize: size];
398     NSAssert (new_view, @"unable to make a saver view");
399
400     [new_view setFrame: (old_view ? [old_view frame] : [cv frame])];
401     [sup addSubview: new_view];
402     [win makeFirstResponder:new_view];
403     [new_view setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
404     [new_view retain];
405     if (launch)
406       [new_view startAnimation];
407   }
408
409   NSUserDefaultsController *ctl =
410     [NSUserDefaultsController sharedUserDefaultsController];
411   [ctl save:self];
412
413 # else  // USE_IPHONE
414
415   NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
416   [prefs setObject:name forKey:@"selectedSaverName"];
417   [prefs synchronize];
418
419   if (saverName && [saverName isEqualToString: name]) {
420     if (launch && ![saverView isAnimating]) {
421       [window addSubview: saverView];
422       [saverView startAnimation];
423     }
424     return;
425   }
426
427   saverName = name;
428
429   if (saverView) {
430     if ([saverView isAnimating])
431       [saverView stopAnimation];
432     [saverView removeFromSuperview];
433   }
434
435   NSSize size = [window frame].size;
436   saverView = [self makeSaverView:name withSize: size];
437
438   if (! saverView) {
439     [[[UIAlertView alloc] initWithTitle: name
440                           message: @"Unable to load!"
441                           delegate: nil
442                           cancelButtonTitle: @"Bummer"
443                           otherButtonTitles: nil]
444      show];
445     return;
446   }
447
448   [saverView setFrame: [window frame]];
449   [saverView retain];
450   [[NSNotificationCenter defaultCenter]
451     addObserver:saverView
452     selector:@selector(didRotate:)
453     name:UIDeviceOrientationDidChangeNotification object:nil];
454
455   if (launch) {
456     [window addSubview: saverView];
457     [saverView startAnimation];
458   }
459 # endif // USE_IPHONE
460 }
461
462
463 - (void)loadSaver:(NSString *)name
464 {
465   [self loadSaver:name launch:YES];
466 }
467
468
469 - (void)aboutPanel:(id)sender
470 {
471 # ifndef USE_IPHONE
472   NSDictionary *bd = [saverBundle infoDictionary];
473   NSMutableDictionary *d = [NSMutableDictionary dictionaryWithCapacity:20];
474
475   [d setValue:[bd objectForKey:@"CFBundleName"] forKey:@"ApplicationName"];
476   [d setValue:[bd objectForKey:@"CFBundleVersion"] forKey:@"Version"];
477   [d setValue:[bd objectForKey:@"CFBundleShortVersionString"] 
478      forKey:@"ApplicationVersion"];
479   [d setValue:[bd objectForKey:@"NSHumanReadableCopyright"] forKey:@"Copy"];
480   [d setValue:[[NSAttributedString alloc]
481                 initWithString: (NSString *) 
482                   [bd objectForKey:@"CFBundleGetInfoString"]]
483      forKey:@"Credits"];
484
485   [[NSApplication sharedApplication]
486     orderFrontStandardAboutPanelWithOptions:d];
487
488 # else  // USE_IPHONE
489
490   NSDictionary *bd = [[NSBundle mainBundle] infoDictionary];
491   NSString *body = [bd objectForKey:@"CFBundleGetInfoString"];
492
493   body = [body stringByReplacingOccurrencesOfString:@", " withString:@",\n"];
494   body = [body stringByAppendingString:
495                @"\n\n"
496                "Double-tap to run.\n\n"
497                "Double-tap again to\n"
498                "return to this list."];
499
500   [[[UIAlertView alloc] initWithTitle: @""
501                         message: body
502                         delegate: nil
503                         cancelButtonTitle: @"OK"
504                         otherButtonTitles: nil]
505     show];
506
507 # endif // USE_IPHONE
508 }
509
510
511
512 - (void)selectedSaverDidChange:(NSDictionary *)change
513 {
514   NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
515   NSString *name = [prefs stringForKey:@"selectedSaverName"];
516
517   if (! name) return;
518
519   if (! [saverNames containsObject:name]) {
520     NSLog (@"saver \"%@\" does not exist", name);
521     return;
522   }
523
524   [self loadSaver: name];
525 }
526
527
528 - (NSArray *) listSaverBundleNamesInDir:(NSString *)dir
529 {
530 # ifndef USE_IPHONE
531   NSString *ext = @"saver";
532 # else
533   NSString *ext = @"xml";
534 # endif
535
536   NSArray *files = [[NSFileManager defaultManager]
537                      contentsOfDirectoryAtPath:dir error:nil];
538   if (! files) return 0;
539   NSMutableArray *result = [NSMutableArray arrayWithCapacity: [files count]+1];
540
541   for (NSString *p in files) {
542     if ([[p pathExtension] caseInsensitiveCompare: ext]) 
543       continue;
544
545 # ifndef USE_IPHONE
546     NSString *name = [[p lastPathComponent] stringByDeletingPathExtension];
547 # else  // !USE_IPHONE
548
549     // Get the saver name's capitalization right by reading the XML file.
550
551     p = [dir stringByAppendingPathComponent: p];
552     NSString *name = [NSString stringWithContentsOfFile:p
553                                encoding:NSISOLatin1StringEncoding
554                                error:nil];
555     NSRange r = [name rangeOfString:@"_label=\"" options:0];
556     name = [name substringFromIndex: r.location + r.length];
557     r = [name rangeOfString:@"\"" options:0];
558     name = [name substringToIndex: r.location];
559
560     NSAssert1 (name, @"no name in %@", p);
561
562 # endif // !USE_IPHONE
563
564     [result addObject: name];
565   }
566
567   return result;
568 }
569
570
571
572 - (NSArray *) listSaverBundleNames
573 {
574   NSMutableArray *dirs = [NSMutableArray arrayWithCapacity: 10];
575
576 # ifndef USE_IPHONE
577   // On MacOS, look in the "Contents/PlugIns/" directory in the bundle.
578   [dirs addObject: [[NSBundle mainBundle] builtInPlugInsPath]];
579
580   // Also look in the same directory as the executable.
581   [dirs addObject: [[[NSBundle mainBundle] bundlePath]
582                      stringByDeletingLastPathComponent]];
583
584   // Finally, look in standard MacOS screensaver directories.
585   [dirs addObject: @"~/Library/Screen Savers"];
586   [dirs addObject: @"/Library/Screen Savers"];
587   [dirs addObject: @"/System/Library/Screen Savers"];
588
589 # else
590   // On iOS, just look in the bundle's root directory.
591   [dirs addObject: [[NSBundle mainBundle] bundlePath]];
592 # endif
593
594   int i;
595   for (i = 0; i < [dirs count]; i++) {
596     NSString *dir = [dirs objectAtIndex:i];
597     NSArray *names = [self listSaverBundleNamesInDir:dir];
598     if (! names) continue;
599     saverDir   = [dir retain];
600     saverNames = [names retain];
601     return names;
602   }
603
604   NSString *err = @"no .saver bundles found in: ";
605   for (i = 0; i < [dirs count]; i++) {
606     if (i) err = [err stringByAppendingString:@", "];
607     err = [err stringByAppendingString:[[dirs objectAtIndex:i] 
608                                          stringByAbbreviatingWithTildeInPath]];
609     err = [err stringByAppendingString:@"/"];
610   }
611   NSLog (@"%@", err);
612   return [NSArray array];
613 }
614
615
616 /* Create the popup menu of available saver names.
617  */
618 #ifndef USE_IPHONE
619
620 - (NSPopUpButton *) makeMenu
621 {
622   NSRect rect;
623   rect.origin.x = rect.origin.y = 0;
624   rect.size.width = 10;
625   rect.size.height = 10;
626   NSPopUpButton *popup = [[NSPopUpButton alloc] initWithFrame:rect
627                                                     pullsDown:NO];
628   int i;
629   float max_width = 0;
630   for (i = 0; i < [saverNames count]; i++) {
631     NSString *name = [saverNames objectAtIndex:i];
632     [popup addItemWithTitle:name];
633     [[popup itemWithTitle:name] setRepresentedObject:name];
634     [popup sizeToFit];
635     NSRect r = [popup frame];
636     if (r.size.width > max_width) max_width = r.size.width;
637   }
638
639   // Bind the menu to preferences, and trigger a callback when an item
640   // is selected.
641   //
642   NSString *key = @"values.selectedSaverName";
643   NSUserDefaultsController *prefs =
644     [NSUserDefaultsController sharedUserDefaultsController];
645   [prefs addObserver:self
646          forKeyPath:key
647             options:0
648             context:@selector(selectedSaverDidChange:)];
649   [popup   bind:@"selectedObject"
650        toObject:prefs
651     withKeyPath:key
652         options:nil];
653   [prefs setAppliesImmediately:YES];
654
655   NSRect r = [popup frame];
656   r.size.width = max_width;
657   [popup setFrame:r];
658   return popup;
659 }
660
661 #else  // USE_IPHONE
662
663 /* Create a dictionary of one-line descriptions of every saver,
664    for display on the UITableView.
665  */
666 - (NSDictionary *)makeDescTable
667 {
668   NSMutableDictionary *dict = 
669     [NSMutableDictionary dictionaryWithCapacity:[saverNames count]];
670
671   for (NSString *saver in saverNames) {
672     NSString *desc = 0;
673     NSString *path = [saverDir stringByAppendingPathComponent:
674                                  [[saver lowercaseString]
675                                    stringByReplacingOccurrencesOfString:@" "
676                                    withString:@""]];
677     NSRange r;
678
679     path = [path stringByAppendingPathExtension:@"xml"];
680     desc = [NSString stringWithContentsOfFile:path
681                      encoding:NSISOLatin1StringEncoding
682                      error:nil];
683     if (! desc) goto FAIL;
684
685     r = [desc rangeOfString:@"<_description>"
686               options:NSCaseInsensitiveSearch];
687     if (r.length == 0) {
688       desc = 0;
689       goto FAIL;
690     }
691     desc = [desc substringFromIndex: r.location + r.length];
692     r = [desc rangeOfString:@"</_description>"
693               options:NSCaseInsensitiveSearch];
694     if (r.length > 0)
695       desc = [desc substringToIndex: r.location];
696
697     // Leading and trailing whitespace.
698     desc = [desc stringByTrimmingCharactersInSet:
699                    [NSCharacterSet whitespaceAndNewlineCharacterSet]];
700
701     // Let's see if we can find a year on the last line.
702     r = [desc rangeOfString:@"\n" options:NSBackwardsSearch];
703     NSString *year = 0;
704     for (NSString *word in
705            [[desc substringFromIndex:r.location + r.length]
706              componentsSeparatedByCharactersInSet:
707                [NSCharacterSet characterSetWithCharactersInString:
708                                  @" \t\n-."]]) {
709       int n = [word doubleValue];
710       if (n > 1970 && n < 2100)
711         year = word;
712     }
713
714     // Delete everything after the first blank line.
715     r = [desc rangeOfString:@"\n\n" options:0];
716     if (r.length > 0)
717       desc = [desc substringToIndex: r.location];
718
719     // Truncate really long ones.
720     int max = 140;
721     if ([desc length] > max)
722       desc = [desc substringToIndex: max];
723
724     if (year)
725       desc = [year stringByAppendingString:
726                      [@": " stringByAppendingString: desc]];
727
728   FAIL:
729     if (! desc) {
730       desc = @"Oops, this module appears to be incomplete.";
731       // NSLog(@"broken saver: %@", path);
732     }
733
734     [dict setObject:desc forKey:saver];
735   }
736
737   return dict;
738 }
739
740
741 #endif // USE_IPHONE
742
743
744
745 /* This is called when the "selectedSaverName" pref changes, e.g.,
746    when a menu selection is made.
747  */
748 - (void)observeValueForKeyPath:(NSString *)keyPath
749                       ofObject:(id)object
750                         change:(NSDictionary *)change
751                        context:(void *)context
752 {
753   SEL dispatchSelector = (SEL)context;
754   if (dispatchSelector != NULL) {
755     [self performSelector:dispatchSelector withObject:change];
756   } else {
757     [super observeValueForKeyPath:keyPath
758                          ofObject:object
759                            change:change
760                           context:context];
761   }
762 }
763
764
765 # ifndef USE_IPHONE
766
767 /* Create the desktop window shell, possibly including a preferences button.
768  */
769 - (NSWindow *) makeWindow
770 {
771   NSRect rect;
772   static int count = 0;
773   Bool simple_p = ([saverNames count] == 1);
774   NSButton *pb = 0;
775   NSPopUpButton *menu = 0;
776   NSBox *gbox = 0;
777   NSBox *pbox = 0;
778
779   NSRect sv_rect;
780   sv_rect.origin.x = sv_rect.origin.y = 0;
781   sv_rect.size.width = 320;
782   sv_rect.size.height = 240;
783   ScreenSaverView *sv = [[ScreenSaverView alloc]  // dummy placeholder
784                           initWithFrame:sv_rect
785                           isPreview:YES];
786
787   // make a "Preferences" button
788   //
789   if (! simple_p) {
790     rect.origin.x = 0;
791     rect.origin.y = 0;
792     rect.size.width = rect.size.height = 10;
793     pb = [[NSButton alloc] initWithFrame:rect];
794     [pb setTitle:@"Preferences"];
795     [pb setBezelStyle:NSRoundedBezelStyle];
796     [pb sizeToFit];
797
798     rect.origin.x = ([sv frame].size.width -
799                      [pb frame].size.width) / 2;
800     [pb setFrameOrigin:rect.origin];
801   
802     // grab the click
803     //
804     [pb setTarget:self];
805     [pb setAction:@selector(openPreferences:)];
806
807     // Make a saver selection menu
808     //
809     menu = [self makeMenu];
810     rect.origin.x = 2;
811     rect.origin.y = 2;
812     [menu setFrameOrigin:rect.origin];
813
814     // make a box to wrap the saverView
815     //
816     rect = [sv frame];
817     rect.origin.x = 0;
818     rect.origin.y = [pb frame].origin.y + [pb frame].size.height;
819     gbox = [[NSBox alloc] initWithFrame:rect];
820     rect.size.width = rect.size.height = 10;
821     [gbox setContentViewMargins:rect.size];
822     [gbox setTitlePosition:NSNoTitle];
823     [gbox addSubview:sv];
824     [gbox sizeToFit];
825
826     // make a box to wrap the other two boxes
827     //
828     rect.origin.x = rect.origin.y = 0;
829     rect.size.width  = [gbox frame].size.width;
830     rect.size.height = [gbox frame].size.height + [gbox frame].origin.y;
831     pbox = [[NSBox alloc] initWithFrame:rect];
832     [pbox setTitlePosition:NSNoTitle];
833     [pbox setBorderType:NSNoBorder];
834     [pbox addSubview:gbox];
835     if (menu) [pbox addSubview:menu];
836     if (pb)   [pbox addSubview:pb];
837     [pbox sizeToFit];
838
839     [pb   setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin];
840     [menu setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin];
841     [gbox setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
842     [pbox setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
843   }
844
845   [sv     setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
846
847
848   // and make a window to hold that.
849   //
850   NSScreen *screen = [NSScreen mainScreen];
851   rect = pbox ? [pbox frame] : [sv frame];
852   rect.origin.x = ([screen frame].size.width  - rect.size.width)  / 2;
853   rect.origin.y = ([screen frame].size.height - rect.size.height) / 2;
854   
855   rect.origin.x += rect.size.width * (count ? 0.55 : -0.55);
856   
857   NSWindow *win = [[NSWindow alloc]
858                       initWithContentRect:rect
859                                 styleMask:(NSTitledWindowMask |
860                                            NSClosableWindowMask |
861                                            NSMiniaturizableWindowMask |
862                                            NSResizableWindowMask)
863                                   backing:NSBackingStoreBuffered
864                                     defer:YES
865                                    screen:screen];
866   [win setMinSize:[win frameRectForContentRect:rect].size];
867   [[win contentView] addSubview: (pbox ? (NSView *) pbox : (NSView *) sv)];
868
869   [win makeKeyAndOrderFront:win];
870   
871   [sv startAnimation]; // this is the dummy saver
872
873   count++;
874
875   return win;
876 }
877
878 # endif // !USE_IPHONE
879
880
881 - (void)applicationDidFinishLaunching:
882 # ifndef USE_IPHONE
883     (NSNotification *) notif
884 # else  // USE_IPHONE
885     (UIApplication *) application
886 # endif // USE_IPHONE
887 {
888   [self listSaverBundleNames];
889
890 # ifndef USE_IPHONE
891   int window_count = ([saverNames count] <= 1 ? 1 : 2);
892   NSMutableArray *a = [[NSMutableArray arrayWithCapacity: window_count+1]
893                         retain];
894   windows = a;
895
896   int i;
897   // Create either one window (for standalone, e.g. Phosphor.app)
898   // or two windows for SaverTester.app.
899   for (i = 0; i < window_count; i++) {
900     NSWindow *win = [self makeWindow];
901     // Get the last-saved window position out of preferences.
902     [win setFrameAutosaveName:
903               [NSString stringWithFormat:@"XScreenSaverWindow%d", i]];
904     [win setFrameUsingName:[win frameAutosaveName]];
905     [a addObject: win];
906   }
907 # else  // USE_IPHONE
908
909 # undef ya_rand_init
910   ya_rand_init (0);     // Now's a good time.
911
912   rootViewController = [[[RotateyViewController alloc] init] retain];
913   [window setRootViewController: rootViewController];
914
915   SaverListController *menu = [[SaverListController alloc] 
916                                 initWithNames:saverNames
917                                 descriptions:[self makeDescTable]];
918   [rootViewController pushViewController:menu animated:YES];
919
920   [window makeKeyAndVisible];
921   [window setAutoresizesSubviews:YES];
922   [window setAutoresizingMask: 
923             (UIViewAutoresizingFlexibleWidth | 
924              UIViewAutoresizingFlexibleHeight)];
925
926   // Has to be after the list window is up, or we get black.
927   [self saveScreenshot];
928
929 # endif // USE_IPHONE
930
931   NSString *forced = 0;
932   /* In the XCode project, each .saver scheme sets this env var when
933      launching SaverTester.app so that it knows which one we are
934      currently debugging.  If this is set, it overrides the default
935      selection in the popup menu.  If unset, that menu persists to
936      whatever it was last time.
937    */
938   const char *f = getenv ("SELECTED_SAVER");
939   if (f && *f)
940     forced = [NSString stringWithCString:(char *)f
941                        encoding:NSUTF8StringEncoding];
942
943   if (forced && ![saverNames containsObject:forced]) {
944     NSLog(@"forced saver \"%@\" does not exist", forced);
945     forced = 0;
946   }
947
948   // If there's only one saver, run that.
949   if (!forced && [saverNames count] == 1)
950     forced = [saverNames objectAtIndex:0];
951
952   NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
953
954 # ifdef USE_IPHONE
955   NSString *prev = [prefs stringForKey:@"selectedSaverName"];
956
957   if (forced)
958     prev = forced;
959
960   // If nothing was selected (e.g., this is the first launch)
961   // then scroll randomly instead of starting up at "A".
962   //
963   if (!prev)
964     prev = [saverNames objectAtIndex: (random() % [saverNames count])];
965
966   if (prev)
967     [menu scrollTo: prev];
968 # endif // USE_IPHONE
969
970   if (forced)
971     [prefs setObject:forced forKey:@"selectedSaverName"];
972
973 # ifdef USE_IPHONE
974   /* Don't auto-launch the saver unless it was running last time.
975      XScreenSaverView manages this, on crash_timer.
976    */
977   if (! [prefs boolForKey:@"wasRunning"])
978     return;
979 # endif
980
981   [self selectedSaverDidChange:nil];
982 }
983
984
985 #ifndef USE_IPHONE
986
987 /* When the window closes, exit (even if prefs still open.)
988 */
989 - (BOOL) applicationShouldTerminateAfterLastWindowClosed: (NSApplication *) n
990 {
991   return YES;
992 }
993
994 # else // USE_IPHONE
995
996 - (void)applicationWillResignActive:(UIApplication *)app
997 {
998   [(XScreenSaverView *)view setScreenLocked:YES];
999 }
1000
1001 - (void)applicationDidBecomeActive:(UIApplication *)app
1002 {
1003   [(XScreenSaverView *)view setScreenLocked:NO];
1004 }
1005
1006 - (void)applicationDidEnterBackground:(UIApplication *)application
1007 {
1008   [(XScreenSaverView *)view setScreenLocked:YES];
1009 }
1010
1011 #endif // USE_IPHONE
1012
1013
1014 @end