From http://www.jwz.org/xscreensaver/xscreensaver-5.35.tar.gz
[xscreensaver] / OSX / SaverRunner.m
index 797cd2cec5b289ee7cd9d86b5181da5076127cb4..ac010b9fa760d1bbb6e0c6ccea8b6ff96086307e 100644 (file)
@@ -1,4 +1,4 @@
-/* xscreensaver, Copyright (c) 2006-2011 Jamie Zawinski <jwz@jwz.org>
+/* xscreensaver, Copyright (c) 2006-2016 Jamie Zawinski <jwz@jwz.org>
  *
  * Permission to use, copy, modify, distribute, and sell this software and its
  * documentation for any purpose is hereby granted without fee, provided that
@@ -9,7 +9,7 @@
  * implied warranty.
  */
 
-/* This program serves two purposes:
+/* This program serves three purposes:
 
    First, It is a test harness for screen savers.  When it launches, it
    looks around for .saver bundles (in the current directory, and then in
 
    Second, it can be used to transform any screen saver into a standalone
    program.  Just put one (and only one) .saver bundle into the app
-   bundle's Contents/PlugIns/ directory, and it will load and run that
+   bundle's Contents/Resources/ directory, and it will load and run that
    saver at start-up (without the saver-selection menu or other chrome).
    This is how the "Phosphor.app" and "Apple2.app" programs work.
+
+   Third, it is the scaffolding which turns a set of screen savers into
+   a single iPhone / iPad program.  In that case, all of the savers are
+   linked in to this executable, since iOS does not allow dynamic loading
+   of bundles that have executable code in them.  Bleh.
  */
 
+#import <TargetConditionals.h>
 #import "SaverRunner.h"
+#import "SaverListController.h"
 #import "XScreenSaverGLView.h"
