From http://www.jwz.org/xscreensaver/xscreensaver-5.22.tar.gz
[xscreensaver] / OSX / XScreenSaverView.m
index 8c6b5ea4762321c225db7e2f49dc23ef8058d323..d773fc84d048ab6b75c4015befb677e5431867b5 100644 (file)
@@ -1,4 +1,4 @@
-/* xscreensaver, Copyright (c) 2006-2012 Jamie Zawinski <jwz@jwz.org>
+/* 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
@@ -22,6 +22,7 @@
 #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.
  */
@@ -44,6 +45,8 @@ 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
@@ -91,6 +94,10 @@ int mono_p = 0;
 
 
 
+@interface XScreenSaverView (Private)
+- (void) stopAndClose:(Bool)relaunch;
+@end
+
 @implementation XScreenSaverView
 
 // Given a lower-cased saver name, returns the function table for it.
@@ -109,19 +116,35 @@ int mono_p = 0;
   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];
 
-  NSString *table_name = [[name lowercaseString]
-                           stringByAppendingString:
-                             @"_xscreensaver_function_table"];
+  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;
 }
 
@@ -146,7 +169,7 @@ int mono_p = 0;
   strcat (npath, opath);
   if (putenv (npath)) {
     perror ("putenv");
-    abort();
+    NSAssert1 (0, @"putenv \"%s\" failed", npath);
   }
 
   /* Don't free (npath) -- MacOS's putenv() does not copy it. */
@@ -167,7 +190,7 @@ int mono_p = 0;
   strcat (env, s);
   if (putenv (env)) {
     perror ("putenv");
-    abort();
+    NSAssert1 (0, @"putenv \"%s\" failed", env);
   }
   /* Don't free (env) -- MacOS's putenv() does not copy it. */
 }
@@ -210,10 +233,14 @@ add_default_options (const XrmOptionDescRec *opts,
 # endif
  // ".textLiteral:        ",
  // ".textFile:           ",
-    ".textURL:            http://twitter.com/statuses/public_timeline.atom",
+    ".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
@@ -325,7 +352,7 @@ double_time (void)
      "org.jwz.xscreensaver.<SAVERNAME>.<NUMBERS>.plist"
    */
   NSString *name = [NSString stringWithCString:xsft->progclass
-                                      encoding:NSUTF8StringEncoding];
+                             encoding:NSISOLatin1StringEncoding];
   name = [@"org.jwz.xscreensaver." stringByAppendingString:name];
   [self setResourcesEnv:name];
 
@@ -343,9 +370,12 @@ double_time (void)
 
   next_frame_time = 0;
   
-# ifdef USE_IPHONE
+# 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
@@ -353,6 +383,15 @@ double_time (void)
   return self;
 }
 
+- (void) initLayer
+{
+# ifndef USE_IPHONE
+  [self setLayer: [CALayer layer]];
+  [self setWantsLayer: YES];
+# endif
+}
+
+
 - (id) initWithFrame:(NSRect)frame isPreview:(BOOL)p
 {
   return [self initWithFrame:frame saverName:0 isPreview:p];
@@ -366,7 +405,7 @@ double_time (void)
   if (xdpy)
     jwxyz_free_display (xdpy);
 
-# ifdef USE_IPHONE
+# ifdef USE_BACKBUFFER
   if (backbuffer)
     CGContextRelease (backbuffer);
 # endif
@@ -375,7 +414,6 @@ double_time (void)
 
   // xsft
   // fpst
-  // orientation_timer
 
   [super dealloc];
 }
@@ -400,10 +438,12 @@ double_time (void)
  */
 - (void) allSystemsGo: (NSTimer *) timer
 {
-  NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
-  [prefs setBool:YES forKey:@"wasRunning"];
   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
 
@@ -421,11 +461,17 @@ double_time (void)
 # 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,
@@ -467,6 +513,7 @@ double_time (void)
   crash_timer = 0;
   NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
   [prefs removeObjectForKey:@"wasRunning"];
+  [prefs synchronize];
 # endif // USE_IPHONE
 
   [super stopAnimation];
@@ -500,48 +547,35 @@ screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
   fps_draw (fpst);
 }
 
+
 #ifdef USE_IPHONE
 
-/* Create a bitmap context into which we render everything.
+/* 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.
  */
-- (void) createBackbuffer
+- (CGFloat) hackedContentScaleFactor
 {
-  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);
-  }
+  GLfloat s = [self contentScaleFactor];
+  CGRect frame = [self bounds];
+  if (frame.size.width  >= 1024 ||
+      frame.size.height >= 1024)
+    s = 1;
+  return s;
 }
 
+
 static GLfloat _global_rot_current_angle_kludge;
 
 double current_device_rotation (void)
@@ -596,19 +630,136 @@ double current_device_rotation (void)
 
 #   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]];
