From http://www.jwz.org/xscreensaver/xscreensaver-5.35.tar.gz
[xscreensaver] / OSX / SaverRunner.m
index a5eb429db9597d33c2cb370d5035e271310e5d3c..ac010b9fa760d1bbb6e0c6ccea8b6ff96086307e 100644 (file)
@@ -1,4 +1,4 @@
-/* xscreensaver, Copyright (c) 2006-2012 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
@@ -19,7 +19,7 @@
 
    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.
 
    of bundles that have executable code in them.  Bleh.
  */
 
+#import <TargetConditionals.h>
 #import "SaverRunner.h"
 #import "SaverListController.h"
 #import "XScreenSaverGLView.h"
-#import <TargetConditionals.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;
                           stringByReplacingOccurrencesOfString:@" "
                           withString:@""]
                          stringByAppendingPathExtension:@"xml"]];
-  NSString *xml = [NSString stringWithContentsOfFile:path
-                            encoding:NSISOLatin1StringEncoding
-                            error:nil];
-  NSAssert (xml, @"no XML: %@", path);
+  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
                           initWithFrame:rect
                           saverName:module
                           isPreview:YES];
-  if (! instance) return 0;
+  if (! instance) {
+    NSLog(@"Failed to instantiate %@ for \"%@\"", new_class, module);
+    return 0;
+  }
 
 
   /* KLUGE: Inform the underlying program that we're in "standalone"
    */
 # ifndef USE_IPHONE
   if ([saverNames count] == 1) {
-    putenv (strdup ("XSCREENSAVER_STANDALONE=1"));
+    setenv ("XSCREENSAVER_STANDALONE", "1", 1);
   }
 # endif
 
-  return (ScreenSaverView *) instance;
+  return (XScreenSaverView *) instance;
 }
 
 
@@ -210,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
@@ -254,8 +662,14 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
 
   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.
@@ -341,49 +755,35 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
 }
 
 
