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