From http://www.jwz.org/xscreensaver/xscreensaver-5.22.tar.gz
[xscreensaver] / OSX / XScreenSaverView.m
1 /* xscreensaver, Copyright (c) 2006-2013 Jamie Zawinski <jwz@jwz.org>
2 *
3 * Permission to use, copy, modify, distribute, and sell this software and its
4 * documentation for any purpose is hereby granted without fee, provided that
5 * the above copyright notice appear in all copies and that both that
6 * copyright notice and this permission notice appear in supporting
7 * documentation.  No representations are made about the suitability of this
8 * software for any purpose.  It is provided "as is" without express or 
9 * implied warranty.
10 */
11
12 /* This is a subclass of Apple's ScreenSaverView that knows how to run
13    xscreensaver programs without X11 via the dark magic of the "jwxyz"
14    library.  In xscreensaver terminology, this is the replacement for
15    the "screenhack.c" module.
16  */
17
18 #import <QuartzCore/QuartzCore.h>
19 #import "XScreenSaverView.h"
20 #import "XScreenSaverConfigSheet.h"
21 #import "screenhackI.h"
22 #import "xlockmoreI.h"
23 #import "jwxyz-timers.h"
24
25
26 /* Garbage collection only exists if we are being compiled against the 
27    10.6 SDK or newer, not if we are building against the 10.4 SDK.
28  */
29 #ifndef  MAC_OS_X_VERSION_10_6
30 # define MAC_OS_X_VERSION_10_6 1060  /* undefined in 10.4 SDK, grr */
31 #endif
32 #if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6  /* 10.6 SDK */
33 # import <objc/objc-auto.h>
34 # define DO_GC_HACKERY
35 #endif
36
37 extern struct xscreensaver_function_table *xscreensaver_function_table;
38
39 /* Global variables used by the screen savers
40  */
41 const char *progname;
42 const char *progclass;
43 int mono_p = 0;
44
45
46 # ifdef USE_IPHONE
47
48 extern NSDictionary *make_function_table_dict(void);  // ios-function-table.m
49
50 /* Stub definition of the superclass, for iPhone.
51  */
52 @implementation ScreenSaverView
53 {
54   NSTimeInterval anim_interval;
55   Bool animating_p;
56   NSTimer *anim_timer;
57 }
58
59 - (id)initWithFrame:(NSRect)frame isPreview:(BOOL)isPreview {
60   self = [super initWithFrame:frame];
61   if (! self) return 0;
62   anim_interval = 1.0/30;
63   return self;
64 }
65 - (NSTimeInterval)animationTimeInterval { return anim_interval; }
66 - (void)setAnimationTimeInterval:(NSTimeInterval)i { anim_interval = i; }
67 - (BOOL)hasConfigureSheet { return NO; }
68 - (NSWindow *)configureSheet { return nil; }
69 - (NSView *)configureView { return nil; }
70 - (BOOL)isPreview { return NO; }
71 - (BOOL)isAnimating { return animating_p; }
72 - (void)animateOneFrame { }
73
74 - (void)startAnimation {
75   if (animating_p) return;
76   animating_p = YES;
77   anim_timer = [NSTimer scheduledTimerWithTimeInterval: anim_interval
78                         target:self
79                         selector:@selector(animateOneFrame)
80                         userInfo:nil
81                         repeats:YES];
82 }
83
84 - (void)stopAnimation {
85   if (anim_timer) {
86     [anim_timer invalidate];
87     anim_timer = 0;
88   }
89   animating_p = NO;
90 }
91 @end
92
93 # endif // !USE_IPHONE
94
95
96
97 @interface XScreenSaverView (Private)
98 - (void) stopAndClose:(Bool)relaunch;
99 @end
100
101 @implementation XScreenSaverView
102
103 // Given a lower-cased saver name, returns the function table for it.
104 // If no name, guess the name from the class's bundle name.
105 //
106 - (struct xscreensaver_function_table *) findFunctionTable:(NSString *)name
107 {
108   NSBundle *nsb = [NSBundle bundleForClass:[self class]];
109   NSAssert1 (nsb, @"no bundle for class %@", [self class]);
110
111   NSString *path = [nsb bundlePath];
112   CFURLRef url = CFURLCreateWithFileSystemPath(kCFAllocatorDefault,
113                                                (CFStringRef) path,
114                                                kCFURLPOSIXPathStyle,
115                                                true);
116   CFBundleRef cfb = CFBundleCreate (kCFAllocatorDefault, url);
117   CFRelease (url);
118   NSAssert1 (cfb, @"no CFBundle for \"%@\"", path);
119   // #### Analyze says "Potential leak of an object stored into cfb"
120   
121   if (! name)
122     name = [[path lastPathComponent] stringByDeletingPathExtension];
123
124   name = [[name lowercaseString]
125            stringByReplacingOccurrencesOfString:@" "
126            withString:@""];
127
128 # ifndef USE_IPHONE
129   // CFBundleGetDataPointerForName doesn't work in "Archive" builds.
130   // I'm guessing that symbol-stripping is mandatory.  Fuck.
131   NSString *table_name = [name stringByAppendingString:
132                                  @"_xscreensaver_function_table"];
133   void *addr = CFBundleGetDataPointerForName (cfb, (CFStringRef) table_name);
134   CFRelease (cfb);
135
136   if (! addr)
137     NSLog (@"no symbol \"%@\" for \"%@\"", table_name, path);
138
139 # else  // USE_IPHONE
140   // Remember: any time you add a new saver to the iOS app,
141   // manually run "make ios-function-table.m"!
142   if (! function_tables)
143     function_tables = [make_function_table_dict() retain];
144   NSValue *v = [function_tables objectForKey: name];
145   void *addr = v ? [v pointerValue] : 0;
146 # endif // USE_IPHONE
147
148   return (struct xscreensaver_function_table *) addr;
149 }
150
151
152 // Add the "Contents/Resources/" subdirectory of this screen saver's .bundle
153 // to $PATH for the benefit of savers that include helper shell scripts.
154 //
155 - (void) setShellPath
156 {
157   NSBundle *nsb = [NSBundle bundleForClass:[self class]];
158   NSAssert1 (nsb, @"no bundle for class %@", [self class]);
159   
160   NSString *nsdir = [nsb resourcePath];
161   NSAssert1 (nsdir, @"no resourcePath for class %@", [self class]);
162   const char *dir = [nsdir cStringUsingEncoding:NSUTF8StringEncoding];
163   const char *opath = getenv ("PATH");
164   if (!opath) opath = "/bin"; // $PATH is unset when running under Shark!
165   char *npath = (char *) malloc (strlen (opath) + strlen (dir) + 30);
166   strcpy (npath, "PATH=");
167   strcat (npath, dir);
168   strcat (npath, ":");
169   strcat (npath, opath);
170   if (putenv (npath)) {
171     perror ("putenv");
172     NSAssert1 (0, @"putenv \"%s\" failed", npath);
173   }
174
175   /* Don't free (npath) -- MacOS's putenv() does not copy it. */
176 }
177
178
179 // set an $XSCREENSAVER_CLASSPATH variable so that included shell scripts
180 // (e.g., "xscreensaver-text") know how to look up resources.
181 //
182 - (void) setResourcesEnv:(NSString *) name
183 {
184   NSBundle *nsb = [NSBundle bundleForClass:[self class]];
185   NSAssert1 (nsb, @"no bundle for class %@", [self class]);
186   
187   const char *s = [name cStringUsingEncoding:NSUTF8StringEncoding];
188   char *env = (char *) malloc (strlen (s) + 40);
189   strcpy (env, "XSCREENSAVER_CLASSPATH=");
190   strcat (env, s);
191   if (putenv (env)) {
192     perror ("putenv");
193     NSAssert1 (0, @"putenv \"%s\" failed", env);
194   }
195   /* Don't free (env) -- MacOS's putenv() does not copy it. */
196 }
197
198
199 static void
200 add_default_options (const XrmOptionDescRec *opts,
201                      const char * const *defs,
202                      XrmOptionDescRec **opts_ret,
203                      const char ***defs_ret)
204 {
205   /* These aren't "real" command-line options (there are no actual command-line
206      options in the Cocoa version); but this is the somewhat kludgey way that
207      the <xscreensaver-text /> and <xscreensaver-image /> tags in the
208      ../hacks/config/\*.xml files communicate with the preferences database.
209   */
210   static const XrmOptionDescRec default_options [] = {
211     { "-text-mode",              ".textMode",          XrmoptionSepArg, 0 },
212     { "-text-literal",           ".textLiteral",       XrmoptionSepArg, 0 },
213     { "-text-file",              ".textFile",          XrmoptionSepArg, 0 },
214     { "-text-url",               ".textURL",           XrmoptionSepArg, 0 },
215     { "-text-program",           ".textProgram",       XrmoptionSepArg, 0 },
216     { "-grab-desktop",           ".grabDesktopImages", XrmoptionNoArg, "True" },
217     { "-no-grab-desktop",        ".grabDesktopImages", XrmoptionNoArg, "False"},
218     { "-choose-random-images",   ".chooseRandomImages",XrmoptionNoArg, "True" },
219     { "-no-choose-random-images",".chooseRandomImages",XrmoptionNoArg, "False"},
220     { "-image-directory",        ".imageDirectory",    XrmoptionSepArg, 0 },
221     { "-fps",                    ".doFPS",             XrmoptionNoArg, "True" },
222     { "-no-fps",                 ".doFPS",             XrmoptionNoArg, "False"},
223     { 0, 0, 0, 0 }
224   };
225   static const char *default_defaults [] = {
226     ".doFPS:              False",
227     ".doubleBuffer:       True",
228     ".multiSample:        False",
229 # ifndef USE_IPHONE
230     ".textMode:           date",
231 # else
232     ".textMode:           url",
233 # endif
234  // ".textLiteral:        ",
235  // ".textFile:           ",
236     ".textURL:            http://en.wikipedia.org/w/index.php?title=Special:NewPages&feed=rss",
237  // ".textProgram:        ",
238     ".grabDesktopImages:  yes",
239 # ifndef USE_IPHONE
240     ".chooseRandomImages: no",
241 # else
242     ".chooseRandomImages: yes",
243 # endif
244     ".imageDirectory:     ~/Pictures",
245     ".relaunchDelay:      2",
246     0
247   };
248
249   int count = 0, i, j;
250   for (i = 0; default_options[i].option; i++)
251     count++;
252   for (i = 0; opts[i].option; i++)
253     count++;
254
255   XrmOptionDescRec *opts2 = (XrmOptionDescRec *) 
256     calloc (count + 1, sizeof (*opts2));
257
258   i = 0;
259   j = 0;
260   while (default_options[j].option) {
261     opts2[i] = default_options[j];
262     i++, j++;
263   }
264   j = 0;
265   while (opts[j].option) {
266     opts2[i] = opts[j];
267     i++, j++;
268   }
269
270   *opts_ret = opts2;
271
272
273   /* now the defaults
274    */
275   count = 0;
276   for (i = 0; default_defaults[i]; i++)
277     count++;
278   for (i = 0; defs[i]; i++)
279     count++;
280
281   const char **defs2 = (const char **) calloc (count + 1, sizeof (*defs2));
282
283   i = 0;
284   j = 0;
285   while (default_defaults[j]) {
286     defs2[i] = default_defaults[j];
287     i++, j++;
288   }
289   j = 0;
290   while (defs[j]) {
291     defs2[i] = defs[j];
292     i++, j++;
293   }
294
295   *defs_ret = defs2;
296 }
297
298
299 #ifdef USE_IPHONE
300 /* Returns the current time in seconds as a double.
301  */
302 static double
303 double_time (void)
304 {
305   struct timeval now;
306 # ifdef GETTIMEOFDAY_TWO_ARGS
307   struct timezone tzp;
308   gettimeofday(&now, &tzp);
309 # else
310   gettimeofday(&now);
311 # endif
312
313   return (now.tv_sec + ((double) now.tv_usec * 0.000001));
314 }
315 #endif // USE_IPHONE
316
317
318 - (id) initWithFrame:(NSRect)frame
319            saverName:(NSString *)saverName
320            isPreview:(BOOL)isPreview
321 {
322 # ifdef USE_IPHONE
323   rot_current_size = frame.size;        // needs to be early, because
324   rot_from = rot_current_size;          // [self setFrame] is called by
325   rot_to = rot_current_size;            // [super initWithFrame].
326   rotation_ratio = -1;
327 # endif
328
329   if (! (self = [super initWithFrame:frame isPreview:isPreview]))
330     return 0;
331   
332   xsft = [self findFunctionTable: saverName];
333   if (! xsft) {
334     [self release];
335     return 0;
336   }
337
338   [self setShellPath];
339
340 # ifdef USE_IPHONE
341   [self setMultipleTouchEnabled:YES];
342   orientation = UIDeviceOrientationUnknown;
343   [self didRotate:nil];
344 # endif // USE_IPHONE
345
346   setup_p = YES;
347   if (xsft->setup_cb)
348     xsft->setup_cb (xsft, xsft->setup_arg);
349
350   /* The plist files for these preferences show up in
351      $HOME/Library/Preferences/ByHost/ in a file named like
352      "org.jwz.xscreensaver.<SAVERNAME>.<NUMBERS>.plist"
353    */
354   NSString *name = [NSString stringWithCString:xsft->progclass
355                              encoding:NSISOLatin1StringEncoding];
356   name = [@"org.jwz.xscreensaver." stringByAppendingString:name];
357   [self setResourcesEnv:name];
358
359   
360   XrmOptionDescRec *opts = 0;
361   const char **defs = 0;
362   add_default_options (xsft->options, xsft->defaults, &opts, &defs);
363   prefsReader = [[PrefsReader alloc]
364                   initWithName:name xrmKeys:opts defaults:defs];
365   free (defs);
366   // free (opts);  // bah, we need these! #### leak!
367   xsft->options = opts;
368   
369   progname = progclass = xsft->progclass;
370
371   next_frame_time = 0;
372   
373 # ifdef USE_BACKBUFFER
374   [self createBackbuffer];
375   [self initLayer];
376 # endif
377
378 # ifdef USE_IPHONE
379   // So we can tell when we're docked.
380   [UIDevice currentDevice].batteryMonitoringEnabled = YES;
381 # endif // USE_IPHONE
382
383   return self;
384 }
385
386 - (void) initLayer
387 {
388 # ifndef USE_IPHONE
389   [self setLayer: [CALayer layer]];
390   [self setWantsLayer: YES];
391 # endif
392 }
393
394
395 - (id) initWithFrame:(NSRect)frame isPreview:(BOOL)p
396 {
397   return [self initWithFrame:frame saverName:0 isPreview:p];
398 }
399
400
401 - (void) dealloc
402 {
403   NSAssert(![self isAnimating], @"still animating");
404   NSAssert(!xdata, @"xdata not yet freed");
405   if (xdpy)
406     jwxyz_free_display (xdpy);
407
408 # ifdef USE_BACKBUFFER
409   if (backbuffer)
410     CGContextRelease (backbuffer);
411 # endif
412
413   [prefsReader release];
414
415   // xsft
416   // fpst
417
418   [super dealloc];
419 }
420
421 - (PrefsReader *) prefsReader
422 {
423   return prefsReader;
424 }
425
426
427 #ifdef USE_IPHONE
428 - (void) lockFocus { }
429 - (void) unlockFocus { }
430 #endif // USE_IPHONE
431
432
433
434 # ifdef USE_IPHONE
435 /* A few seconds after the saver launches, we store the "wasRunning"
436    preference.  This is so that if the saver is crashing at startup,
437    we don't launch it again next time, getting stuck in a crash loop.
438  */
439 - (void) allSystemsGo: (NSTimer *) timer
440 {
441   NSAssert (timer == crash_timer, @"crash timer screwed up");
442   crash_timer = 0;
443
444   NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
445   [prefs setBool:YES forKey:@"wasRunning"];
446   [prefs synchronize];
447 }
448 #endif // USE_IPHONE
449
450
451 - (void) startAnimation
452 {
453   NSAssert(![self isAnimating], @"already animating");
454   NSAssert(!initted_p && !xdata, @"already initialized");
455   [super startAnimation];
456   /* We can't draw on the window from this method, so we actually do the
457      initialization of the screen saver (xsft->init_cb) in the first call
458      to animateOneFrame() instead.
459    */
460
461 # ifdef USE_IPHONE
462   if (crash_timer)
463     [crash_timer invalidate];
464
465   NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
466   [prefs removeObjectForKey:@"wasRunning"];
467   [prefs synchronize];
468
469   crash_timer = [NSTimer scheduledTimerWithTimeInterval: 5
470                          target:self
471                          selector:@selector(allSystemsGo:)
472                          userInfo:nil
473                          repeats:NO];
474
475 # endif // USE_IPHONE
476
477   // Never automatically turn the screen off if we are docked,
478   // and an animation is running.
479   //
480 # ifdef USE_IPHONE
481   [UIApplication sharedApplication].idleTimerDisabled =
482     ([UIDevice currentDevice].batteryState != UIDeviceBatteryStateUnplugged);
483 # endif
484 }
485
486
487 - (void)stopAnimation
488 {
489   NSAssert([self isAnimating], @"not animating");
490
491   if (initted_p) {
492
493     [self lockFocus];       // in case something tries to draw from here
494     [self prepareContext];
495
496     /* I considered just not even calling the free callback at all...
497        But webcollage-cocoa needs it, to kill the inferior webcollage
498        processes (since the screen saver framework never generates a
499        SIGPIPE for them...)  Instead, I turned off the free call in
500        xlockmore.c, which is where all of the bogus calls are anyway.
501      */
502     xsft->free_cb (xdpy, xwindow, xdata);
503     [self unlockFocus];
504
505 //  setup_p = NO; // #### wait, do we need this?
506     initted_p = NO;
507     xdata = 0;
508   }
509
510 # ifdef USE_IPHONE
511   if (crash_timer)
512     [crash_timer invalidate];
513   crash_timer = 0;
514   NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
515   [prefs removeObjectForKey:@"wasRunning"];
516   [prefs synchronize];
517 # endif // USE_IPHONE
518
519   [super stopAnimation];
520
521   // When an animation is no longer running (e.g., looking at the list)
522   // then it's ok to power off the screen when docked.
523   //
524 # ifdef USE_IPHONE
525   [UIApplication sharedApplication].idleTimerDisabled = NO;
526 # endif
527 }
528
529
530 /* Hook for the XScreenSaverGLView subclass
531  */
532 - (void) prepareContext
533 {
534 }
535
536 /* Hook for the XScreenSaverGLView subclass
537  */
538 - (void) resizeContext
539 {
540 }
541
542
543 static void
544 screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
545 {
546   fps_compute (fpst, 0, -1);
547   fps_draw (fpst);
548 }
549
550
551 #ifdef USE_IPHONE
552
553 /* On iPhones with Retina displays, we can draw the savers in "real"
554    pixels, and that works great.  The 320x480 "point" screen is really
555    a 640x960 *pixel* screen.  However, Retina iPads have 768x1024
556    point screens which are 1536x2048 pixels, and apparently that's
557    enough pixels that copying those bits to the screen is slow.  Like,
558    drops us from 15fps to 7fps.  So, on Retina iPads, we don't draw in
559    real pixels.  This will probably make the savers look better
560    anyway, since that's a higher resolution than most desktop monitors
561    have even today.  (This is only true for X11 programs, not GL 
562    programs.  Those are fine at full rez.)
563
564    This method is overridden in XScreenSaverGLView, since this kludge
565    isn't necessary for GL programs, being resolution independent by
566    nature.
567  */
568 - (CGFloat) hackedContentScaleFactor
569 {
570   GLfloat s = [self contentScaleFactor];
571   CGRect frame = [self bounds];
572   if (frame.size.width  >= 1024 ||
573       frame.size.height >= 1024)
574     s = 1;
575   return s;
576 }
577
578
579 static GLfloat _global_rot_current_angle_kludge;
580
581 double current_device_rotation (void)
582 {
583   return -_global_rot_current_angle_kludge;
584 }
585
586
587 - (void) hackRotation
588 {
589   if (rotation_ratio >= 0) {    // in the midst of a rotation animation
590
591 #   define CLAMP180(N) while (N < 0) N += 360; while (N > 180) N -= 360
592     GLfloat f = angle_from;
593     GLfloat t = angle_to;
594     CLAMP180(f);
595     CLAMP180(t);
596     GLfloat dist = -(t-f);
597     CLAMP180(dist);
598
599     // Intermediate angle.
600     rot_current_angle = f - rotation_ratio * dist;
601
602     // Intermediate frame size.
603     rot_current_size.width = rot_from.width + 
604       rotation_ratio * (rot_to.width - rot_from.width);
605     rot_current_size.height = rot_from.height + 
606       rotation_ratio * (rot_to.height - rot_from.height);
607
608     // Tick animation.  Complete rotation in 1/6th sec.
609     double now = double_time();
610     double duration = 1/6.0;
611     rotation_ratio = 1 - ((rot_start_time + duration - now) / duration);
612
613     if (rotation_ratio > 1) {   // Done animating.
614       orientation = new_orientation;
615       rot_current_angle = angle_to;
616       rot_current_size = rot_to;
617       rotation_ratio = -1;
618
619       // Check orientation again in case we rotated again while rotating:
620       // this is a no-op if nothing has changed.
621       [self didRotate:nil];
622     }
623   } else {                      // Not animating a rotation.
624     rot_current_angle = angle_to;
625     rot_current_size = rot_to;
626   }
627
628   CLAMP180(rot_current_angle);
629   _global_rot_current_angle_kludge = rot_current_angle;
630
631 #   undef CLAMP180
632
633   double s = [self hackedContentScaleFactor];
634   if (!ignore_rotation_p &&
635       /* rotation_ratio && */
636       ((int) backbuffer_size.width  != (int) (s * rot_current_size.width) ||
637        (int) backbuffer_size.height != (int) (s * rot_current_size.height)))
638     [self resize_x11];
639 }
640
641
642 - (void)alertView:(UIAlertView *)av clickedButtonAtIndex:(NSInteger)i
643 {
644   if (i == 0) exit (-1);        // Cancel
645   [self stopAndClose:NO];       // Keep going
646 }
647
648 - (void) handleException: (NSException *)e
649 {
650   NSLog (@"Caught exception: %@", e);
651   [[[UIAlertView alloc] initWithTitle:
652                           [NSString stringWithFormat: @"%s crashed!",
653                                     xsft->progclass]
654                         message:
655                           [NSString stringWithFormat:
656                                       @"The error message was:"
657                                     "\n\n%@\n\n"
658                                     "If it keeps crashing, try "
659                                     "resetting its options.",
660                                     e]
661                         delegate: self
662                         cancelButtonTitle: @"Exit"
663                         otherButtonTitles: @"Keep going", nil]
664     show];
665   [self stopAnimation];
666 }
667
668 #endif // USE_IPHONE
669
670
671 #ifdef USE_BACKBUFFER
672
673 /* Create a bitmap context into which we render everything.
674    If the desired size has changed, re-created it.
675  */
676 - (void) createBackbuffer
677 {
678 # ifdef USE_IPHONE
679   double s = [self hackedContentScaleFactor];
680   int new_w = s * rot_current_size.width;
681   int new_h = s * rot_current_size.height;
682 # else
683   int new_w = [self bounds].size.width;
684   int new_h = [self bounds].size.height;
685 # endif
686
687   if (backbuffer &&
688       backbuffer_size.width  == new_w &&
689       backbuffer_size.height == new_h)
690     return;
691
692   CGSize osize = backbuffer_size;
693   CGContextRef ob = backbuffer;
694
695   backbuffer_size.width  = new_w;
696   backbuffer_size.height = new_h;
697
698   CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB();
699   backbuffer = CGBitmapContextCreate (NULL,
700                                       backbuffer_size.width,
701                                       backbuffer_size.height,
702                                       8, 
703                                       backbuffer_size.width * 4,
704                                       cs,
705                                       kCGImageAlphaPremultipliedLast);
706   CGColorSpaceRelease (cs);
707   NSAssert (backbuffer, @"unable to allocate back buffer");
708
709   // Clear it.
710   CGRect r;
711   r.origin.x = r.origin.y = 0;
712   r.size = backbuffer_size;
713   CGContextSetGrayFillColor (backbuffer, 0, 1);
714   CGContextFillRect (backbuffer, r);
715
716   if (ob) {
717     // Restore old bits, as much as possible, to the X11 upper left origin.
718     CGRect rect;
719     rect.origin.x = 0;
720     rect.origin.y = (backbuffer_size.height - osize.height);
721     rect.size  = osize;
722     CGImageRef img = CGBitmapContextCreateImage (ob);
723     CGContextDrawImage (backbuffer, rect, img);
724     CGImageRelease (img);
725     CGContextRelease (ob);
726   }
727 }
728
729 #endif // USE_BACKBUFFER
730
731
732 /* Inform X11 that the size of our window has changed.
733  */
734 - (void) resize_x11
735 {
736   if (!xwindow) return;  // early
737
738 # ifdef USE_BACKBUFFER
739   [self createBackbuffer];
740   jwxyz_window_resized (xdpy, xwindow,
741                         0, 0,
742                         backbuffer_size.width, backbuffer_size.height,
743                         backbuffer);
744 # else   // !USE_BACKBUFFER
745   NSRect r = [self frame];              // ignoring rotation is closer
746   r.size = [self bounds].size;          // to what XGetGeometry expects.
747   jwxyz_window_resized (xdpy, xwindow,
748                         r.origin.x, r.origin.y,
749                         r.size.width, r.size.height,
750                         0);
751 # endif  // !USE_BACKBUFFER
752
753   // Next time render_x11 is called, run the saver's reshape_cb.
754   resized_p = YES;
755 }
756
757
758 - (void) render_x11
759 {
760 # ifdef USE_IPHONE
761   @try {
762
763   if (orientation == UIDeviceOrientationUnknown)
764     [self didRotate:nil];
765   [self hackRotation];
766 # endif
767
768   if (!initted_p) {
769
770     if (! xdpy) {
771 # ifdef USE_BACKBUFFER
772       NSAssert (backbuffer, @"no back buffer");
773       xdpy = jwxyz_make_display (self, backbuffer);
774 # else
775       xdpy = jwxyz_make_display (self, 0);
776 # endif
777       xwindow = XRootWindow (xdpy, 0);
778       [self resize_x11];
779     }
780
781     if (!setup_p) {
782       setup_p = YES;
783       if (xsft->setup_cb)
784         xsft->setup_cb (xsft, xsft->setup_arg);
785     }
786     initted_p = YES;
787     resized_p = NO;
788     NSAssert(!xdata, @"xdata already initialized");
789     
790 # ifdef USE_IPHONE
791   /* Some X11 hacks (fluidballs) want to ignore all rotation events. */
792   ignore_rotation_p =
793     get_boolean_resource (xdpy, "ignoreRotation", "IgnoreRotation");
794 # endif // USE_IPHONE
795
796
797 # undef ya_rand_init
798     ya_rand_init (0);
799     
800     XSetWindowBackground (xdpy, xwindow,
801                           get_pixel_resource (xdpy, 0,
802                                               "background", "Background"));
803     XClearWindow (xdpy, xwindow);
804     
805 # ifndef USE_IPHONE
806     [[self window] setAcceptsMouseMovedEvents:YES];
807 # endif
808
809     /* In MacOS 10.5, this enables "QuartzGL", meaning that the Quartz
810        drawing primitives will run on the GPU instead of the CPU.
811        It seems like it might make things worse rather than better,
812        though...  Plus it makes us binary-incompatible with 10.4.
813
814 # if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5
815     [[self window] setPreferredBackingLocation:
816                      NSWindowBackingLocationVideoMemory];
817 # endif
818      */
819
820     /* Kludge: even though the init_cb functions are declared to take 2 args,
821       actually call them with 3, for the benefit of xlockmore_init() and
822       xlockmore_setup().
823       */
824     void *(*init_cb) (Display *, Window, void *) = 
825       (void *(*) (Display *, Window, void *)) xsft->init_cb;
826     
827     xdata = init_cb (xdpy, xwindow, xsft->setup_arg);
828
829     if (get_boolean_resource (xdpy, "doFPS", "DoFPS")) {
830       fpst = fps_init (xdpy, xwindow);
831       if (! xsft->fps_cb) xsft->fps_cb = screenhack_do_fps;
832     }
833   }
834
835
836   /* I don't understand why we have to do this *every frame*, but we do,
837      or else the cursor comes back on.
838    */
839 # ifndef USE_IPHONE
840   if (![self isPreview])
841     [NSCursor setHiddenUntilMouseMoves:YES];
842 # endif
843
844
845   if (fpst)
846     {
847       /* This is just a guess, but the -fps code wants to know how long
848          we were sleeping between frames.
849        */
850       long usecs = 1000000 * [self animationTimeInterval];
851       usecs -= 200;  // caller apparently sleeps for slightly less sometimes...
852       if (usecs < 0) usecs = 0;
853       fps_slept (fpst, usecs);
854     }
855
856
857   /* It turns out that [ScreenSaverView setAnimationTimeInterval] does nothing.
858      This is bad, because some of the screen hacks want to delay for long 
859      periods (like 5 seconds or a minute!) between frames, and running them
860      all at 60 FPS is no good.
861   
862      So, we don't use setAnimationTimeInterval, and just let the framework call
863      us whenever.  But, we only invoke the screen hack's "draw frame" method
864      when enough time has expired.
865   
866      This means two extra calls to gettimeofday() per frame.  For fast-cycling
867      screen savers, that might actually slow them down.  Oh well.
868
869      #### Also, we do not run the draw callback faster than the system's
870           animationTimeInterval, so if any savers are pickier about timing
871           than that, this may slow them down too much.  If that's a problem,
872           then we could call draw_cb in a loop here (with usleep) until the
873           next call would put us past animationTimeInterval...  But a better
874           approach would probably be to just change the saver to not do that.
875    */
876   struct timeval tv;
877   gettimeofday (&tv, 0);
878   double now = tv.tv_sec + (tv.tv_usec / 1000000.0);
879   if (now < next_frame_time) return;
880   
881   [self prepareContext];
882
883   if (resized_p) {
884     // We do this here instead of in setFrame so that all the
885     // Xlib drawing takes place under the animation timer.
886     [self resizeContext];
887     NSRect r;
888 # ifndef USE_BACKBUFFER
889     r = [self bounds];
890 # else  // USE_BACKBUFFER
891     r.origin.x = 0;
892     r.origin.y = 0;
893     r.size.width  = backbuffer_size.width;
894     r.size.height = backbuffer_size.height;
895 # endif // USE_BACKBUFFER
896
897     xsft->reshape_cb (xdpy, xwindow, xdata, r.size.width, r.size.height);
898     resized_p = NO;
899   }
900
901   // Run any XtAppAddInput callbacks now.
902   // (Note that XtAppAddTimeOut callbacks have already been run by
903   // the Cocoa event loop.)
904   //
905   jwxyz_sources_run (display_sources_data (xdpy));
906
907
908   // And finally:
909   //
910 # ifndef USE_IPHONE
911   NSDisableScreenUpdates();
912 # endif
913   unsigned long delay = xsft->draw_cb (xdpy, xwindow, xdata);
914   if (fpst) xsft->fps_cb (xdpy, xwindow, fpst, xdata);
915 # ifndef USE_IPHONE
916   NSEnableScreenUpdates();
917 # endif
918
919   gettimeofday (&tv, 0);
920   now = tv.tv_sec + (tv.tv_usec / 1000000.0);
921   next_frame_time = now + (delay / 1000000.0);
922
923 # ifdef USE_IPHONE      // Allow savers on the iPhone to run full-tilt.
924   if (delay < [self animationTimeInterval])
925     [self setAnimationTimeInterval:(delay / 1000000.0)];
926 # endif
927
928 # ifdef DO_GC_HACKERY
929   /* Current theory is that the 10.6 garbage collector sucks in the
930      following way:
931
932      It only does a collection when a threshold of outstanding
933      collectable allocations has been surpassed.  However, CoreGraphics
934      creates lots of small collectable allocations that contain pointers
935      to very large non-collectable allocations: a small CG object that's
936      collectable referencing large malloc'd allocations (non-collectable)
937      containing bitmap data.  So the large allocation doesn't get freed
938      until GC collects the small allocation, which triggers its finalizer
939      to run which frees the large allocation.  So GC is deciding that it
940      doesn't really need to run, even though the process has gotten
941      enormous.  GC eventually runs once pageouts have happened, but by
942      then it's too late, and the machine's resident set has been
943      sodomized.
944
945      So, we force an exhaustive garbage collection in this process
946      approximately every 5 seconds whether the system thinks it needs 
947      one or not.
948   */
949   {
950     static int tick = 0;
951     if (++tick > 5*30) {
952       tick = 0;
953       objc_collect (OBJC_EXHAUSTIVE_COLLECTION);
954     }
955   }
956 # endif // DO_GC_HACKERY
957
958 # ifdef USE_IPHONE
959   }
960   @catch (NSException *e) {
961     [self handleException: e];
962   }
963 # endif // USE_IPHONE
964 }
965
966
967 /* drawRect always does nothing, and animateOneFrame renders bits to the
968    screen.  This is (now) true of both X11 and GL on both MacOS and iOS.
969  */
970
971 - (void)drawRect:(NSRect)rect
972 {
973   if (xwindow)    // clear to the X window's bg color, not necessarily black.
974     XClearWindow (xdpy, xwindow);
975   else
976     [super drawRect:rect];    // early: black.
977 }
978
979
980 #ifndef USE_BACKBUFFER
981
982 - (void) animateOneFrame
983 {
984   [self render_x11];
985 }
986
987 #else  // USE_BACKBUFFER
988
989 - (void) animateOneFrame
990 {
991   // Render X11 into the backing store bitmap...
992
993   NSAssert (backbuffer, @"no back buffer");
994
995 # ifdef USE_IPHONE
996   UIGraphicsPushContext (backbuffer);
997 # endif
998
999   [self render_x11];
1000
1001 # ifdef USE_IPHONE
1002   UIGraphicsPopContext();
1003 # endif
1004
1005 # ifdef USE_IPHONE
1006   // Then compute the transformations for rotation.
1007
1008   if (!ignore_rotation_p) {
1009     // The rotation origin for layer.affineTransform is in the center already.
1010     CGAffineTransform t =
1011       CGAffineTransformMakeRotation (rot_current_angle / (180.0 / M_PI));
1012
1013     // Correct the aspect ratio.
1014     CGRect frame = [self bounds];
1015     double s = [self hackedContentScaleFactor];
1016     t = CGAffineTransformScale(t,
1017                             backbuffer_size.width  / (s * frame.size.width),
1018                             backbuffer_size.height / (s * frame.size.height));
1019     self.layer.affineTransform = t;
1020   }
1021 # endif // USE_IPHONE
1022
1023   // Then copy that bitmap to the screen, by just stuffing it into
1024   // the layer.  The superclass drawRect method will handle the rest.
1025
1026   CGImageRef img = CGBitmapContextCreateImage (backbuffer);
1027   self.layer.contents = (id)img;
1028   CGImageRelease (img);
1029 }
1030
1031 #endif // !USE_BACKBUFFER
1032
1033
1034
1035 - (void) setFrame:(NSRect) newRect
1036 {
1037   [super setFrame:newRect];
1038
1039   if (xwindow)     // inform Xlib that the window has changed now.
1040     [self resize_x11];
1041 }
1042
1043
1044 # ifndef USE_IPHONE  // Doesn't exist on iOS
1045 - (void) setFrameSize:(NSSize) newSize
1046 {
1047   [super setFrameSize:newSize];
1048   if (xwindow)
1049     [self resize_x11];
1050 }
1051 # endif // !USE_IPHONE
1052
1053
1054 +(BOOL) performGammaFade
1055 {
1056   return YES;
1057 }
1058
1059 - (BOOL) hasConfigureSheet
1060 {
1061   return YES;
1062 }
1063
1064 #ifndef USE_IPHONE
1065 - (NSWindow *) configureSheet
1066 #else
1067 - (UIViewController *) configureView
1068 #endif
1069 {
1070   NSBundle *bundle = [NSBundle bundleForClass:[self class]];
1071   NSString *file = [NSString stringWithCString:xsft->progclass
1072                                       encoding:NSISOLatin1StringEncoding];
1073   file = [file lowercaseString];
1074   NSString *path = [bundle pathForResource:file ofType:@"xml"];
1075   if (!path) {
1076     NSLog (@"%@.xml does not exist in the application bundle: %@/",
1077            file, [bundle resourcePath]);
1078     return nil;
1079   }
1080   
1081 # ifdef USE_IPHONE
1082   UIViewController *sheet;
1083 # else  // !USE_IPHONE
1084   NSWindow *sheet;
1085 # endif // !USE_IPHONE
1086
1087   sheet = [[XScreenSaverConfigSheet alloc]
1088            initWithXMLFile:path
1089            options:xsft->options
1090            controller:[prefsReader userDefaultsController]
1091              defaults:[prefsReader defaultOptions]];
1092
1093   // #### am I expected to retain this, or not? wtf.
1094   //      I thought not, but if I don't do this, we (sometimes) crash.
1095   // #### Analyze says "potential leak of an object stored into sheet"
1096   [sheet retain];
1097
1098   return sheet;
1099 }
1100
1101
1102 - (NSUserDefaultsController *) userDefaultsController
1103 {
1104   return [prefsReader userDefaultsController];
1105 }
1106
1107
1108 /* Announce our willingness to accept keyboard input.
1109 */
1110 - (BOOL)acceptsFirstResponder
1111 {
1112   return YES;
1113 }
1114
1115
1116 #ifndef USE_IPHONE
1117
1118 /* Convert an NSEvent into an XEvent, and pass it along.
1119    Returns YES if it was handled.
1120  */
1121 - (BOOL) doEvent: (NSEvent *) e
1122             type: (int) type
1123 {
1124   if (![self isPreview] ||     // no event handling if actually screen-saving!
1125       ![self isAnimating] ||
1126       !initted_p)
1127     return NO;
1128
1129   XEvent xe;
1130   memset (&xe, 0, sizeof(xe));
1131   
1132   int state = 0;
1133   
1134   int flags = [e modifierFlags];
1135   if (flags & NSAlphaShiftKeyMask) state |= LockMask;
1136   if (flags & NSShiftKeyMask)      state |= ShiftMask;
1137   if (flags & NSControlKeyMask)    state |= ControlMask;
1138   if (flags & NSAlternateKeyMask)  state |= Mod1Mask;
1139   if (flags & NSCommandKeyMask)    state |= Mod2Mask;
1140   
1141   NSPoint p = [[[e window] contentView] convertPoint:[e locationInWindow]
1142                                             toView:self];
1143 # ifdef USE_IPHONE
1144   double s = [self hackedContentScaleFactor];
1145 # else
1146   int s = 1;
1147 # endif
1148   int x = s * p.x;
1149   int y = s * ([self bounds].size.height - p.y);
1150
1151   xe.xany.type = type;
1152   switch (type) {
1153     case ButtonPress:
1154     case ButtonRelease:
1155       xe.xbutton.x = x;
1156       xe.xbutton.y = y;
1157       xe.xbutton.state = state;
1158       if ([e type] == NSScrollWheel)
1159         xe.xbutton.button = ([e deltaY] > 0 ? Button4 :
1160                              [e deltaY] < 0 ? Button5 :
1161                              [e deltaX] > 0 ? Button6 :
1162                              [e deltaX] < 0 ? Button7 :
1163                              0);
1164       else
1165         xe.xbutton.button = [e buttonNumber] + 1;
1166       break;
1167     case MotionNotify:
1168       xe.xmotion.x = x;
1169       xe.xmotion.y = y;
1170       xe.xmotion.state = state;
1171       break;
1172     case KeyPress:
1173     case KeyRelease:
1174       {
1175         NSString *ns = (([e type] == NSFlagsChanged) ? 0 :
1176                         [e charactersIgnoringModifiers]);
1177         KeySym k = 0;
1178
1179         if (!ns || [ns length] == 0)                    // dead key
1180           {
1181             // Cocoa hides the difference between left and right keys.
1182             // Also we only get KeyPress events for these, no KeyRelease
1183             // (unless we hack the mod state manually.  Bleh.)
1184             //
1185             if      (flags & NSAlphaShiftKeyMask)   k = XK_Caps_Lock;
1186             else if (flags & NSShiftKeyMask)        k = XK_Shift_L;
1187             else if (flags & NSControlKeyMask)      k = XK_Control_L;
1188             else if (flags & NSAlternateKeyMask)    k = XK_Alt_L;
1189             else if (flags & NSCommandKeyMask)      k = XK_Meta_L;
1190           }
1191         else if ([ns length] == 1)                      // real key
1192           {
1193             switch ([ns characterAtIndex:0]) {
1194             case NSLeftArrowFunctionKey:  k = XK_Left;      break;
1195             case NSRightArrowFunctionKey: k = XK_Right;     break;
1196             case NSUpArrowFunctionKey:    k = XK_Up;        break;
1197             case NSDownArrowFunctionKey:  k = XK_Down;      break;
1198             case NSPageUpFunctionKey:     k = XK_Page_Up;   break;
1199             case NSPageDownFunctionKey:   k = XK_Page_Down; break;
1200             case NSHomeFunctionKey:       k = XK_Home;      break;
1201             case NSPrevFunctionKey:       k = XK_Prior;     break;
1202             case NSNextFunctionKey:       k = XK_Next;      break;
1203             case NSBeginFunctionKey:      k = XK_Begin;     break;
1204             case NSEndFunctionKey:        k = XK_End;       break;
1205             default:
1206               {
1207                 const char *s =
1208                   [ns cStringUsingEncoding:NSISOLatin1StringEncoding];
1209                 k = (s && *s ? *s : 0);
1210               }
1211               break;
1212             }
1213           }
1214
1215         if (! k) return YES;   // E.g., "KeyRelease XK_Shift_L"
1216
1217         xe.xkey.keycode = k;
1218         xe.xkey.state = state;
1219         break;
1220       }
1221     default:
1222       NSAssert1 (0, @"unknown X11 event type: %d", type);
1223       break;
1224   }
1225
1226   [self lockFocus];
1227   [self prepareContext];
1228   BOOL result = xsft->event_cb (xdpy, xwindow, xdata, &xe);
1229   [self unlockFocus];
1230   return result;
1231 }
1232
1233
1234 - (void) mouseDown: (NSEvent *) e
1235 {
1236   if (! [self doEvent:e type:ButtonPress])
1237     [super mouseDown:e];
1238 }
1239
1240 - (void) mouseUp: (NSEvent *) e
1241 {
1242   if (! [self doEvent:e type:ButtonRelease])
1243     [super mouseUp:e];
1244 }
1245
1246 - (void) otherMouseDown: (NSEvent *) e
1247 {
1248   if (! [self doEvent:e type:ButtonPress])
1249     [super otherMouseDown:e];
1250 }
1251
1252 - (void) otherMouseUp: (NSEvent *) e
1253 {
1254   if (! [self doEvent:e type:ButtonRelease])
1255     [super otherMouseUp:e];
1256 }
1257
1258 - (void) mouseMoved: (NSEvent *) e
1259 {
1260   if (! [self doEvent:e type:MotionNotify])
1261     [super mouseMoved:e];
1262 }
1263
1264 - (void) mouseDragged: (NSEvent *) e
1265 {
1266   if (! [self doEvent:e type:MotionNotify])
1267     [super mouseDragged:e];
1268 }
1269
1270 - (void) otherMouseDragged: (NSEvent *) e
1271 {
1272   if (! [self doEvent:e type:MotionNotify])
1273     [super otherMouseDragged:e];
1274 }
1275
1276 - (void) scrollWheel: (NSEvent *) e
1277 {
1278   if (! [self doEvent:e type:ButtonPress])
1279     [super scrollWheel:e];
1280 }
1281
1282 - (void) keyDown: (NSEvent *) e
1283 {
1284   if (! [self doEvent:e type:KeyPress])
1285     [super keyDown:e];
1286 }
1287
1288 - (void) keyUp: (NSEvent *) e
1289 {
1290   if (! [self doEvent:e type:KeyRelease])
1291     [super keyUp:e];
1292 }
1293
1294 - (void) flagsChanged: (NSEvent *) e
1295 {
1296   if (! [self doEvent:e type:KeyPress])
1297     [super flagsChanged:e];
1298 }
1299
1300 #else  // USE_IPHONE
1301
1302
1303 - (void) stopAndClose:(Bool)relaunch_p
1304 {
1305   if ([self isAnimating])
1306     [self stopAnimation];
1307
1308   /* Need to make the SaverListController be the firstResponder again
1309      so that it can continue to receive its own shake events.  I
1310      suppose that this abstraction-breakage means that I'm adding
1311      XScreenSaverView to the UINavigationController wrong...
1312    */
1313   UIViewController *v = [[self window] rootViewController];
1314   if ([v isKindOfClass: [UINavigationController class]]) {
1315     UINavigationController *n = (UINavigationController *) v;
1316     [[n topViewController] becomeFirstResponder];
1317   }
1318
1319   UIView *fader = [self superview];  // the "backgroundView" view is our parent
1320
1321   if (relaunch_p) {   // Fake a shake on the SaverListController.
1322     // Why is [self window] sometimes null here?
1323     UIWindow *w = [[UIApplication sharedApplication] keyWindow];
1324     UIViewController *v = [w rootViewController];
1325     if ([v isKindOfClass: [UINavigationController class]]) {
1326       UINavigationController *n = (UINavigationController *) v;
1327       [[n topViewController] motionEnded: UIEventSubtypeMotionShake
1328                                withEvent: nil];
1329     }
1330   } else {      // Not launching another, animate our return to the list.
1331     [UIView animateWithDuration: 0.5
1332             animations:^{ fader.alpha = 0.0; }
1333             completion:^(BOOL finished) {
1334                [fader removeFromSuperview];
1335                fader.alpha = 1.0;
1336             }];
1337   }
1338 }
1339
1340
1341 /* Called after the device's orientation has changed.
1342
1343    Note: we could include a subclass of UIViewController which
1344    contains a shouldAutorotateToInterfaceOrientation method that
1345    returns YES, in which case Core Animation would auto-rotate our
1346    View for us in response to rotation events... but, that interacts
1347    badly with the EAGLContext -- if you introduce Core Animation into
1348    the path, the OpenGL pipeline probably falls back on software
1349    rendering and performance goes to hell.  Also, the scaling and
1350    rotation that Core Animation does interacts incorrectly with the GL
1351    context anyway.
1352
1353    So, we have to hack the rotation animation manually, in the GL world.
1354
1355    Possibly XScreenSaverView should use Core Animation, and 
1356    XScreenSaverGLView should override that.
1357 */
1358 - (void)didRotate:(NSNotification *)notification
1359 {
1360   UIDeviceOrientation current = [[UIDevice currentDevice] orientation];
1361
1362   /* If the simulator starts up in the rotated position, sometimes
1363      the UIDevice says we're in Portrait when we're not -- but it
1364      turns out that the UINavigationController knows what's up!
1365      So get it from there.
1366    */
1367   if (current == UIDeviceOrientationUnknown) {
1368     switch ([[[self window] rootViewController] interfaceOrientation]) {
1369     case UIInterfaceOrientationPortrait:
1370       current = UIDeviceOrientationPortrait;
1371       break;
1372     case UIInterfaceOrientationPortraitUpsideDown:
1373       current = UIDeviceOrientationPortraitUpsideDown;
1374       break;
1375     case UIInterfaceOrientationLandscapeLeft:           // It's opposite day
1376       current = UIDeviceOrientationLandscapeRight;
1377       break;
1378     case UIInterfaceOrientationLandscapeRight:
1379       current = UIDeviceOrientationLandscapeLeft;
1380       break;
1381     default:
1382       break;
1383     }
1384   }
1385
1386   /* On the iPad (but not iPhone 3GS, or the simulator) sometimes we get
1387      an orientation change event with an unknown orientation.  Those seem
1388      to always be immediately followed by another orientation change with
1389      a *real* orientation change, so let's try just ignoring those bogus
1390      ones and hoping that the real one comes in shortly...
1391    */
1392   if (current == UIDeviceOrientationUnknown)
1393     return;
1394
1395   if (rotation_ratio >= 0) return;      // in the midst of rotation animation
1396   if (orientation == current) return;   // no change
1397
1398   // When transitioning to FaceUp or FaceDown, pretend there was no change.
1399   if (current == UIDeviceOrientationFaceUp ||
1400       current == UIDeviceOrientationFaceDown)
1401     return;
1402
1403   new_orientation = current;            // current animation target
1404   rotation_ratio = 0;                   // start animating
1405   rot_start_time = double_time();
1406
1407   switch (orientation) {
1408   case UIDeviceOrientationLandscapeLeft:      angle_from = 90;  break;
1409   case UIDeviceOrientationLandscapeRight:     angle_from = 270; break;
1410   case UIDeviceOrientationPortraitUpsideDown: angle_from = 180; break;
1411   default:                                    angle_from = 0;   break;
1412   }
1413
1414   switch (new_orientation) {
1415   case UIDeviceOrientationLandscapeLeft:      angle_to = 90;  break;
1416   case UIDeviceOrientationLandscapeRight:     angle_to = 270; break;
1417   case UIDeviceOrientationPortraitUpsideDown: angle_to = 180; break;
1418   default:                                    angle_to = 0;   break;
1419   }
1420
1421   NSRect ff = [self bounds];
1422
1423   switch (orientation) {
1424   case UIDeviceOrientationLandscapeRight:       // from landscape
1425   case UIDeviceOrientationLandscapeLeft:
1426     rot_from.width  = ff.size.height;
1427     rot_from.height = ff.size.width;
1428     break;
1429   default:                                      // from portrait
1430     rot_from.width  = ff.size.width;
1431     rot_from.height = ff.size.height;
1432     break;
1433   }
1434
1435   switch (new_orientation) {
1436   case UIDeviceOrientationLandscapeRight:       // to landscape
1437   case UIDeviceOrientationLandscapeLeft:
1438     rot_to.width  = ff.size.height;
1439     rot_to.height = ff.size.width;
1440     break;
1441   default:                                      // to portrait
1442     rot_to.width  = ff.size.width;
1443     rot_to.height = ff.size.height;
1444     break;
1445   }
1446
1447  if (! initted_p) {
1448    // If we've done a rotation but the saver hasn't been initialized yet,
1449    // don't bother going through an X11 resize, but just do it now.
1450    rot_start_time = 0;  // dawn of time
1451    [self hackRotation];
1452  }
1453 }
1454
1455
1456 /* I believe we can't use UIGestureRecognizer for tracking touches
1457    because UIPanGestureRecognizer doesn't give us enough detail in its
1458    callbacks.
1459
1460    Currently we don't handle multi-touches (just the first touch) but
1461    I'm leaving this comment here for future reference:
1462
1463    In the simulator, multi-touch sequences look like this:
1464
1465      touchesBegan [touchA, touchB]
1466      touchesEnd [touchA, touchB]
1467
1468    But on real devices, sometimes you get that, but sometimes you get:
1469
1470      touchesBegan [touchA, touchB]
1471      touchesEnd [touchB]
1472      touchesEnd [touchA]
1473
1474    Or even
1475
1476      touchesBegan [touchA]
1477      touchesBegan [touchB]
1478      touchesEnd [touchA]
1479      touchesEnd [touchB]
1480
1481    So the only way to properly detect a "pinch" gesture is to remember
1482    the start-point of each touch as it comes in; and the end-point of
1483    each touch as those come in; and only process the gesture once the
1484    number of touchEnds matches the number of touchBegins.
1485  */
1486
1487 - (void) rotateMouse:(int)rot x:(int*)x y:(int *)y w:(int)w h:(int)h
1488 {
1489   CGRect frame = [self bounds];         // Correct aspect ratio and scale.
1490   double s = [self hackedContentScaleFactor];
1491   *x *= (backbuffer_size.width  / frame.size.width)  / s;
1492   *y *= (backbuffer_size.height / frame.size.height) / s;
1493 }
1494
1495
1496 #if 0  // AudioToolbox/AudioToolbox.h
1497 - (void) beep
1498 {
1499   // There's no way to play a standard system alert sound!
1500   // We'd have to include our own WAV for that.  Eh, fuck it.
1501   AudioServicesPlaySystemSound (kSystemSoundID_Vibrate);
1502 # if TARGET_IPHONE_SIMULATOR
1503   NSLog(@"BEEP");  // The sim doesn't vibrate.
1504 # endif
1505 }
1506 #endif
1507
1508
1509 /* We distinguish between taps and drags.
1510    - Drags (down, motion, up) are sent to the saver to handle.
1511    - Single-taps exit the saver.
1512    This means a saver cannot respond to a single-tap.  Only a few try to.
1513  */
1514
1515 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
1516 {
1517   // If they are trying to pinch, just do nothing.
1518   if ([[event allTouches] count] > 1)
1519     return;
1520
1521   tap_time = 0;
1522
1523   if (xsft->event_cb && xwindow) {
1524     double s = [self hackedContentScaleFactor];
1525     XEvent xe;
1526     memset (&xe, 0, sizeof(xe));
1527     int i = 0;
1528     // #### 'frame' here or 'bounds'?
1529     int w = s * [self frame].size.width;
1530     int h = s * [self frame].size.height;
1531     for (UITouch *touch in touches) {
1532       CGPoint p = [touch locationInView:self];
1533       xe.xany.type = ButtonPress;
1534       xe.xbutton.button = i + 1;
1535       xe.xbutton.button = i + 1;
1536       xe.xbutton.x      = s * p.x;
1537       xe.xbutton.y      = s * p.y;
1538       [self rotateMouse: rot_current_angle
1539             x: &xe.xbutton.x y: &xe.xbutton.y w: w h: h];
1540       jwxyz_mouse_moved (xdpy, xwindow, xe.xbutton.x, xe.xbutton.y);
1541
1542       // Ignore return code: don't care whether the hack handled it.
1543       xsft->event_cb (xdpy, xwindow, xdata, &xe);
1544
1545       // Remember when/where this was, to determine tap versus drag or hold.
1546       tap_time = double_time();
1547       tap_point = p;
1548
1549       i++;
1550       break;  // No pinches: only look at the first touch.
1551     }
1552   }
1553 }
1554
1555
1556 - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
1557 {
1558   // If they are trying to pinch, just do nothing.
1559   if ([[event allTouches] count] > 1)
1560     return;
1561
1562   if (xsft->event_cb && xwindow) {
1563     double s = [self hackedContentScaleFactor];
1564     XEvent xe;
1565     memset (&xe, 0, sizeof(xe));
1566     int i = 0;
1567     // #### 'frame' here or 'bounds'?
1568     int w = s * [self frame].size.width;
1569     int h = s * [self frame].size.height;
1570     for (UITouch *touch in touches) {
1571       CGPoint p = [touch locationInView:self];
1572
1573       // If the ButtonRelease came less than half a second after ButtonPress,
1574       // and didn't move far, then this was a tap, not a drag or a hold.
1575       // Interpret it as "exit".
1576       //
1577       double dist = sqrt (((p.x - tap_point.x) * (p.x - tap_point.x)) +
1578                           ((p.y - tap_point.y) * (p.y - tap_point.y)));
1579       if (tap_time + 0.5 >= double_time() && dist < 20) {
1580         [self stopAndClose:NO];
1581         return;
1582       }
1583
1584       xe.xany.type      = ButtonRelease;
1585       xe.xbutton.button = i + 1;
1586       xe.xbutton.x      = s * p.x;
1587       xe.xbutton.y      = s * p.y;
1588       [self rotateMouse: rot_current_angle
1589             x: &xe.xbutton.x y: &xe.xbutton.y w: w h: h];
1590       jwxyz_mouse_moved (xdpy, xwindow, xe.xbutton.x, xe.xbutton.y);
1591       xsft->event_cb (xdpy, xwindow, xdata, &xe);
1592       i++;
1593       break;  // No pinches: only look at the first touch.
1594     }
1595   }
1596 }
1597
1598
1599 - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
1600 {
1601   // If they are trying to pinch, just do nothing.
1602   if ([[event allTouches] count] > 1)
1603     return;
1604
1605   if (xsft->event_cb && xwindow) {
1606     double s = [self hackedContentScaleFactor];
1607     XEvent xe;
1608     memset (&xe, 0, sizeof(xe));
1609     int i = 0;
1610     // #### 'frame' here or 'bounds'?
1611     int w = s * [self frame].size.width;
1612     int h = s * [self frame].size.height;
1613     for (UITouch *touch in touches) {
1614       CGPoint p = [touch locationInView:self];
1615       xe.xany.type      = MotionNotify;
1616       xe.xmotion.x      = s * p.x;
1617       xe.xmotion.y      = s * p.y;
1618       [self rotateMouse: rot_current_angle
1619             x: &xe.xbutton.x y: &xe.xbutton.y w: w h: h];
1620       jwxyz_mouse_moved (xdpy, xwindow, xe.xmotion.x, xe.xmotion.y);
1621       xsft->event_cb (xdpy, xwindow, xdata, &xe);
1622       i++;
1623       break;  // No pinches: only look at the first touch.
1624     }
1625   }
1626 }
1627
1628
1629 /* We need this to respond to "shake" gestures
1630  */
1631 - (BOOL)canBecomeFirstResponder
1632 {
1633   return YES;
1634 }
1635
1636 - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
1637 {
1638 }
1639
1640
1641 - (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event
1642 {
1643 }
1644
1645 /* Shake means exit and launch a new saver.
1646  */
1647 - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
1648 {
1649   [self stopAndClose:YES];
1650 }
1651
1652
1653 - (void)setScreenLocked:(BOOL)locked
1654 {
1655   if (screenLocked == locked) return;
1656   screenLocked = locked;
1657   if (locked) {
1658     if ([self isAnimating])
1659       [self stopAnimation];
1660   } else {
1661     if (! [self isAnimating])
1662       [self startAnimation];
1663   }
1664 }
1665
1666
1667 #endif // USE_IPHONE
1668
1669
1670 @end
1671
1672 /* Utility functions...
1673  */
1674
1675 static PrefsReader *
1676 get_prefsReader (Display *dpy)
1677 {
1678   XScreenSaverView *view = jwxyz_window_view (XRootWindow (dpy, 0));
1679   if (!view) return 0;
1680   return [view prefsReader];
1681 }
1682
1683
1684 char *
1685 get_string_resource (Display *dpy, char *name, char *class)
1686 {
1687   return [get_prefsReader(dpy) getStringResource:name];
1688 }
1689
1690 Bool
1691 get_boolean_resource (Display *dpy, char *name, char *class)
1692 {
1693   return [get_prefsReader(dpy) getBooleanResource:name];
1694 }
1695
1696 int
1697 get_integer_resource (Display *dpy, char *name, char *class)
1698 {
1699   return [get_prefsReader(dpy) getIntegerResource:name];
1700 }
1701
1702 double
1703 get_float_resource (Display *dpy, char *name, char *class)
1704 {
1705   return [get_prefsReader(dpy) getFloatResource:name];
1706 }