From http://www.jwz.org/xscreensaver/xscreensaver-5.16.tar.gz
[xscreensaver] / OSX / XScreenSaverView.m
index 404cfc1c42bc5c163592278cf5e8c90d5b282544..8c6b5ea4762321c225db7e2f49dc23ef8058d323 100644 (file)
@@ -1,4 +1,4 @@
-/* xscreensaver, Copyright (c) 2006-2011 Jamie Zawinski <jwz@jwz.org>
+/* xscreensaver, Copyright (c) 2006-2012 Jamie Zawinski <jwz@jwz.org>
 *
 * Permission to use, copy, modify, distribute, and sell this software and its
 * documentation for any purpose is hereby granted without fee, provided that
@@ -15,6 +15,7 @@
    the "screenhack.c" module.
  */
 
+#import <QuartzCore/QuartzCore.h>
 #import "XScreenSaverView.h"
 #import "XScreenSaverConfigSheet.h"
 #import "screenhackI.h"
@@ -41,19 +42,66 @@ const char *progclass;
 int mono_p = 0;
 
 
+# ifdef USE_IPHONE
+
+/* 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
+
+
+
 @implementation XScreenSaverView
 
-- (struct xscreensaver_function_table *) findFunctionTable
+// 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];
-  NSString *name = [[[path lastPathComponent] stringByDeletingPathExtension]
-                    lowercaseString];
-  NSString *suffix = @"_xscreensaver_function_table";
-  NSString *table_name = [name stringByAppendingString:suffix];
-  
   CFURLRef url = CFURLCreateWithFileSystemPath(kCFAllocatorDefault,
                                                (CFStringRef) path,
                                                kCFURLPOSIXPathStyle,
@@ -62,11 +110,18 @@ int mono_p = 0;
   CFRelease (url);
   NSAssert1 (cfb, @"no CFBundle for \"%@\"", path);
   
+  if (! name)
+    name = [[path lastPathComponent] stringByDeletingPathExtension];
+
+  NSString *table_name = [[name lowercaseString]
+                           stringByAppendingString:
+                             @"_xscreensaver_function_table"];
   void *addr = CFBundleGetDataPointerForName (cfb, (CFStringRef) table_name);
-  NSAssert2 (addr, @"no symbol \"%@\" in bundle %@", table_name, path);
   CFRelease (cfb);
 
-//  NSLog (@"%@ = 0x%08X", table_name, (unsigned long) addr);
+  if (! addr)
+    NSLog (@"no symbol \"%@\" for \"%@\"", table_name, path);
+
   return (struct xscreensaver_function_table *) addr;
 }
 
@@ -127,7 +182,7 @@ add_default_options (const XrmOptionDescRec *opts,
   /* 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.
+     ../hacks/config/\*.xml files communicate with the preferences database.
   */
   static const XrmOptionDescRec default_options [] = {
     { "-text-mode",              ".textMode",          XrmoptionSepArg, 0 },
@@ -148,7 +203,11 @@ add_default_options (const XrmOptionDescRec *opts,
     ".doFPS:              False",
     ".doubleBuffer:       True",
     ".multiSample:        False",
+# ifndef USE_IPHONE
     ".textMode:           date",
+# else
+    ".textMode:           url",
+# endif
  // ".textLiteral:        ",
  // ".textFile:           ",
     ".textURL:            http://twitter.com/statuses/public_timeline.atom",
@@ -156,6 +215,7 @@ add_default_options (const XrmOptionDescRec *opts,
     ".grabDesktopImages:  yes",
     ".chooseRandomImages: no",
     ".imageDirectory:     ~/Pictures",
+    ".relaunchDelay:      2",
     0
   };
 
@@ -209,14 +269,53 @@ add_default_options (const XrmOptionDescRec *opts,
 }
 
 
-- (id) initWithFrame:(NSRect)frame isPreview:(BOOL)isPreview
+#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
+  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];
+  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);
@@ -244,16 +343,40 @@ add_default_options (const XrmOptionDescRec *opts,
 
   next_frame_time = 0;
   
+# ifdef USE_IPHONE
+  [self createBackbuffer];
+
+  // So we can tell when we're docked.
+  [UIDevice currentDevice].batteryMonitoringEnabled = YES;
+# endif // USE_IPHONE
+
   return self;
 }
 
