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