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