+- (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_IPHONE
+  if (backbuffer)
+    CGContextRelease (backbuffer);
+# endif
+
   [prefsReader release];
+
+  // xsft
+  // fpst
+  // orientation_timer
+
   [super dealloc];
 }
 
@@ -263,6 +386,28 @@ add_default_options (const XrmOptionDescRec *opts,
 }
 
 
+#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
+{
+  NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
+  [prefs setBool:YES forKey:@"wasRunning"];
+  NSAssert (timer == crash_timer, @"crash timer screwed up");
+  crash_timer = 0;
+}
+#endif // USE_IPHONE
+
+
 - (void) startAnimation
 {
   NSAssert(![self isAnimating], @"already animating");
@@ -272,8 +417,27 @@ add_default_options (const XrmOptionDescRec *opts,
      initialization of the screen saver (xsft->init_cb) in the first call
      to animateOneFrame() instead.
    */
+
+# ifdef USE_IPHONE
+  if (crash_timer)
+    [crash_timer invalidate];
+  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");
@@ -297,7 +461,22 @@ add_default_options (const XrmOptionDescRec *opts,
     xdata = 0;
   }
 
+# ifdef USE_IPHONE
+  if (crash_timer)
+    [crash_timer invalidate];
+  crash_timer = 0;
+  NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
+  [prefs removeObjectForKey:@"wasRunning"];
+# 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
 }
 
 
@@ -321,14 +500,143 @@ screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
   fps_draw (fpst);
 }
 
+#ifdef USE_IPHONE
 
-- (void) animateOneFrame
+/* Create a bitmap context into which we render everything.
+ */
+- (void) createBackbuffer
 {
+  CGContextRef ob = backbuffer;
+  NSSize osize = backbuffer_size;
+
+  CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB();
+  double s = self.contentScaleFactor;
+  backbuffer_size.width  = (int) (s * rot_current_size.width);
+  backbuffer_size.height = (int) (s * rot_current_size.height);
+  backbuffer = CGBitmapContextCreate (NULL,
+                                      backbuffer_size.width,
+                                      backbuffer_size.height,
+                                      8, 
+                                      backbuffer_size.width * 4,
+                                      cs,
+                                      kCGImageAlphaPremultipliedLast);
+  NSAssert (backbuffer, @"unable to allocate back buffer");
+  CGColorSpaceRelease (cs);
+
+  // Clear it.
+  CGContextSetGrayFillColor (backbuffer, 0, 1);
+  CGRect r = CGRectZero;
+  r.size = backbuffer_size;
+  CGContextFillRect (backbuffer, r);
+
+  if (ob) {
+    // Restore old bits, as much as possible, to the X11 upper left origin.
+    NSRect 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);
+  }
+}
+
+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.contentScaleFactor;
+  if (((int) backbuffer_size.width  != (int) (s * rot_current_size.width) ||
+       (int) backbuffer_size.height != (int) (s * rot_current_size.height))
+/*      && rotation_ratio == -1*/)
+    [self setFrame:[self frame]];
+}
+
+#endif // USE_IPHONE
+
+
+- (void) render_x11
+{
+# ifdef USE_IPHONE
+  if (orientation == UIDeviceOrientationUnknown)
+    [self didRotate:nil];
+  [self hackRotation];
+# endif
+
   if (!initted_p) {
 
     if (! xdpy) {
-      xdpy = jwxyz_make_display (self);
+# ifdef USE_IPHONE
+      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
+      jwxyz_window_resized (xdpy, xwindow,
+                            0, 0,
+                            backbuffer_size.width, backbuffer_size.height,
+                            backbuffer);
+# else
+      NSRect r = [self frame];
+      jwxyz_window_resized (xdpy, xwindow,
+                            r.origin.x, r.origin.y,
+                            r.size.width, r.size.height,
+                            0);
+# endif
     }
 
     if (!setup_p) {
@@ -348,7 +656,9 @@ screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
                                               "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.
@@ -376,11 +686,14 @@ screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
     }
   }
 
+
   /* 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)
@@ -388,8 +701,9 @@ screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
       /* This is just a guess, but the -fps code wants to know how long
          we were sleeping between frames.
        */
-      unsigned long usecs = 1000000 * [self animationTimeInterval];
+      long usecs = 1000000 * [self animationTimeInterval];
       usecs -= 200;  // caller apparently sleeps for slightly less sometimes...
+      if (usecs < 0) usecs = 0;
       fps_slept (fpst, usecs);
     }
 
