From http://www.jwz.org/xscreensaver/xscreensaver-5.23.tar.gz
[xscreensaver] / OSX / XScreenSaverView.m.orig
diff --git a/OSX/XScreenSaverView.m.orig b/OSX/XScreenSaverView.m.orig
new file mode 100644 (file)
index 0000000..ee8f3e6
--- /dev/null
@@ -0,0 +1,1804 @@
+/* xscreensaver, Copyright (c) 2006-2013 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
+ * the above copyright notice appear in all copies and that both that
+ * copyright notice and this permission notice appear in supporting
+ * documentation.  No representations are made about the suitability of this
+ * software for any purpose.  It is provided "as is" without express or 
+ * implied warranty.
+ */
+
+/* This is a subclass of Apple's ScreenSaverView that knows how to run
+   xscreensaver programs without X11 via the dark magic of the "jwxyz"
+   library.  In xscreensaver terminology, this is the replacement for
+   the "screenhack.c" module.
+ */
+
+#import <QuartzCore/QuartzCore.h>
+#import <zlib.h>
+#import "XScreenSaverView.h"
+#import "XScreenSaverConfigSheet.h"
+#import "screenhackI.h"
+#import "xlockmoreI.h"
+#import "jwxyz-timers.h"
+
+
+/* Garbage collection only exists if we are being compiled against the 
+   10.6 SDK or newer, not if we are building against the 10.4 SDK.
+ */
+#ifndef  MAC_OS_X_VERSION_10_6
+# define MAC_OS_X_VERSION_10_6 1060  /* undefined in 10.4 SDK, grr */
+#endif
+#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6  /* 10.6 SDK */
+# import <objc/objc-auto.h>
+# define DO_GC_HACKERY
+#endif
+
+extern struct xscreensaver_function_table *xscreensaver_function_table;
+
+/* Global variables used by the screen savers
+ */
+const char *progname;
+const char *progclass;
+int mono_p = 0;
+
+
+# ifdef USE_IPHONE
+
+extern NSDictionary *make_function_table_dict(void);  // ios-function-table.m
+
+/* Stub definition of the superclass, for iPhone.
+ */
+@implementation ScreenSaverView
+{
+  NSTimeInterval anim_interval;
+  Bool animating_p;
+  NSTimer *anim_timer;
+}
+
+- (id)initWithFrame:(NSRect)frame isPreview:(BOOL)isPreview {
+  self = [super initWithFrame:frame];
+  if (! self) return 0;
+  anim_interval = 1.0/30;
+  return self;
+}
+- (NSTimeInterval)animationTimeInterval { return anim_interval; }
+- (void)setAnimationTimeInterval:(NSTimeInterval)i { anim_interval = i; }
+- (BOOL)hasConfigureSheet { return NO; }
+- (NSWindow *)configureSheet { return nil; }
+- (NSView *)configureView { return nil; }
+- (BOOL)isPreview { return NO; }
+- (BOOL)isAnimating { return animating_p; }
+- (void)animateOneFrame { }
+
+- (void)startAnimation {
+  if (animating_p) return;
+  animating_p = YES;
+  anim_timer = [NSTimer scheduledTimerWithTimeInterval: anim_interval
+                        target:self
+                        selector:@selector(animateOneFrame)
+                        userInfo:nil
+                        repeats:YES];
+}
+
+- (void)stopAnimation {
+  if (anim_timer) {
+    [anim_timer invalidate];
+    anim_timer = 0;
+  }
+  animating_p = NO;
+}
+@end
+
+# endif // !USE_IPHONE
+
+
+
+@interface XScreenSaverView (Private)
+- (void) stopAndClose:(Bool)relaunch;
+@end
+
+@implementation XScreenSaverView
+
+// Given a lower-cased saver name, returns the function table for it.
+// If no name, guess the name from the class's bundle name.
+//
+- (struct xscreensaver_function_table *) findFunctionTable:(NSString *)name
+{
+  NSBundle *nsb = [NSBundle bundleForClass:[self class]];
+  NSAssert1 (nsb, @"no bundle for class %@", [self class]);
+
+  NSString *path = [nsb bundlePath];
+  CFURLRef url = CFURLCreateWithFileSystemPath(kCFAllocatorDefault,
+                                               (CFStringRef) path,
+                                               kCFURLPOSIXPathStyle,
+                                               true);
+  CFBundleRef cfb = CFBundleCreate (kCFAllocatorDefault, url);
+  CFRelease (url);
+  NSAssert1 (cfb, @"no CFBundle for \"%@\"", path);
+  // #### Analyze says "Potential leak of an object stored into cfb"
+  
+  if (! name)
+    name = [[path lastPathComponent] stringByDeletingPathExtension];
+
+  name = [[name lowercaseString]
+           stringByReplacingOccurrencesOfString:@" "
+           withString:@""];
+
+# ifndef USE_IPHONE
+  // CFBundleGetDataPointerForName doesn't work in "Archive" builds.
+  // I'm guessing that symbol-stripping is mandatory.  Fuck.
+  NSString *table_name = [name stringByAppendingString:
+                                 @"_xscreensaver_function_table"];
+  void *addr = CFBundleGetDataPointerForName (cfb, (CFStringRef) table_name);
+  CFRelease (cfb);
+
+  if (! addr)
+    NSLog (@"no symbol \"%@\" for \"%@\"", table_name, path);
+
+# else  // USE_IPHONE
+  // Remember: any time you add a new saver to the iOS app,
+  // manually run "make ios-function-table.m"!
+  if (! function_tables)
+    function_tables = [make_function_table_dict() retain];
+  NSValue *v = [function_tables objectForKey: name];
+  void *addr = v ? [v pointerValue] : 0;
+# endif // USE_IPHONE
+
+  return (struct xscreensaver_function_table *) addr;
+}
+
+
+// Add the "Contents/Resources/" subdirectory of this screen saver's .bundle
+// to $PATH for the benefit of savers that include helper shell scripts.
+//
+- (void) setShellPath
+{
+  NSBundle *nsb = [NSBundle bundleForClass:[self class]];
+  NSAssert1 (nsb, @"no bundle for class %@", [self class]);
+  
+  NSString *nsdir = [nsb resourcePath];
+  NSAssert1 (nsdir, @"no resourcePath for class %@", [self class]);
+  const char *dir = [nsdir 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 (dir) + 30);
+  strcpy (npath, "PATH=");
+  strcat (npath, dir);
+  strcat (npath, ":");
+  strcat (npath, opath);
+  if (putenv (npath)) {
+    perror ("putenv");
+    NSAssert1 (0, @"putenv \"%s\" failed", npath);
+  }
+
+  /* Don't free (npath) -- MacOS's putenv() does not copy it. */
+}
+
+
+// set an $XSCREENSAVER_CLASSPATH variable so that included shell scripts
+// (e.g., "xscreensaver-text") know how to look up resources.
+//
+- (void) setResourcesEnv:(NSString *) name
+{
+  NSBundle *nsb = [NSBundle bundleForClass:[self class]];
+  NSAssert1 (nsb, @"no bundle for class %@", [self class]);
+  
+  const char *s = [name cStringUsingEncoding:NSUTF8StringEncoding];
+  char *env = (char *) malloc (strlen (s) + 40);
+  strcpy (env, "XSCREENSAVER_CLASSPATH=");
+  strcat (env, s);
+  if (putenv (env)) {
+    perror ("putenv");
+    NSAssert1 (0, @"putenv \"%s\" failed", env);
+  }
+  /* Don't free (env) -- MacOS's putenv() does not copy it. */
+}
+
+
+static void
+add_default_options (const XrmOptionDescRec *opts,
+                     const char * const *defs,
+                     XrmOptionDescRec **opts_ret,
+                     const char ***defs_ret)
+{
+  /* These aren't "real" command-line options (there are no actual command-line
+     options in the Cocoa version); but this is the somewhat kludgey way that
+     the <xscreensaver-text /> and <xscreensaver-image /> tags in the
+     ../hacks/config/\*.xml files communicate with the preferences database.
+  */
+  static const XrmOptionDescRec default_options [] = {
+    { "-text-mode",              ".textMode",          XrmoptionSepArg, 0 },
+    { "-text-literal",           ".textLiteral",       XrmoptionSepArg, 0 },
+    { "-text-file",              ".textFile",          XrmoptionSepArg, 0 },
+    { "-text-url",               ".textURL",           XrmoptionSepArg, 0 },
+    { "-text-program",           ".textProgram",       XrmoptionSepArg, 0 },
+    { "-grab-desktop",           ".grabDesktopImages", XrmoptionNoArg, "True" },
+    { "-no-grab-desktop",        ".grabDesktopImages", XrmoptionNoArg, "False"},
+    { "-choose-random-images",   ".chooseRandomImages",XrmoptionNoArg, "True" },
+    { "-no-choose-random-images",".chooseRandomImages",XrmoptionNoArg, "False"},
+    { "-image-directory",        ".imageDirectory",    XrmoptionSepArg, 0 },
+    { "-fps",                    ".doFPS",             XrmoptionNoArg, "True" },
+    { "-no-fps",                 ".doFPS",             XrmoptionNoArg, "False"},
+    { "-foreground",             ".foreground",        XrmoptionSepArg, 0 },
+    { "-fg",                     ".foreground",        XrmoptionSepArg, 0 },
+    { "-background",             ".background",        XrmoptionSepArg, 0 },
+    { "-bg",                     ".background",        XrmoptionSepArg, 0 },
+    { 0, 0, 0, 0 }
+  };
+  static const char *default_defaults [] = {
+    ".doFPS:              False",
+    ".doubleBuffer:       True",
+    ".multiSample:        False",
+# ifndef USE_IPHONE
+    ".textMode:           date",
+# else
+    ".textMode:           url",
+# endif
+ // ".textLiteral:        ",
+ // ".textFile:           ",
+    ".textURL:            http://en.wikipedia.org/w/index.php?title=Special:NewPages&feed=rss",
+ // ".textProgram:        ",
+    ".grabDesktopImages:  yes",
+# ifndef USE_IPHONE
+    ".chooseRandomImages: no",
+# else
+    ".chooseRandomImages: yes",
+# endif
+    ".imageDirectory:     ~/Pictures",
+    ".relaunchDelay:      2",
+    0
+  };
+
+  int count = 0, i, j;
+  for (i = 0; default_options[i].option; i++)
+    count++;
+  for (i = 0; opts[i].option; i++)
+    count++;
+
+  XrmOptionDescRec *opts2 = (XrmOptionDescRec *) 
+    calloc (count + 1, sizeof (*opts2));
+
+  i = 0;
+  j = 0;
+  while (default_options[j].option) {
+    opts2[i] = default_options[j];
+    i++, j++;
+  }
+  j = 0;
+  while (opts[j].option) {
+    opts2[i] = opts[j];
+    i++, j++;
+  }
+
+  *opts_ret = opts2;
+
+
+  /* now the defaults
+   */
+  count = 0;
+  for (i = 0; default_defaults[i]; i++)
+    count++;
+  for (i = 0; defs[i]; i++)
+    count++;
+
+  const char **defs2 = (const char **) calloc (count + 1, sizeof (*defs2));
+
+  i = 0;
+  j = 0;
+  while (default_defaults[j]) {
+    defs2[i] = default_defaults[j];
+    i++, j++;
+  }
+  j = 0;
+  while (defs[j]) {
+    defs2[i] = defs[j];
+    i++, j++;
+  }
+
+  *defs_ret = defs2;
+}
+
+
+#ifdef USE_IPHONE
+/* Returns the current time in seconds as a double.
+ */
+static double
+double_time (void)
+{
+  struct timeval now;
+# ifdef GETTIMEOFDAY_TWO_ARGS
+  struct timezone tzp;
+  gettimeofday(&now, &tzp);
+# else
+  gettimeofday(&now);
+# endif
+
+  return (now.tv_sec + ((double) now.tv_usec * 0.000001));
+}
+#endif // USE_IPHONE
+
+
+- (id) initWithFrame:(NSRect)frame
+           saverName:(NSString *)saverName
+           isPreview:(BOOL)isPreview
+{
+# ifdef USE_IPHONE
+  initial_bounds = frame.size;
+  rot_current_size = frame.size;       // needs to be early, because
+  rot_from = rot_current_size;         // [self setFrame] is called by
+  rot_to = rot_current_size;           // [super initWithFrame].
+  rotation_ratio = -1;
+# endif
+
+  if (! (self = [super initWithFrame:frame isPreview:isPreview]))
+    return 0;
+  
+  xsft = [self findFunctionTable: saverName];
+  if (! xsft) {
+    [self release];
+    return 0;
+  }
+
+  [self setShellPath];
+
+# ifdef USE_IPHONE
+  [self setMultipleTouchEnabled:YES];
+  orientation = UIDeviceOrientationUnknown;
+  [self didRotate:nil];
+# endif // USE_IPHONE
+
+  setup_p = YES;
+  if (xsft->setup_cb)
+    xsft->setup_cb (xsft, xsft->setup_arg);
+
+  /* The plist files for these preferences show up in
+     $HOME/Library/Preferences/ByHost/ in a file named like
+     "org.jwz.xscreensaver.<SAVERNAME>.<NUMBERS>.plist"
+   */
+  NSString *name = [NSString stringWithCString:xsft->progclass
+                             encoding:NSISOLatin1StringEncoding];
+  name = [@"org.jwz.xscreensaver." stringByAppendingString:name];
+  [self setResourcesEnv:name];
+
+  
+  XrmOptionDescRec *opts = 0;
+  const char **defs = 0;
+  add_default_options (xsft->options, xsft->defaults, &opts, &defs);
+  prefsReader = [[PrefsReader alloc]
+                  initWithName:name xrmKeys:opts defaults:defs];
+  free (defs);
+  // free (opts);  // bah, we need these! #### leak!
+  xsft->options = opts;
+  
+  progname = progclass = xsft->progclass;
+
+  next_frame_time = 0;
+  
+# ifdef USE_BACKBUFFER
+  [self createBackbuffer];
+  [self initLayer];
+# endif
+
+# ifdef USE_IPHONE
+  // So we can tell when we're docked.
+  [UIDevice currentDevice].batteryMonitoringEnabled = YES;
+# endif // USE_IPHONE
+
+  return self;
+}
+
+- (void) initLayer
+{
+# ifndef USE_IPHONE
+  [self setLayer: [CALayer layer]];
+  self.layer.delegate = self;
+  self.layer.opaque = YES;
+  [self setWantsLayer: YES];
+# endif
+}
+
+
+- (id) initWithFrame:(NSRect)frame isPreview:(BOOL)p
+{
+  return [self initWithFrame:frame saverName:0 isPreview:p];
+}
+
+
+- (void) dealloc
+{
+  NSAssert(![self isAnimating], @"still animating");
+  NSAssert(!xdata, @"xdata not yet freed");
+  if (xdpy)
+    jwxyz_free_display (xdpy);
+
+# ifdef USE_BACKBUFFER
+  if (backbuffer)
+    CGContextRelease (backbuffer);
+# endif
+
+  [prefsReader release];
+
+  // xsft
+  // fpst
+
+  [super dealloc];
+}
+
+- (PrefsReader *) prefsReader
+{
+  return prefsReader;
+}
+
+
+#ifdef USE_IPHONE
+- (void) lockFocus { }
+- (void) unlockFocus { }
+#endif // USE_IPHONE
+
+
+
+# ifdef USE_IPHONE
+/* A few seconds after the saver launches, we store the "wasRunning"
+   preference.  This is so that if the saver is crashing at startup,
+   we don't launch it again next time, getting stuck in a crash loop.
+ */
+- (void) allSystemsGo: (NSTimer *) timer
+{
+  NSAssert (timer == crash_timer, @"crash timer screwed up");
+  crash_timer = 0;
+
+  NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
+  [prefs setBool:YES forKey:@"wasRunning"];
+  [prefs synchronize];
+}
+#endif // USE_IPHONE
+
+
+- (void) startAnimation
+{
+  NSAssert(![self isAnimating], @"already animating");
+  NSAssert(!initted_p && !xdata, @"already initialized");
+  [super startAnimation];
+  /* We can't draw on the window from this method, so we actually do the
+     initialization of the screen saver (xsft->init_cb) in the first call
+     to animateOneFrame() instead.
+   */
+
+# ifdef USE_IPHONE
+  if (crash_timer)
+    [crash_timer invalidate];
+
+  NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
+  [prefs removeObjectForKey:@"wasRunning"];
+  [prefs synchronize];
+
+  crash_timer = [NSTimer scheduledTimerWithTimeInterval: 5
+                         target:self
+                         selector:@selector(allSystemsGo:)
+                         userInfo:nil
+                         repeats:NO];
+
+# endif // USE_IPHONE
+
+  // Never automatically turn the screen off if we are docked,
+  // and an animation is running.
+  //
+# ifdef USE_IPHONE
+  [UIApplication sharedApplication].idleTimerDisabled =
+    ([UIDevice currentDevice].batteryState != UIDeviceBatteryStateUnplugged);
+# endif
+}
+
+
+- (void)stopAnimation
+{
+  NSAssert([self isAnimating], @"not animating");
+
+  if (initted_p) {
+
+    [self lockFocus];       // in case something tries to draw from here
+    [self prepareContext];
+
+    /* I considered just not even calling the free callback at all...
+       But webcollage-cocoa needs it, to kill the inferior webcollage
+       processes (since the screen saver framework never generates a
+       SIGPIPE for them...)  Instead, I turned off the free call in
+       xlockmore.c, which is where all of the bogus calls are anyway.
+     */
+    xsft->free_cb (xdpy, xwindow, xdata);
+    [self unlockFocus];
+
+//  setup_p = NO; // #### wait, do we need this?
+    initted_p = NO;
+    xdata = 0;
+  }
+
+# ifdef USE_IPHONE
+  if (crash_timer)
+    [crash_timer invalidate];
+  crash_timer = 0;
+  NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
+  [prefs removeObjectForKey:@"wasRunning"];
+  [prefs synchronize];
+# endif // USE_IPHONE
+
+  [super stopAnimation];
+
+  // When an animation is no longer running (e.g., looking at the list)
+  // then it's ok to power off the screen when docked.
+  //
+# ifdef USE_IPHONE
+  [UIApplication sharedApplication].idleTimerDisabled = NO;
+# endif
+}
+
+
+/* Hook for the XScreenSaverGLView subclass
+ */
+- (void) prepareContext
+{
+}
+
+/* Hook for the XScreenSaverGLView subclass
+ */
+- (void) resizeContext
+{
+}
+
+
+static void
+screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
+{
+  fps_compute (fpst, 0, -1);
+  fps_draw (fpst);
+}
+
+
+#ifdef USE_IPHONE
+
+/* On iPhones with Retina displays, we can draw the savers in "real"
+   pixels, and that works great.  The 320x480 "point" screen is really
+   a 640x960 *pixel* screen.  However, Retina iPads have 768x1024
+   point screens which are 1536x2048 pixels, and apparently that's
+   enough pixels that copying those bits to the screen is slow.  Like,
+   drops us from 15fps to 7fps.  So, on Retina iPads, we don't draw in
+   real pixels.  This will probably make the savers look better
+   anyway, since that's a higher resolution than most desktop monitors
+   have even today.  (This is only true for X11 programs, not GL 
+   programs.  Those are fine at full rez.)
+
+   This method is overridden in XScreenSaverGLView, since this kludge
+   isn't necessary for GL programs, being resolution independent by
+   nature.
+ */
+- (CGFloat) hackedContentScaleFactor
+{
+  GLfloat s = [self contentScaleFactor];
+  if (initial_bounds.width  >= 1024 ||
+      initial_bounds.height >= 1024)
+    s = 1;
+  return s;
+}
+
+
+static GLfloat _global_rot_current_angle_kludge;
+
+double current_device_rotation (void)
+{
+  return -_global_rot_current_angle_kludge;
+}
+
+
+- (void) hackRotation
+{
+  if (rotation_ratio >= 0) {   // in the midst of a rotation animation
+
+#   define CLAMP180(N) while (N < 0) N += 360; while (N > 180) N -= 360
+    GLfloat f = angle_from;
+    GLfloat t = angle_to;
+    CLAMP180(f);
+    CLAMP180(t);
+    GLfloat dist = -(t-f);
+    CLAMP180(dist);
+
+    // Intermediate angle.
+    rot_current_angle = f - rotation_ratio * dist;
+
+    // Intermediate frame size.
+    rot_current_size.width = rot_from.width + 
+      rotation_ratio * (rot_to.width - rot_from.width);
+    rot_current_size.height = rot_from.height + 
+      rotation_ratio * (rot_to.height - rot_from.height);
+
+    // Tick animation.  Complete rotation in 1/6th sec.
+    double now = double_time();
+    double duration = 1/6.0;
+    rotation_ratio = 1 - ((rot_start_time + duration - now) / duration);
+
+    if (rotation_ratio > 1) {  // Done animating.
+      orientation = new_orientation;
+      rot_current_angle = angle_to;
+      rot_current_size = rot_to;
+      rotation_ratio = -1;
+
+      // Check orientation again in case we rotated again while rotating:
+      // this is a no-op if nothing has changed.
+      [self didRotate:nil];
+    }
+  } else {                     // Not animating a rotation.
+    rot_current_angle = angle_to;
+    rot_current_size = rot_to;
+  }
+
+  CLAMP180(rot_current_angle);
+  _global_rot_current_angle_kludge = rot_current_angle;
+
+#   undef CLAMP180
+
+  double s = [self hackedContentScaleFactor];
+  if (!ignore_rotation_p &&
+      /* rotation_ratio && */
+      ((int) backbuffer_size.width  != (int) (s * rot_current_size.width) ||
+       (int) backbuffer_size.height != (int) (s * rot_current_size.height)))
+    [self resize_x11];
+}
+
+
+- (void)alertView:(UIAlertView *)av clickedButtonAtIndex:(NSInteger)i
+{
+  if (i == 0) exit (-1);       // Cancel
+  [self stopAndClose:NO];      // Keep going
+}
+
+- (void) handleException: (NSException *)e
+{
+  NSLog (@"Caught exception: %@", e);
+  [[[UIAlertView alloc] initWithTitle:
+                          [NSString stringWithFormat: @"%s crashed!",
+                                    xsft->progclass]
+                        message:
+                          [NSString stringWithFormat:
+                                      @"The error message was:"
+                                    "\n\n%@\n\n"
+                                    "If it keeps crashing, try "
+                                    "resetting its options.",
+                                    e]
+                        delegate: self
+                        cancelButtonTitle: @"Exit"
+                        otherButtonTitles: @"Keep going", nil]
+    show];
+  [self stopAnimation];
+}
+
+#endif // USE_IPHONE
+
+
+#ifdef USE_BACKBUFFER
+
+/* Create a bitmap context into which we render everything.
+   If the desired size has changed, re-created it.
+ */
+- (void) createBackbuffer
+{
+# ifdef USE_IPHONE
+  double s = [self hackedContentScaleFactor];
+  CGSize rotsize = ignore_rotation_p ? initial_bounds : rot_current_size;
+  int new_w = s * rotsize.width;
+  int new_h = s * rotsize.height;
+# else
+  int new_w = [self bounds].size.width;
+  int new_h = [self bounds].size.height;
+# endif
+
+  if (backbuffer &&
+      backbuffer_size.width  == new_w &&
+      backbuffer_size.height == new_h)
+    return;
+
+  CGSize osize = backbuffer_size;
+  CGContextRef ob = backbuffer;
+
+  backbuffer_size.width  = new_w;
+  backbuffer_size.height = new_h;
+
+  CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB();
+  backbuffer = CGBitmapContextCreate (NULL,
+                                      backbuffer_size.width,
+                                      backbuffer_size.height,
+                                      8, 
+                                      backbuffer_size.width * 4,
+                                      cs,
+                                      // kCGImageAlphaPremultipliedLast
+                                      (kCGImageAlphaNoneSkipFirst |
+                                       kCGBitmapByteOrder32Host)
+                                      );
+  CGColorSpaceRelease (cs);
+  NSAssert (backbuffer, @"unable to allocate back buffer");
+
+  // Clear it.
+  CGRect r;
+  r.origin.x = r.origin.y = 0;
+  r.size = backbuffer_size;
+  CGContextSetGrayFillColor (backbuffer, 0, 1);
+  CGContextFillRect (backbuffer, r);
+
+  if (ob) {
+    // Restore old bits, as much as possible, to the X11 upper left origin.
+    CGRect rect;
+    rect.origin.x = 0;
+    rect.origin.y = (backbuffer_size.height - osize.height);
+    rect.size  = osize;
+    CGImageRef img = CGBitmapContextCreateImage (ob);
+    CGContextDrawImage (backbuffer, rect, img);
+    CGImageRelease (img);
+    CGContextRelease (ob);
+  }
+}
+
+#endif // USE_BACKBUFFER
+
+
+/* Inform X11 that the size of our window has changed.
+ */
+- (void) resize_x11
+{
+  if (!xwindow) return;  // early
+
+# ifdef USE_BACKBUFFER
+  [self createBackbuffer];
+  jwxyz_window_resized (xdpy, xwindow,
+                        0, 0,
+                        backbuffer_size.width, backbuffer_size.height,
+                        backbuffer);
+# else   // !USE_BACKBUFFER
+  NSRect r = [self frame];             // ignoring rotation is closer
+  r.size = [self bounds].size;         // to what XGetGeometry expects.
+  jwxyz_window_resized (xdpy, xwindow,
+                        r.origin.x, r.origin.y,
+                        r.size.width, r.size.height,
+                        0);
+# endif  // !USE_BACKBUFFER
+
+  // Next time render_x11 is called, run the saver's reshape_cb.
+  resized_p = YES;
+}
+
+
+- (void) render_x11
+{
+# ifdef USE_IPHONE
+  @try {
+
+  if (orientation == UIDeviceOrientationUnknown)
+    [self didRotate:nil];
+  [self hackRotation];
+# endif
+
+  if (!initted_p) {
+
+    if (! xdpy) {
+# ifdef USE_BACKBUFFER
+      NSAssert (backbuffer, @"no back buffer");
+      xdpy = jwxyz_make_display (self, backbuffer);
+# else
+      xdpy = jwxyz_make_display (self, 0);
+# endif
+      xwindow = XRootWindow (xdpy, 0);
+
+# ifdef USE_IPHONE
+      /* Some X11 hacks (fluidballs) want to ignore all rotation events. */
+      ignore_rotation_p =
+        get_boolean_resource (xdpy, "ignoreRotation", "IgnoreRotation");
+# endif // USE_IPHONE
+
+      [self resize_x11];
+    }
+
+    if (!setup_p) {
+      setup_p = YES;
+      if (xsft->setup_cb)
+        xsft->setup_cb (xsft, xsft->setup_arg);
+    }
+    initted_p = YES;
+    resized_p = NO;
+    NSAssert(!xdata, @"xdata already initialized");
+
+
+# undef ya_rand_init
+    ya_rand_init (0);
+    
+    XSetWindowBackground (xdpy, xwindow,
+                          get_pixel_resource (xdpy, 0,
+                                              "background", "Background"));
+    XClearWindow (xdpy, xwindow);
+    
+# ifndef USE_IPHONE
+    [[self window] setAcceptsMouseMovedEvents:YES];
+# endif
+
+    /* In MacOS 10.5, this enables "QuartzGL", meaning that the Quartz
+       drawing primitives will run on the GPU instead of the CPU.
+       It seems like it might make things worse rather than better,
+       though...  Plus it makes us binary-incompatible with 10.4.
+
+# if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5
+    [[self window] setPreferredBackingLocation:
+                     NSWindowBackingLocationVideoMemory];
+# endif
+     */
+
+    /* Kludge: even though the init_cb functions are declared to take 2 args,
+      actually call them with 3, for the benefit of xlockmore_init() and
+      xlockmore_setup().
+      */
+    void *(*init_cb) (Display *, Window, void *) = 
+      (void *(*) (Display *, Window, void *)) xsft->init_cb;
+    
+    xdata = init_cb (xdpy, xwindow, xsft->setup_arg);
+
+    if (get_boolean_resource (xdpy, "doFPS", "DoFPS")) {
+      fpst = fps_init (xdpy, xwindow);
+      if (! xsft->fps_cb) xsft->fps_cb = screenhack_do_fps;
+    }
+  }
+
+
+  /* I don't understand why we have to do this *every frame*, but we do,
+     or else the cursor comes back on.
+   */
+# ifndef USE_IPHONE
+  if (![self isPreview])
+    [NSCursor setHiddenUntilMouseMoves:YES];
+# endif
+
+
+  if (fpst)
+    {
+      /* This is just a guess, but the -fps code wants to know how long
+         we were sleeping between frames.
+       */
+      long usecs = 1000000 * [self animationTimeInterval];
+      usecs -= 200;  // caller apparently sleeps for slightly less sometimes...
+      if (usecs < 0) usecs = 0;
+      fps_slept (fpst, usecs);
+    }
+
+
+  /* It turns out that [ScreenSaverView setAnimationTimeInterval] does nothing.
+     This is bad, because some of the screen hacks want to delay for long 
+     periods (like 5 seconds or a minute!) between frames, and running them
+     all at 60 FPS is no good.
+  
+     So, we don't use setAnimationTimeInterval, and just let the framework call
+     us whenever.  But, we only invoke the screen hack's "draw frame" method
+     when enough time has expired.
+  
+     This means two extra calls to gettimeofday() per frame.  For fast-cycling
+     screen savers, that might actually slow them down.  Oh well.
+
+     #### Also, we do not run the draw callback faster than the system's
+          animationTimeInterval, so if any savers are pickier about timing
+          than that, this may slow them down too much.  If that's a problem,
+          then we could call draw_cb in a loop here (with usleep) until the
+          next call would put us past animationTimeInterval...  But a better
+          approach would probably be to just change the saver to not do that.
+   */
+  struct timeval tv;
+  gettimeofday (&tv, 0);
+  double now = tv.tv_sec + (tv.tv_usec / 1000000.0);
+  if (now < next_frame_time) return;
+  
+  [self prepareContext];
+
+  if (resized_p) {
+    // We do this here instead of in setFrame so that all the
+    // Xlib drawing takes place under the animation timer.
+    [self resizeContext];
+    NSRect r;
+# ifndef USE_BACKBUFFER
+    r = [self bounds];
+# else  // USE_BACKBUFFER
+    r.origin.x = 0;
+    r.origin.y = 0;
+    r.size.width  = backbuffer_size.width;
+    r.size.height = backbuffer_size.height;
+# endif // USE_BACKBUFFER
+
+    xsft->reshape_cb (xdpy, xwindow, xdata, r.size.width, r.size.height);
+    resized_p = NO;
+  }
+
+  // Run any XtAppAddInput callbacks now.
+  // (Note that XtAppAddTimeOut callbacks have already been run by
+  // the Cocoa event loop.)
+  //
+  jwxyz_sources_run (display_sources_data (xdpy));
+
+
+  // And finally:
+  //
+# ifndef USE_IPHONE
+  NSDisableScreenUpdates();
+# endif
+  unsigned long delay = xsft->draw_cb (xdpy, xwindow, xdata);
+  if (fpst) xsft->fps_cb (xdpy, xwindow, fpst, xdata);
+# ifndef USE_IPHONE
+  NSEnableScreenUpdates();
+# endif
+
+  gettimeofday (&tv, 0);
+  now = tv.tv_sec + (tv.tv_usec / 1000000.0);
+  next_frame_time = now + (delay / 1000000.0);
+
+# ifdef USE_IPHONE     // Allow savers on the iPhone to run full-tilt.
+  if (delay < [self animationTimeInterval])
+    [self setAnimationTimeInterval:(delay / 1000000.0)];
+# endif
+
+# ifdef DO_GC_HACKERY
+  /* Current theory is that the 10.6 garbage collector sucks in the
+     following way:
+
+     It only does a collection when a threshold of outstanding
+     collectable allocations has been surpassed.  However, CoreGraphics
+     creates lots of small collectable allocations that contain pointers
+     to very large non-collectable allocations: a small CG object that's
+     collectable referencing large malloc'd allocations (non-collectable)
+     containing bitmap data.  So the large allocation doesn't get freed
+     until GC collects the small allocation, which triggers its finalizer
+     to run which frees the large allocation.  So GC is deciding that it
+     doesn't really need to run, even though the process has gotten
+     enormous.  GC eventually runs once pageouts have happened, but by
+     then it's too late, and the machine's resident set has been
+     sodomized.
+
+     So, we force an exhaustive garbage collection in this process
+     approximately every 5 seconds whether the system thinks it needs 
+     one or not.
+  */
+  {
+    static int tick = 0;
+    if (++tick > 5*30) {
+      tick = 0;
+      objc_collect (OBJC_EXHAUSTIVE_COLLECTION);
+    }
+  }
+# endif // DO_GC_HACKERY
+
+# ifdef USE_IPHONE
+  }
+  @catch (NSException *e) {
+    [self handleException: e];
+  }
+# endif // USE_IPHONE
+}
+
+
+/* drawRect always does nothing, and animateOneFrame renders bits to the
+   screen.  This is (now) true of both X11 and GL on both MacOS and iOS.
+ */
+
+- (void)drawRect:(NSRect)rect
+{
+  if (xwindow)    // clear to the X window's bg color, not necessarily black.
+    XClearWindow (xdpy, xwindow);
+  else
+    [super drawRect:rect];    // early: black.
+}
+
+
+#ifndef USE_BACKBUFFER
+
+- (void) animateOneFrame
+{
+  [self render_x11];
+}
+
+#else  // USE_BACKBUFFER
+
+- (void) animateOneFrame
+{
+  // Render X11 into the backing store bitmap...
+
+  NSAssert (backbuffer, @"no back buffer");
+
+# ifdef USE_IPHONE
+  UIGraphicsPushContext (backbuffer);
+# endif
+
+  [self render_x11];
+
+# ifdef USE_IPHONE
+  UIGraphicsPopContext();
+# endif
+
+# ifdef USE_IPHONE
+  // Then compute the transformations for rotation.
+  double hs = [self hackedContentScaleFactor];
+  double s = [self contentScaleFactor];
+
+  // The rotation origin for layer.affineTransform is in the center already.
+  CGAffineTransform t = ignore_rotation_p ?
+    CGAffineTransformIdentity :
+    CGAffineTransformMakeRotation (rot_current_angle / (180.0 / M_PI));
+
+  CGFloat f = s / hs;
+  self.layer.affineTransform = CGAffineTransformScale(t, f, f);
+
+  CGRect bounds;
+  bounds.origin.x = 0;
+  bounds.origin.y = 0;
+  bounds.size.width = backbuffer_size.width / s;
+  bounds.size.height = backbuffer_size.height / s;
+  self.layer.bounds = bounds;
+# endif // USE_IPHONE
+  [self.layer setNeedsDisplay];
+}
+
+- (void) drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
+{
+  // This "isn't safe" if NULL is passed to CGBitmapCreateContext before iOS 4.
+  char *dest_data = (char *)CGBitmapContextGetData (ctx);
+
+  // The CGContext here is normally upside-down on iOS.
+  if (dest_data &&
+      CGBitmapContextGetBitmapInfo (ctx) ==
+        (kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Host)
+#ifdef USE_IPHONE
+      && CGContextGetCTM (ctx).d < 0
+#endif
+      )
+  {
+    size_t dest_height = CGBitmapContextGetHeight (ctx);
+    size_t dest_bpr = CGBitmapContextGetBytesPerRow (ctx);
+    size_t src_height = CGBitmapContextGetHeight (backbuffer);
+    size_t src_bpr = CGBitmapContextGetBytesPerRow (backbuffer);
+    char *src_data = (char *)CGBitmapContextGetData (backbuffer);
+
+    size_t height = src_height < dest_height ? src_height : dest_height;
+    
+    if (src_bpr == dest_bpr) {
+      // iPad 1: 4.0 ms, iPad 2: 6.7 ms
+      memcpy (dest_data, src_data, src_bpr * height);
+    } else {
+      // iPad 1: 4.6 ms, iPad 2: 7.2 ms
+      size_t bpr = src_bpr < dest_bpr ? src_bpr : dest_bpr;
+      while (height) {
+        memcpy (dest_data, src_data, bpr);
+        --height;
+        src_data += src_bpr;
+        dest_data += dest_bpr;
+      }
+    }
+  } else {
+
+    // iPad 1: 9.6 ms, iPad 2: 12.1 ms
+
+#ifdef USE_IPHONE
+    CGContextScaleCTM (ctx, 1, -1);
+    CGFloat s = [self contentScaleFactor];
+    CGFloat hs = [self hackedContentScaleFactor];
+    CGContextTranslateCTM (ctx, 0, -backbuffer_size.height * hs / s);
+#endif
+    
+    CGImageRef img = CGBitmapContextCreateImage (backbuffer);
+    CGContextDrawImage (ctx, self.layer.bounds, img);
+    CGImageRelease (img);
+  }
+}
+
+#endif // !USE_BACKBUFFER
+
+
+
+- (void) setFrame:(NSRect) newRect
+{
+  [super setFrame:newRect];
+
+  if (xwindow)     // inform Xlib that the window has changed now.
+    [self resize_x11];
+}
+
+
+# ifndef USE_IPHONE  // Doesn't exist on iOS
+- (void) setFrameSize:(NSSize) newSize
+{
+  [super setFrameSize:newSize];
+  if (xwindow)
+    [self resize_x11];
+}
+# endif // !USE_IPHONE
+
+
++(BOOL) performGammaFade
+{
+  return YES;
+}
+
+- (BOOL) hasConfigureSheet
+{
+  return YES;
+}
+
++ (NSString *) decompressXML: (NSData *)data
+{
+  if (! data) return 0;
+  BOOL compressed_p = !!strncmp ((const char *) data.bytes, "<?xml", 5);
+
+  // If it's not already XML, decompress it.
+  NSAssert (compressed_p, @"xml isn't compressed");
+  if (compressed_p) {
+    NSMutableData *data2 = 0;
+    int ret = -1;
+    z_stream zs;
+    memset (&zs, 0, sizeof(zs));
+    ret = inflateInit2 (&zs, 16 + MAX_WBITS);
+    if (ret == Z_OK) {
+      UInt32 usize = * (UInt32 *) (data.bytes + data.length - 4);
+      data2 = [NSMutableData dataWithLength: usize];
+      zs.next_in   = (Bytef *) data.bytes;
+      zs.avail_in  = data.length;
+      zs.next_out  = (Bytef *) data2.bytes;
+      zs.avail_out = data2.length;
+      ret = inflate (&zs, Z_FINISH);
+      inflateEnd (&zs);
+    }
+    if (ret == Z_OK || ret == Z_STREAM_END)
+      data = data2;
+    else
+      NSAssert2 (0, @"gunzip error: %d: %s",
+                 ret, (zs.msg ? zs.msg : "<null>"));
+  }
+
+  return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
+}
+
+
+#ifndef USE_IPHONE
+- (NSWindow *) configureSheet
+#else
+- (UIViewController *) configureView
+#endif
+{
+  NSBundle *bundle = [NSBundle bundleForClass:[self class]];
+  NSString *file = [NSString stringWithCString:xsft->progclass
+                                      encoding:NSISOLatin1StringEncoding];
+  file = [file lowercaseString];
+  NSString *path = [bundle pathForResource:file ofType:@"xml"];
+  if (!path) {
+    NSLog (@"%@.xml does not exist in the application bundle: %@/",
+           file, [bundle resourcePath]);
+    return nil;
+  }
+  
+# ifdef USE_IPHONE
+  UIViewController *sheet;
+# else  // !USE_IPHONE
+  NSWindow *sheet;
+# endif // !USE_IPHONE
+
+  NSData *xmld = [NSData dataWithContentsOfFile:path];
+  NSString *xml = [[self class] decompressXML: xmld];
+  sheet = [[XScreenSaverConfigSheet alloc]
+            initWithXML:[xml dataUsingEncoding:NSUTF8StringEncoding]
+                options:xsft->options
+             controller:[prefsReader userDefaultsController]
+               defaults:[prefsReader defaultOptions]];
+
+  // #### am I expected to retain this, or not? wtf.
+  //      I thought not, but if I don't do this, we (sometimes) crash.
+  // #### Analyze says "potential leak of an object stored into sheet"
+  [sheet retain];
+
+  return sheet;
+}
+
+
+- (NSUserDefaultsController *) userDefaultsController
+{
+  return [prefsReader userDefaultsController];
+}
+
+
+/* Announce our willingness to accept keyboard input.
+*/
+- (BOOL)acceptsFirstResponder
+{
+  return YES;
+}
+
+
+#ifndef USE_IPHONE
+
+/* Convert an NSEvent into an XEvent, and pass it along.
+   Returns YES if it was handled.
+ */
+- (BOOL) doEvent: (NSEvent *) e
+            type: (int) type
+{
+  if (![self isPreview] ||     // no event handling if actually screen-saving!
+      ![self isAnimating] ||
+      !initted_p)
+    return NO;
+
+  XEvent xe;
+  memset (&xe, 0, sizeof(xe));
+  
+  int state = 0;
+  
+  int flags = [e modifierFlags];
+  if (flags & NSAlphaShiftKeyMask) state |= LockMask;
+  if (flags & NSShiftKeyMask)      state |= ShiftMask;
+  if (flags & NSControlKeyMask)    state |= ControlMask;
+  if (flags & NSAlternateKeyMask)  state |= Mod1Mask;
+  if (flags & NSCommandKeyMask)    state |= Mod2Mask;
+  
+  NSPoint p = [[[e window] contentView] convertPoint:[e locationInWindow]
+                                            toView:self];
+# ifdef USE_IPHONE
+  double s = [self hackedContentScaleFactor];
+# else
+  int s = 1;
+# endif
+  int x = s * p.x;
+  int y = s * ([self bounds].size.height - p.y);
+
+  xe.xany.type = type;
+  switch (type) {
+    case ButtonPress:
+    case ButtonRelease:
+      xe.xbutton.x = x;
+      xe.xbutton.y = y;
+      xe.xbutton.state = state;
+      if ([e type] == NSScrollWheel)
+        xe.xbutton.button = ([e deltaY] > 0 ? Button4 :
+                             [e deltaY] < 0 ? Button5 :
+                             [e deltaX] > 0 ? Button6 :
+                             [e deltaX] < 0 ? Button7 :
+                             0);
+      else
+        xe.xbutton.button = [e buttonNumber] + 1;
+      break;
+    case MotionNotify:
+      xe.xmotion.x = x;
+      xe.xmotion.y = y;
+      xe.xmotion.state = state;
+      break;
+    case KeyPress:
+    case KeyRelease:
+      {
+        NSString *ns = (([e type] == NSFlagsChanged) ? 0 :
+                        [e charactersIgnoringModifiers]);
+        KeySym k = 0;
+
+        if (!ns || [ns length] == 0)                   // dead key
+          {
+            // Cocoa hides the difference between left and right keys.
+            // Also we only get KeyPress events for these, no KeyRelease
+            // (unless we hack the mod state manually.  Bleh.)
+            //
+            if      (flags & NSAlphaShiftKeyMask)   k = XK_Caps_Lock;
+            else if (flags & NSShiftKeyMask)        k = XK_Shift_L;
+            else if (flags & NSControlKeyMask)      k = XK_Control_L;
+            else if (flags & NSAlternateKeyMask)    k = XK_Alt_L;
+            else if (flags & NSCommandKeyMask)      k = XK_Meta_L;
+          }
+        else if ([ns length] == 1)                     // real key
+          {
+            switch ([ns characterAtIndex:0]) {
+            case NSLeftArrowFunctionKey:  k = XK_Left;      break;
+            case NSRightArrowFunctionKey: k = XK_Right;     break;
+            case NSUpArrowFunctionKey:    k = XK_Up;        break;
+            case NSDownArrowFunctionKey:  k = XK_Down;      break;
+            case NSPageUpFunctionKey:     k = XK_Page_Up;   break;
+            case NSPageDownFunctionKey:   k = XK_Page_Down; break;
+            case NSHomeFunctionKey:       k = XK_Home;      break;
+            case NSPrevFunctionKey:       k = XK_Prior;     break;
+            case NSNextFunctionKey:       k = XK_Next;      break;
+            case NSBeginFunctionKey:      k = XK_Begin;     break;
+            case NSEndFunctionKey:        k = XK_End;       break;
+            default:
+              {
+                const char *s =
+                  [ns cStringUsingEncoding:NSISOLatin1StringEncoding];
+                k = (s && *s ? *s : 0);
+              }
+              break;
+            }
+          }
+
+        if (! k) return YES;   // E.g., "KeyRelease XK_Shift_L"
+
+        xe.xkey.keycode = k;
+        xe.xkey.state = state;
+        break;
+      }
+    default:
+      NSAssert1 (0, @"unknown X11 event type: %d", type);
+      break;
+  }
+
+  [self lockFocus];
+  [self prepareContext];
+  BOOL result = xsft->event_cb (xdpy, xwindow, xdata, &xe);
+  [self unlockFocus];
+  return result;
+}
+
+
+- (void) mouseDown: (NSEvent *) e
+{
+  if (! [self doEvent:e type:ButtonPress])
+    [super mouseDown:e];
+}
+
+- (void) mouseUp: (NSEvent *) e
+{
+  if (! [self doEvent:e type:ButtonRelease])
+    [super mouseUp:e];
+}
+
+- (void) otherMouseDown: (NSEvent *) e
+{
+  if (! [self doEvent:e type:ButtonPress])
+    [super otherMouseDown:e];
+}
+
+- (void) otherMouseUp: (NSEvent *) e
+{
+  if (! [self doEvent:e type:ButtonRelease])
+    [super otherMouseUp:e];
+}
+
+- (void) mouseMoved: (NSEvent *) e
+{
+  if (! [self doEvent:e type:MotionNotify])
+    [super mouseMoved:e];
+}
+
+- (void) mouseDragged: (NSEvent *) e
+{
+  if (! [self doEvent:e type:MotionNotify])
+    [super mouseDragged:e];
+}
+
+- (void) otherMouseDragged: (NSEvent *) e
+{
+  if (! [self doEvent:e type:MotionNotify])
+    [super otherMouseDragged:e];
+}
+
+- (void) scrollWheel: (NSEvent *) e
+{
+  if (! [self doEvent:e type:ButtonPress])
+    [super scrollWheel:e];
+}
+
+- (void) keyDown: (NSEvent *) e
+{
+  if (! [self doEvent:e type:KeyPress])
+    [super keyDown:e];
+}
+
+- (void) keyUp: (NSEvent *) e
+{
+  if (! [self doEvent:e type:KeyRelease])
+    [super keyUp:e];
+}
+
+- (void) flagsChanged: (NSEvent *) e
+{
+  if (! [self doEvent:e type:KeyPress])
+    [super flagsChanged:e];
+}
+
+#else  // USE_IPHONE
+
+
+- (void) stopAndClose:(Bool)relaunch_p
+{
+  if ([self isAnimating])
+    [self stopAnimation];
+
+  /* Need to make the SaverListController be the firstResponder again
+     so that it can continue to receive its own shake events.  I
+     suppose that this abstraction-breakage means that I'm adding
+     XScreenSaverView to the UINavigationController wrong...
+   */
+  UIViewController *v = [[self window] rootViewController];
+  if ([v isKindOfClass: [UINavigationController class]]) {
+    UINavigationController *n = (UINavigationController *) v;
+    [[n topViewController] becomeFirstResponder];
+  }
+
+  UIView *fader = [self superview];  // the "backgroundView" view is our parent
+
+  if (relaunch_p) {   // Fake a shake on the SaverListController.
+    // Why is [self window] sometimes null here?
+    UIWindow *w = [[UIApplication sharedApplication] keyWindow];
+    UIViewController *v = [w rootViewController];
+    if ([v isKindOfClass: [UINavigationController class]]) {
+      UINavigationController *n = (UINavigationController *) v;
+      [[n topViewController] motionEnded: UIEventSubtypeMotionShake
+                               withEvent: nil];
+    }
+  } else {     // Not launching another, animate our return to the list.
+    [UIView animateWithDuration: 0.5
+            animations:^{ fader.alpha = 0.0; }
+            completion:^(BOOL finished) {
+               [fader removeFromSuperview];
+               fader.alpha = 1.0;
+            }];
+  }
+}
+
+
+/* Called after the device's orientation has changed.
+
+   Note: we could include a subclass of UIViewController which
+   contains a shouldAutorotateToInterfaceOrientation method that
+   returns YES, in which case Core Animation would auto-rotate our
+   View for us in response to rotation events... but, that interacts
+   badly with the EAGLContext -- if you introduce Core Animation into
+   the path, the OpenGL pipeline probably falls back on software
+   rendering and performance goes to hell.  Also, the scaling and
+   rotation that Core Animation does interacts incorrectly with the GL
+   context anyway.
+
+   So, we have to hack the rotation animation manually, in the GL world.
+
+   Possibly XScreenSaverView should use Core Animation, and 
+   XScreenSaverGLView should override that.
+*/
+- (void)didRotate:(NSNotification *)notification
+{
+  UIDeviceOrientation current = [[UIDevice currentDevice] orientation];
+
+  /* If the simulator starts up in the rotated position, sometimes
+     the UIDevice says we're in Portrait when we're not -- but it
+     turns out that the UINavigationController knows what's up!
+     So get it from there.
+   */
+  if (current == UIDeviceOrientationUnknown) {
+    switch ([[[self window] rootViewController] interfaceOrientation]) {
+    case UIInterfaceOrientationPortrait:
+      current = UIDeviceOrientationPortrait;
+      break;
+    case UIInterfaceOrientationPortraitUpsideDown:
+      current = UIDeviceOrientationPortraitUpsideDown;
+      break;
+    case UIInterfaceOrientationLandscapeLeft:          // It's opposite day
+      current = UIDeviceOrientationLandscapeRight;
+      break;
+    case UIInterfaceOrientationLandscapeRight:
+      current = UIDeviceOrientationLandscapeLeft;
+      break;
+    default:
+      break;
+    }
+  }
+
+  /* On the iPad (but not iPhone 3GS, or the simulator) sometimes we get
+     an orientation change event with an unknown orientation.  Those seem
+     to always be immediately followed by another orientation change with
+     a *real* orientation change, so let's try just ignoring those bogus
+     ones and hoping that the real one comes in shortly...
+   */
+  if (current == UIDeviceOrientationUnknown)
+    return;
+
+  if (rotation_ratio >= 0) return;     // in the midst of rotation animation
+  if (orientation == current) return;  // no change
+
+  // When transitioning to FaceUp or FaceDown, pretend there was no change.
+  if (current == UIDeviceOrientationFaceUp ||
+      current == UIDeviceOrientationFaceDown)
+    return;
+
+  new_orientation = current;           // current animation target
+  rotation_ratio = 0;                  // start animating
+  rot_start_time = double_time();
+
+  switch (orientation) {
+  case UIDeviceOrientationLandscapeLeft:      angle_from = 90;  break;
+  case UIDeviceOrientationLandscapeRight:     angle_from = 270; break;
+  case UIDeviceOrientationPortraitUpsideDown: angle_from = 180; break;
+  default:                                    angle_from = 0;   break;
+  }
+
+  switch (new_orientation) {
+  case UIDeviceOrientationLandscapeLeft:      angle_to = 90;  break;
+  case UIDeviceOrientationLandscapeRight:     angle_to = 270; break;
+  case UIDeviceOrientationPortraitUpsideDown: angle_to = 180; break;
+  default:                                    angle_to = 0;   break;
+  }
+
+  switch (orientation) {
+  case UIDeviceOrientationLandscapeRight:      // from landscape
+  case UIDeviceOrientationLandscapeLeft:
+    rot_from.width  = initial_bounds.height;
+    rot_from.height = initial_bounds.width;
+    break;
+  default:                                     // from portrait
+    rot_from.width  = initial_bounds.width;
+    rot_from.height = initial_bounds.height;
+    break;
+  }
+
+  switch (new_orientation) {
+  case UIDeviceOrientationLandscapeRight:      // to landscape
+  case UIDeviceOrientationLandscapeLeft:
+    rot_to.width  = initial_bounds.height;
+    rot_to.height = initial_bounds.width;
+    break;
+  default:                                     // to portrait
+    rot_to.width  = initial_bounds.width;
+    rot_to.height = initial_bounds.height;
+    break;
+  }
+
+ if (! initted_p) {
+   // If we've done a rotation but the saver hasn't been initialized yet,
+   // don't bother going through an X11 resize, but just do it now.
+   rot_start_time = 0;  // dawn of time
+   [self hackRotation];
+ }
+}
+
+
+/* I believe we can't use UIGestureRecognizer for tracking touches
+   because UIPanGestureRecognizer doesn't give us enough detail in its
+   callbacks.
+
+   Currently we don't handle multi-touches (just the first touch) but
+   I'm leaving this comment here for future reference:
+
+   In the simulator, multi-touch sequences look like this:
+
+     touchesBegan [touchA, touchB]
+     touchesEnd [touchA, touchB]
+
+   But on real devices, sometimes you get that, but sometimes you get:
+
+     touchesBegan [touchA, touchB]
+     touchesEnd [touchB]
+     touchesEnd [touchA]
+
+   Or even
+
+     touchesBegan [touchA]
+     touchesBegan [touchB]
+     touchesEnd [touchA]
+     touchesEnd [touchB]
+
+   So the only way to properly detect a "pinch" gesture is to remember
+   the start-point of each touch as it comes in; and the end-point of
+   each touch as those come in; and only process the gesture once the
+   number of touchEnds matches the number of touchBegins.
+ */
+
+- (void) rotateMouse:(int)rot x:(int*)x y:(int *)y w:(int)w h:(int)h
+{
+  // This is a no-op unless contentScaleFactor != hackedContentScaleFactor.
+  // Currently, this is the iPad Retina only.
+  CGRect frame = [self bounds];                // Scale.
+  double s = [self hackedContentScaleFactor];
+  *x *= (backbuffer_size.width  / frame.size.width)  / s;
+  *y *= (backbuffer_size.height / frame.size.height) / s;
+}
+
+
+#if 0  // AudioToolbox/AudioToolbox.h
+- (void) beep
+{
+  // There's no way to play a standard system alert sound!
+  // We'd have to include our own WAV for that.  Eh, fuck it.
+  AudioServicesPlaySystemSound (kSystemSoundID_Vibrate);
+# if TARGET_IPHONE_SIMULATOR
+  NSLog(@"BEEP");  // The sim doesn't vibrate.
+# endif
+}
+#endif
+
+
+/* We distinguish between taps and drags.
+   - Drags (down, motion, up) are sent to the saver to handle.
+   - Single-taps exit the saver.
+   This means a saver cannot respond to a single-tap.  Only a few try to.
+ */
+
+- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
+{
+  // If they are trying to pinch, just do nothing.
+  if ([[event allTouches] count] > 1)
+    return;
+
+  tap_time = 0;
+
+  if (xsft->event_cb && xwindow) {
+    double s = [self hackedContentScaleFactor];
+    XEvent xe;
+    memset (&xe, 0, sizeof(xe));
+    int i = 0;
+    // #### 'frame' here or 'bounds'?
+    int w = s * [self frame].size.width;
+    int h = s * [self frame].size.height;
+    for (UITouch *touch in touches) {
+      CGPoint p = [touch locationInView:self];
+      xe.xany.type = ButtonPress;
+      xe.xbutton.button = i + 1;
+      xe.xbutton.button = i + 1;
+      xe.xbutton.x      = s * p.x;
+      xe.xbutton.y      = s * p.y;
+      [self rotateMouse: rot_current_angle
+            x: &xe.xbutton.x y: &xe.xbutton.y w: w h: h];
+      jwxyz_mouse_moved (xdpy, xwindow, xe.xbutton.x, xe.xbutton.y);
+
+      // Ignore return code: don't care whether the hack handled it.
+      xsft->event_cb (xdpy, xwindow, xdata, &xe);
+
+      // Remember when/where this was, to determine tap versus drag or hold.
+      tap_time = double_time();
+      tap_point = p;
+
+      i++;
+      break;  // No pinches: only look at the first touch.
+    }
+  }
+}
+
+
+- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
+{
+  // If they are trying to pinch, just do nothing.
+  if ([[event allTouches] count] > 1)
+    return;
+
+  if (xsft->event_cb && xwindow) {
+    double s = [self hackedContentScaleFactor];
+    XEvent xe;
+    memset (&xe, 0, sizeof(xe));
+    int i = 0;
+    // #### 'frame' here or 'bounds'?
+    int w = s * [self frame].size.width;
+    int h = s * [self frame].size.height;
+    for (UITouch *touch in touches) {
+      CGPoint p = [touch locationInView:self];
+
+      // If the ButtonRelease came less than half a second after ButtonPress,
+      // and didn't move far, then this was a tap, not a drag or a hold.
+      // Interpret it as "exit".
+      //
+      double dist = sqrt (((p.x - tap_point.x) * (p.x - tap_point.x)) +
+                          ((p.y - tap_point.y) * (p.y - tap_point.y)));
+      if (tap_time + 0.5 >= double_time() && dist < 20) {
+        [self stopAndClose:NO];
+        return;
+      }
+
+      xe.xany.type      = ButtonRelease;
+      xe.xbutton.button = i + 1;
+      xe.xbutton.x      = s * p.x;
+      xe.xbutton.y      = s * p.y;
+      [self rotateMouse: rot_current_angle
+            x: &xe.xbutton.x y: &xe.xbutton.y w: w h: h];
+      jwxyz_mouse_moved (xdpy, xwindow, xe.xbutton.x, xe.xbutton.y);
+      xsft->event_cb (xdpy, xwindow, xdata, &xe);
+      i++;
+      break;  // No pinches: only look at the first touch.
+    }
+  }
+}
+
+
+- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
+{
+  // If they are trying to pinch, just do nothing.
+  if ([[event allTouches] count] > 1)
+    return;
+
+  if (xsft->event_cb && xwindow) {
+    double s = [self hackedContentScaleFactor];
+    XEvent xe;
+    memset (&xe, 0, sizeof(xe));
+    int i = 0;
+    // #### 'frame' here or 'bounds'?
+    int w = s * [self frame].size.width;
+    int h = s * [self frame].size.height;
+    for (UITouch *touch in touches) {
+      CGPoint p = [touch locationInView:self];
+      xe.xany.type      = MotionNotify;
+      xe.xmotion.x      = s * p.x;
+      xe.xmotion.y      = s * p.y;
+      [self rotateMouse: rot_current_angle
+            x: &xe.xbutton.x y: &xe.xbutton.y w: w h: h];
+      jwxyz_mouse_moved (xdpy, xwindow, xe.xmotion.x, xe.xmotion.y);
+      xsft->event_cb (xdpy, xwindow, xdata, &xe);
+      i++;
+      break;  // No pinches: only look at the first touch.
+    }
+  }
+}
+
+
+/* We need this to respond to "shake" gestures
+ */
+- (BOOL)canBecomeFirstResponder
+{
+  return YES;
+}
+
+- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
+{
+}
+
+
+- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event
+{
+}
+
+/* Shake means exit and launch a new saver.
+ */
+- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
+{
+  [self stopAndClose:YES];
+}
+
+
+- (void)setScreenLocked:(BOOL)locked
+{
+  if (screenLocked == locked) return;
+  screenLocked = locked;
+  if (locked) {
+    if ([self isAnimating])
+      [self stopAnimation];
+  } else {
+    if (! [self isAnimating])
+      [self startAnimation];
+  }
+}
+
+
+#endif // USE_IPHONE
+
+
+@end
+
+/* Utility functions...
+ */
+
+static PrefsReader *
+get_prefsReader (Display *dpy)
+{
+  XScreenSaverView *view = jwxyz_window_view (XRootWindow (dpy, 0));
+  if (!view) return 0;
+  return [view prefsReader];
+}
+
+
+char *
+get_string_resource (Display *dpy, char *name, char *class)
+{
+  return [get_prefsReader(dpy) getStringResource:name];
+}
+
+Bool
+get_boolean_resource (Display *dpy, char *name, char *class)
+{
+  return [get_prefsReader(dpy) getBooleanResource:name];
+}
+
+int
+get_integer_resource (Display *dpy, char *name, char *class)
+{
+  return [get_prefsReader(dpy) getIntegerResource:name];
+}
+
+double
+get_float_resource (Display *dpy, char *name, char *class)
+{
+  return [get_prefsReader(dpy) getFloatResource:name];
+}