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