@@ -421,32 +735,50 @@ screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
   [self prepareContext];
 
   if (resized_p) {
-    // We do this here instead of in setFrameSize so that all the
+    // We do this here instead of in setFrame so that all the
     // Xlib drawing takes place under the animation timer.
     [self resizeContext];
-    NSRect r = [self frame];
+    NSRect r;
+# ifndef USE_IPHONE
+    r = [self frame];
+# else  // USE_IPHONE
+    r.origin.x = 0;
+    r.origin.y = 0;
+    r.size.width  = backbuffer_size.width;
+    r.size.height = backbuffer_size.height;
+# endif // USE_IPHONE
+
     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);
-  XSync (xdpy, 0);
+# 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
@@ -480,6 +812,12 @@ screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
 }
 
 
+/* On MacOS:   drawRect does nothing, and animateOneFrame renders.
+   On iOS GL:  drawRect does nothing, and animateOneFrame renders.
+   On iOS X11: drawRect renders, and animateOneFrame marks the view dirty.
+ */
+#ifndef USE_IPHONE
+
 - (void)drawRect:(NSRect)rect
 {
   if (xwindow)    // clear to the X window's bg color, not necessarily black.
@@ -488,21 +826,121 @@ screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
     [super drawRect:rect];    // early: black.
 }
 
+- (void) animateOneFrame
+{
+  [self render_x11];
+}
 
-- (void) setFrameSize:(NSSize) newSize
+#else  // USE_IPHONE
+
+- (void)drawRect:(NSRect)rect
 {
-  [super setFrameSize:newSize];
-  if ([self isAnimating]) {
-    resized_p = YES;
+  // Render X11 into the backing store bitmap...
+
+  NSAssert (backbuffer, @"no back buffer");
+  UIGraphicsPushContext (backbuffer);
+  [self render_x11];
+  UIGraphicsPopContext();
+
+  // Then copy that bitmap to the screen.
+
+  CGContextRef cgc = UIGraphicsGetCurrentContext();
+
+  // Mask it to only update the parts that are exposed.
+//  CGContextClipToRect (cgc, rect);
+
+  double s = self.contentScaleFactor;
+  CGRect frame = [self frame];
+
+  NSRect target;
+  target.size.width  = backbuffer_size.width;
+  target.size.height = backbuffer_size.height;
+  target.origin.x = (s * frame.size.width  - target.size.width)  / 2;
+  target.origin.y = (s * frame.size.height - target.size.height) / 2;
+
+  target.origin.x    /= s;
+  target.origin.y    /= s;
+  target.size.width  /= s;
+  target.size.height /= s;
+
+  CGAffineTransform t = CGAffineTransformIdentity;
+
+  // Rotate around center
+  float cx = frame.size.width  / 2;
+  float cy = frame.size.height / 2;
+  t = CGAffineTransformTranslate (t, cx, cy);
+  t = CGAffineTransformRotate (t, -rot_current_angle / (180.0 / M_PI));
+  t = CGAffineTransformTranslate (t, -cx, -cy);
+
+  // Flip Y axis
+  t = CGAffineTransformConcat (t,
+        CGAffineTransformMake ( 1, 0, 0,
+                               -1, 0, frame.size.height));
+
+  // Clear background (visible in corners of screen during rotation)
+  if (rotation_ratio != -1) {
+    CGContextSetGrayFillColor (cgc, 0, 1);
+    CGContextFillRect (cgc, frame);
   }
+
+  CGContextConcatCTM (cgc, t);
+
+  // Copy the backbuffer to the screen.
+  // Note that CGContextDrawImage measures in "points", not "pixels".
+  CGImageRef img = CGBitmapContextCreateImage (backbuffer);
+  CGContextDrawImage (cgc, target, img);
+  CGImageRelease (img);
+}
+
+- (void) animateOneFrame
+{
+  [self setNeedsDisplay];
 }
 
+#endif // !USE_IPHONE
+
+
+
 - (void) setFrame:(NSRect) newRect
 {
   [super setFrame:newRect];
-  if (xwindow)   // inform Xlib that the window has changed.
-    jwxyz_window_resized (xdpy, xwindow);
+
+# ifdef USE_IPHONE
+  [self createBackbuffer];
+# endif
+
+  resized_p = YES; // The reshape_cb runs in render_x11
+  if (xwindow) {   // inform Xlib that the window has changed now.
+# ifdef USE_IPHONE
+    NSAssert (backbuffer, @"no back buffer");
+    // The backbuffer is the rotated size, and so is the xwindow.
+    jwxyz_window_resized (xdpy, xwindow,
+                          0, 0,
+                          backbuffer_size.width, backbuffer_size.height,
+                          backbuffer);
+# else
+    jwxyz_window_resized (xdpy, xwindow,
+                          newRect.origin.x, newRect.origin.y,
+                          newRect.size.width, newRect.size.height,
+                          0);
+# endif
+  }
+}
+
+
+# ifndef USE_IPHONE  // Doesn't exist on iOS
+- (void) setFrameSize:(NSSize) newSize
+{
+  [super setFrameSize:newSize];
+  resized_p = YES;
+  if (xwindow)
+    jwxyz_window_resized (xdpy, xwindow,
+                          [self frame].origin.x,
+                          [self frame].origin.y,
+                          newSize.width, newSize.height,
+                          0); // backbuffer only on iPhone
 }
+# endif // !USE_IPHONE
 
 
 +(BOOL) performGammaFade
@@ -515,7 +953,11 @@ screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
   return YES;
 }
 
