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