+  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];
+  int new_w = s * rot_current_size.width;
+  int new_h = s * rot_current_size.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);
+  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];
@@ -617,26 +768,14 @@ double current_device_rotation (void)
   if (!initted_p) {
 
     if (! xdpy) {
-# ifdef USE_IPHONE
+# 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
-      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
+      [self resize_x11];
     }
 
     if (!setup_p) {
@@ -648,6 +787,13 @@ double current_device_rotation (void)
     resized_p = NO;
     NSAssert(!xdata, @"xdata already initialized");
     
+# 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
+
+
 # undef ya_rand_init
     ya_rand_init (0);
     
@@ -739,20 +885,19 @@ double current_device_rotation (void)
     // Xlib drawing takes place under the animation timer.
     [self resizeContext];
     NSRect r;
-# ifndef USE_IPHONE
-    r = [self frame];
-# else  // USE_IPHONE
+# 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_IPHONE
+# 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.)
@@ -809,14 +954,19 @@ double current_device_rotation (void)
     }
   }
 # endif // DO_GC_HACKERY
+
+# ifdef USE_IPHONE
+  }
+  @catch (NSException *e) {
+    [self handleException: e];
+  }
+# endif // USE_IPHONE
 }
 
 
-/* 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.
+/* 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.
  */
-#ifndef USE_IPHONE
 
 - (void)drawRect:(NSRect)rect
 {
@@ -826,78 +976,59 @@ double current_device_rotation (void)
     [super drawRect:rect];    // early: black.
 }
 
+
+#ifndef USE_BACKBUFFER
+
 - (void) animateOneFrame
 {
   [self render_x11];
 }
 
-#else  // USE_IPHONE
+#else  // USE_BACKBUFFER
 
-- (void)drawRect:(NSRect)rect
+- (void) animateOneFrame
 {
   // 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;
+# ifdef USE_IPHONE
+  UIGraphicsPushContext (backbuffer);
+# endif
 
-  // 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);
+  [self render_x11];
 
-  // Flip Y axis
-  t = CGAffineTransformConcat (t,
-        CGAffineTransformMake ( 1, 0, 0,
-                               -1, 0, frame.size.height));
+# ifdef USE_IPHONE
+  UIGraphicsPopContext();
+# endif
 
-  // Clear background (visible in corners of screen during rotation)
-  if (rotation_ratio != -1) {
-    CGContextSetGrayFillColor (cgc, 0, 1);
-    CGContextFillRect (cgc, frame);
+# ifdef USE_IPHONE
+  // Then compute the transformations for rotation.
+
+  if (!ignore_rotation_p) {
+    // The rotation origin for layer.affineTransform is in the center already.
+    CGAffineTransform t =
+      CGAffineTransformMakeRotation (rot_current_angle / (180.0 / M_PI));
+
+    // Correct the aspect ratio.
+    CGRect frame = [self bounds];
+    double s = [self hackedContentScaleFactor];
+    t = CGAffineTransformScale(t,
+                            backbuffer_size.width  / (s * frame.size.width),
+                            backbuffer_size.height / (s * frame.size.height));
+    self.layer.affineTransform = t;
   }
+# endif // USE_IPHONE
 
-  CGContextConcatCTM (cgc, t);
+  // Then copy that bitmap to the screen, by just stuffing it into
+  // the layer.  The superclass drawRect method will handle the rest.
 
-  // Copy the backbuffer to the screen.
-  // Note that CGContextDrawImage measures in "points", not "pixels".
   CGImageRef img = CGBitmapContextCreateImage (backbuffer);
-  CGContextDrawImage (cgc, target, img);
+  self.layer.contents = (id)img;
   CGImageRelease (img);
 }
 
-- (void) animateOneFrame
-{
-  [self setNeedsDisplay];
-}
-
-#endif // !USE_IPHONE
+#endif // !USE_BACKBUFFER
 
 
 
@@ -905,26 +1036,8 @@ double current_device_rotation (void)
 {
   [super setFrame:newRect];
 
-# 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
-  }
+  if (xwindow)     // inform Xlib that the window has changed now.
+    [self resize_x11];
 }
 
 
@@ -932,13 +1045,8 @@ double current_device_rotation (void)
 - (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
+    [self resize_x11];
 }
 # endif // !USE_IPHONE
 
@@ -961,7 +1069,7 @@ double current_device_rotation (void)
 {
   NSBundle *bundle = [NSBundle bundleForClass:[self class]];
   NSString *file = [NSString stringWithCString:xsft->progclass
-                                      encoding:NSUTF8StringEncoding];
+                                      encoding:NSISOLatin1StringEncoding];
   file = [file lowercaseString];
   NSString *path = [bundle pathForResource:file ofType:@"xml"];
   if (!path) {
@@ -984,6 +1092,7 @@ double current_device_rotation (void)
 
   // #### 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;
@@ -1032,12 +1141,12 @@ double current_device_rotation (void)
   NSPoint p = [[[e window] contentView] convertPoint:[e locationInWindow]
                                             toView:self];
 # ifdef USE_IPHONE
-  double s = self.contentScaleFactor;
+  double s = [self hackedContentScaleFactor];
 # else
   int s = 1;
 # endif
   int x = s * p.x;
-  int y = s * ([self frame].size.height - p.y);
+  int y = s * ([self bounds].size.height - p.y);
 
   xe.xany.type = type;
   switch (type) {
@@ -1103,12 +1212,15 @@ double current_device_rotation (void)
             }
           }
 
