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