From http://www.jwz.org/xscreensaver/xscreensaver-5.33.tar.gz
[xscreensaver] / OSX / SaverRunner.m
1 /* xscreensaver, Copyright (c) 2006-2014 Jamie Zawinski <jwz@jwz.org>
2  *
3  * Permission to use, copy, modify, distribute, and sell this software and its
4  * documentation for any purpose is hereby granted without fee, provided that
5  * the above copyright notice appear in all copies and that both that
6  * copyright notice and this permission notice appear in supporting
7  * documentation.  No representations are made about the suitability of this
8  * software for any purpose.  It is provided "as is" without express or 
9  * implied warranty.
10  */
11
12 /* This program serves three purposes:
13
14    First, It is a test harness for screen savers.  When it launches, it
15    looks around for .saver bundles (in the current directory, and then in
16    the standard directories) and puts up a pair of windows that allow you
17    to select the saver to run.  This is less clicking than running them
18    through System Preferences.  This is the "SaverTester.app" program.
19
20    Second, it can be used to transform any screen saver into a standalone
21    program.  Just put one (and only one) .saver bundle into the app
22    bundle's Contents/Resources/ directory, and it will load and run that
23    saver at start-up (without the saver-selection menu or other chrome).
24    This is how the "Phosphor.app" and "Apple2.app" programs work.
25
26    Third, it is the scaffolding which turns a set of screen savers into
27    a single iPhone / iPad program.  In that case, all of the savers are
28    linked in to this executable, since iOS does not allow dynamic loading
29    of bundles that have executable code in them.  Bleh.
30  */
31
32 #import <TargetConditionals.h>
33 #import "SaverRunner.h"
34 #import "SaverListController.h"
35 #import "XScreenSaverGLView.h"
36 #import "yarandom.h"
37
38 #ifdef USE_IPHONE
39
40 # ifndef __IPHONE_8_0
41 #  define UIInterfaceOrientationUnknown UIDeviceOrientationUnknown
42 # endif
43 # ifndef NSFoundationVersionNumber_iOS_7_1
44 #  define NSFoundationVersionNumber_iOS_7_1 1047.25
45 # endif
46 # ifndef NSFoundationVersionNumber_iOS_8_0
47 #  define NSFoundationVersionNumber_iOS_8_0 1134.10
48 # endif
49
50 @interface RotateyViewController : UINavigationController
51 {
52   BOOL allowRotation;
53 }
54 @end
55
56 @implementation RotateyViewController
57
58 /* This subclass exists so that we can ask that the SaverListController and
59    preferences panels be auto-rotated by the system.  Note that the 
60    XScreenSaverView is not auto-rotated because it is on a different UIWindow.
61  */
62
63 - (id)initWithRotation:(BOOL)rotatep
64 {
65   self = [super init];
66   allowRotation = rotatep;
67   return self;
68 }
69
70 - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)o
71 {
72   return allowRotation;                         /* Deprecated in iOS 6 */
73 }
74
75 - (BOOL)shouldAutorotate                        /* Added in iOS 6 */
76 {
77   return allowRotation;
78 }
79
80 - (NSUInteger)supportedInterfaceOrientations    /* Added in iOS 6 */
81 {
82   return UIInterfaceOrientationMaskAll;
83 }
84
85 @end
86
87
88 @implementation SaverViewController
89
90 @synthesize saverName;
91
92 - (id)initWithSaverRunner:(SaverRunner *)parent
93 {
94   self = [super init];
95   if (self) {
96     _parent = parent;
97     _storedOrientation = UIInterfaceOrientationUnknown;
98
99     self.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
100     self.wantsFullScreenLayout = YES;
101   }
102   return self;
103 }
104
105
106 - (void)dealloc
107 {
108   [_saverName release];
109   [_saverView dealloc];
110   [super dealloc];
111 }
112
113
114 - (void)loadView
115 {
116   // The UIViewController's view must never change, so it gets set here to
117   // a plain black background.
118
119   // This background view doesn't block the status bar, but that's probably
120   // OK, because it's never on screen for more than a fraction of a second.
121   UIView *backgroundView = [[UIView alloc] initWithFrame:CGRectNull];
122   backgroundView.backgroundColor = [UIColor blackColor];
123   self.view = backgroundView;
124 }
125
126
127 - (void)createSaverView
128 {
129   UIView *parentView = self.view;
130
131   if (_saverView) {
132     [_saverView removeFromSuperview];
133     [_saverView release];
134   }
135
136   if (_storedOrientation != UIInterfaceOrientationUnknown) {
137     [[UIApplication sharedApplication]
138      setStatusBarOrientation:_storedOrientation
139      animated:NO];
140   }
141
142   _saverView = [_parent makeSaverView:_saverName
143                              withSize:parentView.bounds.size];
144
145   if (! _saverView) {
146     [[[UIAlertView alloc] initWithTitle: _saverName
147                           message: @"Unable to load!"
148                           delegate: nil
149                           cancelButtonTitle: @"Bummer"
150                           otherButtonTitles: nil]
151      show];
152     return;
153   }
154
155   _saverView.delegate = _parent;
156
157   [self.view addSubview:_saverView];
158
159   // The first responder must be set only after the view was placed in the view
160   // heirarchy.
161   [_saverView becomeFirstResponder]; // For shakes on iOS 6.
162   [_saverView startAnimation];
163   [_parent aboutPanel:_saverView orientation:_storedOrientation];
164 }
165
166
167 - (void)viewDidAppear:(BOOL)animated
168 {
169   [self createSaverView];
170 }
171
172
173 - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)o
174 {
175   return NO;                                    /* Deprecated in iOS 6 */
176 }
177
178
179 - (BOOL)shouldAutorotate                        /* Added in iOS 6 */
180 {
181   return NO;
182 }
183
184
185 - (NSUInteger)supportedInterfaceOrientations    /* Added in iOS 6 */
186 {
187   // Lies from the iOS docs:
188   // "This method is only called if the view controller's shouldAutorotate
189   // method returns YES."
190   return UIInterfaceOrientationMaskPortrait;
191 }
192
193
194 - (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation
195 {
196   return UIInterfaceOrientationPortrait;
197 }
198
199
200 - (void)setSaverName:(NSString *)name
201 {
202   [name retain];
203   [_saverName release];
204   _saverName = name;
205   _storedOrientation = [UIApplication sharedApplication].statusBarOrientation;
206
207   if (_saverView)
208     [self createSaverView];
209 }
210
211 @end
212
213 #endif // USE_IPHONE
214
215
216 @implementation SaverRunner
217
218
219 - (XScreenSaverView *) makeSaverView: (NSString *) module
220                             withSize: (NSSize) size
221 {
222   Class new_class = 0;
223
224 # ifndef USE_IPHONE
225
226   // Load the XScreenSaverView subclass and code from a ".saver" bundle.
227
228   NSString *name = [module stringByAppendingPathExtension:@"saver"];
229   NSString *path = [saverDir stringByAppendingPathComponent:name];
230
231   if (! [[NSFileManager defaultManager] fileExistsAtPath:path]) {
232     NSLog(@"bundle \"%@\" does not exist", path);
233     return 0;
234   }
235
236   NSLog(@"Loading %@", path);
237
238   // NSBundle *obundle = saverBundle;
239
240   saverBundle = [NSBundle bundleWithPath:path];
241   if (saverBundle)
242     new_class = [saverBundle principalClass];
243
244   // Not entirely unsurprisingly, this tends to break the world.
245   // if (obundle && obundle != saverBundle)
246   //  [obundle unload];
247
248 # else  // USE_IPHONE
249
250   // Determine whether to create an X11 view or an OpenGL view by
251   // looking for the "gl" tag in the xml file.  This is kind of awful.
252
253   NSString *path = [saverDir
254                      stringByAppendingPathComponent:
255                        [[[module lowercaseString]
256                           stringByReplacingOccurrencesOfString:@" "
257                           withString:@""]
258                          stringByAppendingPathExtension:@"xml"]];
259   NSData *xmld = [NSData dataWithContentsOfFile:path];
260   NSAssert (xmld, @"no XML: %@", path);
261   NSString *xml = [XScreenSaverView decompressXML:xmld];
262   Bool gl_p = (xml && [xml rangeOfString:@"gl=\"yes\""].length > 0);
263
264   new_class = (gl_p
265                ? [XScreenSaverGLView class]
266                : [XScreenSaverView class]);
267
268 # endif // USE_IPHONE
269
270   if (! new_class)
271     return 0;
272
273   NSRect rect;
274   rect.origin.x = rect.origin.y = 0;
275   rect.size.width  = size.width;
276   rect.size.height = size.height;
277
278   XScreenSaverView *instance =
279     [(XScreenSaverView *) [new_class alloc]
280                           initWithFrame:rect
281                           saverName:module
282                           isPreview:YES];
283   if (! instance) {
284     NSLog(@"Failed to instantiate %@ for \"%@\"", new_class, module);
285     return 0;
286   }
287
288
289   /* KLUGE: Inform the underlying program that we're in "standalone"
290      mode, e.g. running as "Phosphor.app" rather than "Phosphor.saver".
291      This is kind of horrible but I haven't thought of a more sensible
292      way to make this work.
293    */
294 # ifndef USE_IPHONE
295   if ([saverNames count] == 1) {
296     putenv (strdup ("XSCREENSAVER_STANDALONE=1"));
297   }
298 # endif
299
300   return (XScreenSaverView *) instance;
301 }
302
303
304 #ifndef USE_IPHONE
305
306 static ScreenSaverView *
307 find_saverView_child (NSView *v)
308 {
309   NSArray *kids = [v subviews];
310   int nkids = [kids count];
311   int i;
312   for (i = 0; i < nkids; i++) {
313     NSObject *kid = [kids objectAtIndex:i];
314     if ([kid isKindOfClass:[ScreenSaverView class]]) {
315       return (ScreenSaverView *) kid;
316     } else {
317       ScreenSaverView *sv = find_saverView_child ((NSView *) kid);
318       if (sv) return sv;
319     }
320   }
321   return 0;
322 }
323
324
325 static ScreenSaverView *
326 find_saverView (NSView *v)
327 {
328   while (1) {
329     NSView *p = [v superview];
330     if (p) v = p;
331     else break;
332   }
333   return find_saverView_child (v);
334 }
335
336
337 /* Changes the contents of the menubar menus to correspond to
338    the running saver.  Desktop only.
339  */
340 static void
341 relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
342 {
343   if ([v isKindOfClass:[NSMenu class]]) {
344     NSMenu *m = (NSMenu *)v;
345     [m setTitle: [[m title] stringByReplacingOccurrencesOfString:old_str
346                             withString:new_str]];
347     NSArray *kids = [m itemArray];
348     int nkids = [kids count];
349     int i;
350     for (i = 0; i < nkids; i++) {
351       relabel_menus ([kids objectAtIndex:i], old_str, new_str);
352     }
353   } else if ([v isKindOfClass:[NSMenuItem class]]) {
354     NSMenuItem *mi = (NSMenuItem *)v;
355     [mi setTitle: [[mi title] stringByReplacingOccurrencesOfString:old_str
356                               withString:new_str]];
357     NSMenu *m = [mi submenu];
358     if (m) relabel_menus (m, old_str, new_str);
359   }
360 }
361
362
363 - (void) openPreferences: (id) sender
364 {
365   ScreenSaverView *sv;
366   if ([sender isKindOfClass:[NSView class]]) {  // Sent from button
367     sv = find_saverView ((NSView *) sender);
368   } else {
369     int i;
370     NSWindow *w = 0;
371     for (i = [windows count]-1; i >= 0; i--) {  // Sent from menubar
372       w = [windows objectAtIndex:i];
373       if ([w isKeyWindow]) break;
374     }
375     sv = find_saverView ([w contentView]);
376   }
377
378   NSAssert (sv, @"no saver view");
379   if (!sv) return;
380   NSWindow *prefs = [sv configureSheet];
381
382   [NSApp beginSheet:prefs
383      modalForWindow:[sv window]
384       modalDelegate:self
385      didEndSelector:@selector(preferencesClosed:returnCode:contextInfo:)
386         contextInfo:nil];
387   int code = [NSApp runModalForWindow:prefs];
388   
389   /* Restart the animation if the "OK" button was hit, but not if "Cancel".
390      We have to restart *both* animations, because the xlockmore-style
391      ones will blow up if one re-inits but the other doesn't.
392    */
393   if (code != NSCancelButton) {
394     if ([sv isAnimating])
395       [sv stopAnimation];
396     [sv startAnimation];
397   }
398 }
399
400
401 - (void) preferencesClosed: (NSWindow *) sheet
402                 returnCode: (int) returnCode
403                contextInfo: (void  *) contextInfo
404 {
405   [NSApp stopModalWithCode:returnCode];
406 }
407
408 #else  // USE_IPHONE
409
410
411 - (UIImage *) screenshot
412 {
413   return saved_screenshot;
414 }
415
416 - (void) saveScreenshot
417 {
418   // Most of this is from:
419   // http://developer.apple.com/library/ios/#qa/qa1703/_index.html
420   // The rotation stuff is by me.
421
422   CGSize size = [[UIScreen mainScreen] bounds].size;
423
424   // iOS 7: Needs to be the actual device orientation.
425   // iOS 8: Needs to be UIInterfaceOrientationPortrait.
426
427   UIInterfaceOrientation orient =
428     NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_7_1 ?
429     UIInterfaceOrientationPortrait /* iOS 8 broke -[UIScreen bounds]. */ :
430     [[window rootViewController] interfaceOrientation];
431
432   if (orient == UIInterfaceOrientationLandscapeLeft ||
433       orient == UIInterfaceOrientationLandscapeRight) {
434     // Rotate the shape of the canvas 90 degrees.
435     double s = size.width;
436     size.width = size.height;
437     size.height = s;
438   }
439
440
441   // Create a graphics context with the target size
442   // On iOS 4 and later, use UIGraphicsBeginImageContextWithOptions to
443   // take the scale into consideration
444   // On iOS prior to 4, fall back to use UIGraphicsBeginImageContext
445
446   if (UIGraphicsBeginImageContextWithOptions)
447     UIGraphicsBeginImageContextWithOptions (size, NO, 0);
448   else
449     UIGraphicsBeginImageContext (size);
450
451   CGContextRef ctx = UIGraphicsGetCurrentContext();
452
453
454   // Rotate the graphics context to match current hardware rotation.
455   //
456   switch (orient) {
457   case UIInterfaceOrientationPortraitUpsideDown:
458     CGContextTranslateCTM (ctx,  [window center].x,  [window center].y);
459     CGContextRotateCTM (ctx, M_PI);
460     CGContextTranslateCTM (ctx, -[window center].x, -[window center].y);
461     break;
462   case UIInterfaceOrientationLandscapeLeft:
463   case UIInterfaceOrientationLandscapeRight:
464     CGContextTranslateCTM (ctx,  
465                            ([window frame].size.height -
466                             [window frame].size.width) / 2,
467                            ([window frame].size.width -
468                             [window frame].size.height) / 2);
469     CGContextTranslateCTM (ctx,  [window center].x,  [window center].y);
470     CGContextRotateCTM (ctx, 
471                         (orient == UIInterfaceOrientationLandscapeLeft
472                          ?  M_PI/2
473                          : -M_PI/2));
474     CGContextTranslateCTM (ctx, -[window center].x, -[window center].y);
475     break;
476   default:
477     break;
478   }
479
480   // Iterate over every window from back to front
481   //
482   for (UIWindow *win in [[UIApplication sharedApplication] windows]) {
483     if (![win respondsToSelector:@selector(screen)] ||
484         [win screen] == [UIScreen mainScreen]) {
485
486       // -renderInContext: renders in the coordinate space of the layer,
487       // so we must first apply the layer's geometry to the graphics context
488       CGContextSaveGState (ctx);
489
490       // Center the context around the window's anchor point
491       CGContextTranslateCTM (ctx, [win center].x, [win center].y);
492
493       // Apply the window's transform about the anchor point
494       CGContextConcatCTM (ctx, [win transform]);
495
496       // Offset by the portion of the bounds left of and above anchor point
497       CGContextTranslateCTM (ctx,
498         -[win bounds].size.width  * [[win layer] anchorPoint].x,
499         -[win bounds].size.height * [[win layer] anchorPoint].y);
500
501       // Render the layer hierarchy to the current context
502       [[win layer] renderInContext:ctx];
503
504       // Restore the context
505       CGContextRestoreGState (ctx);
506     }
507   }
508
509   if (saved_screenshot)
510     [saved_screenshot release];
511   saved_screenshot = [UIGraphicsGetImageFromCurrentImageContext() retain];
512
513   UIGraphicsEndImageContext();
514 }
515
516
517 - (void) openPreferences: (NSString *) saver
518 {
519   XScreenSaverView *saverView = [self makeSaverView:saver
520                                            withSize:CGSizeMake(0, 0)];
521   if (! saverView) return;
522
523   NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
524   [prefs setObject:saver forKey:@"selectedSaverName"];
525   [prefs synchronize];
526
527   [rotating_nav pushViewController: [saverView configureView]
528                       animated:YES];
529
530   [saverView release];
531 }
532
533
534 #endif // USE_IPHONE
535
536
537
538 - (void)loadSaver:(NSString *)name
539 {
540 # ifndef USE_IPHONE
541
542   if (saverName && [saverName isEqualToString: name]) {
543     for (NSWindow *win in windows) {
544       ScreenSaverView *sv = find_saverView ([win contentView]);
545       if (![sv isAnimating])
546         [sv startAnimation];
547     }
548     return;
549   }
550
551   saverName = name;
552
553   for (NSWindow *win in windows) {
554     NSView *cv = [win contentView];
555     NSString *old_title = [win title];
556     if (!old_title) old_title = @"XScreenSaver";
557     [win setTitle: name];
558     relabel_menus (menubar, old_title, name);
559
560     ScreenSaverView *old_view = find_saverView (cv);
561     NSView *sup = old_view ? [old_view superview] : cv;
562
563     if (old_view) {
564       if ([old_view isAnimating])
565         [old_view stopAnimation];
566       [old_view removeFromSuperview];
567     }
568
569     NSSize size = [cv frame].size;
570     ScreenSaverView *new_view = [self makeSaverView:name withSize: size];
571     NSAssert (new_view, @"unable to make a saver view");
572
573     [new_view setFrame: (old_view ? [old_view frame] : [cv frame])];
574     [sup addSubview: new_view];
575     [win makeFirstResponder:new_view];
576     [new_view setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
577     [new_view retain];
578     [new_view startAnimation];
579   }
580
581   NSUserDefaultsController *ctl =
582     [NSUserDefaultsController sharedUserDefaultsController];
583   [ctl save:self];
584
585 # else  // USE_IPHONE
586
587 #  if TARGET_IPHONE_SIMULATOR
588   NSLog (@"selecting saver \"%@\"", name);
589 #  endif
590
591   NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
592   [prefs setObject:name forKey:@"selectedSaverName"];
593   [prefs synchronize];
594
595 /* Cacheing this screws up rotation when starting a saver twice in a row.
596   if (saverName && [saverName isEqualToString: name]) {
597     if ([saverView isAnimating])
598       return;
599     else
600       goto LAUNCH;
601   }
602 */
603
604   saverName = name;
605
606   if (nonrotating_controller) {
607     nonrotating_controller.saverName = name;
608     return;
609   }
610
611 # if TARGET_IPHONE_SIMULATOR
612   UIScreen *screen = [UIScreen mainScreen];
613
614   /* 'nativeScale' is very confusing.
615
616      iPhone 4s:
617         bounds:        320x480   scale:        2
618         nativeBounds:  640x960   nativeScale:  2
619      iPhone 5s:
620         bounds:        320x568   scale:        2
621         nativeBounds:  640x1136  nativeScale:  2
622      iPad 2:
623         bounds:       768x1024   scale:        1
624         nativeBounds: 768x1024   nativeScale:  1
625      iPad Retina/Air:
626         bounds:       768x1024   scale:        2
627         nativeBounds: 1536x2048  nativeScale:  2
628      iPhone 6:
629         bounds:        320x568   scale:        2
630         nativeBounds:  640x1136  nativeScale:  2
631      iPhone 6+:
632         bounds:        320x568   scale:        2
633         nativeBounds:  960x1704  nativeScale:  3
634
635      According to a StackOverflow comment:
636
637        The iPhone 6+ renders internally using @3x assets at a virtual
638        resolution of 2208x1242 (with 736x414 points), then samples that down
639        for display. The same as using a scaled resolution on a Retina MacBook
640        -- it lets them hit an integral multiple for pixel assets while still
641        having e.g. 12pt text look the same size on the screen.
642
643        The 6, the 5s, the 5, the 4s and the 4 are all 326 pixels per inch,
644        and use @2x assets to stick to the approximately 160 points per inch
645        of all previous devices.
646
647        The 6+ is 401 pixels per inch. So it'd hypothetically need roughly
648        @2.46x assets. Instead Apple uses @3x assets and scales the complete
649        output down to about 84% of its natural size.
650
651        In practice Apple has decided to go with more like 87%, turning the
652        1080 into 1242. No doubt that was to find something as close as
653        possible to 84% that still produced integral sizes in both directions
654        -- 1242/1080 = 2208/1920 exactly, whereas if you'd turned the 1080
655        into, say, 1286, you'd somehow need to render 2286.22 pixels
656        vertically to scale well.
657    */
658
659   NSLog(@"screen: %.0fx%0.f",
660         [[screen currentMode] size].width,
661         [[screen currentMode] size].height);
662   NSLog(@"bounds: %.0fx%0.f x %.1f = %.0fx%0.f",
663         [screen bounds].size.width,
664         [screen bounds].size.height,
665         [screen scale],
666         [screen scale] * [screen bounds].size.width,
667         [screen scale] * [screen bounds].size.height);
668
669 #  ifdef __IPHONE_8_0
670   if ([screen respondsToSelector:@selector(nativeBounds)])
671     NSLog(@"native: %.0fx%0.f / %.1f = %.0fx%0.f",
672           [screen nativeBounds].size.width,
673           [screen nativeBounds].size.height,
674           [screen nativeScale],
675           [screen nativeBounds].size.width  / [screen nativeScale],
676           [screen nativeBounds].size.height / [screen nativeScale]);
677 #  endif
678 # endif // TARGET_IPHONE_SIMULATOR
679
680   // Take the screen shot before creating the screen saver view, because this
681   // can screw with the layout.
682   [self saveScreenshot];
683
684   // iOS 3.2. Before this were iPhones (and iPods) only, which always did modal
685   // presentation full screen.
686   rotating_nav.modalPresentationStyle = UIModalPresentationFullScreen;
687
688   nonrotating_controller = [[SaverViewController alloc] initWithSaverRunner:self];
689   nonrotating_controller.saverName = name;
690
691   /* LAUNCH: */
692
693   [rotating_nav presentViewController:nonrotating_controller animated:NO completion:nil];
694
695   // Doing this makes savers cut back to the list instead of fading,
696   // even though [XScreenSaverView stopAndClose] does setHidden:NO first.
697   // [window setHidden:YES];
698
699 # endif // USE_IPHONE
700 }
701
702
703 #ifndef USE_IPHONE
704
705 - (void)aboutPanel:(id)sender
706 {
707   NSDictionary *bd = [saverBundle infoDictionary];
708   NSMutableDictionary *d = [NSMutableDictionary dictionaryWithCapacity:20];
709
710   [d setValue:[bd objectForKey:@"CFBundleName"] forKey:@"ApplicationName"];
711   [d setValue:[bd objectForKey:@"CFBundleVersion"] forKey:@"Version"];
712   [d setValue:[bd objectForKey:@"CFBundleShortVersionString"] 
713      forKey:@"ApplicationVersion"];
714   [d setValue:[bd objectForKey:@"NSHumanReadableCopyright"] forKey:@"Copy"];
715   [d setValue:[[NSAttributedString alloc]
716                 initWithString: (NSString *) 
717                   [bd objectForKey:@"CFBundleGetInfoString"]]
718      forKey:@"Credits"];
719
720   [[NSApplication sharedApplication]
721     orderFrontStandardAboutPanelWithOptions:d];
722 }
723
724 #else  // USE_IPHONE
725
726 - (void)aboutPanel:(UIView *)saverView
727        orientation:(UIInterfaceOrientation)orient
728 {
729   if ([saverNames count] == 1)
730     return;
731
732   NSString *name = saverName;
733   NSString *year = [self makeDesc:saverName yearOnly:YES];
734
735
736   CGRect frame = [saverView frame];
737   CGFloat rot;
738   CGFloat pt1 = 24;
739   CGFloat pt2 = 14;
740   UIFont *font1 = [UIFont boldSystemFontOfSize:  pt1];
741   UIFont *font2 = [UIFont italicSystemFontOfSize:pt2];
742
743 # ifdef __IPHONE_7_0
744   CGSize s = CGSizeMake(frame.size.width, frame.size.height);
745   CGSize tsize1 = [[[NSAttributedString alloc]
746                      initWithString: name
747                      attributes:@{ NSFontAttributeName: font1 }]
748                     boundingRectWithSize: s
749                     options: NSStringDrawingUsesLineFragmentOrigin
750                     context: nil].size;
751   CGSize tsize2 = [[[NSAttributedString alloc]
752                      initWithString: name
753                      attributes:@{ NSFontAttributeName: font2 }]
754                     boundingRectWithSize: s
755                     options: NSStringDrawingUsesLineFragmentOrigin
756                     context: nil].size;
757 # else // iOS 6 or Cocoa
758   CGSize tsize1 = [name sizeWithFont:font1
759                    constrainedToSize:CGSizeMake(frame.size.width,
760                                                 frame.size.height)];
761   CGSize tsize2 = [year sizeWithFont:font2
762                    constrainedToSize:CGSizeMake(frame.size.width,
763                                                 frame.size.height)];
764 # endif
765
766   CGSize tsize = CGSizeMake (tsize1.width > tsize2.width ?
767                              tsize1.width : tsize2.width,
768                              tsize1.height + tsize2.height);
769
770   tsize.width  = ceilf(tsize.width);
771   tsize.height = ceilf(tsize.height);
772
773   // Don't know how to find inner margin of UITextView.
774   CGFloat margin = 10;
775   tsize.width  += margin * 4;
776   tsize.height += margin * 2;
777
778   if ([saverView frame].size.width >= 768)
779     tsize.height += pt1 * 3;  // extra bottom margin on iPad
780
781   frame = CGRectMake (0, 0, tsize.width, tsize.height);
782
783   /* Get the text oriented properly, and move it to the bottom of the
784      screen, since many savers have action in the middle.
785    */
786   switch (orient) {
787   case UIInterfaceOrientationLandscapeLeft:
788     rot = -M_PI/2;
789     frame.origin.x = ([saverView frame].size.width
790                       - (tsize.width - tsize.height) / 2
791                       - tsize.height);
792     frame.origin.y = ([saverView frame].size.height - tsize.height) / 2;
793     break;
794   case UIInterfaceOrientationLandscapeRight:
795     rot = M_PI/2;
796     frame.origin.x = -(tsize.width - tsize.height) / 2;
797     frame.origin.y = ([saverView frame].size.height - tsize.height) / 2;
798     break;
799   case UIInterfaceOrientationPortraitUpsideDown:
800     rot = M_PI;
801     frame.origin.x = ([saverView frame].size.width  - tsize.width) / 2;
802     frame.origin.y = 0;
803     break;
804   default:
805     rot = 0;
806     frame.origin.x = ([saverView frame].size.width  - tsize.width) / 2;
807     frame.origin.y =  [saverView frame].size.height - tsize.height;
808     break;
809   }
810
811   if (aboutBox)
812     [aboutBox removeFromSuperview];
813
814   aboutBox = [[UIView alloc] initWithFrame:frame];
815
816   aboutBox.transform = CGAffineTransformMakeRotation (rot);
817   aboutBox.backgroundColor = [UIColor clearColor];
818
819   /* There seems to be no easy way to stroke the font, so instead draw
820      it 5 times, 4 in black and 1 in yellow, offset by 1 pixel, and add
821      a black shadow to each.  (You'd think the shadow alone would be
822      enough, but there's no way to make it dark enough to be legible.)
823    */
824   for (int i = 0; i < 5; i++) {
825     UITextView *textview;
826     int off = 1;
827     frame.origin.x = frame.origin.y = 0;
828     switch (i) {
829       case 0: frame.origin.x = -off; break;
830       case 1: frame.origin.x =  off; break;
831       case 2: frame.origin.y = -off; break;
832       case 3: frame.origin.y =  off; break;
833     }
834
835     for (int j = 0; j < 2; j++) {
836
837       frame.origin.y = (j == 0 ? 0 : pt1);
838       textview = [[UITextView alloc] initWithFrame:frame];
839       textview.font = (j == 0 ? font1 : font2);
840       textview.text = (j == 0 ? name  : year);
841       textview.textAlignment = NSTextAlignmentCenter;
842       textview.showsHorizontalScrollIndicator = NO;
843       textview.showsVerticalScrollIndicator   = NO;
844       textview.scrollEnabled = NO;
845       textview.editable = NO;
846       textview.userInteractionEnabled = NO;
847       textview.backgroundColor = [UIColor clearColor];
848       textview.textColor = (i == 4 
849                             ? [UIColor yellowColor]
850                             : [UIColor blackColor]);
851
852       CALayer *textLayer = (CALayer *)
853         [textview.layer.sublayers objectAtIndex:0];
854       textLayer.shadowColor   = [UIColor blackColor].CGColor;
855       textLayer.shadowOffset  = CGSizeMake(0, 0);
856       textLayer.shadowOpacity = 1;
857       textLayer.shadowRadius  = 2;
858
859       [aboutBox addSubview:textview];
860     }
861   }
862
863   CABasicAnimation *anim = 
864     [CABasicAnimation animationWithKeyPath:@"opacity"];
865   anim.duration     = 0.3;
866   anim.repeatCount  = 1;
867   anim.autoreverses = NO;
868   anim.fromValue    = [NSNumber numberWithFloat:0.0];
869   anim.toValue      = [NSNumber numberWithFloat:1.0];
870   [aboutBox.layer addAnimation:anim forKey:@"animateOpacity"];
871
872   [saverView addSubview:aboutBox];
873
874   if (splashTimer)
875     [splashTimer invalidate];
876
877   splashTimer =
878     [NSTimer scheduledTimerWithTimeInterval: anim.duration + 2
879              target:self
880              selector:@selector(aboutOff)
881              userInfo:nil
882              repeats:NO];
883 }
884
885
886 - (void)aboutOff
887 {
888   if (aboutBox) {
889     if (splashTimer) {
890       [splashTimer invalidate];
891       splashTimer = 0;
892     }
893     CABasicAnimation *anim = 
894       [CABasicAnimation animationWithKeyPath:@"opacity"];
895     anim.duration     = 0.3;
896     anim.repeatCount  = 1;
897     anim.autoreverses = NO;
898     anim.fromValue    = [NSNumber numberWithFloat: 1];
899     anim.toValue      = [NSNumber numberWithFloat: 0];
900     anim.delegate     = self;
901     aboutBox.layer.opacity = 0;
902     [aboutBox.layer addAnimation:anim forKey:@"animateOpacity"];
903   }
904 }
905 #endif // USE_IPHONE
906
907
908
909 - (void)selectedSaverDidChange:(NSDictionary *)change
910 {
911   NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
912   NSString *name = [prefs stringForKey:@"selectedSaverName"];
913
914   if (! name) return;
915
916   if (! [saverNames containsObject:name]) {
917     NSLog (@"saver \"%@\" does not exist", name);
918     return;
919   }
920
921   [self loadSaver: name];
922 }
923
924
925 - (NSArray *) listSaverBundleNamesInDir:(NSString *)dir
926 {
927 # ifndef USE_IPHONE
928   NSString *ext = @"saver";
929 # else
930   NSString *ext = @"xml";
931 # endif
932
933   NSArray *files = [[NSFileManager defaultManager]
934                      contentsOfDirectoryAtPath:dir error:nil];
935   if (! files) return 0;
936   NSMutableArray *result = [NSMutableArray arrayWithCapacity: [files count]+1];
937
938   for (NSString *p in files) {
939     if ([[p pathExtension] caseInsensitiveCompare: ext]) 
940       continue;
941
942     NSString *name = [[p lastPathComponent] stringByDeletingPathExtension];
943
944 # ifdef USE_IPHONE
945     // Get the saver name's capitalization right by reading the XML file.
946
947     p = [dir stringByAppendingPathComponent: p];
948     NSData *xmld = [NSData dataWithContentsOfFile:p];
949     NSAssert (xmld, @"no XML: %@", p);
950     NSString *xml = [XScreenSaverView decompressXML:xmld];
951     NSRange r = [xml rangeOfString:@"_label=\"" options:0];
952     NSAssert1 (r.length, @"no name in %@", p);
953     if (r.length) {
954       xml = [xml substringFromIndex: r.location + r.length];
955       r = [xml rangeOfString:@"\"" options:0];
956       if (r.length) name = [xml substringToIndex: r.location];
957     }
958
959 # endif // USE_IPHONE
960
961     NSAssert1 (name, @"no name in %@", p);
962     if (name) [result addObject: name];
963   }
964
965   if (! [result count])
966     result = 0;
967
968   return result;
969 }
970
971
972
973 - (NSArray *) listSaverBundleNames
974 {
975   NSMutableArray *dirs = [NSMutableArray arrayWithCapacity: 10];
976
977 # ifndef USE_IPHONE
978   // On MacOS, look in the "Contents/Resources/" and "Contents/PlugIns/"
979   // directories in the bundle.
980   [dirs addObject: [[[[NSBundle mainBundle] bundlePath]
981                       stringByAppendingPathComponent:@"Contents"]
982                      stringByAppendingPathComponent:@"Resources"]];
983   [dirs addObject: [[NSBundle mainBundle] builtInPlugInsPath]];
984
985   // Also look in the same directory as the executable.
986   [dirs addObject: [[[NSBundle mainBundle] bundlePath]
987                      stringByDeletingLastPathComponent]];
988
989   // Finally, look in standard MacOS screensaver directories.
990 //  [dirs addObject: @"~/Library/Screen Savers"];
991 //  [dirs addObject: @"/Library/Screen Savers"];
992 //  [dirs addObject: @"/System/Library/Screen Savers"];
993
994 # else  // USE_IPHONE
995
996   // On iOS, only look in the bundle's root directory.
997   [dirs addObject: [[NSBundle mainBundle] bundlePath]];
998
999 # endif // USE_IPHONE
1000
1001   int i;
1002   for (i = 0; i < [dirs count]; i++) {
1003     NSString *dir = [dirs objectAtIndex:i];
1004     NSArray *names = [self listSaverBundleNamesInDir:dir];
1005     if (! names) continue;
1006     saverDir   = [dir retain];
1007     saverNames = [names retain];
1008     return names;
1009   }
1010
1011   NSString *err = @"no .saver bundles found in: ";
1012   for (i = 0; i < [dirs count]; i++) {
1013     if (i) err = [err stringByAppendingString:@", "];
1014     err = [err stringByAppendingString:[[dirs objectAtIndex:i] 
1015                                          stringByAbbreviatingWithTildeInPath]];
1016     err = [err stringByAppendingString:@"/"];
1017   }
1018   NSLog (@"%@", err);
1019   return [NSArray array];
1020 }
1021
1022
1023 /* Create the popup menu of available saver names.
1024  */
1025 #ifndef USE_IPHONE
1026
1027 - (NSPopUpButton *) makeMenu
1028 {
1029   NSRect rect;
1030   rect.origin.x = rect.origin.y = 0;
1031   rect.size.width = 10;
1032   rect.size.height = 10;
1033   NSPopUpButton *popup = [[NSPopUpButton alloc] initWithFrame:rect
1034                                                     pullsDown:NO];
1035   int i;
1036   float max_width = 0;
1037   for (i = 0; i < [saverNames count]; i++) {
1038     NSString *name = [saverNames objectAtIndex:i];
1039     [popup addItemWithTitle:name];
1040     [[popup itemWithTitle:name] setRepresentedObject:name];
1041     [popup sizeToFit];
1042     NSRect r = [popup frame];
1043     if (r.size.width > max_width) max_width = r.size.width;
1044   }
1045
1046   // Bind the menu to preferences, and trigger a callback when an item
1047   // is selected.
1048   //
1049   NSString *key = @"values.selectedSaverName";
1050   NSUserDefaultsController *prefs =
1051     [NSUserDefaultsController sharedUserDefaultsController];
1052   [prefs addObserver:self
1053          forKeyPath:key
1054             options:0
1055             context:@selector(selectedSaverDidChange:)];
1056   [popup   bind:@"selectedObject"
1057        toObject:prefs
1058     withKeyPath:key
1059         options:nil];
1060   [prefs setAppliesImmediately:YES];
1061
1062   NSRect r = [popup frame];
1063   r.size.width = max_width;
1064   [popup setFrame:r];
1065   return popup;
1066 }
1067
1068 #else  // USE_IPHONE
1069
1070 - (NSString *) makeDesc:(NSString *)saver
1071                   yearOnly:(BOOL) yearp
1072 {
1073   NSString *desc = 0;
1074   NSString *path = [saverDir stringByAppendingPathComponent:
1075                                [[saver lowercaseString]
1076                                  stringByReplacingOccurrencesOfString:@" "
1077                                  withString:@""]];
1078   NSRange r;
1079
1080   path = [path stringByAppendingPathExtension:@"xml"];
1081   NSData *xmld = [NSData dataWithContentsOfFile:path];
1082   if (! xmld) goto FAIL;
1083   desc = [XScreenSaverView decompressXML:xmld];
1084   if (! desc) goto FAIL;
1085
1086   r = [desc rangeOfString:@"<_description>"
1087             options:NSCaseInsensitiveSearch];
1088   if (r.length == 0) {
1089     desc = 0;
1090     goto FAIL;
1091   }
1092   desc = [desc substringFromIndex: r.location + r.length];
1093   r = [desc rangeOfString:@"</_description>"
1094             options:NSCaseInsensitiveSearch];
1095   if (r.length > 0)
1096     desc = [desc substringToIndex: r.location];
1097
1098   // Leading and trailing whitespace.
1099   desc = [desc stringByTrimmingCharactersInSet:
1100                  [NSCharacterSet whitespaceAndNewlineCharacterSet]];
1101
1102   // Let's see if we can find a year on the last line.
1103   r = [desc rangeOfString:@"\n" options:NSBackwardsSearch];
1104   NSString *year = 0;
1105   for (NSString *word in
1106          [[desc substringFromIndex:r.location + r.length]
1107            componentsSeparatedByCharactersInSet:
1108              [NSCharacterSet characterSetWithCharactersInString:
1109                                @" \t\n-."]]) {
1110     int n = [word doubleValue];
1111     if (n > 1970 && n < 2100)
1112       year = word;
1113   }
1114
1115   // Delete everything after the first blank line.
1116   //
1117   r = [desc rangeOfString:@"\n\n" options:0];
1118   if (r.length > 0)
1119     desc = [desc substringToIndex: r.location];
1120
1121   // Unwrap lines and compress whitespace.
1122   {
1123     NSString *result = @"";
1124     for (NSString *s in [desc componentsSeparatedByCharactersInSet:
1125                           [NSCharacterSet whitespaceAndNewlineCharacterSet]]) {
1126       if ([result length] == 0)
1127         result = s;
1128       else if ([s length] > 0)
1129         result = [NSString stringWithFormat: @"%@ %@", result, s];
1130       desc = result;
1131     }
1132   }
1133
1134   if (year)
1135     desc = [year stringByAppendingString:
1136                    [@": " stringByAppendingString: desc]];
1137
1138   if (yearp)
1139     desc = year ? year : @"";
1140
1141 FAIL:
1142   if (! desc) {
1143     if ([saverNames count] > 1)
1144       desc = @"Oops, this module appears to be incomplete.";
1145     else
1146       desc = @"";
1147   }
1148
1149   return desc;
1150 }
1151
1152 - (NSString *) makeDesc:(NSString *)saver
1153 {
1154   return [self makeDesc:saver yearOnly:NO];
1155 }
1156
1157
1158
1159 /* Create a dictionary of one-line descriptions of every saver,
1160    for display on the UITableView.
1161  */
1162 - (NSDictionary *)makeDescTable
1163 {
1164   NSMutableDictionary *dict = 
1165     [NSMutableDictionary dictionaryWithCapacity:[saverNames count]];
1166   for (NSString *saver in saverNames) {
1167     [dict setObject:[self makeDesc:saver] forKey:saver];
1168   }
1169   return dict;
1170 }
1171
1172
1173 - (void) wantsFadeOut:(XScreenSaverView *)sender
1174 {
1175   rotating_nav.view.hidden = NO; // In case it was hidden during startup.
1176
1177   /* The XScreenSaverView screws with the status bar orientation, mostly to
1178      keep the simulator oriented properly. But on iOS 8.1 (and maybe 8.0
1179      and/or 8.2), this confuses the UINavigationController, so put the
1180      orientation back to portrait before dismissing the SaverViewController.
1181    */
1182   [[UIApplication sharedApplication]
1183    setStatusBarOrientation:UIInterfaceOrientationPortrait
1184    animated:NO];
1185
1186   [rotating_nav dismissViewControllerAnimated:YES completion:^() {
1187     [nonrotating_controller release];
1188     nonrotating_controller = nil;
1189     [[rotating_nav view] becomeFirstResponder];
1190   }];
1191 }
1192
1193
1194 - (void) didShake:(XScreenSaverView *)sender
1195 {
1196 # if TARGET_IPHONE_SIMULATOR
1197   NSLog (@"simulating shake on saver list");
1198 # endif
1199   [[rotating_nav topViewController] motionEnded: UIEventSubtypeMotionShake
1200                                       withEvent: nil];
1201 }
1202
1203
1204 #endif // USE_IPHONE
1205
1206
1207
1208 /* This is called when the "selectedSaverName" pref changes, e.g.,
1209    when a menu selection is made.
1210  */
1211 - (void)observeValueForKeyPath:(NSString *)keyPath
1212                       ofObject:(id)object
1213                         change:(NSDictionary *)change
1214                        context:(void *)context
1215 {
1216   SEL dispatchSelector = (SEL)context;
1217   if (dispatchSelector != NULL) {
1218     [self performSelector:dispatchSelector withObject:change];
1219   } else {
1220     [super observeValueForKeyPath:keyPath
1221                          ofObject:object
1222                            change:change
1223                           context:context];
1224   }
1225 }
1226
1227
1228 # ifndef USE_IPHONE
1229
1230 /* Create the desktop window shell, possibly including a preferences button.
1231  */
1232 - (NSWindow *) makeWindow
1233 {
1234   NSRect rect;
1235   static int count = 0;
1236   Bool simple_p = ([saverNames count] == 1);
1237   NSButton *pb = 0;
1238   NSPopUpButton *menu = 0;
1239   NSBox *gbox = 0;
1240   NSBox *pbox = 0;
1241
1242   NSRect sv_rect;
1243   sv_rect.origin.x = sv_rect.origin.y = 0;
1244   sv_rect.size.width = 320;
1245   sv_rect.size.height = 240;
1246   ScreenSaverView *sv = [[ScreenSaverView alloc]  // dummy placeholder
1247                           initWithFrame:sv_rect
1248                           isPreview:YES];
1249
1250   // make a "Preferences" button
1251   //
1252   if (! simple_p) {
1253     rect.origin.x = 0;
1254     rect.origin.y = 0;
1255     rect.size.width = rect.size.height = 10;
1256     pb = [[NSButton alloc] initWithFrame:rect];
1257     [pb setTitle:@"Preferences"];
1258     [pb setBezelStyle:NSRoundedBezelStyle];
1259     [pb sizeToFit];
1260
1261     rect.origin.x = ([sv frame].size.width -
1262                      [pb frame].size.width) / 2;
1263     [pb setFrameOrigin:rect.origin];
1264   
1265     // grab the click
1266     //
1267     [pb setTarget:self];
1268     [pb setAction:@selector(openPreferences:)];
1269
1270     // Make a saver selection menu
1271     //
1272     menu = [self makeMenu];
1273     rect.origin.x = 2;
1274     rect.origin.y = 2;
1275     [menu setFrameOrigin:rect.origin];
1276
1277     // make a box to wrap the saverView
1278     //
1279     rect = [sv frame];
1280     rect.origin.x = 0;
1281     rect.origin.y = [pb frame].origin.y + [pb frame].size.height;
1282     gbox = [[NSBox alloc] initWithFrame:rect];
1283     rect.size.width = rect.size.height = 10;
1284     [gbox setContentViewMargins:rect.size];
1285     [gbox setTitlePosition:NSNoTitle];
1286     [gbox addSubview:sv];
1287     [gbox sizeToFit];
1288
1289     // make a box to wrap the other two boxes
1290     //
1291     rect.origin.x = rect.origin.y = 0;
1292     rect.size.width  = [gbox frame].size.width;
1293     rect.size.height = [gbox frame].size.height + [gbox frame].origin.y;
1294     pbox = [[NSBox alloc] initWithFrame:rect];
1295     [pbox setTitlePosition:NSNoTitle];
1296     [pbox setBorderType:NSNoBorder];
1297     [pbox addSubview:gbox];
1298     if (menu) [pbox addSubview:menu];
1299     if (pb)   [pbox addSubview:pb];
1300     [pbox sizeToFit];
1301
1302     [pb   setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin];
1303     [menu setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin];
1304     [gbox setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1305     [pbox setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1306   }
1307
1308   [sv     setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
1309
1310
1311   // and make a window to hold that.
1312   //
1313   NSScreen *screen = [NSScreen mainScreen];
1314   rect = pbox ? [pbox frame] : [sv frame];
1315   rect.origin.x = ([screen frame].size.width  - rect.size.width)  / 2;
1316   rect.origin.y = ([screen frame].size.height - rect.size.height) / 2;
1317   
1318   rect.origin.x += rect.size.width * (count ? 0.55 : -0.55);
1319   
1320   NSWindow *win = [[NSWindow alloc]
1321                       initWithContentRect:rect
1322                                 styleMask:(NSTitledWindowMask |
1323                                            NSClosableWindowMask |
1324                                            NSMiniaturizableWindowMask |
1325                                            NSResizableWindowMask)
1326                                   backing:NSBackingStoreBuffered
1327                                     defer:YES
1328                                    screen:screen];
1329   [win setMinSize:[win frameRectForContentRect:rect].size];
1330   [[win contentView] addSubview: (pbox ? (NSView *) pbox : (NSView *) sv)];
1331
1332   [win makeKeyAndOrderFront:win];
1333   
1334   [sv startAnimation]; // this is the dummy saver
1335
1336   count++;
1337
1338   return win;
1339 }
1340
1341
1342 - (void) animTimer
1343 {
1344   for (NSWindow *win in windows) {
1345     ScreenSaverView *sv = find_saverView ([win contentView]);
1346     if ([sv isAnimating])
1347       [sv animateOneFrame];
1348   }
1349 }
1350
1351 # endif // !USE_IPHONE
1352
1353
1354 - (void)applicationDidFinishLaunching:
1355 # ifndef USE_IPHONE
1356     (NSNotification *) notif
1357 # else  // USE_IPHONE
1358     (UIApplication *) application
1359 # endif // USE_IPHONE
1360 {
1361   [self listSaverBundleNames];
1362
1363   NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
1364
1365 # ifndef USE_IPHONE
1366   int window_count = ([saverNames count] <= 1 ? 1 : 2);
1367   NSMutableArray *a = [[NSMutableArray arrayWithCapacity: window_count+1]
1368                         retain];
1369   windows = a;
1370
1371   int i;
1372   // Create either one window (for standalone, e.g. Phosphor.app)
1373   // or two windows for SaverTester.app.
1374   for (i = 0; i < window_count; i++) {
1375     NSWindow *win = [self makeWindow];
1376     // Get the last-saved window position out of preferences.
1377     [win setFrameAutosaveName:
1378               [NSString stringWithFormat:@"XScreenSaverWindow%d", i]];
1379     [win setFrameUsingName:[win frameAutosaveName]];
1380     [a addObject: win];
1381     // This prevents clicks from being seen by savers.
1382     // [win setMovableByWindowBackground:YES];
1383   }
1384 # else  // USE_IPHONE
1385
1386 # undef ya_rand_init
1387   ya_rand_init (0);     // Now's a good time.
1388
1389
1390   /* iOS docs say:
1391      "You must call this method before attempting to get orientation data from
1392       the receiver. This method enables the device's accelerometer hardware
1393       and begins the delivery of acceleration events to the receiver."
1394
1395      Adding or removing this doesn't seem to make any difference. It's
1396      probably getting called by the UINavigationController. Still... */
1397   [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
1398
1399   rotating_nav = [[[RotateyViewController alloc] initWithRotation:YES]
1400                          retain];
1401
1402   if ([prefs boolForKey:@"wasRunning"]) // Prevents menu flicker on startup.
1403     rotating_nav.view.hidden = YES;
1404
1405   [window setRootViewController: rotating_nav];
1406   [window setAutoresizesSubviews:YES];
1407   [window setAutoresizingMask: 
1408             (UIViewAutoresizingFlexibleWidth | 
1409              UIViewAutoresizingFlexibleHeight)];
1410
1411   SaverListController *menu = [[SaverListController alloc] 
1412                                 initWithNames:saverNames
1413                                 descriptions:[self makeDescTable]];
1414   [rotating_nav pushViewController:menu animated:YES];
1415   [menu becomeFirstResponder];
1416
1417   application.applicationSupportsShakeToEdit = YES;
1418
1419
1420 # endif // USE_IPHONE
1421
1422   NSString *forced = 0;
1423   /* In the XCode project, each .saver scheme sets this env var when
1424      launching SaverTester.app so that it knows which one we are
1425      currently debugging.  If this is set, it overrides the default
1426      selection in the popup menu.  If unset, that menu persists to
1427      whatever it was last time.
1428    */
1429   const char *f = getenv ("SELECTED_SAVER");
1430   if (f && *f)
1431     forced = [NSString stringWithCString:(char *)f
1432                        encoding:NSUTF8StringEncoding];
1433
1434   if (forced && ![saverNames containsObject:forced]) {
1435     NSLog(@"forced saver \"%@\" does not exist", forced);
1436     forced = 0;
1437   }
1438
1439   // If there's only one saver, run that.
1440   if (!forced && [saverNames count] == 1)
1441     forced = [saverNames objectAtIndex:0];
1442
1443 # ifdef USE_IPHONE
1444   NSString *prev = [prefs stringForKey:@"selectedSaverName"];
1445
1446   if (forced)
1447     prev = forced;
1448
1449   // If nothing was selected (e.g., this is the first launch)
1450   // then scroll randomly instead of starting up at "A".
1451   //
1452   if (!prev)
1453     prev = [saverNames objectAtIndex: (random() % [saverNames count])];
1454
1455   if (prev)
1456     [menu scrollTo: prev];
1457 # endif // USE_IPHONE
1458
1459   if (forced)
1460     [prefs setObject:forced forKey:@"selectedSaverName"];
1461
1462 # ifdef USE_IPHONE
1463   /* Don't auto-launch the saver unless it was running last time.
1464      XScreenSaverView manages this, on crash_timer.
1465      Unless forced.
1466    */
1467   if (!forced && ![prefs boolForKey:@"wasRunning"])
1468     return;
1469 # endif
1470
1471   [self selectedSaverDidChange:nil];
1472 //  [NSTimer scheduledTimerWithTimeInterval: 0
1473 //           target:self
1474 //           selector:@selector(selectedSaverDidChange:)
1475 //           userInfo:nil
1476 //           repeats:NO];
1477
1478
1479
1480 # ifndef USE_IPHONE
1481   /* On 10.8 and earlier, [ScreenSaverView startAnimation] causes the
1482      ScreenSaverView to run its own timer calling animateOneFrame.
1483      On 10.9, that fails because the private class ScreenSaverModule
1484      is only initialized properly by ScreenSaverEngine, and in the
1485      context of SaverRunner, the null ScreenSaverEngine instance
1486      behaves as if [ScreenSaverEngine needsAnimationTimer] returned false.
1487      So, if it looks like this is the 10.9 version of ScreenSaverModule
1488      instead of the 10.8 version, we run our own timer here.  This sucks.
1489    */
1490   if (!anim_timer) {
1491     Class ssm = NSClassFromString (@"ScreenSaverModule");
1492     if (ssm && [ssm instancesRespondToSelector:
1493                       @selector(needsAnimationTimer)]) {
1494       NSWindow *win = [windows objectAtIndex:0];
1495       ScreenSaverView *sv = find_saverView ([win contentView]);
1496       anim_timer = [NSTimer scheduledTimerWithTimeInterval:
1497                               [sv animationTimeInterval]
1498                             target:self
1499                             selector:@selector(animTimer)
1500                             userInfo:nil
1501                             repeats:YES];
1502     }
1503   }
1504 # endif // !USE_IPHONE
1505 }
1506
1507
1508 #ifndef USE_IPHONE
1509
1510 /* When the window closes, exit (even if prefs still open.)
1511  */
1512 - (BOOL) applicationShouldTerminateAfterLastWindowClosed: (NSApplication *) n
1513 {
1514   return YES;
1515 }
1516
1517 # else // USE_IPHONE
1518
1519 - (void)applicationWillResignActive:(UIApplication *)app
1520 {
1521   [(XScreenSaverView *)view setScreenLocked:YES];
1522 }
1523
1524 - (void)applicationDidBecomeActive:(UIApplication *)app
1525 {
1526   [(XScreenSaverView *)view setScreenLocked:NO];
1527 }
1528
1529 - (void)applicationDidEnterBackground:(UIApplication *)application
1530 {
1531   [(XScreenSaverView *)view setScreenLocked:YES];
1532 }
1533
1534 #endif // USE_IPHONE
1535
1536
1537 @end