+        if (! k) return YES;   // E.g., "KeyRelease XK_Shift_L"
+
         xe.xkey.keycode = k;
         xe.xkey.state = state;
         break;
       }
     default:
-      abort();
+      NSAssert1 (0, @"unknown X11 event type: %d", type);
+      break;
   }
 
   [self lockFocus];
@@ -1188,6 +1300,44 @@ double current_device_rotation (void)
 #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
@@ -1245,29 +1395,34 @@ double current_device_rotation (void)
   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 UIInterfaceOrientationLandscapeRight:     angle_from = 90;  break;
-  case UIInterfaceOrientationLandscapeLeft:      angle_from = 270; break;
-  case UIInterfaceOrientationPortraitUpsideDown: angle_from = 180; break;
-  default:                                      angle_from = 0;   break;
+  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 UIInterfaceOrientationLandscapeRight:     angle_to = 90;  break;
-  case UIInterfaceOrientationLandscapeLeft:      angle_to = 270; break;
-  case UIInterfaceOrientationPortraitUpsideDown: angle_to = 180; break;
-  default:                                      angle_to = 0;   break;
+  case UIDeviceOrientationLandscapeLeft:      angle_to = 90;  break;
+  case UIDeviceOrientationLandscapeRight:     angle_to = 270; break;
+  case UIDeviceOrientationPortraitUpsideDown: angle_to = 180; break;
+  default:                                    angle_to = 0;   break;
   }
 
-  NSRect ff = [self frame];
+  NSRect ff = [self bounds];
 
   switch (orientation) {
-  case UIInterfaceOrientationLandscapeLeft:    // from landscape
-  case UIInterfaceOrientationLandscapeRight:
+  case UIDeviceOrientationLandscapeRight:      // from landscape
+  case UIDeviceOrientationLandscapeLeft:
     rot_from.width  = ff.size.height;
     rot_from.height = ff.size.width;
     break;
@@ -1278,8 +1433,8 @@ double current_device_rotation (void)
   }
 
   switch (new_orientation) {
-  case UIInterfaceOrientationLandscapeLeft:    // to landscape
-  case UIInterfaceOrientationLandscapeRight:
+  case UIDeviceOrientationLandscapeRight:      // to landscape
+  case UIDeviceOrientationLandscapeLeft:
     rot_to.width  = ff.size.height;
     rot_to.height = ff.size.width;
     break;
@@ -1298,7 +1453,14 @@ double current_device_rotation (void)
 }
 
 
-/* In the simulator, multi-touch sequences look like this:
+/* 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]
@@ -1322,23 +1484,48 @@ double current_device_rotation (void)
    number of touchEnds matches the number of touchBegins.
  */
 
-static void
-rotate_mouse (int *x, int *y, int w, int h, int rot)
+- (void) rotateMouse:(int)rot x:(int*)x y:(int *)y w:(int)w h:(int)h
 {
-  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; }
+  CGRect frame = [self bounds];                // Correct aspect ratio and 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.contentScaleFactor;
+    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) {
@@ -1348,10 +1535,19 @@ rotate_mouse (int *x, int *y, int w, int h, int rot)
       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);
+      [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.
     }
   }
 }
@@ -1359,35 +1555,42 @@ rotate_mouse (int *x, int *y, int w, int h, int rot)
 
 - (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 they are trying to pinch, just do nothing.
+  if ([[event allTouches] count] > 1)
+    return;
 
   if (xsft->event_cb && xwindow) {
-    double s = self.contentScaleFactor;
+    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;
-      rotate_mouse (&xe.xbutton.x, &xe.xbutton.y, w, h, rot_current_angle);
+      [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.
     }
   }
 }
@@ -1395,11 +1598,16 @@ rotate_mouse (int *x, int *y, int w, int h, int rot)
 
 - (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.contentScaleFactor;
+    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) {
@@ -1407,10 +1615,12 @@ rotate_mouse (int *x, int *y, int w, int h, int rot)
       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);
+      [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.
     }
   }
 }
@@ -1418,10 +1628,27 @@ rotate_mouse (int *x, int *y, int w, int h, int rot)
 
 /* We need this to respond to "shake" gestures
  */
-- (BOOL)canBecomeFirstResponder {
+- (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
 {
@@ -1449,7 +1676,7 @@ static PrefsReader *
 get_prefsReader (Display *dpy)
 {
   XScreenSaverView *view = jwxyz_window_view (XRootWindow (dpy, 0));
-  if (!view) abort();
+  if (!view) return 0;
   return [view prefsReader];
 }