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