-- (void) openPreferences: (id) sender
+- (void) openPreferences: (NSString *) saver
 {
-  NSString *saver = [listController selected];
-  if (! saver) return;
-
-  [self loadSaver:saver launch:NO];
+  XScreenSaverView *saverView = [self newSaverView:saver
+                                          withSize:CGSizeMake(0, 0)];
   if (! saverView) return;
 
-  [rootViewController pushViewController: [saverView configureView]
+  NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
+  [prefs setObject:saver forKey:@"selectedSaverName"];
+  [prefs synchronize];
+
+  [rotating_nav 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
+- (void)loadSaver:(NSString *)name
 {
-  // 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];
-      }
+    for (NSWindow *win in windows) {
+      ScreenSaverView *sv = find_saverView ([win contentView]);
+      if (![sv isAnimating])
+        [sv startAnimation];
+    }
     return;
   }
 
@@ -406,16 +806,15 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
     }
 
     NSSize size = [cv frame].size;
-    ScreenSaverView *new_view = [self makeSaverView:name withSize: size];
+    ScreenSaverView *new_view = [self newSaverView:name withSize: size];
     NSAssert (new_view, @"unable to make a saver view");
 
     [new_view setFrame: (old_view ? [old_view frame] : [cv frame])];
     [sup addSubview: new_view];
     [win makeFirstResponder:new_view];
     [new_view setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
-    [new_view retain];
-    if (launch)
-      [new_view startAnimation];
+    [new_view startAnimation];
+    [new_view release];
   }
 
   NSUserDefaultsController *ctl =
@@ -424,59 +823,128 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
 
 # else  // USE_IPHONE
 
-  if (saverName && [saverName isEqualToString: name]) {
-    if (launch && ![saverView isAnimating]) {
-      [window addSubview: saverView];
-      [saverView startAnimation];
-    }
-    return;
-  }
+#  if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
+  NSLog (@"selecting saver \"%@\"", name);
+#  endif
 
-  saverName = name;
+  NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
+  [prefs setObject:name forKey:@"selectedSaverName"];
+  [prefs synchronize];
 
-  if (saverView) {
+/* Cacheing this screws up rotation when starting a saver twice in a row.
+  if (saverName && [saverName isEqualToString: name]) {
     if ([saverView isAnimating])
-      [saverView stopAnimation];
-    [saverView removeFromSuperview];
+      return;
+    else
+      goto LAUNCH;
   }
+*/
 
-  NSSize size = [window frame].size;
-  saverView = [self makeSaverView:name withSize: size];
+  saverName = name;
 
-  if (! saverView) {
-    [[[UIAlertView alloc] initWithTitle: name
-                          message: @"Unable to load!"
-                          delegate: nil
-                          cancelButtonTitle: @"Bummer"
-                          otherButtonTitles: nil]
-     show];
+  if (nonrotating_controller) {
+    nonrotating_controller.saverName = name;
     return;
   }
 
-  [saverView setFrame: [window frame]];
-  [saverView retain];
-  [[NSNotificationCenter defaultCenter]
-    addObserver:saverView
-    selector:@selector(didRotate:)
-    name:UIDeviceOrientationDidChangeNotification object:nil];
+# 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];
 
-  if (launch) {
-    [window addSubview: saverView];
-    [saverView startAnimation];
-  }
-# endif // USE_IPHONE
-}
+  // 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;
 
-- (void)loadSaver:(NSString *)name
-{
-  [self loadSaver:name launch:YES];
+  /* 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
 {
-# ifndef USE_IPHONE
   NSDictionary *bd = [saverBundle infoDictionary];
   NSMutableDictionary *d = [NSMutableDictionary dictionaryWithCapacity:20];
 
@@ -485,36 +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];
-
-# 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
 }
 
+#endif // !USE_IPHONE
+
 
 
 - (void)selectedSaverDidChange:(NSDictionary *)change
@@ -550,28 +1000,32 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
     if ([[p pathExtension] caseInsensitiveCompare: ext]) 
       continue;
 
-# ifndef USE_IPHONE
     NSString *name = [[p lastPathComponent] stringByDeletingPathExtension];
-# else  // !USE_IPHONE
 
+# ifdef 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);
+    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
+# endif // USE_IPHONE
 
-    [result addObject: name];
+    NSAssert1 (name, @"no name in %@", p);
+    if (name) [result addObject: name];
   }
 
+  if (! [result count])
+    result = 0;
+
   return result;
 }
 
@@ -582,7 +1036,11 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
   NSMutableArray *dirs = [NSMutableArray arrayWithCapacity: 10];
 
 # ifndef USE_IPHONE
-  // On MacOS, look in the "Contents/PlugIns/" directory in the bundle.
+  // 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]];
 
   // Also look in the same directory as the executable.
@@ -590,14 +1048,16 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
                      stringByDeletingLastPathComponent]];
 
   // Finally, look in standard MacOS screensaver directories.
-  [dirs addObject: @"~/Library/Screen Savers"];
-  [dirs addObject: @"/Library/Screen Savers"];
-  [dirs addObject: @"/System/Library/Screen Savers"];
+//  [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.
+# else  // USE_IPHONE
+
+  // On iOS, only look in the bundle's root directory.
   [dirs addObject: [[NSBundle mainBundle] bundlePath]];
-# endif
+
+# endif // USE_IPHONE
 
   int i;
   for (i = 0; i < [dirs count]; i++) {
@@ -663,11 +1123,101 @@ 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.
  */
@@ -675,112 +1225,58 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
 {
   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;
-    }
+    [dict setObject:[self makeDesc:saver] forKey:saver];
+  }
+  return dict;
+}
 
-    // 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];
+- (void) wantsFadeOut:(XScreenSaverView *)sender
+{
+  rotating_nav.view.hidden = NO; // In case it was hidden during startup.
 
-    if (year)
-      desc = [year stringByAppendingString:
-                     [@": " stringByAppendingString: desc]];
+  /* 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
 
-  FAIL:
-    if (! desc) {
-      desc = @"Oops, this module appears to be incomplete.";
-      // NSLog(@"broken saver: %@", path);
+  /* 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;
+      }
     }
-
-    [dict setObject:desc forKey:saver];
   }
 
-  return dict;
+  [rotating_nav dismissViewControllerAnimated:YES completion:^() {
+    [nonrotating_controller release];
+    nonrotating_controller = nil;
+    [[rotating_nav view] becomeFirstResponder];
+  }];
 }
 
 
-- (UIViewController *) makeMenu
+- (void) didShake:(XScreenSaverView *)sender
 {
-  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;
+# if TARGET_IPHONE_SIMULATOR
+  NSLog (@"simulating shake on saver list");
+# endif
+  [[rotating_nav topViewController] motionEnded: UIEventSubtypeMotionShake
+                                      withEvent: nil];
 }
 
+
 #endif // USE_IPHONE
 
 
@@ -875,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];
@@ -908,16 +1406,28 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
                                    screen:screen];
   [win setMinSize:[win frameRectForContentRect:rect].size];
   [[win contentView] addSubview: (pbox ? (NSView *) pbox : (NSView *) sv)];
+  [pbox release];
 
   [win makeKeyAndOrderFront:win];
   
   [sv startAnimation]; // this is the dummy saver
+  [sv autorelease];
 
   count++;
 
   return win;
 }
 
+
+- (void) animTimer
+{
+  for (NSWindow *win in windows) {
+    ScreenSaverView *sv = find_saverView ([win contentView]);
+    if ([sv isAnimating])
+      [sv animateOneFrame];
+  }
+}
+
 # endif // !USE_IPHONE
 
 
@@ -930,6 +1440,8 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
 {
   [self listSaverBundleNames];
 
+  NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
+
 # ifndef USE_IPHONE
   int window_count = ([saverNames count] <= 1 ? 1 : 2);
   NSMutableArray *a = [[NSMutableArray arrayWithCapacity: window_count+1]
@@ -946,25 +1458,46 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
               [NSString stringWithFormat:@"XScreenSaverWindow%d", i]];
     [win setFrameUsingName:[win frameAutosaveName]];
     [a addObject: win];
+    // This prevents clicks from being seen by savers.
+    // [win setMovableByWindowBackground:YES];
+    [win release];
   }
 # else  // USE_IPHONE
 
 # undef ya_rand_init
   ya_rand_init (0);    // Now's a good time.
 
-  rootViewController = [[[RotateyViewController alloc] init] retain];
-  [window setRootViewController: rootViewController];
-  [rootViewController pushViewController:[self makeMenu]
-                      animated:YES];
 
-  [window makeKeyAndVisible];
+  /* 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)];
 
-  // Has to be after the list window is up, or we get black.
-  [self saveScreenshot];
+  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
 
@@ -989,10 +1522,9 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
   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;
 
@@ -1003,7 +1535,7 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
     prev = [saverNames objectAtIndex: (random() % [saverNames count])];
 
   if (prev)
-    [listController scrollTo: prev];
+    [menu scrollTo: prev];
 # endif // USE_IPHONE
 
   if (forced)
@@ -1012,19 +1544,53 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
 # ifdef USE_IPHONE
   /* Don't auto-launch the saver unless it was running last time.
      XScreenSaverView manages this, on crash_timer.
+     Unless forced.
    */
-  if (! [prefs boolForKey:@"wasRunning"])
+  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;