From http://www.jwz.org/xscreensaver/xscreensaver-5.16.tar.gz
[xscreensaver] / OSX / SaverRunner.m
index 797cd2cec5b289ee7cd9d86b5181da5076127cb4..a5eb429db9597d33c2cb370d5035e271310e5d3c 100644 (file)
@@ -1,4 +1,4 @@
-/* xscreensaver, Copyright (c) 2006-2011 Jamie Zawinski <jwz@jwz.org>
+/* xscreensaver, Copyright (c) 2006-2012 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
    bundle's Contents/PlugIns/ 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 "SaverRunner.h"
+#import "SaverListController.h"
 #import "XScreenSaverGLView.h"
+#import <TargetConditionals.h>
+
+#ifdef USE_IPHONE
+
+@interface RotateyViewController : UINavigationController
+@end
+
+@implementation RotateyViewController
+- (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)o
+{
+  return YES;
+}
+@end
+
+#endif // USE_IPHONE
+
 
 @implementation SaverRunner
 
+
 - (ScreenSaverView *) makeSaverView: (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"]];
+  NSString *xml = [NSString stringWithContentsOfFile:path
+                            encoding:NSISOLatin1StringEncoding
+                            error:nil];
+  NSAssert (xml, @"no XML: %@", path);
+  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) 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"));
   }
+# endif
 
   return (ScreenSaverView *) instance;
 }
 
 
+#ifndef USE_IPHONE
+
 static ScreenSaverView *
 find_saverView_child (NSView *v)
 {
@@ -90,6 +168,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 +197,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 {
@@ -144,11 +224,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,40 +238,245 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
   [NSApp stopModalWithCode:returnCode];
 }
 
+#else  // USE_IPHONE
 
-- (void)loadSaver:(NSString *)name
+
+- (UIImage *) screenshot
 {
-  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];
+  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;
+
+  UIInterfaceOrientation orient =
+    [[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;
+  }
 
-    NSString *old_title = [window title];
+  // 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: (id) sender
+{
+  NSString *saver = [listController selected];
+  if (! saver) return;
+
+  [self loadSaver:saver launch:NO];
+  if (! saverView) return;
+
+  [rootViewController pushViewController: [saverView configureView]
+                      animated:YES];
+}
+
+
+- (void) loadSaverMenu: (id) sender
+{
+  NSString *saver = [listController selected];
+  NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
+  if (saver) {
+    [prefs setObject:saver forKey:@"selectedSaverName"];
+  } else {
+    [prefs removeObjectForKey:@"selectedSaverName"];
+  }
+  [self saveScreenshot];
+  [self selectedSaverDidChange:nil];
+}
+
+#endif // USE_IPHONE
+
+
+
+- (void)loadSaver:(NSString *)name launch:(BOOL)launch
+{
+  // NSLog (@"selecting saver \"%@\"", name);
+
+# ifndef USE_IPHONE
+
+  if (saverName && [saverName isEqualToString: name]) {
+    if (launch)
+      for (NSWindow *win in windows) {
+        ScreenSaverView *sv = find_saverView ([win contentView]);
+        if (![sv isAnimating])
+          [sv startAnimation];
+      }
+    return;
+  }
+
+  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 makeSaverView: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 retain];
+    if (launch)
+      [new_view startAnimation];
   }
 
   NSUserDefaultsController *ctl =
     [NSUserDefaultsController sharedUserDefaultsController];
   [ctl save:self];
+
+# else  // USE_IPHONE
+
+  if (saverName && [saverName isEqualToString: name]) {
+    if (launch && ![saverView isAnimating]) {
+      [window addSubview: saverView];
+      [saverView startAnimation];
+    }
+    return;
+  }
+
+  saverName = name;
+
+  if (saverView) {
+    if ([saverView isAnimating])
+      [saverView stopAnimation];
+    [saverView removeFromSuperview];
+  }
+
+  NSSize size = [window frame].size;
+  saverView = [self makeSaverView:name withSize: size];
+
+  if (! saverView) {
+    [[[UIAlertView alloc] initWithTitle: name
+                          message: @"Unable to load!"
+                          delegate: nil
+                          cancelButtonTitle: @"Bummer"
+                          otherButtonTitles: nil]
+     show];
+    return;
+  }
+
+  [saverView setFrame: [window frame]];
+  [saverView retain];
+  [[NSNotificationCenter defaultCenter]
+    addObserver:saverView
+    selector:@selector(didRotate:)
+    name:UIDeviceOrientationDidChangeNotification object:nil];
+
+  if (launch) {
+    [window addSubview: saverView];
+    [saverView startAnimation];
+  }
+# endif // USE_IPHONE
+}
+
+
+- (void)loadSaver:(NSString *)name
+{
+  [self loadSaver:name launch:YES];
 }
 
 
 - (void)aboutPanel:(id)sender
 {
+# ifndef USE_IPHONE
   NSDictionary *bd = [saverBundle infoDictionary];
   NSMutableDictionary *d = [NSMutableDictionary dictionaryWithCapacity:20];
 
@@ -205,6 +492,27 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
 
   [[NSApplication sharedApplication]
     orderFrontStandardAboutPanelWithOptions:d];
+
+# else  // USE_IPHONE
+
+  NSDictionary *bd = [[NSBundle mainBundle] infoDictionary];
+  NSString *body = [bd objectForKey:@"CFBundleGetInfoString"];
+
+  body = [body stringByReplacingOccurrencesOfString:@", " withString:@",\n"];
+  body = [body stringByAppendingString:
+               @"\n\n"
+               "Double-tap to run.\n\n"
+               "Double-tap again to\n"
+               "return to this list."];
+
+  [[[UIAlertView alloc] initWithTitle: @""
+                        message: body
+                        delegate: nil
+                        cancelButtonTitle: @"OK"
+                        otherButtonTitles: nil]
+    show];
+
+# endif // USE_IPHONE
 }
 
 
@@ -214,77 +522,90 @@ 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]];
+
+# ifndef USE_IPHONE
+    NSString *name = [[p lastPathComponent] stringByDeletingPathExtension];
+# else  // !USE_IPHONE
+
+    // Get the saver name's capitalization right by reading the XML file.
+
+    p = [dir stringByAppendingPathComponent: p];
+    NSString *name = [NSString stringWithContentsOfFile:p
+                               encoding:NSISOLatin1StringEncoding
+                               error:nil];
+    NSRange r = [name rangeOfString:@"_label=\"" options:0];
+    name = [name substringFromIndex: r.location + r.length];
+    r = [name rangeOfString:@"\"" options:0];
+    name = [name substringToIndex: r.location];
+
+    NSAssert1 (name, @"no name in %@", p);
+
+# endif // !USE_IPHONE
+
+    [result addObject: name];
   }
 
   return result;
 }
 
 
+
 - (NSArray *) listSaverBundleNames
 {
   NSMutableArray *dirs = [NSMutableArray arrayWithCapacity: 10];
 
-  // First look in the bundle itself.
+# ifndef USE_IPHONE
+  // On MacOS, look in the "Contents/PlugIns/" directory in the bundle.
   [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.
+  // Finally, look in standard MacOS screensaver directories.
   [dirs addObject: @"~/Library/Screen Savers"];
   [dirs addObject: @"/Library/Screen Savers"];
   [dirs addObject: @"/System/Library/Screen Savers"];
 
+# else
+  // On iOS, just look in the bundle's root directory.
+  [dirs addObject: [[NSBundle mainBundle] bundlePath]];
+# endif
+
   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 +617,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;
@@ -341,6 +666,124 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
   return popup;
 }
 
+#else  // USE_IPHONE
+
+/* 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) {
+    NSString *desc = 0;
+    NSString *path = [saverDir stringByAppendingPathComponent:
+                                 [[saver lowercaseString]
+                                   stringByReplacingOccurrencesOfString:@" "
+                                   withString:@""]];
+    NSRange r;
+
+    path = [path stringByAppendingPathExtension:@"xml"];
+    desc = [NSString stringWithContentsOfFile:path
+                     encoding:NSISOLatin1StringEncoding
+                     error:nil];
+    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];
+
+    // Truncate really long ones.
+    int max = 140;
+    if ([desc length] > max)
+      desc = [desc substringToIndex: max];
+
+    if (year)
+      desc = [year stringByAppendingString:
+                     [@": " stringByAppendingString: desc]];
+
+  FAIL:
+    if (! desc) {
+      desc = @"Oops, this module appears to be incomplete.";
+      // NSLog(@"broken saver: %@", path);
+    }
+
+    [dict setObject:desc forKey:saver];
+  }
+
+  return dict;
+}
+
+
+- (UIViewController *) makeMenu
+{
+  listController = [[[SaverListController alloc] 
+                      initWithNames:saverNames
+                      descriptions:[self makeDescTable]]
+                     retain];
+  UIBarButtonItem *run  = [[[UIBarButtonItem alloc]
+                             initWithTitle:@"Run"
+                             style: UIBarButtonItemStylePlain
+                             target: self
+                             action: @selector(loadSaverMenu:)]
+                            autorelease];
+  UIBarButtonItem *about = [[[UIBarButtonItem alloc]
+                             initWithTitle: @"About"
+                             style: UIBarButtonItemStylePlain
+                             target: self
+                             action: @selector(aboutPanel:)]
+                            autorelease];
+  UIBarButtonItem *pref = [[[UIBarButtonItem alloc]
+                             initWithTitle:@"Settings"
+                             style: UIBarButtonItemStylePlain
+                             target: self
+                             action: @selector(openPreferences:)]
+                            autorelease];
+  NSArray *a = [NSArray arrayWithObjects: pref, about, nil];
+
+  [run   setEnabled:NO];
+  [about setEnabled:YES];
+  [pref  setEnabled:NO];
+  listController.navigationItem.leftBarButtonItem  = run;
+  listController.navigationItem.rightBarButtonItems = a;
+
+  return listController;
+}
+
+#endif // USE_IPHONE
+
+
 
 /* This is called when the "selectedSaverName" pref changes, e.g.,
    when a menu selection is made.
@@ -362,6 +805,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;
@@ -450,7 +897,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,61 +906,123 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
                                   backing:NSBackingStoreBuffered
                                     defer:YES
                                    screen:screen];
-  [window setMinSize:[window frameRectForContentRect:rect].size];
-
-  [[window contentView] addSubview: (pbox ? (NSView *) pbox : (NSView *) sv)];
+  [win setMinSize:[win frameRectForContentRect:rect].size];
+  [[win contentView] addSubview: (pbox ? (NSView *) pbox : (NSView *) sv)];
 
-  [window makeKeyAndOrderFront:window];
+  [win makeKeyAndOrderFront:win];
   
   [sv startAnimation]; // this is the dummy saver
 
   count++;
 
-  return window;
+  return win;
 }
 
+# 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];
+# 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];
   }
+# 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"];
-    }
+  rootViewController = [[[RotateyViewController alloc] init] retain];
+  [window setRootViewController: rootViewController];
+  [rootViewController pushViewController:[self makeMenu]
+                      animated:YES];
 
-    [self selectedSaverDidChange:nil];
+  [window makeKeyAndVisible];
+  [window setAutoresizesSubviews:YES];
+  [window setAutoresizingMask: 
+            (UIViewAutoresizingFlexibleWidth | 
+             UIViewAutoresizingFlexibleHeight)];
+
+  // Has to be after the list window is up, or we get black.
+  [self saveScreenshot];
+
+# 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];
+
+  NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
+
+# 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)
+    [listController 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.
+   */
+  if (! [prefs boolForKey:@"wasRunning"])
+    return;
+# endif
+
+  [self selectedSaverDidChange:nil];
 }
 
 
+#ifndef USE_IPHONE
+
 /* When the window closes, exit (even if prefs still open.)
 */
 - (BOOL) applicationShouldTerminateAfterLastWindowClosed: (NSApplication *) n
@@ -521,4 +1030,24 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
   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