From http://www.jwz.org/xscreensaver/xscreensaver-5.27.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
964     if (get_boolean_resource (xdpy, "doFPS", "DoFPS")) {
965       fpst = fps_init (xdpy, xwindow);
966       if (! xsft->fps_cb) xsft->fps_cb = screenhack_do_fps;
967     } else {
968       xsft->fps_cb = 0;
969     }
970
971     [self checkForUpdates];
972   }
973
974
975   /* I don't understand why we have to do this *every frame*, but we do,
976      or else the cursor comes back on.
977    */
978 # ifndef USE_IPHONE
979   if (![self isPreview])
980     [NSCursor setHiddenUntilMouseMoves:YES];
981 # endif
982
983
984   if (fpst)
985     {
986       /* This is just a guess, but the -fps code wants to know how long
987          we were sleeping between frames.
988        */
989       long usecs = 1000000 * [self animationTimeInterval];
990       usecs -= 200;  // caller apparently sleeps for slightly less sometimes...
991       if (usecs < 0) usecs = 0;
992       fps_slept (fpst, usecs);
993     }
994
995
996   /* It turns out that on some systems (possibly only 10.5 and older?)
997      [ScreenSaverView setAnimationTimeInterval] does nothing.  This means
998      that we cannot rely on it.
999
1000      Some of the screen hacks want to delay for long periods, and letting the
1001      framework run the update function at 30 FPS when it really wanted half a
1002      minute between frames would be bad.  So instead, we assume that the
1003      framework's animation timer might fire whenever, but we only invoke the
1004      screen hack's "draw frame" method when enough time has expired.
1005   
1006      This means two extra calls to gettimeofday() per frame.  For fast-cycling
1007      screen savers, that might actually slow them down.  Oh well.
1008
1009      A side-effect of this is that it's not possible for a saver to request
1010      an animation interval that is faster than animationTimeInterval.
1011
1012      HOWEVER!  On modern systems where setAnimationTimeInterval is *not*
1013      ignored, it's important that it be faster than 30 FPS.  120 FPS is good.
1014
1015      An NSTimer won't fire if the timer is already running the invocation
1016      function from a previous firing.  So, if we use a 30 FPS
1017      animationTimeInterval (33333 Âµs) and a screenhack takes 40000 Âµs for a
1018      frame, there will be a 26666 Âµs delay until the next frame, 66666 Âµs
1019      after the beginning of the current frame.  In other words, 25 FPS
1020      becomes 15 FPS.
1021
1022      Frame rates tend to snap to values of 30/N, where N is a positive
1023      integer, i.e. 30 FPS, 15 FPS, 10, 7.5, 6. And the 'snapped' frame rate
1024      is rounded down from what it would normally be.
1025
1026      So if we set animationTimeInterval to 1/120 instead of 1/30, frame rates
1027      become values of 60/N, 120/N, or 240/N, with coarser or finer frame rate
1028      steps for higher or lower animation time intervals respectively.
1029    */
1030   struct timeval tv;
1031   gettimeofday (&tv, 0);
1032   double now = tv.tv_sec + (tv.tv_usec / 1000000.0);
1033   if (now < next_frame_time) return;
1034   
1035   [self prepareContext];
1036
1037   if (resized_p) {
1038     // We do this here instead of in setFrame so that all the
1039     // Xlib drawing takes place under the animation timer.
1040     [self resizeContext];
1041     NSRect r;
1042 # ifndef USE_BACKBUFFER
1043     r = [self bounds];
1044 # else  // USE_BACKBUFFER
1045     r.origin.x = 0;
1046     r.origin.y = 0;
1047     r.size.width  = backbuffer_size.width;
1048     r.size.height = backbuffer_size.height;
1049 # endif // USE_BACKBUFFER
1050
1051     xsft->reshape_cb (xdpy, xwindow, xdata, r.size.width, r.size.height);
1052     resized_p = NO;
1053   }
1054
1055   // Run any XtAppAddInput callbacks now.
1056   // (Note that XtAppAddTimeOut callbacks have already been run by
1057   // the Cocoa event loop.)
1058   //
1059   jwxyz_sources_run (display_sources_data (xdpy));
1060
1061
1062   // And finally:
1063   //
1064 # ifndef USE_IPHONE
1065   NSDisableScreenUpdates();
1066 # endif
1067   unsigned long delay = xsft->draw_cb (xdpy, xwindow, xdata);
1068   if (fpst) xsft->fps_cb (xdpy, xwindow, fpst, xdata);
1069 # ifndef USE_IPHONE
1070   NSEnableScreenUpdates();
1071 # endif
1072
1073   gettimeofday (&tv, 0);
1074   now = tv.tv_sec + (tv.tv_usec / 1000000.0);
1075   next_frame_time = now + (delay / 1000000.0);
1076
1077 # ifdef USE_IPHONE      // Allow savers on the iPhone to run full-tilt.
1078   if (delay < [self animationTimeInterval])
1079     [self setAnimationTimeInterval:(delay / 1000000.0)];
1080 # endif
1081
1082 # ifdef DO_GC_HACKERY
1083   /* Current theory is that the 10.6 garbage collector sucks in the
1084      following way:
1085
1086      It only does a collection when a threshold of outstanding
1087      collectable allocations has been surpassed.  However, CoreGraphics
1088      creates lots of small collectable allocations that contain pointers
1089      to very large non-collectable allocations: a small CG object that's
1090      collectable referencing large malloc'd allocations (non-collectable)
1091      containing bitmap data.  So the large allocation doesn't get freed
1092      until GC collects the small allocation, which triggers its finalizer
1093      to run which frees the large allocation.  So GC is deciding that it
1094      doesn't really need to run, even though the process has gotten
1095      enormous.  GC eventually runs once pageouts have happened, but by
1096      then it's too late, and the machine's resident set has been
1097      sodomized.
1098
1099      So, we force an exhaustive garbage collection in this process
1100      approximately every 5 seconds whether the system thinks it needs 
1101      one or not.
1102   */
1103   {
1104     static int tick = 0;
1105     if (++tick > 5*30) {
1106       tick = 0;
1107       objc_collect (OBJC_EXHAUSTIVE_COLLECTION);
1108     }
1109   }
1110 # endif // DO_GC_HACKERY
1111
1112 # ifdef USE_IPHONE
1113   }
1114   @catch (NSException *e) {
1115     [self handleException: e];
1116   }
1117 # endif // USE_IPHONE
1118 }
1119
1120
1121 /* drawRect always does nothing, and animateOneFrame renders bits to the
1122    screen.  This is (now) true of both X11 and GL on both MacOS and iOS.
1123  */
1124
1125 - (void)drawRect:(NSRect)rect
1126 {
1127   if (xwindow)    // clear to the X window's bg color, not necessarily black.
1128     XClearWindow (xdpy, xwindow);
1129   else
1130     [super drawRect:rect];    // early: black.
1131 }
1132
1133
1134 #ifndef USE_BACKBUFFER
1135
1136 - (void) animateOneFrame
1137 {
1138   [self render_x11];
1139   jwxyz_flush_context(xdpy);
1140 }
1141
1142 #else  // USE_BACKBUFFER
1143
1144 - (void) animateOneFrame
1145 {
1146   // Render X11 into the backing store bitmap...
1147
1148   NSAssert (backbuffer, @"no back buffer");
1149
1150 # ifdef USE_IPHONE
1151   UIGraphicsPushContext (backbuffer);
1152 # endif
1153
1154   [self render_x11];
1155
1156 # ifdef USE_IPHONE
1157   UIGraphicsPopContext();
1158 # endif
1159
1160 # ifdef USE_IPHONE
1161   // Then compute the transformations for rotation.
1162   double hs = [self hackedContentScaleFactor];
1163   double s = [self contentScaleFactor];
1164
1165   // The rotation origin for layer.affineTransform is in the center already.
1166   CGAffineTransform t = ignore_rotation_p ?
1167     CGAffineTransformIdentity :
1168     CGAffineTransformMakeRotation (rot_current_angle / (180.0 / M_PI));
1169
1170   CGFloat f = s / hs;
1171   self.layer.affineTransform = CGAffineTransformScale(t, f, f);
1172
1173   CGRect bounds;
1174   bounds.origin.x = 0;
1175   bounds.origin.y = 0;
1176   bounds.size.width = backbuffer_size.width / s;
1177   bounds.size.height = backbuffer_size.height / s;
1178   self.layer.bounds = bounds;
1179 # endif // USE_IPHONE
1180  
1181 # if defined(BACKBUFFER_CALAYER)
1182   [self.layer setNeedsDisplay];
1183 # elif defined(BACKBUFFER_CGCONTEXT)
1184   size_t
1185     w = CGBitmapContextGetWidth (backbuffer),
1186     h = CGBitmapContextGetHeight (backbuffer);
1187   
1188   size_t bpl = CGBitmapContextGetBytesPerRow (backbuffer);
1189   CGDataProviderRef prov = CGDataProviderCreateWithData (NULL,
1190                                             CGBitmapContextGetData(backbuffer),
1191                                                          bpl * h,
1192                                                          NULL);
1193
1194
1195   CGImageRef img = CGImageCreate (w, h,
1196                                   8, 32,
1197                                   CGBitmapContextGetBytesPerRow(backbuffer),
1198                                   colorspace,
1199                                   CGBitmapContextGetBitmapInfo(backbuffer),
1200                                   prov, NULL, NO,
1201                                   kCGRenderingIntentDefault);
1202
1203   CGDataProviderRelease (prov);
1204   
1205   CGRect rect;
1206   rect.origin.x = 0;
1207   rect.origin.y = 0;
1208   rect.size = backbuffer_size;
1209   CGContextDrawImage (window_ctx, rect, img);
1210   
1211   CGImageRelease (img);
1212
1213   CGContextFlush (window_ctx);
1214 # endif // BACKBUFFER_CGCONTEXT
1215 }
1216
1217 # ifdef BACKBUFFER_CALAYER
1218
1219 - (void) drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
1220 {
1221   // This "isn't safe" if NULL is passed to CGBitmapCreateContext before iOS 4.
1222   char *dest_data = (char *)CGBitmapContextGetData (ctx);
1223
1224   // The CGContext here is normally upside-down on iOS.
1225   if (dest_data &&
1226       CGBitmapContextGetBitmapInfo (ctx) ==
1227         (kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Host)
1228 #  ifdef USE_IPHONE
1229       && CGContextGetCTM (ctx).d < 0
1230 #  endif // USE_IPHONE
1231       )
1232   {
1233     size_t dest_height = CGBitmapContextGetHeight (ctx);
1234     size_t dest_bpr = CGBitmapContextGetBytesPerRow (ctx);
1235     size_t src_height = CGBitmapContextGetHeight (backbuffer);
1236     size_t src_bpr = CGBitmapContextGetBytesPerRow (backbuffer);
1237     char *src_data = (char *)CGBitmapContextGetData (backbuffer);
1238
1239     size_t height = src_height < dest_height ? src_height : dest_height;
1240     
1241     if (src_bpr == dest_bpr) {
1242       // iPad 1: 4.0 ms, iPad 2: 6.7 ms
1243       memcpy (dest_data, src_data, src_bpr * height);
1244     } else {
1245       // iPad 1: 4.6 ms, iPad 2: 7.2 ms
1246       size_t bpr = src_bpr < dest_bpr ? src_bpr : dest_bpr;
1247       while (height) {
1248         memcpy (dest_data, src_data, bpr);
1249         --height;
1250         src_data += src_bpr;
1251         dest_data += dest_bpr;
1252       }
1253     }
1254   } else {
1255
1256     // iPad 1: 9.6 ms, iPad 2: 12.1 ms
1257
1258 #  ifdef USE_IPHONE
1259     CGContextScaleCTM (ctx, 1, -1);
1260     CGFloat s = [self contentScaleFactor];
1261     CGFloat hs = [self hackedContentScaleFactor];
1262     CGContextTranslateCTM (ctx, 0, -backbuffer_size.height * hs / s);
1263 #  endif // USE_IPHONE
1264     
1265     CGImageRef img = CGBitmapContextCreateImage (backbuffer);
1266     CGContextDrawImage (ctx, self.layer.bounds, img);
1267     CGImageRelease (img);
1268   }
1269 }
1270 # endif  // BACKBUFFER_CALAYER
1271
1272 #endif // USE_BACKBUFFER
1273
1274
1275
1276 - (void) setFrame:(NSRect) newRect
1277 {
1278   [super setFrame:newRect];
1279
1280   if (xwindow)     // inform Xlib that the window has changed now.
1281     [self resize_x11];
1282 }
1283
1284
1285 # ifndef USE_IPHONE  // Doesn't exist on iOS
1286 - (void) setFrameSize:(NSSize) newSize
1287 {
1288   [super setFrameSize:newSize];
1289   if (xwindow)
1290     [self resize_x11];
1291 }
1292 # endif // !USE_IPHONE
1293
1294
1295 +(BOOL) performGammaFade
1296 {
1297   return YES;
1298 }
1299
1300 - (BOOL) hasConfigureSheet
1301 {
1302   return YES;
1303 }
1304
1305 + (NSString *) decompressXML: (NSData *)data
1306 {
1307   if (! data) return 0;
1308   BOOL compressed_p = !!strncmp ((const char *) data.bytes, "<?xml", 5);
1309
1310   // If it's not already XML, decompress it.
1311   NSAssert (compressed_p, @"xml isn't compressed");
1312   if (compressed_p) {
1313     NSMutableData *data2 = 0;
1314     int ret = -1;
1315     z_stream zs;
1316     memset (&zs, 0, sizeof(zs));
1317     ret = inflateInit2 (&zs, 16 + MAX_WBITS);
1318     if (ret == Z_OK) {
1319       UInt32 usize = * (UInt32 *) (data.bytes + data.length - 4);
1320       data2 = [NSMutableData dataWithLength: usize];
1321       zs.next_in   = (Bytef *) data.bytes;
1322       zs.avail_in  = (uint) data.length;
1323       zs.next_out  = (Bytef *) data2.bytes;
1324       zs.avail_out = (uint) data2.length;
1325       ret = inflate (&zs, Z_FINISH);
1326       inflateEnd (&zs);
1327     }
1328     if (ret == Z_OK || ret == Z_STREAM_END)
1329       data = data2;
1330     else
1331       NSAssert2 (0, @"gunzip error: %d: %s",
1332                  ret, (zs.msg ? zs.msg : "<null>"));
1333   }
1334
1335   return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
1336 }
1337
1338
1339 #ifndef USE_IPHONE
1340 - (NSWindow *) configureSheet
1341 #else
1342 - (UIViewController *) configureView
1343 #endif
1344 {
1345   NSBundle *bundle = [NSBundle bundleForClass:[self class]];
1346   NSString *file = [NSString stringWithCString:xsft->progclass
1347                                       encoding:NSISOLatin1StringEncoding];
1348   file = [file lowercaseString];
1349   NSString *path = [bundle pathForResource:file ofType:@"xml"];
1350   if (!path) {
1351     NSLog (@"%@.xml does not exist in the application bundle: %@/",
1352            file, [bundle resourcePath]);
1353     return nil;
1354   }
1355   
1356 # ifdef USE_IPHONE
1357   UIViewController *sheet;
1358 # else  // !USE_IPHONE
1359   NSWindow *sheet;
1360 # endif // !USE_IPHONE
1361
1362   NSData *xmld = [NSData dataWithContentsOfFile:path];
1363   NSString *xml = [[self class] decompressXML: xmld];
1364   sheet = [[XScreenSaverConfigSheet alloc]
1365             initWithXML:[xml dataUsingEncoding:NSUTF8StringEncoding]
1366                 options:xsft->options
1367              controller:[prefsReader userDefaultsController]
1368        globalController:[prefsReader globalDefaultsController]
1369                defaults:[prefsReader defaultOptions]];
1370
1371   // #### am I expected to retain this, or not? wtf.
1372   //      I thought not, but if I don't do this, we (sometimes) crash.
1373   // #### Analyze says "potential leak of an object stored into sheet"
1374   // [sheet retain];
1375
1376   return sheet;
1377 }
1378
1379
1380 - (NSUserDefaultsController *) userDefaultsController
1381 {
1382   return [prefsReader userDefaultsController];
1383 }
1384
1385
1386 /* Announce our willingness to accept keyboard input.
1387  */
1388 - (BOOL)acceptsFirstResponder
1389 {
1390   return YES;
1391 }
1392
1393
1394 #ifndef USE_IPHONE
1395
1396 /* Convert an NSEvent into an XEvent, and pass it along.
1397    Returns YES if it was handled.
1398  */
1399 - (BOOL) doEvent: (NSEvent *) e
1400             type: (int) type
1401 {
1402   if (![self isPreview] ||     // no event handling if actually screen-saving!
1403       ![self isAnimating] ||
1404       !initted_p)
1405     return NO;
1406
1407   XEvent xe;
1408   memset (&xe, 0, sizeof(xe));
1409   
1410   int state = 0;
1411   
1412   int flags = [e modifierFlags];
1413   if (flags & NSAlphaShiftKeyMask) state |= LockMask;
1414   if (flags & NSShiftKeyMask)      state |= ShiftMask;
1415   if (flags & NSControlKeyMask)    state |= ControlMask;
1416   if (flags & NSAlternateKeyMask)  state |= Mod1Mask;
1417   if (flags & NSCommandKeyMask)    state |= Mod2Mask;
1418   
1419   NSPoint p = [[[e window] contentView] convertPoint:[e locationInWindow]
1420                                             toView:self];
1421 # ifdef USE_IPHONE
1422   double s = [self hackedContentScaleFactor];
1423 # else
1424   int s = 1;
1425 # endif
1426   int x = s * p.x;
1427   int y = s * ([self bounds].size.height - p.y);
1428
1429   xe.xany.type = type;
1430   switch (type) {
1431     case ButtonPress:
1432     case ButtonRelease:
1433       xe.xbutton.x = x;
1434       xe.xbutton.y = y;
1435       xe.xbutton.state = state;
1436       if ([e type] == NSScrollWheel)
1437         xe.xbutton.button = ([e deltaY] > 0 ? Button4 :
1438                              [e deltaY] < 0 ? Button5 :
1439                              [e deltaX] > 0 ? Button6 :
1440                              [e deltaX] < 0 ? Button7 :
1441                              0);
1442       else
1443         xe.xbutton.button = [e buttonNumber] + 1;
1444       break;
1445     case MotionNotify:
1446       xe.xmotion.x = x;
1447       xe.xmotion.y = y;
1448       xe.xmotion.state = state;
1449       break;
1450     case KeyPress:
1451     case KeyRelease:
1452       {
1453         NSString *ns = (([e type] == NSFlagsChanged) ? 0 :
1454                         [e charactersIgnoringModifiers]);
1455         KeySym k = 0;
1456
1457         if (!ns || [ns length] == 0)                    // dead key
1458           {
1459             // Cocoa hides the difference between left and right keys.
1460             // Also we only get KeyPress events for these, no KeyRelease
1461             // (unless we hack the mod state manually.  Bleh.)
1462             //
1463             if      (flags & NSAlphaShiftKeyMask)   k = XK_Caps_Lock;
1464             else if (flags & NSShiftKeyMask)        k = XK_Shift_L;
1465             else if (flags & NSControlKeyMask)      k = XK_Control_L;
1466             else if (flags & NSAlternateKeyMask)    k = XK_Alt_L;
1467             else if (flags & NSCommandKeyMask)      k = XK_Meta_L;
1468           }
1469         else if ([ns length] == 1)                      // real key
1470           {
1471             switch ([ns characterAtIndex:0]) {
1472             case NSLeftArrowFunctionKey:  k = XK_Left;      break;
1473             case NSRightArrowFunctionKey: k = XK_Right;     break;
1474             case NSUpArrowFunctionKey:    k = XK_Up;        break;
1475             case NSDownArrowFunctionKey:  k = XK_Down;      break;
1476             case NSPageUpFunctionKey:     k = XK_Page_Up;   break;
1477             case NSPageDownFunctionKey:   k = XK_Page_Down; break;
1478             case NSHomeFunctionKey:       k = XK_Home;      break;
1479             case NSPrevFunctionKey:       k = XK_Prior;     break;
1480             case NSNextFunctionKey:       k = XK_Next;      break;
1481             case NSBeginFunctionKey:      k = XK_Begin;     break;
1482             case NSEndFunctionKey:        k = XK_End;       break;
1483             default:
1484               {
1485                 const char *s =
1486                   [ns cStringUsingEncoding:NSISOLatin1StringEncoding];
1487                 k = (s && *s ? *s : 0);
1488               }
1489               break;
1490             }
1491           }
1492
1493         if (! k) return YES;   // E.g., "KeyRelease XK_Shift_L"
1494
1495         xe.xkey.keycode = k;
1496         xe.xkey.state = state;
1497         break;
1498       }
1499     default:
1500       NSAssert1 (0, @"unknown X11 event type: %d", type);
1501       break;
1502   }
1503
1504   [self lockFocus];
1505   [self prepareContext];
1506   BOOL result = xsft->event_cb (xdpy, xwindow, xdata, &xe);
1507   [self unlockFocus];
1508   return result;
1509 }
1510
1511
1512 - (void) mouseDown: (NSEvent *) e
1513 {
1514   if (! [self doEvent:e type:ButtonPress])
1515     [super mouseDown:e];
1516 }
1517
1518 - (void) mouseUp: (NSEvent *) e
1519 {
1520   if (! [self doEvent:e type:ButtonRelease])
1521     [super mouseUp:e];
1522 }
1523
1524 - (void) otherMouseDown: (NSEvent *) e
1525 {
1526   if (! [self doEvent:e type:ButtonPress])
1527     [super otherMouseDown:e];
1528 }
1529
1530 - (void) otherMouseUp: (NSEvent *) e
1531 {
1532   if (! [self doEvent:e type:ButtonRelease])
1533     [super otherMouseUp:e];
1534 }
1535
1536 - (void) mouseMoved: (NSEvent *) e
1537 {
1538   if (! [self doEvent:e type:MotionNotify])
1539     [super mouseMoved:e];
1540 }
1541
1542 - (void) mouseDragged: (NSEvent *) e
1543 {
1544   if (! [self doEvent:e type:MotionNotify])
1545     [super mouseDragged:e];
1546 }
1547
1548 - (void) otherMouseDragged: (NSEvent *) e
1549 {
1550   if (! [self doEvent:e type:MotionNotify])
1551     [super otherMouseDragged:e];
1552 }
1553
1554 - (void) scrollWheel: (NSEvent *) e
1555 {
1556   if (! [self doEvent:e type:ButtonPress])
1557     [super scrollWheel:e];
1558 }
1559
1560 - (void) keyDown: (NSEvent *) e
1561 {
1562   if (! [self doEvent:e type:KeyPress])
1563     [super keyDown:e];
1564 }
1565
1566 - (void) keyUp: (NSEvent *) e
1567 {
1568   if (! [self doEvent:e type:KeyRelease])
1569     [super keyUp:e];
1570 }
1571
1572 - (void) flagsChanged: (NSEvent *) e
1573 {
1574   if (! [self doEvent:e type:KeyPress])
1575     [super flagsChanged:e];
1576 }
1577
1578 #else  // USE_IPHONE
1579
1580
1581 - (void) stopAndClose:(Bool)relaunch_p
1582 {
1583   if ([self isAnimating])
1584     [self stopAnimation];
1585
1586   /* Need to make the SaverListController be the firstResponder again
1587      so that it can continue to receive its own shake events.  I
1588      suppose that this abstraction-breakage means that I'm adding
1589      XScreenSaverView to the UINavigationController wrong...
1590    */
1591   UIViewController *v = [[self window] rootViewController];
1592   if ([v isKindOfClass: [UINavigationController class]]) {
1593     UINavigationController *n = (UINavigationController *) v;
1594     [[n topViewController] becomeFirstResponder];
1595   }
1596
1597   UIView *fader = [self superview];  // the "backgroundView" view is our parent
1598
1599   if (relaunch_p) {   // Fake a shake on the SaverListController.
1600     // Why is [self window] sometimes null here?
1601     UIWindow *w = [[UIApplication sharedApplication] keyWindow];
1602     UIViewController *v = [w rootViewController];
1603     if ([v isKindOfClass: [UINavigationController class]]) {
1604       UINavigationController *n = (UINavigationController *) v;
1605       [[n topViewController] motionEnded: UIEventSubtypeMotionShake
1606                                withEvent: nil];
1607     }
1608   } else {      // Not launching another, animate our return to the list.
1609     [UIView animateWithDuration: 0.5
1610             animations:^{ fader.alpha = 0.0; }
1611             completion:^(BOOL finished) {
1612                [fader removeFromSuperview];
1613                fader.alpha = 1.0;
1614             }];
1615   }
1616 }
1617
1618
1619 /* Called after the device's orientation has changed.
1620
1621    Note: we could include a subclass of UIViewController which
1622    contains a shouldAutorotateToInterfaceOrientation method that
1623    returns YES, in which case Core Animation would auto-rotate our
1624    View for us in response to rotation events... but, that interacts
1625    badly with the EAGLContext -- if you introduce Core Animation into
1626    the path, the OpenGL pipeline probably falls back on software
1627    rendering and performance goes to hell.  Also, the scaling and
1628    rotation that Core Animation does interacts incorrectly with the GL
1629    context anyway.
1630
1631    So, we have to hack the rotation animation manually, in the GL world.
1632
1633    Possibly XScreenSaverView should use Core Animation, and 
1634    XScreenSaverGLView should override that.
1635  */
1636 - (void)didRotate:(NSNotification *)notification
1637 {
1638   UIDeviceOrientation current = [[UIDevice currentDevice] orientation];
1639
1640   /* If the simulator starts up in the rotated position, sometimes
1641      the UIDevice says we're in Portrait when we're not -- but it
1642      turns out that the UINavigationController knows what's up!
1643      So get it from there.
1644    */
1645   if (current == UIDeviceOrientationUnknown) {
1646     switch ([[[self window] rootViewController] interfaceOrientation]) {
1647     case UIInterfaceOrientationPortrait:
1648       current = UIDeviceOrientationPortrait;
1649       break;
1650     case UIInterfaceOrientationPortraitUpsideDown:
1651       current = UIDeviceOrientationPortraitUpsideDown;
1652       break;
1653     case UIInterfaceOrientationLandscapeLeft:           // It's opposite day
1654       current = UIDeviceOrientationLandscapeRight;
1655       break;
1656     case UIInterfaceOrientationLandscapeRight:
1657       current = UIDeviceOrientationLandscapeLeft;
1658       break;
1659     default:
1660       break;
1661     }
1662   }
1663
1664   /* On the iPad (but not iPhone 3GS, or the simulator) sometimes we get
1665      an orientation change event with an unknown orientation.  Those seem
1666      to always be immediately followed by another orientation change with
1667      a *real* orientation change, so let's try just ignoring those bogus
1668      ones and hoping that the real one comes in shortly...
1669    */
1670   if (current == UIDeviceOrientationUnknown)
1671     return;
1672
1673   if (rotation_ratio >= 0) return;      // in the midst of rotation animation
1674   if (orientation == current) return;   // no change
1675
1676   // When transitioning to FaceUp or FaceDown, pretend there was no change.
1677   if (current == UIDeviceOrientationFaceUp ||
1678       current == UIDeviceOrientationFaceDown)
1679     return;
1680
1681   new_orientation = current;            // current animation target
1682   rotation_ratio = 0;                   // start animating
1683   rot_start_time = double_time();
1684
1685   switch (orientation) {
1686   case UIDeviceOrientationLandscapeLeft:      angle_from = 90;  break;
1687   case UIDeviceOrientationLandscapeRight:     angle_from = 270; break;
1688   case UIDeviceOrientationPortraitUpsideDown: angle_from = 180; break;
1689   default:                                    angle_from = 0;   break;
1690   }
1691
1692   switch (new_orientation) {
1693   case UIDeviceOrientationLandscapeLeft:      angle_to = 90;  break;
1694   case UIDeviceOrientationLandscapeRight:     angle_to = 270; break;
1695   case UIDeviceOrientationPortraitUpsideDown: angle_to = 180; break;
1696   default:                                    angle_to = 0;   break;
1697   }
1698
1699   switch (orientation) {
1700   case UIDeviceOrientationLandscapeRight:       // from landscape
1701   case UIDeviceOrientationLandscapeLeft:
1702     rot_from.width  = initial_bounds.height;
1703     rot_from.height = initial_bounds.width;
1704     break;
1705   default:                                      // from portrait
1706     rot_from.width  = initial_bounds.width;
1707     rot_from.height = initial_bounds.height;
1708     break;
1709   }
1710
1711   switch (new_orientation) {
1712   case UIDeviceOrientationLandscapeRight:       // to landscape
1713   case UIDeviceOrientationLandscapeLeft:
1714     rot_to.width  = initial_bounds.height;
1715     rot_to.height = initial_bounds.width;
1716     break;
1717   default:                                      // to portrait
1718     rot_to.width  = initial_bounds.width;
1719     rot_to.height = initial_bounds.height;
1720     break;
1721   }
1722
1723  if (! initted_p) {
1724    // If we've done a rotation but the saver hasn't been initialized yet,
1725    // don't bother going through an X11 resize, but just do it now.
1726    rot_start_time = 0;  // dawn of time
1727    [self hackRotation];
1728  }
1729 }
1730
1731
1732 /* I believe we can't use UIGestureRecognizer for tracking touches
1733    because UIPanGestureRecognizer doesn't give us enough detail in its
1734    callbacks.
1735
1736    Currently we don't handle multi-touches (just the first touch) but
1737    I'm leaving this comment here for future reference:
1738
1739    In the simulator, multi-touch sequences look like this:
1740
1741      touchesBegan [touchA, touchB]
1742      touchesEnd [touchA, touchB]
1743
1744    But on real devices, sometimes you get that, but sometimes you get:
1745
1746      touchesBegan [touchA, touchB]
1747      touchesEnd [touchB]
1748      touchesEnd [touchA]
1749
1750    Or even
1751
1752      touchesBegan [touchA]
1753      touchesBegan [touchB]
1754      touchesEnd [touchA]
1755      touchesEnd [touchB]
1756
1757    So the only way to properly detect a "pinch" gesture is to remember
1758    the start-point of each touch as it comes in; and the end-point of
1759    each touch as those come in; and only process the gesture once the
1760    number of touchEnds matches the number of touchBegins.
1761  */
1762
1763 - (void) rotateMouse:(int)rot x:(int*)x y:(int *)y w:(int)w h:(int)h
1764 {
1765   // This is a no-op unless contentScaleFactor != hackedContentScaleFactor.
1766   // Currently, this is the iPad Retina only.
1767   CGRect frame = [self bounds];         // Scale.
1768   double s = [self hackedContentScaleFactor];
1769   *x *= (backbuffer_size.width  / frame.size.width)  / s;
1770   *y *= (backbuffer_size.height / frame.size.height) / s;
1771 }
1772
1773
1774 #if 0  // AudioToolbox/AudioToolbox.h
1775 - (void) beep
1776 {
1777   // There's no way to play a standard system alert sound!
1778   // We'd have to include our own WAV for that.  Eh, fuck it.
1779   AudioServicesPlaySystemSound (kSystemSoundID_Vibrate);
1780 # if TARGET_IPHONE_SIMULATOR
1781   NSLog(@"BEEP");  // The sim doesn't vibrate.
1782 # endif
1783 }
1784 #endif
1785
1786
1787 /* We distinguish between taps and drags.
1788    - Drags (down, motion, up) are sent to the saver to handle.
1789    - Single-taps exit the saver.
1790    This means a saver cannot respond to a single-tap.  Only a few try to.
1791  */
1792
1793 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
1794 {
1795   // If they are trying to pinch, just do nothing.
1796   if ([[event allTouches] count] > 1)
1797     return;
1798
1799   tap_time = 0;
1800
1801   if (xsft->event_cb && xwindow) {
1802     double s = [self hackedContentScaleFactor];
1803     XEvent xe;
1804     memset (&xe, 0, sizeof(xe));
1805     int i = 0;
1806     // #### 'frame' here or 'bounds'?
1807     int w = s * [self frame].size.width;
1808     int h = s * [self frame].size.height;
1809     for (UITouch *touch in touches) {
1810       CGPoint p = [touch locationInView:self];
1811       xe.xany.type = ButtonPress;
1812       xe.xbutton.button = i + 1;
1813       xe.xbutton.button = i + 1;
1814       xe.xbutton.x      = s * p.x;
1815       xe.xbutton.y      = s * p.y;
1816       [self rotateMouse: rot_current_angle
1817             x: &xe.xbutton.x y: &xe.xbutton.y w: w h: h];
1818       jwxyz_mouse_moved (xdpy, xwindow, xe.xbutton.x, xe.xbutton.y);
1819
1820       // Ignore return code: don't care whether the hack handled it.
1821       xsft->event_cb (xdpy, xwindow, xdata, &xe);
1822
1823       // Remember when/where this was, to determine tap versus drag or hold.
1824       tap_time = double_time();
1825       tap_point = p;
1826
1827       i++;
1828       break;  // No pinches: only look at the first touch.
1829     }
1830   }
1831 }
1832
1833
1834 - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
1835 {
1836   // If they are trying to pinch, just do nothing.
1837   if ([[event allTouches] count] > 1)
1838     return;
1839
1840   if (xsft->event_cb && xwindow) {
1841     double s = [self hackedContentScaleFactor];
1842     XEvent xe;
1843     memset (&xe, 0, sizeof(xe));
1844     int i = 0;
1845     // #### 'frame' here or 'bounds'?
1846     int w = s * [self frame].size.width;
1847     int h = s * [self frame].size.height;
1848     for (UITouch *touch in touches) {
1849       CGPoint p = [touch locationInView:self];
1850
1851       // If the ButtonRelease came less than half a second after ButtonPress,
1852       // and didn't move far, then this was a tap, not a drag or a hold.
1853       // Interpret it as "exit".
1854       //
1855       double dist = sqrt (((p.x - tap_point.x) * (p.x - tap_point.x)) +
1856                           ((p.y - tap_point.y) * (p.y - tap_point.y)));
1857       if (tap_time + 0.5 >= double_time() && dist < 20) {
1858         [self stopAndClose:NO];
1859         return;
1860       }
1861
1862       xe.xany.type      = ButtonRelease;
1863       xe.xbutton.button = i + 1;
1864       xe.xbutton.x      = s * p.x;
1865       xe.xbutton.y      = s * p.y;
1866       [self rotateMouse: rot_current_angle
1867             x: &xe.xbutton.x y: &xe.xbutton.y w: w h: h];
1868       jwxyz_mouse_moved (xdpy, xwindow, xe.xbutton.x, xe.xbutton.y);
1869       xsft->event_cb (xdpy, xwindow, xdata, &xe);
1870       i++;
1871       break;  // No pinches: only look at the first touch.
1872     }
1873   }
1874 }
1875
1876
1877 - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
1878 {
1879   // If they are trying to pinch, just do nothing.
1880   if ([[event allTouches] count] > 1)
1881     return;
1882
1883   if (xsft->event_cb && xwindow) {
1884     double s = [self hackedContentScaleFactor];
1885     XEvent xe;
1886     memset (&xe, 0, sizeof(xe));
1887     int i = 0;
1888     // #### 'frame' here or 'bounds'?
1889     int w = s * [self frame].size.width;
1890     int h = s * [self frame].size.height;
1891     for (UITouch *touch in touches) {
1892       CGPoint p = [touch locationInView:self];
1893       xe.xany.type      = MotionNotify;
1894       xe.xmotion.x      = s * p.x;
1895       xe.xmotion.y      = s * p.y;
1896       [self rotateMouse: rot_current_angle
1897             x: &xe.xbutton.x y: &xe.xbutton.y w: w h: h];
1898       jwxyz_mouse_moved (xdpy, xwindow, xe.xmotion.x, xe.xmotion.y);
1899       xsft->event_cb (xdpy, xwindow, xdata, &xe);
1900       i++;
1901       break;  // No pinches: only look at the first touch.
1902     }
1903   }
1904 }
1905
1906
1907 /* We need this to respond to "shake" gestures
1908  */
1909 - (BOOL)canBecomeFirstResponder
1910 {
1911   return YES;
1912 }
1913
1914 - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
1915 {
1916 }
1917
1918
1919 - (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event
1920 {
1921 }
1922
1923 /* Shake means exit and launch a new saver.
1924  */
1925 - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
1926 {
1927   [self stopAndClose:YES];
1928 }
1929
1930
1931 - (void)setScreenLocked:(BOOL)locked
1932 {
1933   if (screenLocked == locked) return;
1934   screenLocked = locked;
1935   if (locked) {
1936     if ([self isAnimating])
1937       [self stopAnimation];
1938   } else {
1939     if (! [self isAnimating])
1940       [self startAnimation];
1941   }
1942 }
1943
1944 #endif // USE_IPHONE
1945
1946
1947 - (void) checkForUpdates
1948 {
1949 # ifndef USE_IPHONE
1950   // We only check once at startup, even if there are multiple screens,
1951   // and even if this saver is running for many days.
1952   // (Uh, except this doesn't work because this static isn't shared,
1953   // even if we make it an exported global. Not sure why. Oh well.)
1954   static BOOL checked_p = NO;
1955   if (checked_p) return;
1956   checked_p = YES;
1957
1958   // If it's off, don't bother running the updater.  Otherwise, the
1959   // updater will decide if it's time to hit the network.
1960   if (! get_boolean_resource (xdpy,
1961                               SUSUEnableAutomaticChecksKey,
1962                               SUSUEnableAutomaticChecksKey))
1963     return;
1964
1965   NSString *updater = @"XScreenSaverUpdater.app";
1966
1967   // There may be multiple copies of the updater: e.g., one in /Applications
1968   // and one in the mounted installer DMG!  It's important that we run the
1969   // one from the disk and not the DMG, so search for the right one.
1970   //
1971   NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
1972   NSBundle *bundle = [NSBundle bundleForClass:[self class]];
1973   NSArray *search =
1974     @[[[bundle bundlePath] stringByDeletingLastPathComponent],
1975       [@"~/Library/Screen Savers" stringByExpandingTildeInPath],
1976       @"/Library/Screen Savers",
1977       @"/System/Library/Screen Savers",
1978       @"/Applications",
1979       @"/Applications/Utilities"];
1980   NSString *app_path = nil;
1981   for (NSString *dir in search) {
1982     NSString *p = [dir stringByAppendingPathComponent:updater];
1983     if ([[NSFileManager defaultManager] fileExistsAtPath:p]) {
1984       app_path = p;
1985       break;
1986     }
1987   }
1988
1989   if (! app_path)
1990     app_path = [workspace fullPathForApplication:updater];
1991
1992   if (app_path && [app_path hasPrefix:@"/Volumes/XScreenSaver "])
1993     app_path = 0;  // The DMG version will not do.
1994
1995   if (!app_path) {
1996     NSLog(@"Unable to find %@", updater);
1997     return;
1998   }
1999
2000   NSError *err = nil;
2001   if (! [workspace launchApplicationAtURL:[NSURL fileURLWithPath:app_path]
2002                    options:(NSWorkspaceLaunchWithoutAddingToRecents |
2003                             NSWorkspaceLaunchWithoutActivation |
2004                             NSWorkspaceLaunchAndHide)
2005                    configuration:nil
2006                    error:&err]) {
2007     NSLog(@"Unable to launch %@: %@", app_path, err);
2008   }
2009
2010 # endif // !USE_IPHONE
2011 }
2012
2013
2014 @end
2015
2016 /* Utility functions...
2017  */
2018
2019 static PrefsReader *
2020 get_prefsReader (Display *dpy)
2021 {
2022   XScreenSaverView *view = jwxyz_window_view (XRootWindow (dpy, 0));
2023   if (!view) return 0;
2024   return [view prefsReader];
2025 }
2026
2027
2028 char *
2029 get_string_resource (Display *dpy, char *name, char *class)
2030 {
2031   return [get_prefsReader(dpy) getStringResource:name];
2032 }
2033
2034 Bool
2035 get_boolean_resource (Display *dpy, char *name, char *class)
2036 {
2037   return [get_prefsReader(dpy) getBooleanResource:name];
2038 }
2039
2040 int
2041 get_integer_resource (Display *dpy, char *name, char *class)
2042 {
2043   return [get_prefsReader(dpy) getIntegerResource:name];
2044 }
2045
2046 double
2047 get_float_resource (Display *dpy, char *name, char *class)
2048 {
2049   return [get_prefsReader(dpy) getFloatResource:name];
2050 }