+#ifndef USE_IPHONE
 - (NSWindow *) configureSheet
+#else
+- (UIViewController *) configureView
+#endif
 {
   NSBundle *bundle = [NSBundle bundleForClass:[self class]];
   NSString *file = [NSString stringWithCString:xsft->progclass
@@ -528,11 +970,18 @@ screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
     return nil;
   }
   
-  NSWindow *sheet = [[XScreenSaverConfigSheet alloc]
-                     initWithXMLFile:path
-                             options:xsft->options
-                          controller:[prefsReader userDefaultsController]];
-  
+# ifdef USE_IPHONE
+  UIViewController *sheet;
+# else  // !USE_IPHONE
+  NSWindow *sheet;
+# endif // !USE_IPHONE
+
+  sheet = [[XScreenSaverConfigSheet alloc]
+           initWithXMLFile:path
+           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.
   [sheet retain];
@@ -555,6 +1004,8 @@ screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
 }
 
 
+#ifndef USE_IPHONE
+
 /* Convert an NSEvent into an XEvent, and pass it along.
    Returns YES if it was handled.
  */
@@ -565,7 +1016,7 @@ screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
       ![self isAnimating] ||
       !initted_p)
     return NO;
-  
+
   XEvent xe;
   memset (&xe, 0, sizeof(xe));
   
@@ -580,9 +1031,14 @@ screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
   
   NSPoint p = [[[e window] contentView] convertPoint:[e locationInWindow]
                                             toView:self];
-  int x = p.x;
-  int y = [self frame].size.height - p.y;
-  
+# ifdef USE_IPHONE
+  double s = self.contentScaleFactor;
+# else
+  int s = 1;
+# endif
+  int x = s * p.x;
+  int y = s * ([self frame].size.height - p.y);
+
   xe.xany.type = type;
   switch (type) {
     case ButtonPress:
@@ -607,9 +1063,47 @@ screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
     case KeyPress:
     case KeyRelease:
       {
-        NSString *nss = [e characters];
-        const char *s = [nss cStringUsingEncoding:NSISOLatin1StringEncoding];
-        xe.xkey.keycode = (s && *s ? *s : 0);
+        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;
+            }
+          }
+
+        xe.xkey.keycode = k;
         xe.xkey.state = state;
         break;
       }
@@ -685,6 +1179,266 @@ screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
     [super keyUp:e];
 }
 