+#import "yarandom.h"
+
+#ifdef USE_IPHONE
+
+# ifndef __IPHONE_8_0
+#  define UIInterfaceOrientationUnknown UIDeviceOrientationUnknown
+# endif
+# ifndef NSFoundationVersionNumber_iOS_7_1
+#  define NSFoundationVersionNumber_iOS_7_1 1047.25
+# endif
+# ifndef NSFoundationVersionNumber_iOS_8_0
+#  define NSFoundationVersionNumber_iOS_8_0 1134.10
+# endif
+
+@interface RotateyViewController : UINavigationController
+{
+  BOOL allowRotation;
+}
+@end
+
+@implementation RotateyViewController
+
+/* This subclass exists so that we can ask that the SaverListController and
+   preferences panels be auto-rotated by the system.  Note that the 
+   XScreenSaverView is not auto-rotated because it is on a different UIWindow.
+ */
+
+- (id)initWithRotation:(BOOL)rotatep
+{
+  self = [super init];
+  allowRotation = rotatep;
+  return self;
+}
+
+- (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)o
+{
+  return allowRotation;                                /* Deprecated in iOS 6 */
+}
+
+- (BOOL)shouldAutorotate                       /* Added in iOS 6 */
+{
+  return allowRotation;
+}
+
+- (UIInterfaceOrientationMask)supportedInterfaceOrientations   /* Added in iOS 6 */
+{
+  return UIInterfaceOrientationMaskAll;
+}
+
+@end
+
+
+@implementation SaverViewController
+
+@synthesize saverName;
+
+- (id)initWithSaverRunner:(SaverRunner *)parent
+             showAboutBox:(BOOL)showAboutBox
+{
+  self = [super init];
+  if (self) {
+    _parent = parent;
+    // _storedOrientation = UIInterfaceOrientationUnknown;
+    _showAboutBox = showAboutBox;
+
+    self.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
+    self.wantsFullScreenLayout = YES;
+  }
+  return self;
+}
+
+
+- (void)dealloc
+{
+  [_saverName release];
+  // iOS: When a UIView deallocs, it doesn't do [UIView removeFromSuperView]
+  // for its subviews, so the subviews end up with a dangling pointer in their
+  // superview properties.
+  [aboutBox removeFromSuperview];
+  [aboutBox release];
+  [_saverView removeFromSuperview];
+  [_saverView release];
+  [super dealloc];
+}
+
+
+- (void)loadView
+{
+  // The UIViewController's view must never change, so it gets set here to
+  // a plain black background.
+
+  // This background view doesn't block the status bar, but that's probably
+  // OK, because it's never on screen for more than a fraction of a second.
+  UIView *backgroundView = [[UIView alloc] initWithFrame:CGRectNull];
+  backgroundView.backgroundColor = [UIColor blackColor];
+  self.view = backgroundView;
+  [backgroundView release];
+}
+
+
+- (void)aboutPanel:(UIView *)saverView
+       orientation:(UIInterfaceOrientation)orient
+{
+  if (!_showAboutBox)
+    return;
+
+  NSString *name = _saverName;
+  NSString *year = [_parent makeDesc:_saverName yearOnly:YES];
+
+
+  CGRect frame = [saverView frame];
+  CGFloat rot;
+  CGFloat pt1 = 24;
+  CGFloat pt2 = 14;
+  UIFont *font1 = [UIFont boldSystemFontOfSize:  pt1];
+  UIFont *font2 = [UIFont italicSystemFontOfSize:pt2];
+
+# ifdef __IPHONE_7_0
+  CGSize s = CGSizeMake(frame.size.width, frame.size.height);
+  CGSize tsize1 = [[[NSAttributedString alloc]
+                     initWithString: name
+                     attributes:@{ NSFontAttributeName: font1 }]
+                    boundingRectWithSize: s
+                    options: NSStringDrawingUsesLineFragmentOrigin
+                    context: nil].size;
+  CGSize tsize2 = [[[NSAttributedString alloc]
+                     initWithString: name
+                     attributes:@{ NSFontAttributeName: font2 }]
+                    boundingRectWithSize: s
+                    options: NSStringDrawingUsesLineFragmentOrigin
+                    context: nil].size;
+# else // iOS 6 or Cocoa
+  CGSize tsize1 = [name sizeWithFont:font1
+                   constrainedToSize:CGSizeMake(frame.size.width,
+                                                frame.size.height)];
+  CGSize tsize2 = [year sizeWithFont:font2
+                   constrainedToSize:CGSizeMake(frame.size.width,
+                                                frame.size.height)];
+# endif
+
+  CGSize tsize = CGSizeMake (tsize1.width > tsize2.width ?
+                             tsize1.width : tsize2.width,
+                             tsize1.height + tsize2.height);
+
+  tsize.width  = ceilf(tsize.width);
+  tsize.height = ceilf(tsize.height);
+
+  // Don't know how to find inner margin of UITextView.
+  CGFloat margin = 10;
+  tsize.width  += margin * 4;
+  tsize.height += margin * 2;
+
+  if ([saverView frame].size.width >= 768)
+    tsize.height += pt1 * 3;  // extra bottom margin on iPad
+
+  frame = CGRectMake (0, 0, tsize.width, tsize.height);
+
+  /* Get the text oriented properly, and move it to the bottom of the
+     screen, since many savers have action in the middle.
+   */
+  switch (orient) {
+  case UIInterfaceOrientationLandscapeLeft:
+    rot = -M_PI/2;
+    frame.origin.x = ([saverView frame].size.width
+                      - (tsize.width - tsize.height) / 2
+                      - tsize.height);
+    frame.origin.y = ([saverView frame].size.height - tsize.height) / 2;
+    break;
+  case UIInterfaceOrientationLandscapeRight:
+    rot = M_PI/2;
+    frame.origin.x = -(tsize.width - tsize.height) / 2;
+    frame.origin.y = ([saverView frame].size.height - tsize.height) / 2;
+    break;
+  case UIInterfaceOrientationPortraitUpsideDown:
+    rot = M_PI;
+    frame.origin.x = ([saverView frame].size.width  - tsize.width) / 2;
+    frame.origin.y = 0;
+    break;
+  default:
+    rot = 0;
+    frame.origin.x = ([saverView frame].size.width  - tsize.width) / 2;
+    frame.origin.y =  [saverView frame].size.height - tsize.height;
+    break;
+  }
+
+  if (aboutBox) {
+    [aboutBox removeFromSuperview];
+    [aboutBox release];
+  }
+
+  aboutBox = [[UIView alloc] initWithFrame:frame];
+
+  aboutBox.transform = CGAffineTransformMakeRotation (rot);
+  aboutBox.backgroundColor = [UIColor clearColor];
+
+  /* There seems to be no easy way to stroke the font, so instead draw
+     it 5 times, 4 in black and 1 in yellow, offset by 1 pixel, and add
+     a black shadow to each.  (You'd think the shadow alone would be
+     enough, but there's no way to make it dark enough to be legible.)
+   */
+  for (int i = 0; i < 5; i++) {
+    UITextView *textview;
+    int off = 1;
+    frame.origin.x = frame.origin.y = 0;
+    switch (i) {
+      case 0: frame.origin.x = -off; break;
+      case 1: frame.origin.x =  off; break;
+      case 2: frame.origin.y = -off; break;
+      case 3: frame.origin.y =  off; break;
+    }
+
+    for (int j = 0; j < 2; j++) {
+
+      frame.origin.y = (j == 0 ? 0 : pt1);
+      textview = [[UITextView alloc] initWithFrame:frame];
+      textview.font = (j == 0 ? font1 : font2);
+      textview.text = (j == 0 ? name  : year);
+      textview.textAlignment = NSTextAlignmentCenter;
+      textview.showsHorizontalScrollIndicator = NO;
+      textview.showsVerticalScrollIndicator   = NO;
+      textview.scrollEnabled = NO;
+      textview.editable = NO;
+      textview.userInteractionEnabled = NO;
+      textview.backgroundColor = [UIColor clearColor];
+      textview.textColor = (i == 4 
+                            ? [UIColor yellowColor]
+                            : [UIColor blackColor]);
+
+      CALayer *textLayer = (CALayer *)
+        [textview.layer.sublayers objectAtIndex:0];
+      textLayer.shadowColor   = [UIColor blackColor].CGColor;
+      textLayer.shadowOffset  = CGSizeMake(0, 0);
+      textLayer.shadowOpacity = 1;
+      textLayer.shadowRadius  = 2;
+
+      [aboutBox addSubview:textview];
+    }
+  }
+
+  CABasicAnimation *anim = 
+    [CABasicAnimation animationWithKeyPath:@"opacity"];
+  anim.duration     = 0.3;
+  anim.repeatCount  = 1;
+  anim.autoreverses = NO;
+  anim.fromValue    = [NSNumber numberWithFloat:0.0];
+  anim.toValue      = [NSNumber numberWithFloat:1.0];
+  [aboutBox.layer addAnimation:anim forKey:@"animateOpacity"];
+
+  [saverView addSubview:aboutBox];
+
+  if (splashTimer)
+    [splashTimer invalidate];
+
+  splashTimer =
+    [NSTimer scheduledTimerWithTimeInterval: anim.duration + 2
+             target:self
+             selector:@selector(aboutOff)
+             userInfo:nil
+             repeats:NO];
+}
+
+
+- (void)aboutOff
+{
+  [self aboutOff:FALSE];
+}
+
+- (void)aboutOff:(BOOL)fast
+{
+  if (aboutBox) {
+    if (splashTimer) {
+      [splashTimer invalidate];
+      splashTimer = 0;
+    }
+    if (fast) {
+      aboutBox.layer.opacity = 0;
+      return;
+    }
+
+    CABasicAnimation *anim = 
+      [CABasicAnimation animationWithKeyPath:@"opacity"];
+    anim.duration     = 0.3;
+    anim.repeatCount  = 1;
+    anim.autoreverses = NO;
+    anim.fromValue    = [NSNumber numberWithFloat: 1];
+    anim.toValue      = [NSNumber numberWithFloat: 0];
+    // anim.delegate     = self;
+    aboutBox.layer.opacity = 0;
+    [aboutBox.layer addAnimation:anim forKey:@"animateOpacity"];
+  }
+}
+
+
+- (void)createSaverView
+{
+  UIView *parentView = self.view;
+
+  if (_saverView) {
+    [_saverView removeFromSuperview];
+    [_saverView release];
+  }
+
+# if 0
+  if (_storedOrientation != UIInterfaceOrientationUnknown) {
+    [[UIApplication sharedApplication]
+     setStatusBarOrientation:_storedOrientation
+     animated:NO];
+  }
+# endif
+
+  _saverView = [_parent newSaverView:_saverName
+                            withSize:parentView.bounds.size];
+
+  if (! _saverView) {
+    [[[UIAlertView alloc] initWithTitle: _saverName
+                          message: @"Unable to load!"
+                          delegate: nil
+                          cancelButtonTitle: @"Bummer"
+                          otherButtonTitles: nil]
+     show];
+    return;
+  }
+
+  _saverView.delegate = _parent;
+  _saverView.autoresizingMask =
+    UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
+
+  [self.view addSubview:_saverView];
+
+  // The first responder must be set only after the view was placed in the view
+  // heirarchy.
+  [_saverView becomeFirstResponder]; // For shakes on iOS 6.
+  [_saverView startAnimation];
+  [self aboutPanel:_saverView
+       orientation:/* _storedOrientation */ UIInterfaceOrientationPortrait];
+}
+
+
+- (void)viewDidAppear:(BOOL)animated
+{
+  [super viewDidAppear:animated];
+  [self createSaverView];
+}
+
+
+- (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)o
+{
+  return NO;                                   /* Deprecated in iOS 6 */
+}
+
+
+- (BOOL)shouldAutorotate                       /* Added in iOS 6 */
+{
+  return YES;
+}
+
+
+- (UIInterfaceOrientationMask)supportedInterfaceOrientations   /* Added in iOS 6 */
+{
+  // Lies from the iOS docs:
+  // "This method is only called if the view controller's shouldAutorotate
+  // method returns YES."
+  return UIInterfaceOrientationMaskAll;
+}
+
+
+/*
+- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation
+{
+  return UIInterfaceOrientationPortrait;
+}
+*/
+
+
+- (void)setSaverName:(NSString *)name
+{
+  [name retain];
+  [_saverName release];
+  _saverName = name;
+  // _storedOrientation =
+  //   [UIApplication sharedApplication].statusBarOrientation;
+
+  if (_saverView)
+    [self createSaverView];
+}
+
+
+- (void)viewWillTransitionToSize: (CGSize)size
+       withTransitionCoordinator: 
+        (id<UIViewControllerTransitionCoordinator>) coordinator
+{
+  [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
+  if (!_saverView)
+    return;
+
+  [CATransaction begin];
+
+  // Completely suppress the rotation animation, since we
+  // will not (visually) be rotating at all.
+  if ([_saverView suppressRotationAnimation])
+    [CATransaction setDisableActions:YES];
+
+  [self aboutOff:TRUE];  // It does goofy things if we rotate while it's up
+
+  [coordinator animateAlongsideTransition:^
+               (id <UIViewControllerTransitionCoordinatorContext> context) {
+    // This executes repeatedly during the rotation.
+  } completion:^(id <UIViewControllerTransitionCoordinatorContext> context) {
+    // This executes once when the rotation has finished.
+    [CATransaction commit];
+    [_saverView orientationChanged];
+  }];
+  // No code goes here, as it would execute before the above completes.
+}
+
+@end
+
+#endif // USE_IPHONE
+
 
 @implementation SaverRunner
 
-- (ScreenSaverView *) makeSaverView: (NSString *) module
+
+- (XScreenSaverView *) newSaverView: (NSString *) module
+                           withSize: (NSSize) size
 {
+  Class new_class = 0;
+
+# ifndef USE_IPHONE
+
+  // Load the XScreenSaverView subclass and code from a ".saver" bundle.
+
   NSString *name = [module stringByAppendingPathExtension:@"saver"];
   NSString *path = [saverDir stringByAppendingPathComponent:name];
+
+  if (! [[NSFileManager defaultManager] fileExistsAtPath:path]) {
+    NSLog(@"bundle \"%@\" does not exist", path);
+    return 0;
+  }
+
+  NSLog(@"Loading %@", path);
+
+  // NSBundle *obundle = saverBundle;
+
   saverBundle = [NSBundle bundleWithPath:path];
-  Class new_class = [saverBundle principalClass];
-  NSAssert1 (new_class, @"unable to load \"%@\"", path);
+  if (saverBundle)
+    new_class = [saverBundle principalClass];
+
+  // Not entirely unsurprisingly, this tends to break the world.
+  // if (obundle && obundle != saverBundle)
+  //  [obundle unload];
 
+# else  // USE_IPHONE
+
+  // Determine whether to create an X11 view or an OpenGL view by
+  // looking for the "gl" tag in the xml file.  This is kind of awful.
+
+  NSString *path = [saverDir
+                     stringByAppendingPathComponent:
+                       [[[module lowercaseString]
+                          stringByReplacingOccurrencesOfString:@" "
+                          withString:@""]
+                         stringByAppendingPathExtension:@"xml"]];
+  NSData *xmld = [NSData dataWithContentsOfFile:path];
+  NSAssert (xmld, @"no XML: %@", path);
+  NSString *xml = [XScreenSaverView decompressXML:xmld];
+  Bool gl_p = (xml && [xml rangeOfString:@"gl=\"yes\""].length > 0);
+
+  new_class = (gl_p
+               ? [XScreenSaverGLView class]
+               : [XScreenSaverView class]);
+
+# endif // USE_IPHONE
+
+  if (! new_class)
+    return 0;
 
   NSRect rect;
   rect.origin.x = rect.origin.y = 0;
-  rect.size.width = 320;
-  rect.size.height = 240;
+  rect.size.width  = size.width;
+  rect.size.height = size.height;
 
-  id instance = [[new_class alloc] initWithFrame:rect isPreview:YES];
-  NSAssert1 (instance, @"unable to instantiate %@", new_class);
+  XScreenSaverView *instance =
+    [(XScreenSaverView *) [new_class alloc]
+                          initWithFrame:rect
+                          saverName:module
+                          isPreview:YES];
+  if (! instance) {
+    NSLog(@"Failed to instantiate %@ for \"%@\"", new_class, module);
+    return 0;
+  }
 
 
   /* KLUGE: Inform the underlying program that we're in "standalone"
-     mode.  This is kind of horrible but I haven't thought of a more
-     sensible way to make this work.
+     mode, e.g. running as "Phosphor.app" rather than "Phosphor.saver".
+     This is kind of horrible but I haven't thought of a more sensible
+     way to make this work.
    */
+# ifndef USE_IPHONE
   if ([saverNames count] == 1) {
-    putenv (strdup ("XSCREENSAVER_STANDALONE=1"));
+    setenv ("XSCREENSAVER_STANDALONE", "1", 1);
   }
+# endif
 
-  return (ScreenSaverView *) instance;
+  return (XScreenSaverView *) instance;
 }
 
 
+#ifndef USE_IPHONE
+
 static ScreenSaverView *
 find_saverView_child (NSView *v)
 {
@@ -90,6 +575,9 @@ find_saverView (NSView *v)
 }
 
 
+/* Changes the contents of the menubar menus to correspond to
+   the running saver.  Desktop only.
+ */
 static void
 relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
 {
@@ -116,7 +604,6 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
 - (void) openPreferences: (id) sender
 {
   ScreenSaverView *sv;
-
   if ([sender isKindOfClass:[NSView class]]) { // Sent from button
     sv = find_saverView ((NSView *) sender);
   } else {
@@ -130,6 +617,7 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
   }
 
   NSAssert (sv, @"no saver view");
+  if (!sv) return;
   NSWindow *prefs = [sv configureSheet];
 
   [NSApp beginSheet:prefs
@@ -144,11 +632,13 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
      ones will blow up if one re-inits but the other doesn't.
    */
   if (code != NSCancelButton) {
-    [sv stopAnimation];
+    if ([sv isAnimating])
+      [sv stopAnimation];
     [sv startAnimation];
   }
 }
 
+
 - (void) preferencesClosed: (NSWindow *) sheet
                 returnCode: (int) returnCode
                contextInfo: (void  *) contextInfo
@@ -156,38 +646,303 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
   [NSApp stopModalWithCode:returnCode];
 }
 
+#else  // USE_IPHONE
+
+
+- (UIImage *) screenshot
+{
+  return saved_screenshot;
+}
+
+- (void) saveScreenshot
+{
+  // Most of this is from:
+  // http://developer.apple.com/library/ios/#qa/qa1703/_index.html
+  // The rotation stuff is by me.
+
+  CGSize size = [[UIScreen mainScreen] bounds].size;
+
+  // iOS 7: Needs to be the actual device orientation.
+  // iOS 8: Needs to be UIInterfaceOrientationPortrait.
+
+  UIInterfaceOrientation orient =
+    NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_7_1 ?
+    UIInterfaceOrientationPortrait /* iOS 8 broke -[UIScreen bounds]. */ :
+    [[window rootViewController] interfaceOrientation];
+
+  if (orient == UIInterfaceOrientationLandscapeLeft ||
+      orient == UIInterfaceOrientationLandscapeRight) {
+    // Rotate the shape of the canvas 90 degrees.
+    double s = size.width;
+    size.width = size.height;
+    size.height = s;
+  }
+
+
+  // Create a graphics context with the target size
+  // On iOS 4 and later, use UIGraphicsBeginImageContextWithOptions to
+  // take the scale into consideration
+  // On iOS prior to 4, fall back to use UIGraphicsBeginImageContext
+
+  if (UIGraphicsBeginImageContextWithOptions)
+    UIGraphicsBeginImageContextWithOptions (size, NO, 0);
+  else
+    UIGraphicsBeginImageContext (size);
+
+  CGContextRef ctx = UIGraphicsGetCurrentContext();
+
+
+  // Rotate the graphics context to match current hardware rotation.
+  //
+  switch (orient) {
+  case UIInterfaceOrientationPortraitUpsideDown:
+    CGContextTranslateCTM (ctx,  [window center].x,  [window center].y);
+    CGContextRotateCTM (ctx, M_PI);
+    CGContextTranslateCTM (ctx, -[window center].x, -[window center].y);
+    break;
+  case UIInterfaceOrientationLandscapeLeft:
+  case UIInterfaceOrientationLandscapeRight:
+    CGContextTranslateCTM (ctx,  
+                           ([window frame].size.height -
+                            [window frame].size.width) / 2,
+                           ([window frame].size.width -
+                            [window frame].size.height) / 2);
+    CGContextTranslateCTM (ctx,  [window center].x,  [window center].y);
+    CGContextRotateCTM (ctx, 
+                        (orient == UIInterfaceOrientationLandscapeLeft
+                         ?  M_PI/2
+                         : -M_PI/2));
+    CGContextTranslateCTM (ctx, -[window center].x, -[window center].y);
+    break;
+  default:
+    break;
+  }
+
+  // Iterate over every window from back to front
+  //
+  for (UIWindow *win in [[UIApplication sharedApplication] windows]) {
+    if (![win respondsToSelector:@selector(screen)] ||
+        [win screen] == [UIScreen mainScreen]) {
+
+      // -renderInContext: renders in the coordinate space of the layer,
+      // so we must first apply the layer's geometry to the graphics context
+      CGContextSaveGState (ctx);
+
+      // Center the context around the window's anchor point
+      CGContextTranslateCTM (ctx, [win center].x, [win center].y);
+
+      // Apply the window's transform about the anchor point
+      CGContextConcatCTM (ctx, [win transform]);
+
+      // Offset by the portion of the bounds left of and above anchor point
+      CGContextTranslateCTM (ctx,
+        -[win bounds].size.width  * [[win layer] anchorPoint].x,
+        -[win bounds].size.height * [[win layer] anchorPoint].y);
+
+      // Render the layer hierarchy to the current context
+      [[win layer] renderInContext:ctx];
+
+      // Restore the context
+      CGContextRestoreGState (ctx);
+    }
+  }
+
+  if (saved_screenshot)
+    [saved_screenshot release];
+  saved_screenshot = [UIGraphicsGetImageFromCurrentImageContext() retain];
+
+  UIGraphicsEndImageContext();
+}
+
+
+- (void) openPreferences: (NSString *) saver
+{
+  XScreenSaverView *saverView = [self newSaverView:saver
+                                          withSize:CGSizeMake(0, 0)];
+  if (! saverView) return;
+
+  NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
+  [prefs setObject:saver forKey:@"selectedSaverName"];
+  [prefs synchronize];
+
+  [rotating_nav pushViewController: [saverView configureView]
+                      animated:YES];
+}
+
+
+#endif // USE_IPHONE
+
+
 
 - (void)loadSaver:(NSString *)name
 {
-  int i;
-  for (i = 0; i < [windows count]; i++) {
-    NSWindow *window = [windows objectAtIndex:i];
-    NSView *cv = [window contentView];
-    ScreenSaverView *old_view = find_saverView (cv);
-    NSView *sup = [old_view superview];
+# ifndef USE_IPHONE
+
+  if (saverName && [saverName isEqualToString: name]) {
+    for (NSWindow *win in windows) {
+      ScreenSaverView *sv = find_saverView ([win contentView]);
+      if (![sv isAnimating])
+        [sv startAnimation];
+    }
+    return;
+  }
 
-    NSString *old_title = [window title];
+  saverName = name;
+
+  for (NSWindow *win in windows) {
+    NSView *cv = [win contentView];
+    NSString *old_title = [win title];
     if (!old_title) old_title = @"XScreenSaver";
-    [window setTitle: name];
+    [win setTitle: name];
     relabel_menus (menubar, old_title, name);
 
-    [old_view stopAnimation];
-    [old_view removeFromSuperview];
+    ScreenSaverView *old_view = find_saverView (cv);
+    NSView *sup = old_view ? [old_view superview] : cv;
+
+    if (old_view) {
+      if ([old_view isAnimating])
+        [old_view stopAnimation];
+      [old_view removeFromSuperview];
+    }
+
+    NSSize size = [cv frame].size;
+    ScreenSaverView *new_view = [self newSaverView:name withSize: size];
+    NSAssert (new_view, @"unable to make a saver view");
 
-    ScreenSaverView *new_view = [self makeSaverView:name];
-    [new_view setFrame: [old_view frame]];
+    [new_view setFrame: (old_view ? [old_view frame] : [cv frame])];
     [sup addSubview: new_view];
-    [window makeFirstResponder:new_view];
+    [win makeFirstResponder:new_view];
     [new_view setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
     [new_view startAnimation];
+    [new_view release];
   }
 
   NSUserDefaultsController *ctl =
     [NSUserDefaultsController sharedUserDefaultsController];
   [ctl save:self];
+
+# else  // USE_IPHONE
+
+#  if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
+  NSLog (@"selecting saver \"%@\"", name);
+#  endif
+
+  NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
+  [prefs setObject:name forKey:@"selectedSaverName"];
+  [prefs synchronize];
+
+/* Cacheing this screws up rotation when starting a saver twice in a row.
+  if (saverName && [saverName isEqualToString: name]) {
+    if ([saverView isAnimating])
+      return;
+    else
+      goto LAUNCH;
+  }
+*/
+
+  saverName = name;
+
+  if (nonrotating_controller) {
+    nonrotating_controller.saverName = name;
+    return;
+  }
+
+# if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
+  UIScreen *screen = [UIScreen mainScreen];
+
+  /* 'nativeScale' is very confusing.
+
+     iPhone 4s:
+        bounds:        320x480   scale:        2
+        nativeBounds:  640x960   nativeScale:  2
+     iPhone 5s:
+        bounds:        320x568   scale:        2
+        nativeBounds:  640x1136  nativeScale:  2
+     iPad 2:
+        bounds:       768x1024   scale:        1
+        nativeBounds: 768x1024   nativeScale:  1
+     iPad Retina/Air:
+        bounds:       768x1024   scale:        2
+        nativeBounds: 1536x2048  nativeScale:  2
+     iPhone 6:
+        bounds:        320x568   scale:        2
+        nativeBounds:  640x1136  nativeScale:  2
+     iPhone 6+:
+        bounds:        320x568   scale:        2
+        nativeBounds:  960x1704  nativeScale:  3
+
+     According to a StackOverflow comment:
+
+       The iPhone 6+ renders internally using @3x assets at a virtual
+       resolution of 2208x1242 (with 736x414 points), then samples that down
+       for display. The same as using a scaled resolution on a Retina MacBook
+       -- it lets them hit an integral multiple for pixel assets while still
+       having e.g. 12pt text look the same size on the screen.
+
+       The 6, the 5s, the 5, the 4s and the 4 are all 326 pixels per inch,
+       and use @2x assets to stick to the approximately 160 points per inch
+       of all previous devices.
+
+       The 6+ is 401 pixels per inch. So it'd hypothetically need roughly
+       @2.46x assets. Instead Apple uses @3x assets and scales the complete
+       output down to about 84% of its natural size.
+
+       In practice Apple has decided to go with more like 87%, turning the
+       1080 into 1242. No doubt that was to find something as close as
+       possible to 84% that still produced integral sizes in both directions
+       -- 1242/1080 = 2208/1920 exactly, whereas if you'd turned the 1080
+       into, say, 1286, you'd somehow need to render 2286.22 pixels
+       vertically to scale well.
+   */
+
+  NSLog(@"screen: %.0fx%0.f",
+        [[screen currentMode] size].width,
+        [[screen currentMode] size].height);
+  NSLog(@"bounds: %.0fx%0.f x %.1f = %.0fx%0.f",
+        [screen bounds].size.width,
+        [screen bounds].size.height,
+        [screen scale],
+        [screen scale] * [screen bounds].size.width,
+        [screen scale] * [screen bounds].size.height);
+
+#  ifdef __IPHONE_8_0
+  if ([screen respondsToSelector:@selector(nativeBounds)])
+    NSLog(@"native: %.0fx%0.f / %.1f = %.0fx%0.f",
+          [screen nativeBounds].size.width,
+          [screen nativeBounds].size.height,
+          [screen nativeScale],
+          [screen nativeBounds].size.width  / [screen nativeScale],
+          [screen nativeBounds].size.height / [screen nativeScale]);
+#  endif
+# endif // TARGET_IPHONE_SIMULATOR
+
+  // Take the screen shot before creating the screen saver view, because this
+  // can screw with the layout.
+  [self saveScreenshot];
+
+  // iOS 3.2. Before this were iPhones (and iPods) only, which always did modal
+  // presentation full screen.
+  rotating_nav.modalPresentationStyle = UIModalPresentationFullScreen;
+
+  nonrotating_controller = [[SaverViewController alloc]
+                            initWithSaverRunner:self
+                            showAboutBox:[saverNames count] != 1];
+  nonrotating_controller.saverName = name;
+
+  /* LAUNCH: */
+
+  [rotating_nav presentViewController:nonrotating_controller animated:NO completion:nil];
+
+  // Doing this makes savers cut back to the list instead of fading,
+  // even though [XScreenSaverView stopAndClose] does setHidden:NO first.
+  // [window setHidden:YES];
+
+# endif // USE_IPHONE
 }
 
 
+#ifndef USE_IPHONE
+
 - (void)aboutPanel:(id)sender
 {
   NSDictionary *bd = [saverBundle infoDictionary];
@@ -198,15 +953,18 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
   [d setValue:[bd objectForKey:@"CFBundleShortVersionString"] 
      forKey:@"ApplicationVersion"];
   [d setValue:[bd objectForKey:@"NSHumanReadableCopyright"] forKey:@"Copy"];
-  [d setValue:[[NSAttributedString alloc]
-                initWithString: (NSString *) 
-                  [bd objectForKey:@"CFBundleGetInfoString"]]
-     forKey:@"Credits"];
-
+  NSAttributedString *s = [[NSAttributedString alloc]
+                           initWithString: (NSString *)
+                           [bd objectForKey:@"CFBundleGetInfoString"]];
+  [d setValue:s forKey:@"Credits"];
+  [s release];
+  
   [[NSApplication sharedApplication]
     orderFrontStandardAboutPanelWithOptions:d];
 }
 
+#endif // !USE_IPHONE
+
 
 
 - (void)selectedSaverDidChange:(NSDictionary *)change
@@ -214,77 +972,100 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
   NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
   NSString *name = [prefs stringForKey:@"selectedSaverName"];
 
+  if (! name) return;
+
   if (! [saverNames containsObject:name]) {
-    NSLog (@"Saver \"%@\" does not exist", name);
+    NSLog (@"saver \"%@\" does not exist", name);
     return;
   }
 
-  if (name) [self loadSaver: name];
+  [self loadSaver: name];
 }
 
 
 - (NSArray *) listSaverBundleNamesInDir:(NSString *)dir
 {
+# ifndef USE_IPHONE
+  NSString *ext = @"saver";
+# else
+  NSString *ext = @"xml";
+# endif
+
   NSArray *files = [[NSFileManager defaultManager]
                      contentsOfDirectoryAtPath:dir error:nil];
   if (! files) return 0;
+  NSMutableArray *result = [NSMutableArray arrayWithCapacity: [files count]+1];
 
-  int n = [files count];
-  NSMutableArray *result = [NSMutableArray arrayWithCapacity: n+1];
-
-  int i;
-  for (i = 0; i < n; i++) {
-    NSString *p = [files objectAtIndex:i];
-    if ([[p pathExtension] caseInsensitiveCompare:@"saver"]) 
+  for (NSString *p in files) {
+    if ([[p pathExtension] caseInsensitiveCompare: ext]) 
       continue;
-    [result addObject: [[p lastPathComponent] stringByDeletingPathExtension]];
+
+    NSString *name = [[p lastPathComponent] stringByDeletingPathExtension];
+
+# ifdef USE_IPHONE
+    // Get the saver name's capitalization right by reading the XML file.
+
+    p = [dir stringByAppendingPathComponent: p];
+    NSData *xmld = [NSData dataWithContentsOfFile:p];
+    NSAssert (xmld, @"no XML: %@", p);
+    NSString *xml = [XScreenSaverView decompressXML:xmld];
+    NSRange r = [xml rangeOfString:@"_label=\"" options:0];
+    NSAssert1 (r.length, @"no name in %@", p);
+    if (r.length) {
+      xml = [xml substringFromIndex: r.location + r.length];
+      r = [xml rangeOfString:@"\"" options:0];
+      if (r.length) name = [xml substringToIndex: r.location];
+    }
+
+# endif // USE_IPHONE
+
+    NSAssert1 (name, @"no name in %@", p);
+    if (name) [result addObject: name];
   }
 
+  if (! [result count])
+    result = 0;
+
   return result;
 }
 
 
+
 - (NSArray *) listSaverBundleNames
 {
   NSMutableArray *dirs = [NSMutableArray arrayWithCapacity: 10];
 
-  // First look in the bundle itself.
+# ifndef USE_IPHONE
+  // On MacOS, look in the "Contents/Resources/" and "Contents/PlugIns/"
+  // directories in the bundle.
+  [dirs addObject: [[[[NSBundle mainBundle] bundlePath]
+                      stringByAppendingPathComponent:@"Contents"]
+                     stringByAppendingPathComponent:@"Resources"]];
   [dirs addObject: [[NSBundle mainBundle] builtInPlugInsPath]];
 
-  // Then look in the same directory as the executable.
+  // Also look in the same directory as the executable.
   [dirs addObject: [[[NSBundle mainBundle] bundlePath]
                      stringByDeletingLastPathComponent]];
 
-  // Then look in standard screensaver directories.
-  [dirs addObject: @"~/Library/Screen Savers"];
-  [dirs addObject: @"/Library/Screen Savers"];
-  [dirs addObject: @"/System/Library/Screen Savers"];
+  // Finally, look in standard MacOS screensaver directories.
+//  [dirs addObject: @"~/Library/Screen Savers"];
+//  [dirs addObject: @"/Library/Screen Savers"];
+//  [dirs addObject: @"/System/Library/Screen Savers"];
+
+# else  // USE_IPHONE
+
+  // On iOS, only look in the bundle's root directory.
+  [dirs addObject: [[NSBundle mainBundle] bundlePath]];
+
+# endif // USE_IPHONE
 
   int i;
   for (i = 0; i < [dirs count]; i++) {
     NSString *dir = [dirs objectAtIndex:i];
     NSArray *names = [self listSaverBundleNamesInDir:dir];
     if (! names) continue;
-
-    // Make sure this directory is on $PATH.
-
-    const char *cdir = [dir cStringUsingEncoding:NSUTF8StringEncoding];
-    const char *opath = getenv ("PATH");
-    if (!opath) opath = "/bin"; // $PATH is unset when running under Shark!
-    char *npath = (char *) malloc (strlen (opath) + strlen (cdir) + 30);
-    strcpy (npath, "PATH=");
-    strcat (npath, cdir);
-    strcat (npath, ":");
-    strcat (npath, opath);
-    if (putenv (npath)) {
-      perror ("putenv");
-      abort();
-    }
-    /* Don't free (npath) -- MacOS's putenv() does not copy it. */
-
     saverDir   = [dir retain];
     saverNames = [names retain];
-
     return names;
   }
 
@@ -296,10 +1077,14 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
     err = [err stringByAppendingString:@"/"];
   }
   NSLog (@"%@", err);
-  exit (1);
+  return [NSArray array];
 }
 
 
+/* Create the popup menu of available saver names.
+ */
+#ifndef USE_IPHONE
+
 - (NSPopUpButton *) makeMenu
 {
   NSRect rect;
@@ -338,9 +1123,163 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
   NSRect r = [popup frame];
   r.size.width = max_width;
   [popup setFrame:r];
+  [popup autorelease];
   return popup;
 }
 
+#else  // USE_IPHONE
+
+- (NSString *) makeDesc:(NSString *)saver
+                  yearOnly:(BOOL) yearp
+{
+  NSString *desc = 0;
+  NSString *path = [saverDir stringByAppendingPathComponent:
+                               [[saver lowercaseString]
+                                 stringByReplacingOccurrencesOfString:@" "
+                                 withString:@""]];
+  NSRange r;
+
+  path = [path stringByAppendingPathExtension:@"xml"];
+  NSData *xmld = [NSData dataWithContentsOfFile:path];
+  if (! xmld) goto FAIL;
+  desc = [XScreenSaverView decompressXML:xmld];
+  if (! desc) goto FAIL;
+
+  r = [desc rangeOfString:@"<_description>"
+            options:NSCaseInsensitiveSearch];
+  if (r.length == 0) {
+    desc = 0;
+    goto FAIL;
+  }
+  desc = [desc substringFromIndex: r.location + r.length];
+  r = [desc rangeOfString:@"</_description>"
+            options:NSCaseInsensitiveSearch];
+  if (r.length > 0)
+    desc = [desc substringToIndex: r.location];
+
+  // Leading and trailing whitespace.
+  desc = [desc stringByTrimmingCharactersInSet:
+                 [NSCharacterSet whitespaceAndNewlineCharacterSet]];
+
+  // Let's see if we can find a year on the last line.
+  r = [desc rangeOfString:@"\n" options:NSBackwardsSearch];
+  NSString *year = 0;
+  for (NSString *word in
+         [[desc substringFromIndex:r.location + r.length]
+           componentsSeparatedByCharactersInSet:
+             [NSCharacterSet characterSetWithCharactersInString:
+                               @" \t\n-."]]) {
+    int n = [word doubleValue];
+    if (n > 1970 && n < 2100)
+      year = word;
+  }
+
+  // Delete everything after the first blank line.
+  //
+  r = [desc rangeOfString:@"\n\n" options:0];
+  if (r.length > 0)
+    desc = [desc substringToIndex: r.location];
+
+  // Unwrap lines and compress whitespace.
+  {
+    NSString *result = @"";
+    for (NSString *s in [desc componentsSeparatedByCharactersInSet:
+                          [NSCharacterSet whitespaceAndNewlineCharacterSet]]) {
+      if ([result length] == 0)
+        result = s;
+      else if ([s length] > 0)
+        result = [NSString stringWithFormat: @"%@ %@", result, s];
+      desc = result;
+    }
+  }
+
+  if (year)
+    desc = [year stringByAppendingString:
+                   [@": " stringByAppendingString: desc]];
+
+  if (yearp)
+    desc = year ? year : @"";
+
+FAIL:
+  if (! desc) {
+    if ([saverNames count] > 1)
+      desc = @"Oops, this module appears to be incomplete.";
+    else
+      desc = @"";
+  }
+
+  return desc;
+}
+
+- (NSString *) makeDesc:(NSString *)saver
+{
+  return [self makeDesc:saver yearOnly:NO];
+}
+
+
+
+/* Create a dictionary of one-line descriptions of every saver,
+   for display on the UITableView.
+ */
+- (NSDictionary *)makeDescTable
+{
+  NSMutableDictionary *dict = 
+    [NSMutableDictionary dictionaryWithCapacity:[saverNames count]];
+  for (NSString *saver in saverNames) {
+    [dict setObject:[self makeDesc:saver] forKey:saver];
+  }
+  return dict;
+}
+
+
+- (void) wantsFadeOut:(XScreenSaverView *)sender
+{
+  rotating_nav.view.hidden = NO; // In case it was hidden during startup.
+
+  /* The XScreenSaverView screws with the status bar orientation, mostly to
+     keep the simulator oriented properly. But on iOS 8.1 (and maybe 8.0
+     and/or 8.2), this confuses the UINavigationController, so put the
+     orientation back to portrait before dismissing the SaverViewController.
+   */
+# if 0
+  [[UIApplication sharedApplication]
+   setStatusBarOrientation:UIInterfaceOrientationPortrait
+   animated:NO];
+# endif
+
+  /* Make sure the most-recently-run saver is visible.  Sometimes it ends
+     up scrolled half a line off the bottom of the screen.
+   */
+  if (saverName) {
+    for (UIViewController *v in [rotating_nav viewControllers]) {
+      if ([v isKindOfClass:[SaverListController class]]) {
+        [(SaverListController *)v scrollTo: saverName];
+        break;
+      }
+    }
+  }
+
+  [rotating_nav dismissViewControllerAnimated:YES completion:^() {
+    [nonrotating_controller release];
+    nonrotating_controller = nil;
+    [[rotating_nav view] becomeFirstResponder];
+  }];
+}
+
+
+- (void) didShake:(XScreenSaverView *)sender
+{
+# if TARGET_IPHONE_SIMULATOR
+  NSLog (@"simulating shake on saver list");
+# endif
+  [[rotating_nav topViewController] motionEnded: UIEventSubtypeMotionShake
+                                      withEvent: nil];
+}
+
+
+#endif // USE_IPHONE
+
+
 
 /* This is called when the "selectedSaverName" pref changes, e.g.,
    when a menu selection is made.
@@ -362,6 +1301,10 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
 }
 
 
+# ifndef USE_IPHONE
+
+/* Create the desktop window shell, possibly including a preferences button.
+ */
 - (NSWindow *) makeWindow
 {
   NSRect rect;
@@ -428,8 +1371,10 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
     [pbox setTitlePosition:NSNoTitle];
     [pbox setBorderType:NSNoBorder];
     [pbox addSubview:gbox];
+    [gbox release];
     if (menu) [pbox addSubview:menu];
     if (pb)   [pbox addSubview:pb];
+    [pb release];
     [pbox sizeToFit];
 
     [pb   setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin];
@@ -450,7 +1395,7 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
   
   rect.origin.x += rect.size.width * (count ? 0.55 : -0.55);
   
-  NSWindow *window = [[NSWindow alloc]
+  NSWindow *win = [[NSWindow alloc]
                       initWithContentRect:rect
                                 styleMask:(NSTitledWindowMask |
                                            NSClosableWindowMask |
@@ -459,66 +1404,216 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
                                   backing:NSBackingStoreBuffered
                                     defer:YES
                                    screen:screen];
-  [window setMinSize:[window frameRectForContentRect:rect].size];
+  [win setMinSize:[win frameRectForContentRect:rect].size];
+  [[win contentView] addSubview: (pbox ? (NSView *) pbox : (NSView *) sv)];
+  [pbox release];
 
-  [[window contentView] addSubview: (pbox ? (NSView *) pbox : (NSView *) sv)];
-
-  [window makeKeyAndOrderFront:window];
+  [win makeKeyAndOrderFront:win];
   
   [sv startAnimation]; // this is the dummy saver
+  [sv autorelease];
 
   count++;
 
-  return window;
+  return win;
+}
+
+
+- (void) animTimer
+{
+  for (NSWindow *win in windows) {
+    ScreenSaverView *sv = find_saverView ([win contentView]);
+    if ([sv isAnimating])
+      [sv animateOneFrame];
+  }
 }
 
+# endif // !USE_IPHONE
+
 
-- (void)applicationDidFinishLaunching: (NSNotification *) notif
+- (void)applicationDidFinishLaunching:
+# ifndef USE_IPHONE
+    (NSNotification *) notif
+# else  // USE_IPHONE
+    (UIApplication *) application
+# endif // USE_IPHONE
 {
   [self listSaverBundleNames];
 
-  int n = ([saverNames count] == 1 ? 1 : 2);
-  NSMutableArray *a = [[NSMutableArray arrayWithCapacity: n+1] retain];
+  NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
+
+# ifndef USE_IPHONE
+  int window_count = ([saverNames count] <= 1 ? 1 : 2);
+  NSMutableArray *a = [[NSMutableArray arrayWithCapacity: window_count+1]
+                        retain];
   windows = a;
+
   int i;
-  for (i = 0; i < n; i++) {
-    NSWindow *window = [self makeWindow];
+  // Create either one window (for standalone, e.g. Phosphor.app)
+  // or two windows for SaverTester.app.
+  for (i = 0; i < window_count; i++) {
+    NSWindow *win = [self makeWindow];
     // Get the last-saved window position out of preferences.
-    [window setFrameAutosaveName:
+    [win setFrameAutosaveName:
               [NSString stringWithFormat:@"XScreenSaverWindow%d", i]];
-    [window setFrameUsingName:[window frameAutosaveName]];
-    [a addObject: window];
+    [win setFrameUsingName:[win frameAutosaveName]];
+    [a addObject: win];
+    // This prevents clicks from being seen by savers.
+    // [win setMovableByWindowBackground:YES];
+    [win release];
   }
+# else  // USE_IPHONE
 
-  if (n == 1) {
-    [self loadSaver:[saverNames objectAtIndex:0]];
-  } else {
+# undef ya_rand_init
+  ya_rand_init (0);    // Now's a good time.
 
-    /* In the XCode project, each .saver scheme sets this env var when
-       launching SaverTester.app so that it knows which one we are
-       currently debugging.  If this is set, it overrides the default
-       selection in the popup menu.  If unset, that menu persists to
-       whatever it was last time.
-     */
-    const char *forced = getenv ("SELECTED_SAVER");
-    if (forced && *forced) {
-      NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
-      NSString *s = [NSString stringWithCString:(char *)forced
-                              encoding:NSUTF8StringEncoding];
-      NSLog (@"selecting saver %@", s);
-      [prefs setObject:s forKey:@"selectedSaverName"];
-    }
 
-    [self selectedSaverDidChange:nil];
+  /* iOS docs say:
+     "You must call this method before attempting to get orientation data from
+      the receiver. This method enables the device's accelerometer hardware
+      and begins the delivery of acceleration events to the receiver."
+
+     Adding or removing this doesn't seem to make any difference. It's
+     probably getting called by the UINavigationController. Still... */
+  [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
+
+  rotating_nav = [[[RotateyViewController alloc] initWithRotation:YES]
+                         retain];
+
+  if ([prefs boolForKey:@"wasRunning"]) // Prevents menu flicker on startup.
+    rotating_nav.view.hidden = YES;
+
+  [window setRootViewController: rotating_nav];
+  [window setAutoresizesSubviews:YES];
+  [window setAutoresizingMask: 
+            (UIViewAutoresizingFlexibleWidth | 
+             UIViewAutoresizingFlexibleHeight)];
+
+  SaverListController *menu = [[SaverListController alloc] 
+                                initWithNames:saverNames
+                                descriptions:[self makeDescTable]];
+  [rotating_nav pushViewController:menu animated:YES];
+  [menu becomeFirstResponder];
+  [menu autorelease];
+
+  application.applicationSupportsShakeToEdit = YES;
+
+
+# endif // USE_IPHONE
+
+  NSString *forced = 0;
+  /* In the XCode project, each .saver scheme sets this env var when
+     launching SaverTester.app so that it knows which one we are
+     currently debugging.  If this is set, it overrides the default
+     selection in the popup menu.  If unset, that menu persists to
+     whatever it was last time.
+   */
+  const char *f = getenv ("SELECTED_SAVER");
+  if (f && *f)
+    forced = [NSString stringWithCString:(char *)f
+                       encoding:NSUTF8StringEncoding];
+
+  if (forced && ![saverNames containsObject:forced]) {
+    NSLog(@"forced saver \"%@\" does not exist", forced);
+    forced = 0;
+  }
+
+  // If there's only one saver, run that.
+  if (!forced && [saverNames count] == 1)
+    forced = [saverNames objectAtIndex:0];
+
+# ifdef USE_IPHONE
+  NSString *prev = [prefs stringForKey:@"selectedSaverName"];
+
+  if (forced)
+    prev = forced;
+
+  // If nothing was selected (e.g., this is the first launch)
+  // then scroll randomly instead of starting up at "A".
+  //
+  if (!prev)
+    prev = [saverNames objectAtIndex: (random() % [saverNames count])];
+
+  if (prev)
+    [menu scrollTo: prev];
+# endif // USE_IPHONE
+
+  if (forced)
+    [prefs setObject:forced forKey:@"selectedSaverName"];
+
+# ifdef USE_IPHONE
+  /* Don't auto-launch the saver unless it was running last time.
+     XScreenSaverView manages this, on crash_timer.
+     Unless forced.
+   */
+  if (!forced && ![prefs boolForKey:@"wasRunning"])
+    return;
+# endif
+
+  [self selectedSaverDidChange:nil];
+//  [NSTimer scheduledTimerWithTimeInterval: 0
+//           target:self
+//           selector:@selector(selectedSaverDidChange:)
+//           userInfo:nil
+//           repeats:NO];
+
+
+
+# ifndef USE_IPHONE
+  /* On 10.8 and earlier, [ScreenSaverView startAnimation] causes the
+     ScreenSaverView to run its own timer calling animateOneFrame.
+     On 10.9, that fails because the private class ScreenSaverModule
+     is only initialized properly by ScreenSaverEngine, and in the
+     context of SaverRunner, the null ScreenSaverEngine instance
+     behaves as if [ScreenSaverEngine needsAnimationTimer] returned false.
+     So, if it looks like this is the 10.9 version of ScreenSaverModule
+     instead of the 10.8 version, we run our own timer here.  This sucks.
+   */
+  if (!anim_timer) {
+    Class ssm = NSClassFromString (@"ScreenSaverModule");
+    if (ssm && [ssm instancesRespondToSelector:
+                      @selector(needsAnimationTimer)]) {
+      NSWindow *win = [windows objectAtIndex:0];
+      ScreenSaverView *sv = find_saverView ([win contentView]);
+      anim_timer = [NSTimer scheduledTimerWithTimeInterval:
+                              [sv animationTimeInterval]
+                            target:self
+                            selector:@selector(animTimer)
+                            userInfo:nil
+                            repeats:YES];
+    }
   }
+# endif // !USE_IPHONE
 }
 
 
+#ifndef USE_IPHONE
+
 /* When the window closes, exit (even if prefs still open.)
-*/
+ */
 - (BOOL) applicationShouldTerminateAfterLastWindowClosed: (NSApplication *) n
 {
   return YES;
 }
 
+# else // USE_IPHONE
+
+- (void)applicationWillResignActive:(UIApplication *)app
+{
+  [(XScreenSaverView *)view setScreenLocked:YES];
+}
+
+- (void)applicationDidBecomeActive:(UIApplication *)app
+{
+  [(XScreenSaverView *)view setScreenLocked:NO];
+}
+
+- (void)applicationDidEnterBackground:(UIApplication *)application
+{
+  [(XScreenSaverView *)view setScreenLocked:YES];
+}
+
+#endif // USE_IPHONE
+
+
 @end