+- (void) flagsChanged: (NSEvent *) e
+{
+  if (! [self doEvent:e type:KeyPress])
+    [super flagsChanged:e];
+}
+
+#else  // USE_IPHONE
+
+
+/* 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
+
+  new_orientation = current;           // current animation target
+  rotation_ratio = 0;                  // start animating
+  rot_start_time = double_time();
+
+  switch (orientation) {
+  case UIInterfaceOrientationLandscapeRight:     angle_from = 90;  break;
+  case UIInterfaceOrientationLandscapeLeft:      angle_from = 270; break;
+  case UIInterfaceOrientationPortraitUpsideDown: angle_from = 180; break;
+  default:                                      angle_from = 0;   break;
+  }
+
+  switch (new_orientation) {
+  case UIInterfaceOrientationLandscapeRight:     angle_to = 90;  break;
+  case UIInterfaceOrientationLandscapeLeft:      angle_to = 270; break;
+  case UIInterfaceOrientationPortraitUpsideDown: angle_to = 180; break;
+  default:                                      angle_to = 0;   break;
+  }
+
+  NSRect ff = [self frame];
+
+  switch (orientation) {
+  case UIInterfaceOrientationLandscapeLeft:    // from landscape
+  case UIInterfaceOrientationLandscapeRight:
+    rot_from.width  = ff.size.height;
+    rot_from.height = ff.size.width;
+    break;
+  default:                                     // from portrait
+    rot_from.width  = ff.size.width;
+    rot_from.height = ff.size.height;
+    break;
+  }
+
+  switch (new_orientation) {
+  case UIInterfaceOrientationLandscapeLeft:    // to landscape
+  case UIInterfaceOrientationLandscapeRight:
+    rot_to.width  = ff.size.height;
+    rot_to.height = ff.size.width;
+    break;
+  default:                                     // to portrait
+    rot_to.width  = ff.size.width;
+    rot_to.height = ff.size.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];
+ }
+}
+
+
+/* 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.
+ */
+
+static void
+rotate_mouse (int *x, int *y, int w, int h, int rot)
+{
+  int ox = *x, oy = *y;
+  if      (rot >  45 && rot <  135) { *x = oy;   *y = w-ox; }
+  else if (rot < -45 && rot > -135) { *x = h-oy; *y = ox;   }
+  else if (rot > 135 || rot < -135) { *x = w-ox; *y = h-oy; }
+}
+
+
+- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
+{
+  if (xsft->event_cb && xwindow) {
+    double s = self.contentScaleFactor;
+    XEvent xe;
+    memset (&xe, 0, sizeof(xe));
+    int i = 0;
+    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;
+      rotate_mouse (&xe.xbutton.x, &xe.xbutton.y, w, h, rot_current_angle);
+      jwxyz_mouse_moved (xdpy, xwindow, xe.xbutton.x, xe.xbutton.y);
+      xsft->event_cb (xdpy, xwindow, xdata, &xe);
+      i++;
+    }
+  }
+}
+
+
+- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
+{
+
+  // Double-tap means "exit" and return to selection menu.
+  //
+  for (UITouch *touch in touches) {
+    if ([touch tapCount] >= 2) {
+      if ([self isAnimating])
+        [self stopAnimation];
+      [self removeFromSuperview];
+      return;
+    }
+  }
+
+  if (xsft->event_cb && xwindow) {
+    double s = self.contentScaleFactor;
+    XEvent xe;
+    memset (&xe, 0, sizeof(xe));
+    int i = 0;
+    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      = ButtonRelease;
+      xe.xbutton.button = i + 1;
+      xe.xbutton.x      = s * p.x;
+      xe.xbutton.y      = s * p.y;
+      rotate_mouse (&xe.xbutton.x, &xe.xbutton.y, w, h, rot_current_angle);
+      jwxyz_mouse_moved (xdpy, xwindow, xe.xbutton.x, xe.xbutton.y);
+      xsft->event_cb (xdpy, xwindow, xdata, &xe);
+      i++;
+    }
+  }
+}
+
+
+- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
+{
+  if (xsft->event_cb && xwindow) {
+    double s = self.contentScaleFactor;
+    XEvent xe;
+    memset (&xe, 0, sizeof(xe));
+    int i = 0;
+    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;
+      rotate_mouse (&xe.xbutton.x, &xe.xbutton.y, w, h, rot_current_angle);
+      jwxyz_mouse_moved (xdpy, xwindow, xe.xmotion.x, xe.xmotion.y);
+      xsft->event_cb (xdpy, xwindow, xdata, &xe);
+      i++;
+    }
+  }
+}
+
+
+/* We need this to respond to "shake" gestures
+ */
+- (BOOL)canBecomeFirstResponder {
+  return 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