From http://www.jwz.org/xscreensaver/xscreensaver-5.34.tar.gz
[xscreensaver] / OSX / XScreenSaverView.m
1 /* xscreensaver, Copyright (c) 2006-2015 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 <sys/mman.h>
20 #import <zlib.h>
21 #import "XScreenSaverView.h"
22 #import "XScreenSaverConfigSheet.h"
23 #import "Updater.h"
24 #import "screenhackI.h"
25 #import "xlockmoreI.h"
26 #import "jwxyz-timers.h"
27
28 #ifndef USE_IPHONE
29 # import <OpenGL/glu.h>
30 #endif
31
32 /* Garbage collection only exists if we are being compiled against the 
33    10.6 SDK or newer, not if we are building against the 10.4 SDK.
34  */
35 #ifndef  MAC_OS_X_VERSION_10_6
36 # define MAC_OS_X_VERSION_10_6 1060  /* undefined in 10.4 SDK, grr */
37 #endif
38 #if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6  /* 10.6 SDK */
39 # import <objc/objc-auto.h>
40 # define DO_GC_HACKERY
41 #endif
42
43 /* Duplicated in xlockmoreI.h and XScreenSaverGLView.m. */
44 extern void clear_gl_error (void);
45 extern void check_gl_error (const char *type);
46
47 extern struct xscreensaver_function_table *xscreensaver_function_table;
48
49 /* Global variables used by the screen savers
50  */
51 const char *progname;
52 const char *progclass;
53 int mono_p = 0;
54
55
56 # ifdef USE_IPHONE
57
58 #  define NSSizeToCGSize(x) (x)
59
60 extern NSDictionary *make_function_table_dict(void);  // ios-function-table.m
61
62 /* Stub definition of the superclass, for iPhone.
63  */
64 @implementation ScreenSaverView
65 {
66   NSTimeInterval anim_interval;
67   Bool animating_p;
68   NSTimer *anim_timer;
69 }
70
71 - (id)initWithFrame:(NSRect)frame isPreview:(BOOL)isPreview {
72   self = [super initWithFrame:frame];
73   if (! self) return 0;
74   anim_interval = 1.0/30;
75   return self;
76 }
77 - (NSTimeInterval)animationTimeInterval { return anim_interval; }
78 - (void)setAnimationTimeInterval:(NSTimeInterval)i { anim_interval = i; }
79 - (BOOL)hasConfigureSheet { return NO; }
80 - (NSWindow *)configureSheet { return nil; }
81 - (NSView *)configureView { return nil; }
82 - (BOOL)isPreview { return NO; }
83 - (BOOL)isAnimating { return animating_p; }
84 - (void)animateOneFrame { }
85
86 - (void)startAnimation {
87   if (animating_p) return;
88   animating_p = YES;
89   anim_timer = [NSTimer scheduledTimerWithTimeInterval: anim_interval
90                         target:self
91                         selector:@selector(animateOneFrame)
92                         userInfo:nil
93                         repeats:YES];
94 }
95
96 - (void)stopAnimation {
97   if (anim_timer) {
98     [anim_timer invalidate];
99     anim_timer = 0;
100   }
101   animating_p = NO;
102 }
103 @end
104
105 # endif // !USE_IPHONE
106
107
108
109 @interface XScreenSaverView (Private)
110 - (void) stopAndClose:(Bool)relaunch;
111 @end
112
113 @implementation XScreenSaverView
114
115 // Given a lower-cased saver name, returns the function table for it.
116 // If no name, guess the name from the class's bundle name.
117 //
118 - (struct xscreensaver_function_table *) findFunctionTable:(NSString *)name
119 {
120   NSBundle *nsb = [NSBundle bundleForClass:[self class]];
121   NSAssert1 (nsb, @"no bundle for class %@", [self class]);
122
123   NSString *path = [nsb bundlePath];
124   CFURLRef url = CFURLCreateWithFileSystemPath(kCFAllocatorDefault,
125                                                (CFStringRef) path,
126                                                kCFURLPOSIXPathStyle,
127                                                true);
128   CFBundleRef cfb = CFBundleCreate (kCFAllocatorDefault, url);
129   CFRelease (url);
130   NSAssert1 (cfb, @"no CFBundle for \"%@\"", path);
131   // #### Analyze says "Potential leak of an object stored into cfb"
132   
133   if (! name)
134     name = [[path lastPathComponent] stringByDeletingPathExtension];
135
136   name = [[name lowercaseString]
137            stringByReplacingOccurrencesOfString:@" "
138            withString:@""];
139
140 # ifndef USE_IPHONE
141   // CFBundleGetDataPointerForName doesn't work in "Archive" builds.
142   // I'm guessing that symbol-stripping is mandatory.  Fuck.
143   NSString *table_name = [name stringByAppendingString:
144                                  @"_xscreensaver_function_table"];
145   void *addr = CFBundleGetDataPointerForName (cfb, (CFStringRef) table_name);
146   CFRelease (cfb);
147
148   if (! addr)
149     NSLog (@"no symbol \"%@\" for \"%@\"", table_name, path);
150
151 # else  // USE_IPHONE
152   // Depends on the auto-generated "ios-function-table.m" being up to date.
153   if (! function_tables)
154     function_tables = [make_function_table_dict() retain];
155   NSValue *v = [function_tables objectForKey: name];
156   void *addr = v ? [v pointerValue] : 0;
157 # endif // USE_IPHONE
158
159   return (struct xscreensaver_function_table *) addr;
160 }
161
162
163 // Add the "Contents/Resources/" subdirectory of this screen saver's .bundle
164 // to $PATH for the benefit of savers that include helper shell scripts.
165 //
166 - (void) setShellPath
167 {
168   NSBundle *nsb = [NSBundle bundleForClass:[self class]];
169   NSAssert1 (nsb, @"no bundle for class %@", [self class]);
170   
171   NSString *nsdir = [nsb resourcePath];
172   NSAssert1 (nsdir, @"no resourcePath for class %@", [self class]);
173   const char *dir = [nsdir cStringUsingEncoding:NSUTF8StringEncoding];
174   const char *opath = getenv ("PATH");
175   if (!opath) opath = "/bin"; // $PATH is unset when running under Shark!
176   char *npath = (char *) malloc (strlen (opath) + strlen (dir) + 30);
177   strcpy (npath, "PATH=");
178   strcat (npath, dir);
179   strcat (npath, ":");
180   strcat (npath, opath);
181   if (putenv (npath)) {
182     perror ("putenv");
183     NSAssert1 (0, @"putenv \"%s\" failed", npath);
184   }
185
186   /* Don't free (npath) -- MacOS's putenv() does not copy it. */
187 }
188
189
190 // set an $XSCREENSAVER_CLASSPATH variable so that included shell scripts
191 // (e.g., "xscreensaver-text") know how to look up resources.
192 //
193 - (void) setResourcesEnv:(NSString *) name
194 {
195   NSBundle *nsb = [NSBundle bundleForClass:[self class]];
196   NSAssert1 (nsb, @"no bundle for class %@", [self class]);
197   
198   const char *s = [name cStringUsingEncoding:NSUTF8StringEncoding];
199   char *env = (char *) malloc (strlen (s) + 40);
200   strcpy (env, "XSCREENSAVER_CLASSPATH=");
201   strcat (env, s);
202   if (putenv (env)) {
203     perror ("putenv");
204     NSAssert1 (0, @"putenv \"%s\" failed", env);
205   }
206   /* Don't free (env) -- MacOS's putenv() does not copy it. */
207 }
208
209
210 static void
211 add_default_options (const XrmOptionDescRec *opts,
212                      const char * const *defs,
213                      XrmOptionDescRec **opts_ret,
214                      const char ***defs_ret)
215 {
216   /* These aren't "real" command-line options (there are no actual command-line
217      options in the Cocoa version); but this is the somewhat kludgey way that
218      the <xscreensaver-text /> and <xscreensaver-image /> tags in the
219      ../hacks/config/\*.xml files communicate with the preferences database.
220   */
221   static const XrmOptionDescRec default_options [] = {
222     { "-text-mode",              ".textMode",          XrmoptionSepArg, 0 },
223     { "-text-literal",           ".textLiteral",       XrmoptionSepArg, 0 },
224     { "-text-file",              ".textFile",          XrmoptionSepArg, 0 },
225     { "-text-url",               ".textURL",           XrmoptionSepArg, 0 },
226     { "-text-program",           ".textProgram",       XrmoptionSepArg, 0 },
227     { "-grab-desktop",           ".grabDesktopImages", XrmoptionNoArg, "True" },
228     { "-no-grab-desktop",        ".grabDesktopImages", XrmoptionNoArg, "False"},
229     { "-choose-random-images",   ".chooseRandomImages",XrmoptionNoArg, "True" },
230     { "-no-choose-random-images",".chooseRandomImages",XrmoptionNoArg, "False"},
231     { "-image-directory",        ".imageDirectory",    XrmoptionSepArg, 0 },
232     { "-fps",                    ".doFPS",             XrmoptionNoArg, "True" },
233     { "-no-fps",                 ".doFPS",             XrmoptionNoArg, "False"},
234     { "-foreground",             ".foreground",        XrmoptionSepArg, 0 },
235     { "-fg",                     ".foreground",        XrmoptionSepArg, 0 },
236     { "-background",             ".background",        XrmoptionSepArg, 0 },
237     { "-bg",                     ".background",        XrmoptionSepArg, 0 },
238
239 # ifndef USE_IPHONE
240     // <xscreensaver-updater />
241     {    "-" SUSUEnableAutomaticChecksKey,
242          "." SUSUEnableAutomaticChecksKey, XrmoptionNoArg, "True"  },
243     { "-no-" SUSUEnableAutomaticChecksKey,
244          "." SUSUEnableAutomaticChecksKey, XrmoptionNoArg, "False" },
245     {    "-" SUAutomaticallyUpdateKey,
246          "." SUAutomaticallyUpdateKey, XrmoptionNoArg, "True"  },
247     { "-no-" SUAutomaticallyUpdateKey,
248          "." SUAutomaticallyUpdateKey, XrmoptionNoArg, "False" },
249     {    "-" SUSendProfileInfoKey,
250          "." SUSendProfileInfoKey, XrmoptionNoArg,"True" },
251     { "-no-" SUSendProfileInfoKey,
252          "." SUSendProfileInfoKey, XrmoptionNoArg,"False"},
253     {    "-" SUScheduledCheckIntervalKey,
254          "." SUScheduledCheckIntervalKey, XrmoptionSepArg, 0 },
255 # endif // !USE_IPHONE
256
257     { 0, 0, 0, 0 }
258   };
259   static const char *default_defaults [] = {
260     ".doFPS:              False",
261     ".doubleBuffer:       True",
262     ".multiSample:        False",
263 # ifndef USE_IPHONE
264     ".textMode:           date",
265 # else
266     ".textMode:           url",
267 # endif
268  // ".textLiteral:        ",
269  // ".textFile:           ",
270     ".textURL:            https://en.wikipedia.org/w/index.php?title=Special:NewPages&feed=rss",
271  // ".textProgram:        ",
272     ".grabDesktopImages:  yes",
273 # ifndef USE_IPHONE
274     ".chooseRandomImages: no",
275 # else
276     ".chooseRandomImages: yes",
277 # endif
278     ".imageDirectory:     ~/Pictures",
279     ".relaunchDelay:      2",
280     ".texFontCacheSize:   30",
281
282 # ifndef USE_IPHONE
283 #  define STR1(S) #S
284 #  define STR(S) STR1(S)
285 #  define __objc_yes Yes
286 #  define __objc_no  No
287     "." SUSUEnableAutomaticChecksKey ": " STR(SUSUEnableAutomaticChecksDef),
288     "." SUAutomaticallyUpdateKey ":  "    STR(SUAutomaticallyUpdateDef),
289     "." SUSendProfileInfoKey ": "         STR(SUSendProfileInfoDef),
290     "." SUScheduledCheckIntervalKey ": "  STR(SUScheduledCheckIntervalDef),
291 #  undef __objc_yes
292 #  undef __objc_no
293 #  undef STR1
294 #  undef STR
295 # endif // USE_IPHONE
296     0
297   };
298
299   int count = 0, i, j;
300   for (i = 0; default_options[i].option; i++)
301     count++;
302   for (i = 0; opts[i].option; i++)
303     count++;
304
305   XrmOptionDescRec *opts2 = (XrmOptionDescRec *) 
306     calloc (count + 1, sizeof (*opts2));
307
308   i = 0;
309   j = 0;
310   while (default_options[j].option) {
311     opts2[i] = default_options[j];
312     i++, j++;
313   }
314   j = 0;
315   while (opts[j].option) {
316     opts2[i] = opts[j];
317     i++, j++;
318   }
319
320   *opts_ret = opts2;
321
322
323   /* now the defaults
324    */
325   count = 0;
326   for (i = 0; default_defaults[i]; i++)
327     count++;
328   for (i = 0; defs[i]; i++)
329     count++;
330
331   const char **defs2 = (const char **) calloc (count + 1, sizeof (*defs2));
332
333   i = 0;
334   j = 0;
335   while (default_defaults[j]) {
336     defs2[i] = default_defaults[j];
337     i++, j++;
338   }
339   j = 0;
340   while (defs[j]) {
341     defs2[i] = defs[j];
342     i++, j++;
343   }
344
345   *defs_ret = defs2;
346 }
347
348
349 #ifdef USE_IPHONE
350 /* Returns the current time in seconds as a double.
351  */
352 static double
353 double_time (void)
354 {
355   struct timeval now;
356 # ifdef GETTIMEOFDAY_TWO_ARGS
357   struct timezone tzp;
358   gettimeofday(&now, &tzp);
359 # else
360   gettimeofday(&now);
361 # endif
362
363   return (now.tv_sec + ((double) now.tv_usec * 0.000001));
364 }
365 #endif // USE_IPHONE
366
367 #if TARGET_IPHONE_SIMULATOR
368 static const char *
369 orientname(unsigned long o)
370 {
371   switch (o) {
372   case UIDeviceOrientationUnknown:              return "Unknown";
373   case UIDeviceOrientationPortrait:             return "Portrait";
374   case UIDeviceOrientationPortraitUpsideDown:   return "PortraitUpsideDown";
375   case UIDeviceOrientationLandscapeLeft:        return "LandscapeLeft";
376   case UIDeviceOrientationLandscapeRight:       return "LandscapeRight";
377   case UIDeviceOrientationFaceUp:               return "FaceUp";
378   case UIDeviceOrientationFaceDown:             return "FaceDown";
379   default:                                      return "ERROR";
380   }
381 }
382 #endif // TARGET_IPHONE_SIMULATOR
383
384
385 - (id) initWithFrame:(NSRect)frame
386            saverName:(NSString *)saverName
387            isPreview:(BOOL)isPreview
388 {
389   if (! (self = [super initWithFrame:frame isPreview:isPreview]))
390     return 0;
391   
392   xsft = [self findFunctionTable: saverName];
393   if (! xsft) {
394     [self release];
395     return 0;
396   }
397
398   [self setShellPath];
399
400   setup_p = YES;
401   if (xsft->setup_cb)
402     xsft->setup_cb (xsft, xsft->setup_arg);
403
404   /* The plist files for these preferences show up in
405      $HOME/Library/Preferences/ByHost/ in a file named like
406      "org.jwz.xscreensaver.<SAVERNAME>.<NUMBERS>.plist"
407    */
408   NSString *name = [NSString stringWithCString:xsft->progclass
409                              encoding:NSISOLatin1StringEncoding];
410   name = [@"org.jwz.xscreensaver." stringByAppendingString:name];
411   [self setResourcesEnv:name];
412
413   
414   XrmOptionDescRec *opts = 0;
415   const char **defs = 0;
416   add_default_options (xsft->options, xsft->defaults, &opts, &defs);
417   prefsReader = [[PrefsReader alloc]
418                   initWithName:name xrmKeys:opts defaults:defs];
419   free (defs);
420   // free (opts);  // bah, we need these! #### leak!
421   xsft->options = opts;
422   
423   progname = progclass = xsft->progclass;
424
425   next_frame_time = 0;
426
427 # ifndef USE_IPHONE
428   // When the view fills the screen and double buffering is enabled, OS X will
429   // use page flipping for a minor CPU/FPS boost. In windowed mode, double
430   // buffering reduces the frame rate to 1/2 the screen's refresh rate.
431   double_buffered_p = !isPreview;
432 # endif
433
434 # ifdef USE_IPHONE
435   double s = [self hackedContentScaleFactor];
436 # else
437   double s = 1;
438 # endif
439
440   CGSize bb_size;       // pixels, not points
441   bb_size.width  = s * frame.size.width;
442   bb_size.height = s * frame.size.height;
443
444 # ifdef USE_IPHONE
445   initial_bounds = rot_current_size = rot_from = rot_to = bb_size;
446   rotation_ratio = -1;
447
448   orientation = UIDeviceOrientationUnknown;
449   [self didRotate:nil];
450   [self initGestures];
451
452   // So we can tell when we're docked.
453   [UIDevice currentDevice].batteryMonitoringEnabled = YES;
454
455   [self setBackgroundColor:[NSColor blackColor]];
456
457   [[NSNotificationCenter defaultCenter]
458    addObserver:self
459    selector:@selector(didRotate:)
460    name:UIDeviceOrientationDidChangeNotification object:nil];
461 # endif // USE_IPHONE
462
463   return self;
464 }
465
466
467 #ifdef USE_IPHONE
468 + (Class) layerClass
469 {
470   return [CAEAGLLayer class];
471 }
472 #endif
473
474
475 - (id) initWithFrame:(NSRect)frame isPreview:(BOOL)p
476 {
477   return [self initWithFrame:frame saverName:0 isPreview:p];
478 }
479
480
481 - (void) dealloc
482 {
483   if ([self isAnimating])
484     [self stopAnimation];
485   NSAssert(!xdata, @"xdata not yet freed");
486   NSAssert(!xdpy, @"xdpy not yet freed");
487
488 # ifdef USE_IPHONE
489   [[NSNotificationCenter defaultCenter] removeObserver:self];
490 # endif
491
492 # ifdef USE_BACKBUFFER
493
494 #  ifdef BACKBUFFER_OPENGL
495   [ogl_ctx release];
496   // Releasing the OpenGL context should also free any OpenGL objects,
497   // including the backbuffer texture and frame/render/depthbuffers.
498 #  endif // BACKBUFFER_OPENGL
499
500   if (colorspace)
501     CGColorSpaceRelease (colorspace);
502
503 # endif // USE_BACKBUFFER
504
505   [prefsReader release];
506
507   // xsft
508   // fpst
509
510   [super dealloc];
511 }
512
513 - (PrefsReader *) prefsReader
514 {
515   return prefsReader;
516 }
517
518
519 #ifdef USE_IPHONE
520 - (void) lockFocus { }
521 - (void) unlockFocus { }
522 #endif // USE_IPHONE
523
524
525
526 # ifdef USE_IPHONE
527 /* A few seconds after the saver launches, we store the "wasRunning"
528    preference.  This is so that if the saver is crashing at startup,
529    we don't launch it again next time, getting stuck in a crash loop.
530  */
531 - (void) allSystemsGo: (NSTimer *) timer
532 {
533   NSAssert (timer == crash_timer, @"crash timer screwed up");
534   crash_timer = 0;
535
536   NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
537   [prefs setBool:YES forKey:@"wasRunning"];
538   [prefs synchronize];
539 }
540 #endif // USE_IPHONE
541
542
543 - (void) startAnimation
544 {
545   NSAssert(![self isAnimating], @"already animating");
546   NSAssert(!initted_p && !xdata, @"already initialized");
547
548   // See comment in render_x11() for why this value is important:
549   [self setAnimationTimeInterval: 1.0 / 240.0];
550
551   [super startAnimation];
552   /* We can't draw on the window from this method, so we actually do the
553      initialization of the screen saver (xsft->init_cb) in the first call
554      to animateOneFrame() instead.
555    */
556
557 # ifdef USE_IPHONE
558   {
559     CGSize b = self.bounds.size;
560     double s = [self hackedContentScaleFactor];
561     b.width  *= s;
562     b.height *= s;
563     NSAssert (initial_bounds.width == b.width &&
564               initial_bounds.height == b.height,
565               @"bounds changed unexpectedly");
566   }
567
568   if (crash_timer)
569     [crash_timer invalidate];
570
571   NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
572   [prefs removeObjectForKey:@"wasRunning"];
573   [prefs synchronize];
574
575   crash_timer = [NSTimer scheduledTimerWithTimeInterval: 5
576                          target:self
577                          selector:@selector(allSystemsGo:)
578                          userInfo:nil
579                          repeats:NO];
580
581 # endif // USE_IPHONE
582
583   // Never automatically turn the screen off if we are docked,
584   // and an animation is running.
585   //
586 # ifdef USE_IPHONE
587   [UIApplication sharedApplication].idleTimerDisabled =
588     ([UIDevice currentDevice].batteryState != UIDeviceBatteryStateUnplugged);
589   [[UIApplication sharedApplication]
590     setStatusBarHidden:YES withAnimation:UIStatusBarAnimationNone];
591 # endif
592
593 #ifdef BACKBUFFER_OPENGL
594   CGSize new_backbuffer_size;
595
596   {
597 # ifndef USE_IPHONE
598     if (!ogl_ctx) {
599
600       NSOpenGLPixelFormat *pixfmt = [self getGLPixelFormat];
601
602       NSAssert (pixfmt, @"unable to create NSOpenGLPixelFormat");
603
604       [pixfmt retain]; // #### ???
605
606       // Fun: On OS X 10.7, the second time an OpenGL context is created, after
607       // the preferences dialog is launched in SaverTester, the context only
608       // lasts until the first full GC. Then it turns black. Solution is to
609       // reuse the OpenGL context after this point.
610       ogl_ctx = [[NSOpenGLContext alloc] initWithFormat:pixfmt
611                                          shareContext:nil];
612
613       // Sync refreshes to the vertical blanking interval
614       GLint r = 1;
615       [ogl_ctx setValues:&r forParameter:NSOpenGLCPSwapInterval];
616 //    check_gl_error ("NSOpenGLCPSwapInterval");  // SEGV sometimes. Too early?
617     }
618
619     [ogl_ctx makeCurrentContext];
620     check_gl_error ("makeCurrentContext");
621
622     // NSOpenGLContext logs an 'invalid drawable' when this is called
623     // from initWithFrame.
624     [ogl_ctx setView:self];
625
626     // Clear frame buffer ASAP, else there are bits left over from other apps.
627     glClearColor (0, 0, 0, 1);
628     glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
629 //    glFinish ();
630 //    glXSwapBuffers (mi->dpy, mi->window);
631
632
633     // Enable multi-threading, if possible.  This runs most OpenGL commands
634     // and GPU management on a second CPU.
635     {
636 #  ifndef  kCGLCEMPEngine
637 #   define kCGLCEMPEngine 313  // Added in MacOS 10.4.8 + XCode 2.4.
638 #  endif
639       CGLContextObj cctx = CGLGetCurrentContext();
640       CGLError err = CGLEnable (cctx, kCGLCEMPEngine);
641       if (err != kCGLNoError) {
642         NSLog (@"enabling multi-threaded OpenGL failed: %d", err);
643       }
644     }
645
646     new_backbuffer_size = NSSizeToCGSize ([self bounds].size);
647
648 # else  // USE_IPHONE
649     if (!ogl_ctx) {
650       CAEAGLLayer *eagl_layer = (CAEAGLLayer *) self.layer;
651       eagl_layer.opaque = TRUE;
652       eagl_layer.drawableProperties = [self getGLProperties];
653
654       // Without this, the GL frame buffer is half the screen resolution!
655       eagl_layer.contentsScale = [UIScreen mainScreen].scale;
656
657       ogl_ctx = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES1];
658     }
659
660     [EAGLContext setCurrentContext: ogl_ctx];
661
662     CGSize screen_size = [[[UIScreen mainScreen] currentMode] size];
663     // iPad, simulator: 768x1024
664     // iPad, physical: 1024x768
665     if (screen_size.width > screen_size.height) {
666       CGFloat w = screen_size.width;
667       screen_size.width = screen_size.height;
668       screen_size.height = w;
669     }
670
671     if (gl_framebuffer)  glDeleteFramebuffersOES  (1, &gl_framebuffer);
672     if (gl_renderbuffer) glDeleteRenderbuffersOES (1, &gl_renderbuffer);
673
674     glGenFramebuffersOES  (1, &gl_framebuffer);
675     glBindFramebufferOES  (GL_FRAMEBUFFER_OES,  gl_framebuffer);
676
677     glGenRenderbuffersOES (1, &gl_renderbuffer);
678     glBindRenderbufferOES (GL_RENDERBUFFER_OES, gl_renderbuffer);
679
680 //   redundant?
681 //     glRenderbufferStorageOES (GL_RENDERBUFFER_OES, GL_RGBA8_OES,
682 //                               (int)size.width, (int)size.height);
683     [ogl_ctx renderbufferStorage:GL_RENDERBUFFER_OES
684                     fromDrawable:(CAEAGLLayer*)self.layer];
685
686     glFramebufferRenderbufferOES (GL_FRAMEBUFFER_OES,  GL_COLOR_ATTACHMENT0_OES,
687                                   GL_RENDERBUFFER_OES, gl_renderbuffer);
688
689     [self addExtraRenderbuffers:screen_size];
690
691     int err = glCheckFramebufferStatusOES (GL_FRAMEBUFFER_OES);
692     switch (err) {
693       case GL_FRAMEBUFFER_COMPLETE_OES:
694         break;
695       case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT_OES:
696         NSAssert (0, @"framebuffer incomplete attachment");
697         break;
698       case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT_OES:
699         NSAssert (0, @"framebuffer incomplete missing attachment");
700         break;
701       case GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_OES:
702         NSAssert (0, @"framebuffer incomplete dimensions");
703         break;
704       case GL_FRAMEBUFFER_INCOMPLETE_FORMATS_OES:
705         NSAssert (0, @"framebuffer incomplete formats");
706         break;
707       case GL_FRAMEBUFFER_UNSUPPORTED_OES:
708         NSAssert (0, @"framebuffer unsupported");
709         break;
710 /*
711       case GL_FRAMEBUFFER_UNDEFINED:
712         NSAssert (0, @"framebuffer undefined");
713         break;
714       case GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER:
715         NSAssert (0, @"framebuffer incomplete draw buffer");
716         break;
717       case GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER:
718         NSAssert (0, @"framebuffer incomplete read buffer");
719         break;
720       case GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE:
721         NSAssert (0, @"framebuffer incomplete multisample");
722         break;
723       case GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS:
724         NSAssert (0, @"framebuffer incomplete layer targets");
725         break;
726  */
727     default:
728       NSAssert (0, @"framebuffer incomplete, unknown error 0x%04X", err);
729       break;
730     }
731
732     glViewport (0, 0, screen_size.width, screen_size.height);
733
734     new_backbuffer_size = initial_bounds;
735
736 # endif // USE_IPHONE
737
738     check_gl_error ("startAnimation");
739
740 //  NSLog (@"%s / %s / %s\n", glGetString (GL_VENDOR),
741 //         glGetString (GL_RENDERER), glGetString (GL_VERSION));
742
743     [self enableBackbuffer:new_backbuffer_size];
744   }
745 #endif // BACKBUFFER_OPENGL
746
747 #ifdef USE_BACKBUFFER
748   [self createBackbuffer:new_backbuffer_size];
749 #endif
750 }
751
752 - (void)stopAnimation
753 {
754   NSAssert([self isAnimating], @"not animating");
755
756   if (initted_p) {
757
758     [self lockFocus];       // in case something tries to draw from here
759     [self prepareContext];
760
761     /* I considered just not even calling the free callback at all...
762        But webcollage-cocoa needs it, to kill the inferior webcollage
763        processes (since the screen saver framework never generates a
764        SIGPIPE for them...)  Instead, I turned off the free call in
765        xlockmore.c, which is where all of the bogus calls are anyway.
766      */
767     xsft->free_cb (xdpy, xwindow, xdata);
768     [self unlockFocus];
769
770     // xdpy must be freed before dealloc is called, because xdpy owns a
771     // circular reference to the parent XScreenSaverView.
772     jwxyz_free_display (xdpy);
773     xdpy = NULL;
774     xwindow = NULL;
775
776 //  setup_p = NO; // #### wait, do we need this?
777     initted_p = NO;
778     xdata = 0;
779   }
780
781 # ifdef USE_IPHONE
782   if (crash_timer)
783     [crash_timer invalidate];
784   crash_timer = 0;
785   NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
786   [prefs removeObjectForKey:@"wasRunning"];
787   [prefs synchronize];
788 # endif // USE_IPHONE
789
790   [super stopAnimation];
791
792   // When an animation is no longer running (e.g., looking at the list)
793   // then it's ok to power off the screen when docked.
794   //
795 # ifdef USE_IPHONE
796   [UIApplication sharedApplication].idleTimerDisabled = NO;
797   [[UIApplication sharedApplication]
798     setStatusBarHidden:NO withAnimation:UIStatusBarAnimationNone];
799 # endif
800
801   // Without this, the GL frame stays on screen when switching tabs
802   // in System Preferences.
803   // (Or perhaps it used to. It doesn't seem to matter on 10.9.)
804   //
805 # ifndef USE_IPHONE
806   [NSOpenGLContext clearCurrentContext];
807 # endif // !USE_IPHONE
808
809   clear_gl_error();     // This hack is defunct, don't let this linger.
810
811   CGContextRelease (backbuffer);
812   backbuffer = nil;
813
814   if (backbuffer_len)
815     munmap (backbuffer_data, backbuffer_len);
816   backbuffer_data = NULL;
817   backbuffer_len = 0;
818 }
819
820
821 // #### maybe this could/should just be on 'lockFocus' instead?
822 - (void) prepareContext
823 {
824   if (ogl_ctx) {
825 #ifdef USE_IPHONE
826     [EAGLContext setCurrentContext:ogl_ctx];
827 #else  // !USE_IPHONE
828     [ogl_ctx makeCurrentContext];
829 //    check_gl_error ("makeCurrentContext");
830 #endif // !USE_IPHONE
831   }
832 }
833
834
835 static void
836 screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
837 {
838   fps_compute (fpst, 0, -1);
839   fps_draw (fpst);
840 }
841
842
843 #ifdef USE_IPHONE
844
845 /* On iPhones with Retina displays, we can draw the savers in "real"
846    pixels, and that works great.  The 320x480 "point" screen is really
847    a 640x960 *pixel* screen.  However, Retina iPads have 768x1024
848    point screens which are 1536x2048 pixels, and apparently that's
849    enough pixels that copying those bits to the screen is slow.  Like,
850    drops us from 15fps to 7fps.  So, on Retina iPads, we don't draw in
851    real pixels.  This will probably make the savers look better
852    anyway, since that's a higher resolution than most desktop monitors
853    have even today.  (This is only true for X11 programs, not GL 
854    programs.  Those are fine at full rez.)
855
856    This method is overridden in XScreenSaverGLView, since this kludge
857    isn't necessary for GL programs, being resolution independent by
858    nature.
859  */
860 - (CGFloat) hackedContentScaleFactor
861 {
862   NSSize ssize = [[[UIScreen mainScreen] currentMode] size];
863   NSSize bsize = [self bounds].size;
864
865   CGFloat
866     max_ssize = ssize.width > ssize.height ? ssize.width : ssize.height,
867     max_bsize = bsize.width > bsize.height ? bsize.width : bsize.height;
868
869   // Ratio of screen size in pixels to view size in points.
870   CGFloat s = max_ssize / max_bsize;
871
872   // Two constraints:
873
874   // 1. Don't exceed -- let's say 1280 pixels in either direction.
875   //    (Otherwise the frame rate gets bad.)
876   CGFloat mag0 = ceil(max_ssize / 1280);
877
878   // 2. Don't let the pixel size get too small.
879   //    (Otherwise pixels in IFS and similar are too fine.)
880   //    So don't let the result be > 2 pixels per point.
881   CGFloat mag1 = ceil(s / 2);
882
883   // As of iPhone 6, mag0 is always >= mag1. This may not be true in the future.
884   // (desired scale factor) = s / (desired magnification factor)
885   return s / (mag0 > mag1 ? mag0 : mag1);
886 }
887
888
889 static GLfloat _global_rot_current_angle_kludge;
890
891 double current_device_rotation (void)
892 {
893   return -_global_rot_current_angle_kludge;
894 }
895
896
897 - (void) hackRotation
898 {
899   if (rotation_ratio >= 0) {    // in the midst of a rotation animation
900
901 #   define CLAMP180(N) while (N < 0) N += 360; while (N > 180) N -= 360
902     GLfloat f = angle_from;
903     GLfloat t = angle_to;
904     CLAMP180(f);
905     CLAMP180(t);
906     GLfloat dist = -(t-f);
907     CLAMP180(dist);
908
909     // Intermediate angle.
910     rot_current_angle = f - rotation_ratio * dist;
911
912     // Intermediate frame size.
913     rot_current_size.width = floor(rot_from.width +
914       rotation_ratio * (rot_to.width - rot_from.width));
915     rot_current_size.height = floor(rot_from.height +
916       rotation_ratio * (rot_to.height - rot_from.height));
917
918     // Tick animation.  Complete rotation in 1/6th sec.
919     double now = double_time();
920     double duration = 1/6.0;
921     rotation_ratio = 1 - ((rot_start_time + duration - now) / duration);
922
923     if (rotation_ratio > 1 || ignore_rotation_p) {      // Done animating.
924       orientation = new_orientation;
925       rot_current_angle = angle_to;
926       rot_current_size = rot_to;
927       rotation_ratio = -1;
928
929 # if TARGET_IPHONE_SIMULATOR
930       NSLog (@"rotation ended: %s %d, %d x %d",
931              orientname(orientation), (int) rot_current_angle,
932              (int) rot_current_size.width, (int) rot_current_size.height);
933 # endif
934
935       // Check orientation again in case we rotated again while rotating:
936       // this is a no-op if nothing has changed.
937       [self didRotate:nil];
938     }
939   } else {                      // Not animating a rotation.
940     rot_current_angle = angle_to;
941     rot_current_size = rot_to;
942   }
943
944   CLAMP180(rot_current_angle);
945   _global_rot_current_angle_kludge = rot_current_angle;
946
947 #   undef CLAMP180
948
949   CGSize rotsize = ((ignore_rotation_p || ![self reshapeRotatedWindow])
950                     ? initial_bounds
951                     : rot_current_size);
952   if ((int) backbuffer_size.width  != (int) rotsize.width ||
953       (int) backbuffer_size.height != (int) rotsize.height)
954     [self resize_x11];
955 }
956
957
958 - (void)alertView:(UIAlertView *)av clickedButtonAtIndex:(NSInteger)i
959 {
960   if (i == 0) exit (-1);        // Cancel
961   [self stopAndClose:NO];       // Keep going
962 }
963
964 - (void) handleException: (NSException *)e
965 {
966   NSLog (@"Caught exception: %@", e);
967   [[[UIAlertView alloc] initWithTitle:
968                           [NSString stringWithFormat: @"%s crashed!",
969                                     xsft->progclass]
970                         message:
971                           [NSString stringWithFormat:
972                                       @"The error message was:"
973                                     "\n\n%@\n\n"
974                                     "If it keeps crashing, try "
975                                     "resetting its options.",
976                                     e]
977                         delegate: self
978                         cancelButtonTitle: @"Exit"
979                         otherButtonTitles: @"Keep going", nil]
980     show];
981   [self stopAnimation];
982 }
983
984 #endif // USE_IPHONE
985
986
987 #ifdef USE_BACKBUFFER
988
989 # ifndef USE_IPHONE
990
991 struct gl_version
992 {
993   // iOS always uses OpenGL ES 1.1.
994   unsigned major;
995   unsigned minor;
996 };
997
998 static GLboolean
999 gl_check_ver (const struct gl_version *caps,
1000               unsigned gl_major,
1001               unsigned gl_minor)
1002 {
1003   return caps->major > gl_major ||
1004            (caps->major == gl_major && caps->minor >= gl_minor);
1005 }
1006
1007 # else
1008
1009 static GLboolean
1010 gluCheckExtension (const GLubyte *ext_name, const GLubyte *ext_string)
1011 {
1012   size_t ext_len = strlen ((const char *)ext_name);
1013
1014   for (;;) {
1015     const GLubyte *found = (const GLubyte *)strstr ((const char *)ext_string,
1016                                                     (const char *)ext_name);
1017     if (!found)
1018       break;
1019
1020     char last_ch = found[ext_len];
1021     if ((found == ext_string || found[-1] == ' ') &&
1022         (last_ch == ' ' || !last_ch)) {
1023       return GL_TRUE;
1024     }
1025
1026     ext_string = found + ext_len;
1027   }
1028
1029   return GL_FALSE;
1030 }
1031
1032 # endif
1033
1034 /* Called during startAnimation before the first call to createBackbuffer. */
1035 - (void) enableBackbuffer:(CGSize)new_backbuffer_size
1036 {
1037 # ifndef USE_IPHONE
1038   struct gl_version version;
1039
1040   {
1041     const char *version_str = (const char *)glGetString (GL_VERSION);
1042
1043     /* iPhone is always OpenGL ES 1.1. */
1044     if (sscanf ((const char *)version_str, "%u.%u",
1045                 &version.major, &version.minor) < 2)
1046     {
1047       version.major = 1;
1048       version.minor = 1;
1049     }
1050   }
1051 # endif
1052
1053   // The OpenGL extensions in use in here are pretty are pretty much ubiquitous
1054   // on OS X, but it's still good form to check.
1055   const GLubyte *extensions = glGetString (GL_EXTENSIONS);
1056
1057   glGenTextures (1, &backbuffer_texture);
1058
1059   // On really old systems, it would make sense to split the texture
1060   // into subsections
1061 # ifndef USE_IPHONE
1062   gl_texture_target = (gluCheckExtension ((const GLubyte *)
1063                                          "GL_ARB_texture_rectangle",
1064                                          extensions)
1065                        ? GL_TEXTURE_RECTANGLE_EXT : GL_TEXTURE_2D);
1066 # else
1067   // OES_texture_npot also provides this, but iOS never provides it.
1068   gl_limited_npot_p = gluCheckExtension ((const GLubyte *)
1069                                          "GL_APPLE_texture_2D_limited_npot",
1070                                          extensions);
1071   gl_texture_target = GL_TEXTURE_2D;
1072 # endif
1073
1074   glBindTexture (gl_texture_target, &backbuffer_texture);
1075   glTexParameteri (gl_texture_target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
1076   // GL_LINEAR might make sense on Retina iPads.
1077   glTexParameteri (gl_texture_target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
1078   glTexParameteri (gl_texture_target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
1079   glTexParameteri (gl_texture_target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
1080
1081 # ifndef USE_IPHONE
1082   // There isn't much sense in supporting one of these if the other
1083   // isn't present.
1084   gl_apple_client_storage_p =
1085     gluCheckExtension ((const GLubyte *)"GL_APPLE_client_storage",
1086                        extensions) &&
1087     gluCheckExtension ((const GLubyte *)"GL_APPLE_texture_range", extensions);
1088
1089   if (gl_apple_client_storage_p) {
1090     glTexParameteri (gl_texture_target, GL_TEXTURE_STORAGE_HINT_APPLE,
1091                      GL_STORAGE_SHARED_APPLE);
1092     glPixelStorei (GL_UNPACK_CLIENT_STORAGE_APPLE, GL_TRUE);
1093   }
1094 # endif
1095
1096   // If a video adapter suports BGRA textures, then that's probably as fast as
1097   // you're gonna get for getting a texture onto the screen.
1098 # ifdef USE_IPHONE
1099   gl_pixel_format =
1100     gluCheckExtension ((const GLubyte *)"GL_APPLE_texture_format_BGRA8888",
1101                        extensions) ? GL_BGRA : GL_RGBA;
1102   gl_pixel_type = GL_UNSIGNED_BYTE;
1103   // See also OES_read_format.
1104 # else
1105   if (gl_check_ver (&version, 1, 2) ||
1106       (gluCheckExtension ((const GLubyte *)"GL_EXT_bgra", extensions) &&
1107        gluCheckExtension ((const GLubyte *)"GL_APPLE_packed_pixels",
1108                           extensions))) {
1109     gl_pixel_format = GL_BGRA;
1110     // Both Intel and PowerPC-era docs say to use GL_UNSIGNED_INT_8_8_8_8_REV.
1111     gl_pixel_type = GL_UNSIGNED_INT_8_8_8_8_REV;
1112   } else {
1113     gl_pixel_format = GL_RGBA;
1114     gl_pixel_type = GL_UNSIGNED_BYTE;
1115   }
1116   // GL_ABGR_EXT/GL_UNSIGNED_BYTE is another possibilty that may have made more
1117   // sense on PowerPC.
1118 # endif
1119
1120   glEnable (gl_texture_target);
1121   glEnableClientState (GL_VERTEX_ARRAY);
1122   glEnableClientState (GL_TEXTURE_COORD_ARRAY);
1123
1124 # ifdef USE_IPHONE
1125   glMatrixMode (GL_PROJECTION);
1126   glLoadIdentity();
1127   NSAssert (new_backbuffer_size.width != 0 && new_backbuffer_size.height != 0,
1128             @"initial_bounds never got set");
1129   // This is pretty similar to the glOrtho in createBackbuffer for OS X.
1130   glOrthof (-new_backbuffer_size.width, new_backbuffer_size.width,
1131             -new_backbuffer_size.height, new_backbuffer_size.height, -1, 1);
1132 # endif // USE_IPHONE
1133
1134   check_gl_error ("enableBackbuffer");
1135 }
1136
1137
1138 static GLsizei
1139 to_pow2 (size_t x)
1140 {
1141   if (x <= 1)
1142     return 1;
1143
1144   size_t mask = (size_t)-1;
1145   unsigned bits = sizeof(x) * CHAR_BIT;
1146   unsigned log2 = bits;
1147
1148   --x;
1149   while (bits) {
1150     if (!(x & mask)) {
1151       log2 -= bits;
1152       x <<= bits;
1153     }
1154
1155     bits >>= 1;
1156     mask <<= bits;
1157   }
1158
1159   return 1 << log2;
1160 }
1161
1162
1163 /* Create a bitmap context into which we render everything.
1164    If the desired size has changed, re-created it.
1165    new_size is in rotated pixels, not points: the same size
1166    and shape as the X11 window as seen by the hacks.
1167  */
1168 - (void) createBackbuffer:(CGSize)new_size
1169 {
1170   // Colorspaces and CGContexts only happen with non-GL hacks.
1171   if (colorspace)
1172     CGColorSpaceRelease (colorspace);
1173
1174 # ifdef BACKBUFFER_OPENGL
1175   NSAssert ([NSOpenGLContext currentContext] ==
1176             ogl_ctx, @"invalid GL context");
1177
1178   // This almost isn't necessary, except for the ugly aliasing artifacts.
1179 #  ifndef USE_IPHONE
1180   glViewport (0, 0, new_size.width, new_size.height);
1181
1182   glMatrixMode (GL_PROJECTION);
1183   glLoadIdentity();
1184   // This is pretty similar to the glOrthof in enableBackbuffer for iPhone.
1185   glOrtho (-new_size.width, new_size.width, -new_size.height, new_size.height,
1186            -1, 1);
1187 #  endif // !USE_IPHONE
1188 # endif // BACKBUFFER_OPENGL
1189         
1190   NSWindow *window = [self window];
1191
1192   if (window && xdpy) {
1193     [self lockFocus];
1194
1195 # ifdef BACKBUFFER_OPENGL
1196     // Was apparently faster until 10.9.
1197     colorspace = CGColorSpaceCreateDeviceRGB ();
1198 # endif // BACKBUFFER_OPENGL
1199
1200     [self unlockFocus];
1201   } else {
1202     colorspace = CGColorSpaceCreateDeviceRGB();
1203   }
1204
1205   if (backbuffer &&
1206       (int)backbuffer_size.width  == (int)new_size.width &&
1207       (int)backbuffer_size.height == (int)new_size.height)
1208     return;
1209
1210   CGContextRef ob = backbuffer;
1211   void *odata = backbuffer_data;
1212   size_t olen = backbuffer_len;
1213
1214   CGSize osize = backbuffer_size;       // pixels, not points.
1215   backbuffer_size = new_size;           // pixels, not points.
1216
1217 # if TARGET_IPHONE_SIMULATOR
1218   NSLog(@"backbuffer %.0fx%.0f",
1219         backbuffer_size.width, backbuffer_size.height);
1220 # endif
1221
1222   /* OS X uses APPLE_client_storage and APPLE_texture_range, as described in
1223      <https://developer.apple.com/library/mac/documentation/GraphicsImaging/Conceptual/OpenGL-MacProgGuide/opengl_texturedata/opengl_texturedata.html>.
1224
1225      iOS uses bog-standard glTexImage2D (for now).
1226
1227      glMapBuffer is the standard way to get data from system RAM to video
1228      memory asynchronously and without a memcpy, but support for
1229      APPLE_client_storage is ubiquitous on OS X (not so for glMapBuffer),
1230      and on iOS GL_PIXEL_UNPACK_BUFFER is only available on OpenGL ES 3
1231      (iPhone 5S or newer). Plus, glMapBuffer doesn't work well with
1232      CGBitmapContext: glMapBuffer can return a different pointer on each
1233      call, but a CGBitmapContext doesn't allow its data pointer to be
1234      changed -- and recreating the context for a new pointer can be
1235      expensive (glyph caches get dumped, for instance).
1236
1237      glMapBufferRange has MAP_FLUSH_EXPLICIT_BIT and MAP_UNSYNCHRONIZED_BIT,
1238      and these seem to allow mapping the buffer and leaving it where it is
1239      in client address space while OpenGL works with the buffer, but it
1240      requires OpenGL 3 Core profile on OS X (and ES 3 on iOS for
1241      GL_PIXEL_UNPACK_BUFFER), so point goes to APPLE_client_storage.
1242
1243      AMD_pinned_buffer provides the same advantage as glMapBufferRange, but
1244      Apple never implemented that one for OS X.
1245    */
1246
1247   backbuffer_data = NULL;
1248   gl_texture_w = (int)backbuffer_size.width;
1249   gl_texture_h = (int)backbuffer_size.height;
1250
1251   NSAssert (gl_texture_target == GL_TEXTURE_2D
1252 # ifndef USE_IPHONE
1253             || gl_texture_target == GL_TEXTURE_RECTANGLE_EXT
1254 # endif
1255                   , @"unexpected GL texture target");
1256
1257 # ifndef USE_IPHONE
1258   if (gl_texture_target != GL_TEXTURE_RECTANGLE_EXT)
1259 # else
1260   if (!gl_limited_npot_p)
1261 # endif
1262   {
1263     gl_texture_w = to_pow2 (gl_texture_w);
1264     gl_texture_h = to_pow2 (gl_texture_h);
1265   }
1266
1267   size_t bytes_per_row = gl_texture_w * 4;
1268
1269 # if defined(BACKBUFFER_OPENGL) && !defined(USE_IPHONE)
1270   // APPLE_client_storage requires texture width to be aligned to 32 bytes, or
1271   // it will fall back to a memcpy.
1272   // https://developer.apple.com/library/mac/documentation/GraphicsImaging/Conceptual/OpenGL-MacProgGuide/opengl_texturedata/opengl_texturedata.html#//apple_ref/doc/uid/TP40001987-CH407-SW24
1273   bytes_per_row = (bytes_per_row + 31) & ~31;
1274 # endif // BACKBUFFER_OPENGL && !USE_IPHONE
1275
1276   backbuffer_len = bytes_per_row * gl_texture_h;
1277   if (backbuffer_len) // mmap requires this to be non-zero.
1278     backbuffer_data = mmap (NULL, backbuffer_len,
1279                             PROT_READ | PROT_WRITE, MAP_ANON | MAP_SHARED,
1280                             -1, 0);
1281
1282   BOOL alpha_first_p, order_little_p;
1283
1284   if (gl_pixel_format == GL_BGRA) {
1285     alpha_first_p = YES;
1286     order_little_p = YES;
1287 /*
1288   } else if (gl_pixel_format == GL_ABGR_EXT) {
1289     alpha_first_p = NO;
1290     order_little_p = YES; */
1291   } else {
1292     NSAssert (gl_pixel_format == GL_RGBA, @"unknown GL pixel format");
1293     alpha_first_p = NO;
1294     order_little_p = NO;
1295   }
1296
1297 #ifdef USE_IPHONE
1298   NSAssert (gl_pixel_type == GL_UNSIGNED_BYTE, @"unknown GL pixel type");
1299 #else
1300   NSAssert (gl_pixel_type == GL_UNSIGNED_INT_8_8_8_8 ||
1301             gl_pixel_type == GL_UNSIGNED_INT_8_8_8_8_REV ||
1302             gl_pixel_type == GL_UNSIGNED_BYTE,
1303             @"unknown GL pixel type");
1304
1305 #if defined __LITTLE_ENDIAN__
1306   const GLenum backwards_pixel_type = GL_UNSIGNED_INT_8_8_8_8;
1307 #elif defined __BIG_ENDIAN__
1308   const GLenum backwards_pixel_type = GL_UNSIGNED_INT_8_8_8_8_REV;
1309 #else
1310 # error Unknown byte order.
1311 #endif
1312
1313   if (gl_pixel_type == backwards_pixel_type)
1314     order_little_p ^= YES;
1315 #endif
1316
1317   CGBitmapInfo bitmap_info =
1318     (alpha_first_p ? kCGImageAlphaNoneSkipFirst : kCGImageAlphaNoneSkipLast) |
1319     (order_little_p ? kCGBitmapByteOrder32Little : kCGBitmapByteOrder32Big);
1320
1321   backbuffer = CGBitmapContextCreate (backbuffer_data,
1322                                       (int)backbuffer_size.width,
1323                                       (int)backbuffer_size.height,
1324                                       8,
1325                                       bytes_per_row,
1326                                       colorspace,
1327                                       bitmap_info);
1328   NSAssert (backbuffer, @"unable to allocate back buffer");
1329
1330   // Clear it.
1331   CGRect r;
1332   r.origin.x = r.origin.y = 0;
1333   r.size = backbuffer_size;
1334   CGContextSetGrayFillColor (backbuffer, 0, 1);
1335   CGContextFillRect (backbuffer, r);
1336
1337 # if defined(BACKBUFFER_OPENGL) && !defined(USE_IPHONE)
1338   if (gl_apple_client_storage_p)
1339     glTextureRangeAPPLE (gl_texture_target, backbuffer_len, backbuffer_data);
1340 # endif // BACKBUFFER_OPENGL && !USE_IPHONE
1341
1342   if (ob) {
1343     // Restore old bits, as much as possible, to the X11 upper left origin.
1344
1345     CGRect rect;   // pixels, not points
1346     rect.origin.x = 0;
1347     rect.origin.y = (backbuffer_size.height - osize.height);
1348     rect.size = osize;
1349
1350     CGImageRef img = CGBitmapContextCreateImage (ob);
1351     CGContextDrawImage (backbuffer, rect, img);
1352     CGImageRelease (img);
1353     CGContextRelease (ob);
1354
1355     if (olen)
1356       // munmap should round len up to the nearest page.
1357       munmap (odata, olen);
1358   }
1359
1360   check_gl_error ("createBackbuffer");
1361 }
1362
1363
1364 - (void) drawBackbuffer
1365 {
1366 # ifdef BACKBUFFER_OPENGL
1367
1368   NSAssert ([ogl_ctx isKindOfClass:[NSOpenGLContext class]],
1369             @"ogl_ctx is not an NSOpenGLContext");
1370
1371   NSAssert (! (CGBitmapContextGetBytesPerRow (backbuffer) % 4),
1372             @"improperly-aligned backbuffer");
1373
1374   // This gets width and height from the backbuffer in case
1375   // APPLE_client_storage is in use. See the note in createBackbuffer.
1376   // This still has to happen every frame even when APPLE_client_storage has
1377   // the video adapter pulling texture data straight from
1378   // XScreenSaverView-owned memory.
1379   glTexImage2D (gl_texture_target, 0, GL_RGBA,
1380                 (GLsizei)(CGBitmapContextGetBytesPerRow (backbuffer) / 4),
1381                 gl_texture_h, 0, gl_pixel_format, gl_pixel_type,
1382                 backbuffer_data);
1383
1384   GLfloat vertices[4][2] =
1385   {
1386     {-backbuffer_size.width,  backbuffer_size.height},
1387     { backbuffer_size.width,  backbuffer_size.height},
1388     { backbuffer_size.width, -backbuffer_size.height},
1389     {-backbuffer_size.width, -backbuffer_size.height}
1390   };
1391
1392   GLfloat tex_coords[4][2];
1393
1394 #  ifndef USE_IPHONE
1395   if (gl_texture_target == GL_TEXTURE_RECTANGLE_EXT) {
1396     tex_coords[0][0] = 0;
1397     tex_coords[0][1] = 0;
1398     tex_coords[1][0] = backbuffer_size.width;
1399     tex_coords[1][1] = 0;
1400     tex_coords[2][0] = backbuffer_size.width;
1401     tex_coords[2][1] = backbuffer_size.height;
1402     tex_coords[3][0] = 0;
1403     tex_coords[3][1] = backbuffer_size.height;
1404   } else
1405 #  endif // USE_IPHONE
1406   {
1407     GLfloat x = backbuffer_size.width / gl_texture_w;
1408     GLfloat y = backbuffer_size.height / gl_texture_h;
1409     tex_coords[0][0] = 0;
1410     tex_coords[0][1] = 0;
1411     tex_coords[1][0] = x;
1412     tex_coords[1][1] = 0;
1413     tex_coords[2][0] = x;
1414     tex_coords[2][1] = y;
1415     tex_coords[3][0] = 0;
1416     tex_coords[3][1] = y;
1417   }
1418
1419 #  ifdef USE_IPHONE
1420   if (!ignore_rotation_p) {
1421     glMatrixMode (GL_MODELVIEW);
1422     glLoadIdentity();
1423     glRotatef (rot_current_angle, 0, 0, -1);
1424
1425     if (rotation_ratio >= 0)
1426       glClear (GL_COLOR_BUFFER_BIT);
1427   }
1428 #  endif  // USE_IPHONE
1429
1430   glVertexPointer (2, GL_FLOAT, 0, vertices);
1431   glTexCoordPointer (2, GL_FLOAT, 0, tex_coords);
1432   glDrawArrays (GL_TRIANGLE_FAN, 0, 4);
1433
1434 #  if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
1435   check_gl_error ("drawBackbuffer");
1436 #  endif
1437
1438   // This can also happen near the beginning of render_x11.
1439   [self flushBackbuffer];
1440
1441 # endif // BACKBUFFER_OPENGL
1442 }
1443
1444
1445 - (void)flushBackbuffer
1446 {
1447 # ifndef USE_IPHONE
1448   // The OpenGL pipeline is not automatically synchronized with the contents
1449   // of the backbuffer, so without glFinish, OpenGL can start rendering from
1450   // the backbuffer texture at the same time that JWXYZ is clearing and
1451   // drawing the next frame in the backing store for the backbuffer texture.
1452   glFinish();
1453
1454   if (double_buffered_p)
1455     [ogl_ctx flushBuffer]; // despite name, this actually swaps
1456 # else
1457   glBindRenderbufferOES (GL_RENDERBUFFER_OES, gl_renderbuffer);
1458   [ogl_ctx presentRenderbuffer:GL_RENDERBUFFER_OES];
1459 # endif
1460
1461 # if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
1462   // glGetError waits for the OpenGL command pipe to flush, so skip it in
1463   // release builds.
1464   // OpenGL Programming Guide for Mac -> OpenGL Application Design
1465   // Strategies -> Allow OpenGL to Manage Your Resources
1466   // https://developer.apple.com/library/mac/documentation/GraphicsImaging/Conceptual/OpenGL-MacProgGuide/opengl_designstrategies/opengl_designstrategies.html#//apple_ref/doc/uid/TP40001987-CH2-SW7
1467   check_gl_error ("flushBackbuffer");
1468 # endif
1469 }
1470
1471
1472 #endif // USE_BACKBUFFER
1473
1474
1475 /* Inform X11 that the size of our window has changed.
1476  */
1477 - (void) resize_x11
1478 {
1479   if (!xwindow) return;  // early
1480
1481   CGSize new_size;      // pixels, not points
1482
1483 # ifdef USE_BACKBUFFER
1484
1485   [self prepareContext];
1486
1487 #  if defined(BACKBUFFER_OPENGL) && !defined(USE_IPHONE)
1488   [ogl_ctx update];
1489 #  endif // BACKBUFFER_OPENGL && !USE_IPHONE
1490
1491 #  ifdef USE_IPHONE
1492   CGSize rotsize = ((ignore_rotation_p || ![self reshapeRotatedWindow])
1493                     ? initial_bounds
1494                     : rot_current_size);
1495   new_size.width  = rotsize.width;
1496   new_size.height = rotsize.height;
1497 #  else  // !USE_IPHONE
1498   new_size = NSSizeToCGSize([self bounds].size);
1499 #  endif // !USE_IPHONE
1500
1501   [self createBackbuffer:new_size];
1502   jwxyz_window_resized (xdpy, xwindow, 0, 0, new_size.width, new_size.height,
1503                         backbuffer);
1504 # else   // !USE_BACKBUFFER
1505   new_size = [self bounds].size;
1506   jwxyz_window_resized (xdpy, xwindow, 0, 0, new_size.width, new_size.height,
1507                         0);
1508 # endif  // !USE_BACKBUFFER
1509
1510 # if TARGET_IPHONE_SIMULATOR
1511   NSLog(@"reshape %.0fx%.0f", new_size.width, new_size.height);
1512 # endif
1513
1514   // Next time render_x11 is called, run the saver's reshape_cb.
1515   resized_p = YES;
1516 }
1517
1518
1519 - (void) render_x11
1520 {
1521 # ifdef USE_IPHONE
1522   @try {
1523
1524   if (orientation == UIDeviceOrientationUnknown)
1525     [self didRotate:nil];
1526   [self hackRotation];
1527 # endif
1528
1529   if (!initted_p) {
1530
1531     if (! xdpy) {
1532 # ifdef USE_BACKBUFFER
1533       NSAssert (backbuffer, @"no back buffer");
1534       xdpy = jwxyz_make_display (self, backbuffer);
1535 # else
1536       xdpy = jwxyz_make_display (self, 0);
1537 # endif
1538       xwindow = XRootWindow (xdpy, 0);
1539
1540 # ifdef USE_IPHONE
1541       /* Some X11 hacks (fluidballs) want to ignore all rotation events. */
1542       ignore_rotation_p =
1543         get_boolean_resource (xdpy, "ignoreRotation", "IgnoreRotation");
1544 # endif // USE_IPHONE
1545
1546       [self resize_x11];
1547     }
1548
1549     if (!setup_p) {
1550       setup_p = YES;
1551       if (xsft->setup_cb)
1552         xsft->setup_cb (xsft, xsft->setup_arg);
1553     }
1554     initted_p = YES;
1555     resized_p = NO;
1556     NSAssert(!xdata, @"xdata already initialized");
1557
1558
1559 # undef ya_rand_init
1560     ya_rand_init (0);
1561     
1562     XSetWindowBackground (xdpy, xwindow,
1563                           get_pixel_resource (xdpy, 0,
1564                                               "background", "Background"));
1565     XClearWindow (xdpy, xwindow);
1566     
1567 # ifndef USE_IPHONE
1568     [[self window] setAcceptsMouseMovedEvents:YES];
1569 # endif
1570
1571     /* In MacOS 10.5, this enables "QuartzGL", meaning that the Quartz
1572        drawing primitives will run on the GPU instead of the CPU.
1573        It seems like it might make things worse rather than better,
1574        though...  Plus it makes us binary-incompatible with 10.4.
1575
1576 # if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5
1577     [[self window] setPreferredBackingLocation:
1578                      NSWindowBackingLocationVideoMemory];
1579 # endif
1580      */
1581
1582     /* Kludge: even though the init_cb functions are declared to take 2 args,
1583       actually call them with 3, for the benefit of xlockmore_init() and
1584       xlockmore_setup().
1585       */
1586     void *(*init_cb) (Display *, Window, void *) = 
1587       (void *(*) (Display *, Window, void *)) xsft->init_cb;
1588     
1589     xdata = init_cb (xdpy, xwindow, xsft->setup_arg);
1590     // NSAssert(xdata, @"no xdata from init");
1591     if (! xdata) abort();
1592
1593     if (get_boolean_resource (xdpy, "doFPS", "DoFPS")) {
1594       fpst = fps_init (xdpy, xwindow);
1595       if (! xsft->fps_cb) xsft->fps_cb = screenhack_do_fps;
1596     } else {
1597       fpst = NULL;
1598       xsft->fps_cb = 0;
1599     }
1600
1601     [self checkForUpdates];
1602   }
1603
1604
1605   /* I don't understand why we have to do this *every frame*, but we do,
1606      or else the cursor comes back on.
1607    */
1608 # ifndef USE_IPHONE
1609   if (![self isPreview])
1610     [NSCursor setHiddenUntilMouseMoves:YES];
1611 # endif
1612
1613
1614   if (fpst)
1615     {
1616       /* This is just a guess, but the -fps code wants to know how long
1617          we were sleeping between frames.
1618        */
1619       long usecs = 1000000 * [self animationTimeInterval];
1620       usecs -= 200;  // caller apparently sleeps for slightly less sometimes...
1621       if (usecs < 0) usecs = 0;
1622       fps_slept (fpst, usecs);
1623     }
1624
1625
1626   /* It turns out that on some systems (possibly only 10.5 and older?)
1627      [ScreenSaverView setAnimationTimeInterval] does nothing.  This means
1628      that we cannot rely on it.
1629
1630      Some of the screen hacks want to delay for long periods, and letting the
1631      framework run the update function at 30 FPS when it really wanted half a
1632      minute between frames would be bad.  So instead, we assume that the
1633      framework's animation timer might fire whenever, but we only invoke the
1634      screen hack's "draw frame" method when enough time has expired.
1635   
1636      This means two extra calls to gettimeofday() per frame.  For fast-cycling
1637      screen savers, that might actually slow them down.  Oh well.
1638
1639      A side-effect of this is that it's not possible for a saver to request
1640      an animation interval that is faster than animationTimeInterval.
1641
1642      HOWEVER!  On modern systems where setAnimationTimeInterval is *not*
1643      ignored, it's important that it be faster than 30 FPS.  240 FPS is good.
1644
1645      An NSTimer won't fire if the timer is already running the invocation
1646      function from a previous firing.  So, if we use a 30 FPS
1647      animationTimeInterval (33333 Âµs) and a screenhack takes 40000 Âµs for a
1648      frame, there will be a 26666 Âµs delay until the next frame, 66666 Âµs
1649      after the beginning of the current frame.  In other words, 25 FPS
1650      becomes 15 FPS.
1651
1652      Frame rates tend to snap to values of 30/N, where N is a positive
1653      integer, i.e. 30 FPS, 15 FPS, 10, 7.5, 6. And the 'snapped' frame rate
1654      is rounded down from what it would normally be.
1655
1656      So if we set animationTimeInterval to 1/240 instead of 1/30, frame rates
1657      become values of 60/N, 120/N, or 240/N, with coarser or finer frame rate
1658      steps for higher or lower animation time intervals respectively.
1659    */
1660   struct timeval tv;
1661   gettimeofday (&tv, 0);
1662   double now = tv.tv_sec + (tv.tv_usec / 1000000.0);
1663   if (now < next_frame_time) return;
1664
1665   [self prepareContext]; // resize_x11 also calls this.
1666   // [self flushBackbuffer];
1667
1668   if (resized_p) {
1669     // We do this here instead of in setFrame so that all the
1670     // Xlib drawing takes place under the animation timer.
1671
1672 # ifndef USE_IPHONE
1673     if (ogl_ctx)
1674       [ogl_ctx setView:self];
1675 # endif // !USE_IPHONE
1676
1677     NSRect r;
1678 # ifndef USE_BACKBUFFER
1679     r = [self bounds];
1680 # else  // USE_BACKBUFFER
1681     r.origin.x = 0;
1682     r.origin.y = 0;
1683     r.size.width  = backbuffer_size.width;
1684     r.size.height = backbuffer_size.height;
1685 # endif // USE_BACKBUFFER
1686
1687     xsft->reshape_cb (xdpy, xwindow, xdata, r.size.width, r.size.height);
1688     resized_p = NO;
1689   }
1690
1691   // Run any XtAppAddInput callbacks now.
1692   // (Note that XtAppAddTimeOut callbacks have already been run by
1693   // the Cocoa event loop.)
1694   //
1695   jwxyz_sources_run (display_sources_data (xdpy));
1696
1697
1698   // And finally:
1699   //
1700   // NSAssert(xdata, @"no xdata when drawing");
1701   if (! xdata) abort();
1702   unsigned long delay = xsft->draw_cb (xdpy, xwindow, xdata);
1703   if (fpst && xsft->fps_cb)
1704     xsft->fps_cb (xdpy, xwindow, fpst, xdata);
1705
1706   gettimeofday (&tv, 0);
1707   now = tv.tv_sec + (tv.tv_usec / 1000000.0);
1708   next_frame_time = now + (delay / 1000000.0);
1709
1710   [self drawBackbuffer];
1711
1712 # ifdef USE_IPHONE      // Allow savers on the iPhone to run full-tilt.
1713   if (delay < [self animationTimeInterval])
1714     [self setAnimationTimeInterval:(delay / 1000000.0)];
1715 # endif
1716
1717 # ifdef DO_GC_HACKERY
1718   /* Current theory is that the 10.6 garbage collector sucks in the
1719      following way:
1720
1721      It only does a collection when a threshold of outstanding
1722      collectable allocations has been surpassed.  However, CoreGraphics
1723      creates lots of small collectable allocations that contain pointers
1724      to very large non-collectable allocations: a small CG object that's
1725      collectable referencing large malloc'd allocations (non-collectable)
1726      containing bitmap data.  So the large allocation doesn't get freed
1727      until GC collects the small allocation, which triggers its finalizer
1728      to run which frees the large allocation.  So GC is deciding that it
1729      doesn't really need to run, even though the process has gotten
1730      enormous.  GC eventually runs once pageouts have happened, but by
1731      then it's too late, and the machine's resident set has been
1732      sodomized.
1733
1734      So, we force an exhaustive garbage collection in this process
1735      approximately every 5 seconds whether the system thinks it needs 
1736      one or not.
1737   */
1738   {
1739     static int tick = 0;
1740     if (++tick > 5*30) {
1741       tick = 0;
1742       objc_collect (OBJC_EXHAUSTIVE_COLLECTION);
1743     }
1744   }
1745 # endif // DO_GC_HACKERY
1746
1747 # ifdef USE_IPHONE
1748   }
1749   @catch (NSException *e) {
1750     [self handleException: e];
1751   }
1752 # endif // USE_IPHONE
1753 }
1754
1755
1756 #ifndef USE_BACKBUFFER
1757
1758 - (void) animateOneFrame
1759 {
1760   [self render_x11];
1761   jwxyz_flush_context(xdpy);
1762 }
1763
1764 #else  // USE_BACKBUFFER
1765
1766 - (void) animateOneFrame
1767 {
1768   // Render X11 into the backing store bitmap...
1769
1770   NSAssert (backbuffer, @"no back buffer");
1771
1772 # ifdef USE_IPHONE
1773   UIGraphicsPushContext (backbuffer);
1774 # endif
1775
1776   [self render_x11];
1777
1778 # if defined USE_IPHONE && defined USE_BACKBUFFER
1779   UIGraphicsPopContext();
1780 # endif
1781 }
1782
1783 #endif // USE_BACKBUFFER
1784
1785
1786
1787 - (void) setFrame:(NSRect) newRect
1788 {
1789   [super setFrame:newRect];
1790
1791   if (xwindow)     // inform Xlib that the window has changed now.
1792     [self resize_x11];
1793 }
1794
1795
1796 # ifndef USE_IPHONE  // Doesn't exist on iOS
1797 - (void) setFrameSize:(NSSize) newSize
1798 {
1799   [super setFrameSize:newSize];
1800   if (xwindow)
1801     [self resize_x11];
1802 }
1803 # endif // !USE_IPHONE
1804
1805
1806 +(BOOL) performGammaFade
1807 {
1808   return YES;
1809 }
1810
1811 - (BOOL) hasConfigureSheet
1812 {
1813   return YES;
1814 }
1815
1816 + (NSString *) decompressXML: (NSData *)data
1817 {
1818   if (! data) return 0;
1819   BOOL compressed_p = !!strncmp ((const char *) data.bytes, "<?xml", 5);
1820
1821   // If it's not already XML, decompress it.
1822   NSAssert (compressed_p, @"xml isn't compressed");
1823   if (compressed_p) {
1824     NSMutableData *data2 = 0;
1825     int ret = -1;
1826     z_stream zs;
1827     memset (&zs, 0, sizeof(zs));
1828     ret = inflateInit2 (&zs, 16 + MAX_WBITS);
1829     if (ret == Z_OK) {
1830       UInt32 usize = * (UInt32 *) (data.bytes + data.length - 4);
1831       data2 = [NSMutableData dataWithLength: usize];
1832       zs.next_in   = (Bytef *) data.bytes;
1833       zs.avail_in  = (uint) data.length;
1834       zs.next_out  = (Bytef *) data2.bytes;
1835       zs.avail_out = (uint) data2.length;
1836       ret = inflate (&zs, Z_FINISH);
1837       inflateEnd (&zs);
1838     }
1839     if (ret == Z_OK || ret == Z_STREAM_END)
1840       data = data2;
1841     else
1842       NSAssert2 (0, @"gunzip error: %d: %s",
1843                  ret, (zs.msg ? zs.msg : "<null>"));
1844   }
1845
1846   return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
1847 }
1848
1849
1850 #ifndef USE_IPHONE
1851 - (NSWindow *) configureSheet
1852 #else
1853 - (UIViewController *) configureView
1854 #endif
1855 {
1856   NSBundle *bundle = [NSBundle bundleForClass:[self class]];
1857   NSString *file = [NSString stringWithCString:xsft->progclass
1858                                       encoding:NSISOLatin1StringEncoding];
1859   file = [file lowercaseString];
1860   NSString *path = [bundle pathForResource:file ofType:@"xml"];
1861   if (!path) {
1862     NSLog (@"%@.xml does not exist in the application bundle: %@/",
1863            file, [bundle resourcePath]);
1864     return nil;
1865   }
1866   
1867 # ifdef USE_IPHONE
1868   UIViewController *sheet;
1869 # else  // !USE_IPHONE
1870   NSWindow *sheet;
1871 # endif // !USE_IPHONE
1872
1873   NSData *xmld = [NSData dataWithContentsOfFile:path];
1874   NSString *xml = [[self class] decompressXML: xmld];
1875   sheet = [[XScreenSaverConfigSheet alloc]
1876             initWithXML:[xml dataUsingEncoding:NSUTF8StringEncoding]
1877                 options:xsft->options
1878              controller:[prefsReader userDefaultsController]
1879        globalController:[prefsReader globalDefaultsController]
1880                defaults:[prefsReader defaultOptions]];
1881
1882   // #### am I expected to retain this, or not? wtf.
1883   //      I thought not, but if I don't do this, we (sometimes) crash.
1884   // #### Analyze says "potential leak of an object stored into sheet"
1885   // [sheet retain];
1886
1887   return sheet;
1888 }
1889
1890
1891 - (NSUserDefaultsController *) userDefaultsController
1892 {
1893   return [prefsReader userDefaultsController];
1894 }
1895
1896
1897 /* Announce our willingness to accept keyboard input.
1898  */
1899 - (BOOL)acceptsFirstResponder
1900 {
1901   return YES;
1902 }
1903
1904
1905 - (void) beep
1906 {
1907 # ifndef USE_IPHONE
1908   NSBeep();
1909 # else // USE_IPHONE 
1910
1911   // There's no way to play a standard system alert sound!
1912   // We'd have to include our own WAV for that.
1913   //
1914   // Or we could vibrate:
1915   // #import <AudioToolbox/AudioToolbox.h>
1916   // AudioServicesPlaySystemSound (kSystemSoundID_Vibrate);
1917   //
1918   // Instead, just flash the screen white, then fade.
1919   //
1920   UIView *v = [[UIView alloc] initWithFrame: [self frame]]; 
1921   [v setBackgroundColor: [UIColor whiteColor]];
1922   [[self window] addSubview:v];
1923   [UIView animateWithDuration: 0.1
1924           animations:^{ [v setAlpha: 0.0]; }
1925           completion:^(BOOL finished) { [v removeFromSuperview]; } ];
1926
1927 # endif  // USE_IPHONE
1928 }
1929
1930
1931 /* Send an XEvent to the hack.  Returns YES if it was handled.
1932  */
1933 - (BOOL) sendEvent: (XEvent *) e
1934 {
1935   if (!initted_p || ![self isAnimating]) // no event handling unless running.
1936     return NO;
1937
1938   [self lockFocus];
1939   [self prepareContext];
1940   BOOL result = xsft->event_cb (xdpy, xwindow, xdata, e);
1941   [self unlockFocus];
1942   return result;
1943 }
1944
1945
1946 #ifndef USE_IPHONE
1947
1948 /* Convert an NSEvent into an XEvent, and pass it along.
1949    Returns YES if it was handled.
1950  */
1951 - (BOOL) convertEvent: (NSEvent *) e
1952             type: (int) type
1953 {
1954   XEvent xe;
1955   memset (&xe, 0, sizeof(xe));
1956   
1957   int state = 0;
1958   
1959   int flags = [e modifierFlags];
1960   if (flags & NSAlphaShiftKeyMask) state |= LockMask;
1961   if (flags & NSShiftKeyMask)      state |= ShiftMask;
1962   if (flags & NSControlKeyMask)    state |= ControlMask;
1963   if (flags & NSAlternateKeyMask)  state |= Mod1Mask;
1964   if (flags & NSCommandKeyMask)    state |= Mod2Mask;
1965   
1966   NSPoint p = [[[e window] contentView] convertPoint:[e locationInWindow]
1967                                             toView:self];
1968 # ifdef USE_IPHONE
1969   double s = [self hackedContentScaleFactor];
1970 # else
1971   int s = 1;
1972 # endif
1973   int x = s * p.x;
1974   int y = s * ([self bounds].size.height - p.y);
1975
1976   xe.xany.type = type;
1977   switch (type) {
1978     case ButtonPress:
1979     case ButtonRelease:
1980       xe.xbutton.x = x;
1981       xe.xbutton.y = y;
1982       xe.xbutton.state = state;
1983       if ([e type] == NSScrollWheel)
1984         xe.xbutton.button = ([e deltaY] > 0 ? Button4 :
1985                              [e deltaY] < 0 ? Button5 :
1986                              [e deltaX] > 0 ? Button6 :
1987                              [e deltaX] < 0 ? Button7 :
1988                              0);
1989       else
1990         xe.xbutton.button = [e buttonNumber] + 1;
1991       break;
1992     case MotionNotify:
1993       xe.xmotion.x = x;
1994       xe.xmotion.y = y;
1995       xe.xmotion.state = state;
1996       break;
1997     case KeyPress:
1998     case KeyRelease:
1999       {
2000         NSString *ns = (([e type] == NSFlagsChanged) ? 0 :
2001                         [e charactersIgnoringModifiers]);
2002         KeySym k = 0;
2003
2004         if (!ns || [ns length] == 0)                    // dead key
2005           {
2006             // Cocoa hides the difference between left and right keys.
2007             // Also we only get KeyPress events for these, no KeyRelease
2008             // (unless we hack the mod state manually.  Bleh.)
2009             //
2010             if      (flags & NSAlphaShiftKeyMask)   k = XK_Caps_Lock;
2011             else if (flags & NSShiftKeyMask)        k = XK_Shift_L;
2012             else if (flags & NSControlKeyMask)      k = XK_Control_L;
2013             else if (flags & NSAlternateKeyMask)    k = XK_Alt_L;
2014             else if (flags & NSCommandKeyMask)      k = XK_Meta_L;
2015           }
2016         else if ([ns length] == 1)                      // real key
2017           {
2018             switch ([ns characterAtIndex:0]) {
2019             case NSLeftArrowFunctionKey:  k = XK_Left;      break;
2020             case NSRightArrowFunctionKey: k = XK_Right;     break;
2021             case NSUpArrowFunctionKey:    k = XK_Up;        break;
2022             case NSDownArrowFunctionKey:  k = XK_Down;      break;
2023             case NSPageUpFunctionKey:     k = XK_Page_Up;   break;
2024             case NSPageDownFunctionKey:   k = XK_Page_Down; break;
2025             case NSHomeFunctionKey:       k = XK_Home;      break;
2026             case NSPrevFunctionKey:       k = XK_Prior;     break;
2027             case NSNextFunctionKey:       k = XK_Next;      break;
2028             case NSBeginFunctionKey:      k = XK_Begin;     break;
2029             case NSEndFunctionKey:        k = XK_End;       break;
2030             case NSF1FunctionKey:         k = XK_F1;        break;
2031             case NSF2FunctionKey:         k = XK_F2;        break;
2032             case NSF3FunctionKey:         k = XK_F3;        break;
2033             case NSF4FunctionKey:         k = XK_F4;        break;
2034             case NSF5FunctionKey:         k = XK_F5;        break;
2035             case NSF6FunctionKey:         k = XK_F6;        break;
2036             case NSF7FunctionKey:         k = XK_F7;        break;
2037             case NSF8FunctionKey:         k = XK_F8;        break;
2038             case NSF9FunctionKey:         k = XK_F9;        break;
2039             case NSF10FunctionKey:        k = XK_F10;       break;
2040             case NSF11FunctionKey:        k = XK_F11;       break;
2041             case NSF12FunctionKey:        k = XK_F12;       break;
2042             default:
2043               {
2044                 const char *s =
2045                   [ns cStringUsingEncoding:NSISOLatin1StringEncoding];
2046                 k = (s && *s ? *s : 0);
2047               }
2048               break;
2049             }
2050           }
2051
2052         if (! k) return YES;   // E.g., "KeyRelease XK_Shift_L"
2053
2054         xe.xkey.keycode = k;
2055         xe.xkey.state = state;
2056         break;
2057       }
2058     default:
2059       NSAssert1 (0, @"unknown X11 event type: %d", type);
2060       break;
2061   }
2062
2063   return [self sendEvent: &xe];
2064 }
2065
2066
2067 - (void) mouseDown: (NSEvent *) e
2068 {
2069   if (! [self convertEvent:e type:ButtonPress])
2070     [super mouseDown:e];
2071 }
2072
2073 - (void) mouseUp: (NSEvent *) e
2074 {
2075   if (! [self convertEvent:e type:ButtonRelease])
2076     [super mouseUp:e];
2077 }
2078
2079 - (void) otherMouseDown: (NSEvent *) e
2080 {
2081   if (! [self convertEvent:e type:ButtonPress])
2082     [super otherMouseDown:e];
2083 }
2084
2085 - (void) otherMouseUp: (NSEvent *) e
2086 {
2087   if (! [self convertEvent:e type:ButtonRelease])
2088     [super otherMouseUp:e];
2089 }
2090
2091 - (void) mouseMoved: (NSEvent *) e
2092 {
2093   if (! [self convertEvent:e type:MotionNotify])
2094     [super mouseMoved:e];
2095 }
2096
2097 - (void) mouseDragged: (NSEvent *) e
2098 {
2099   if (! [self convertEvent:e type:MotionNotify])
2100     [super mouseDragged:e];
2101 }
2102
2103 - (void) otherMouseDragged: (NSEvent *) e
2104 {
2105   if (! [self convertEvent:e type:MotionNotify])
2106     [super otherMouseDragged:e];
2107 }
2108
2109 - (void) scrollWheel: (NSEvent *) e
2110 {
2111   if (! [self convertEvent:e type:ButtonPress])
2112     [super scrollWheel:e];
2113 }
2114
2115 - (void) keyDown: (NSEvent *) e
2116 {
2117   if (! [self convertEvent:e type:KeyPress])
2118     [super keyDown:e];
2119 }
2120
2121 - (void) keyUp: (NSEvent *) e
2122 {
2123   if (! [self convertEvent:e type:KeyRelease])
2124     [super keyUp:e];
2125 }
2126
2127 - (void) flagsChanged: (NSEvent *) e
2128 {
2129   if (! [self convertEvent:e type:KeyPress])
2130     [super flagsChanged:e];
2131 }
2132
2133
2134 - (NSOpenGLPixelFormat *) getGLPixelFormat
2135 {
2136   NSAssert (prefsReader, @"no prefsReader for getGLPixelFormat");
2137
2138   NSOpenGLPixelFormatAttribute attrs[40];
2139   int i = 0;
2140   attrs[i++] = NSOpenGLPFAColorSize; attrs[i++] = 24;
2141
2142   if (double_buffered_p)
2143     attrs[i++] = NSOpenGLPFADoubleBuffer;
2144
2145   attrs[i] = 0;
2146
2147   return [[NSOpenGLPixelFormat alloc] initWithAttributes:attrs];
2148 }
2149
2150 #else  // USE_IPHONE
2151
2152
2153 - (void) stopAndClose:(Bool)relaunch_p
2154 {
2155   if ([self isAnimating])
2156     [self stopAnimation];
2157
2158   /* Need to make the SaverListController be the firstResponder again
2159      so that it can continue to receive its own shake events.  I
2160      suppose that this abstraction-breakage means that I'm adding
2161      XScreenSaverView to the UINavigationController wrong...
2162    */
2163 //  UIViewController *v = [[self window] rootViewController];
2164 //  if ([v isKindOfClass: [UINavigationController class]]) {
2165 //    UINavigationController *n = (UINavigationController *) v;
2166 //    [[n topViewController] becomeFirstResponder];
2167 //  }
2168   [self resignFirstResponder];
2169
2170   if (relaunch_p) {   // Fake a shake on the SaverListController.
2171     [_delegate didShake:self];
2172   } else {      // Not launching another, animate our return to the list.
2173 # if TARGET_IPHONE_SIMULATOR
2174     NSLog (@"fading back to saver list");
2175 # endif
2176     [_delegate wantsFadeOut:self];
2177   }
2178 }
2179
2180
2181 /* Whether the shape of the X11 Window should be changed to HxW when the
2182    device is in a landscape orientation.  X11 hacks want this, but OpenGL
2183    hacks do not.
2184  */
2185 - (BOOL)reshapeRotatedWindow
2186 {
2187   return YES;
2188 }
2189
2190
2191 /* Called after the device's orientation has changed.
2192    
2193    Rotation is complicated: the UI, X11 and OpenGL work in 3 different ways.
2194
2195    The UI (list of savers, preferences panels) is rotated by the system,
2196    because its UIWindow is under a UINavigationController that does
2197    automatic rotation, using Core Animation.
2198
2199    The savers are under a different UIWindow and a UINavigationController
2200    that does not do automatic rotation.
2201
2202    We have to do it this way because using Core Animation on an EAGLContext
2203    causes the OpenGL pipeline used on both X11 and GL savers to fall back on
2204    software rendering and performance goes to hell.
2205
2206    During and after rotation, the size/shape of the X11 window changes,
2207    and ConfigureNotify events are generated.
2208
2209    X11 code (jwxyz) continues to draw into the (reshaped) backbuffer, which is
2210    rendered onto a rotating OpenGL quad.
2211
2212    GL code always recieves a portrait-oriented X11 Window whose size never
2213    changes.  The GL COLOR_BUFFER is displayed on the hardware directly and
2214    unrotated, so the GL hacks themselves are responsible for rotating the
2215    GL scene to match current_device_rotation().
2216
2217    Touch events are converted to mouse clicks, and those XEvent coordinates
2218    are reported in the coordinate system currently in use by the X11 window.
2219    Again, GL must convert those.
2220  */
2221 - (void)didRotate:(NSNotification *)notification
2222 {
2223   UIDeviceOrientation current = [[UIDevice currentDevice] orientation];
2224
2225   /* Sometimes UIDevice doesn't know the proper orientation, or the device is
2226      face up/face down, so in those cases fall back to the status bar
2227      orientation. The SaverViewController tries to set the status bar to the
2228      proper orientation before it creates the XScreenSaverView; see
2229      _storedOrientation in SaverViewController.
2230    */
2231   if (current == UIDeviceOrientationUnknown ||
2232     current == UIDeviceOrientationFaceUp ||
2233     current == UIDeviceOrientationFaceDown) {
2234     /* Mind the differences between UIInterfaceOrientation and
2235        UIDeviceOrientaiton:
2236        1. UIInterfaceOrientation does not include FaceUp and FaceDown.
2237        2. LandscapeLeft and LandscapeRight are swapped between the two. But
2238           converting between device and interface orientation doesn't need to
2239           take this into account, because (from the UIInterfaceOrientation
2240           description): "rotating the device requires rotating the content in
2241           the opposite direction."
2242          */
2243     current = [UIApplication sharedApplication].statusBarOrientation;
2244   }
2245
2246   /* On the iPad (but not iPhone 3GS, or the simulator) sometimes we get
2247      an orientation change event with an unknown orientation.  Those seem
2248      to always be immediately followed by another orientation change with
2249      a *real* orientation change, so let's try just ignoring those bogus
2250      ones and hoping that the real one comes in shortly...
2251    */
2252   if (current == UIDeviceOrientationUnknown)
2253     return;
2254
2255   if (rotation_ratio >= 0) return;      // in the midst of rotation animation
2256   if (orientation == current) return;   // no change
2257
2258   new_orientation = current;            // current animation target
2259   rotation_ratio = 0;                   // start animating
2260   rot_start_time = double_time();
2261
2262   switch (orientation) {
2263   case UIDeviceOrientationLandscapeLeft:      angle_from = 90;  break;
2264   case UIDeviceOrientationLandscapeRight:     angle_from = 270; break;
2265   case UIDeviceOrientationPortraitUpsideDown: angle_from = 180; break;
2266   default:                                    angle_from = 0;   break;
2267   }
2268
2269   switch (new_orientation) {
2270   case UIDeviceOrientationLandscapeLeft:      angle_to = 90;  break;
2271   case UIDeviceOrientationLandscapeRight:     angle_to = 270; break;
2272   case UIDeviceOrientationPortraitUpsideDown: angle_to = 180; break;
2273   default:                                    angle_to = 0;   break;
2274   }
2275
2276   switch (orientation) {
2277   case UIDeviceOrientationLandscapeRight:       // from landscape
2278   case UIDeviceOrientationLandscapeLeft:
2279     rot_from.width  = initial_bounds.height;
2280     rot_from.height = initial_bounds.width;
2281     break;
2282   default:                                      // from portrait
2283     rot_from.width  = initial_bounds.width;
2284     rot_from.height = initial_bounds.height;
2285     break;
2286   }
2287
2288   switch (new_orientation) {
2289   case UIDeviceOrientationLandscapeRight:       // to landscape
2290   case UIDeviceOrientationLandscapeLeft:
2291     rot_to.width  = initial_bounds.height;
2292     rot_to.height = initial_bounds.width;
2293     break;
2294   default:                                      // to portrait
2295     rot_to.width  = initial_bounds.width;
2296     rot_to.height = initial_bounds.height;
2297     break;
2298   }
2299
2300 # if TARGET_IPHONE_SIMULATOR
2301   NSLog (@"%srotation begun: %s %d -> %s %d; %d x %d",
2302          initted_p ? "" : "initial ",
2303          orientname(orientation), (int) rot_current_angle,
2304          orientname(new_orientation), (int) angle_to,
2305          (int) rot_current_size.width, (int) rot_current_size.height);
2306 # endif
2307
2308   // Even though the status bar isn't on the screen, this still does two things:
2309   // 1. It fixes the orientation of the iOS simulator.
2310   // 2. It places the iOS notification center on the expected edge.
2311   // 3. It prevents the notification center from causing rotation events.
2312   [[UIApplication sharedApplication] setStatusBarOrientation:new_orientation
2313                                                     animated:NO];
2314
2315  if (! initted_p) {
2316    // If we've done a rotation but the saver hasn't been initialized yet,
2317    // don't bother going through an X11 resize, but just do it now.
2318    rot_start_time = 0;  // dawn of time
2319    [self hackRotation];
2320  }
2321 }
2322
2323
2324 /* We distinguish between taps and drags.
2325
2326    - Drags/pans (down, motion, up) are sent to the saver to handle.
2327    - Single-taps exit the saver.
2328    - Double-taps are sent to the saver as a "Space" keypress.
2329    - Swipes (really, two-finger drags/pans) send Up/Down/Left/RightArrow keys.
2330
2331    This means a saver cannot respond to a single-tap.  Only a few try to.
2332  */
2333
2334 - (void)initGestures
2335 {
2336   UITapGestureRecognizer *dtap = [[UITapGestureRecognizer alloc]
2337                                    initWithTarget:self
2338                                    action:@selector(handleDoubleTap)];
2339   dtap.numberOfTapsRequired = 2;
2340   dtap.numberOfTouchesRequired = 1;
2341
2342   UITapGestureRecognizer *stap = [[UITapGestureRecognizer alloc]
2343                                    initWithTarget:self
2344                                    action:@selector(handleTap)];
2345   stap.numberOfTapsRequired = 1;
2346   stap.numberOfTouchesRequired = 1;
2347  
2348   UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc]
2349                                   initWithTarget:self
2350                                   action:@selector(handlePan:)];
2351   pan.maximumNumberOfTouches = 1;
2352   pan.minimumNumberOfTouches = 1;
2353  
2354   // I couldn't get Swipe to work, but using a second Pan recognizer works.
2355   UIPanGestureRecognizer *pan2 = [[UIPanGestureRecognizer alloc]
2356                                    initWithTarget:self
2357                                    action:@selector(handlePan2:)];
2358   pan2.maximumNumberOfTouches = 2;
2359   pan2.minimumNumberOfTouches = 2;
2360
2361   // Also handle long-touch, and treat that the same as Pan.
2362   // Without this, panning doesn't start until there's motion, so the trick
2363   // of holding down your finger to freeze the scene doesn't work.
2364   //
2365   UILongPressGestureRecognizer *hold = [[UILongPressGestureRecognizer alloc]
2366                                          initWithTarget:self
2367                                          action:@selector(handleLongPress:)];
2368   hold.numberOfTapsRequired = 0;
2369   hold.numberOfTouchesRequired = 1;
2370   hold.minimumPressDuration = 0.25;   /* 1/4th second */
2371
2372   [stap requireGestureRecognizerToFail: dtap];
2373   [stap requireGestureRecognizerToFail: hold];
2374   [dtap requireGestureRecognizerToFail: hold];
2375   [pan  requireGestureRecognizerToFail: hold];
2376
2377   [self setMultipleTouchEnabled:YES];
2378
2379   [self addGestureRecognizer: dtap];
2380   [self addGestureRecognizer: stap];
2381   [self addGestureRecognizer: pan];
2382   [self addGestureRecognizer: pan2];
2383   [self addGestureRecognizer: hold];
2384
2385   [dtap release];
2386   [stap release];
2387   [pan  release];
2388   [pan2 release];
2389   [hold release];
2390 }
2391
2392
2393 /* Given a mouse (touch) coordinate in unrotated, unscaled view coordinates,
2394    convert it to what X11 and OpenGL expect.
2395
2396    Getting this crap right is tricky, given the confusion of the various
2397    scale factors, so here's a checklist that I think covers all of the X11
2398    and OpenGL cases. For each of these: rotate to all 4 orientations;
2399    ensure the mouse tracks properly to all 4 corners.
2400
2401    Test it in Xcode 6, because Xcode 5.0.2 can't run the iPhone6+ simulator.
2402
2403    Test hacks must cover:
2404      X11 ignoreRotation = true
2405      X11 ignoreRotation = false
2406      OpenGL (rotation is handled manually, so they never ignoreRotation)
2407
2408    Test devices must cover:
2409      contentScaleFactor = 1, hackedContentScaleFactor = 1 (iPad 2)
2410      contentScaleFactor = 2, hackedContentScaleFactor = 1 (iPad Retina Air)
2411      contentScaleFactor = 2, hackedContentScaleFactor = 2 (iPhone 5 5s 6 6+)
2412
2413      iPad 2:    768x1024 / 1 = 768x1024
2414      iPad Air: 1536x2048 / 2 = 768x1024 (iPad Retina is identical)
2415      iPhone 4s:  640x960 / 2 = 320x480
2416      iPhone 5:  640x1136 / 2 = 320x568 (iPhone 5s and iPhone 6 are identical)
2417      iPhone 6+: 640x1136 / 2 = 320x568 (nativeBounds 960x1704 nativeScale 3)
2418    
2419    Tests:
2420                       iPad2 iPadAir iPhone4s iPhone5 iPhone6+
2421      Attraction X  yes  Y       Y       Y       Y       Y
2422      Fireworkx  X  no   Y       Y       Y       Y       Y
2423      Carousel   GL yes  Y       Y       Y       Y       Y
2424      Voronoi    GL no   Y       Y       Y       Y       Y
2425  */
2426 - (void) convertMouse:(int)rot x:(int*)x y:(int *)y
2427 {
2428   int xx = *x, yy = *y;
2429
2430 # if TARGET_IPHONE_SIMULATOR
2431   {
2432     XWindowAttributes xgwa;
2433     XGetWindowAttributes (xdpy, xwindow, &xgwa);
2434     NSLog (@"TOUCH %4d, %-4d in %4d x %-4d  ig=%d rr=%d cs=%.0f hcs=%.0f\n",
2435            *x, *y, 
2436            xgwa.width, xgwa.height,
2437            ignore_rotation_p, [self reshapeRotatedWindow],
2438            [self contentScaleFactor],
2439            [self hackedContentScaleFactor]);
2440   }
2441 # endif // TARGET_IPHONE_SIMULATOR
2442
2443   if (!ignore_rotation_p && [self reshapeRotatedWindow]) {
2444     //
2445     // For X11 hacks with ignoreRotation == false, we need to rotate the
2446     // coordinates to match the unrotated X11 window.  We do not do this
2447     // for GL hacks, or for X11 hacks with ignoreRotation == true.
2448     //
2449     int w = [self frame].size.width;
2450     int h = [self frame].size.height;
2451     int swap;
2452     switch (orientation) {
2453     case UIDeviceOrientationLandscapeRight:
2454       swap = xx; xx = h-yy; yy = swap;
2455       break;
2456     case UIDeviceOrientationLandscapeLeft:
2457       swap = xx; xx = yy; yy = w-swap;
2458       break;
2459     case UIDeviceOrientationPortraitUpsideDown: 
2460       xx = w-xx; yy = h-yy;
2461     default:
2462       break;
2463     }
2464   }
2465
2466   double s = [self hackedContentScaleFactor];
2467   *x = xx * s;
2468   *y = yy * s;
2469
2470 # if TARGET_IPHONE_SIMULATOR || !defined __OPTIMIZE__
2471   {
2472     XWindowAttributes xgwa;
2473     XGetWindowAttributes (xdpy, xwindow, &xgwa);
2474     NSLog (@"touch %4d, %-4d in %4d x %-4d  ig=%d rr=%d cs=%.0f hcs=%.0f\n",
2475            *x, *y, 
2476            xgwa.width, xgwa.height,
2477            ignore_rotation_p, [self reshapeRotatedWindow],
2478            [self contentScaleFactor],
2479            [self hackedContentScaleFactor]);
2480     if (*x < 0 || *y < 0 || *x > xgwa.width || *y > xgwa.height)
2481       abort();
2482   }
2483 # endif // TARGET_IPHONE_SIMULATOR
2484 }
2485
2486
2487 /* Single click exits saver.
2488  */
2489 - (void) handleTap
2490 {
2491   [self stopAndClose:NO];
2492 }
2493
2494
2495 /* Double click sends Space KeyPress.
2496  */
2497 - (void) handleDoubleTap
2498 {
2499   if (!xsft->event_cb || !xwindow) return;
2500
2501   XEvent xe;
2502   memset (&xe, 0, sizeof(xe));
2503   xe.xkey.keycode = ' ';
2504   xe.xany.type = KeyPress;
2505   BOOL ok1 = [self sendEvent: &xe];
2506   xe.xany.type = KeyRelease;
2507   BOOL ok2 = [self sendEvent: &xe];
2508   if (!(ok1 || ok2))
2509     [self beep];
2510 }
2511
2512
2513 /* Drag with one finger down: send MotionNotify.
2514  */
2515 - (void) handlePan:(UIGestureRecognizer *)sender
2516 {
2517   if (!xsft->event_cb || !xwindow) return;
2518
2519   XEvent xe;
2520   memset (&xe, 0, sizeof(xe));
2521
2522   CGPoint p = [sender locationInView:self];  // this is in points, not pixels
2523   int x = p.x;
2524   int y = p.y;
2525   [self convertMouse: rot_current_angle x:&x y:&y];
2526   jwxyz_mouse_moved (xdpy, xwindow, x, y);
2527
2528   switch (sender.state) {
2529   case UIGestureRecognizerStateBegan:
2530     xe.xany.type = ButtonPress;
2531     xe.xbutton.button = 1;
2532     xe.xbutton.x = x;
2533     xe.xbutton.y = y;
2534     break;
2535
2536   case UIGestureRecognizerStateEnded:
2537     xe.xany.type = ButtonRelease;
2538     xe.xbutton.button = 1;
2539     xe.xbutton.x = x;
2540     xe.xbutton.y = y;
2541     break;
2542
2543   case UIGestureRecognizerStateChanged:
2544     xe.xany.type = MotionNotify;
2545     xe.xmotion.x = x;
2546     xe.xmotion.y = y;
2547     break;
2548
2549   default:
2550     break;
2551   }
2552
2553   BOOL ok = [self sendEvent: &xe];
2554   if (!ok && xe.xany.type == ButtonRelease)
2555     [self beep];
2556 }
2557
2558
2559 /* Hold one finger down: assume we're about to start dragging.
2560    Treat the same as Pan.
2561  */
2562 - (void) handleLongPress:(UIGestureRecognizer *)sender
2563 {
2564   [self handlePan:sender];
2565 }
2566
2567
2568
2569 /* Drag with 2 fingers down: send arrow keys.
2570  */
2571 - (void) handlePan2:(UIPanGestureRecognizer *)sender
2572 {
2573   if (!xsft->event_cb || !xwindow) return;
2574
2575   if (sender.state != UIGestureRecognizerStateEnded)
2576     return;
2577
2578   XEvent xe;
2579   memset (&xe, 0, sizeof(xe));
2580
2581   CGPoint p = [sender locationInView:self];  // this is in points, not pixels
2582   int x = p.x;
2583   int y = p.y;
2584   [self convertMouse: rot_current_angle x:&x y:&y];
2585
2586   if (abs(x) > abs(y))
2587     xe.xkey.keycode = (x > 0 ? XK_Right : XK_Left);
2588   else
2589     xe.xkey.keycode = (y > 0 ? XK_Down : XK_Up);
2590
2591   BOOL ok1 = [self sendEvent: &xe];
2592   xe.xany.type = KeyRelease;
2593   BOOL ok2 = [self sendEvent: &xe];
2594   if (!(ok1 || ok2))
2595     [self beep];
2596 }
2597
2598
2599 /* We need this to respond to "shake" gestures
2600  */
2601 - (BOOL)canBecomeFirstResponder
2602 {
2603   return YES;
2604 }
2605
2606 - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
2607 {
2608 }
2609
2610
2611 - (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event
2612 {
2613 }
2614
2615 /* Shake means exit and launch a new saver.
2616  */
2617 - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
2618 {
2619   [self stopAndClose:YES];
2620 }
2621
2622
2623 - (void)setScreenLocked:(BOOL)locked
2624 {
2625   if (screenLocked == locked) return;
2626   screenLocked = locked;
2627   if (locked) {
2628     if ([self isAnimating])
2629       [self stopAnimation];
2630   } else {
2631     if (! [self isAnimating])
2632       [self startAnimation];
2633   }
2634 }
2635
2636 - (NSDictionary *)getGLProperties
2637 {
2638   return [NSDictionary dictionaryWithObjectsAndKeys:
2639           kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat,
2640           nil];
2641 }
2642
2643 - (void)addExtraRenderbuffers:(CGSize)size
2644 {
2645   // No extra renderbuffers are needed for 2D screenhacks.
2646 }
2647
2648 #endif // USE_IPHONE
2649
2650
2651 - (void) checkForUpdates
2652 {
2653 # ifndef USE_IPHONE
2654   // We only check once at startup, even if there are multiple screens,
2655   // and even if this saver is running for many days.
2656   // (Uh, except this doesn't work because this static isn't shared,
2657   // even if we make it an exported global. Not sure why. Oh well.)
2658   static BOOL checked_p = NO;
2659   if (checked_p) return;
2660   checked_p = YES;
2661
2662   // If it's off, don't bother running the updater.  Otherwise, the
2663   // updater will decide if it's time to hit the network.
2664   if (! get_boolean_resource (xdpy,
2665                               SUSUEnableAutomaticChecksKey,
2666                               SUSUEnableAutomaticChecksKey))
2667     return;
2668
2669   NSString *updater = @"XScreenSaverUpdater.app";
2670
2671   // There may be multiple copies of the updater: e.g., one in /Applications
2672   // and one in the mounted installer DMG!  It's important that we run the
2673   // one from the disk and not the DMG, so search for the right one.
2674   //
2675   NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
2676   NSBundle *bundle = [NSBundle bundleForClass:[self class]];
2677   NSArray *search =
2678     @[[[bundle bundlePath] stringByDeletingLastPathComponent],
2679       [@"~/Library/Screen Savers" stringByExpandingTildeInPath],
2680       @"/Library/Screen Savers",
2681       @"/System/Library/Screen Savers",
2682       @"/Applications",
2683       @"/Applications/Utilities"];
2684   NSString *app_path = nil;
2685   for (NSString *dir in search) {
2686     NSString *p = [dir stringByAppendingPathComponent:updater];
2687     if ([[NSFileManager defaultManager] fileExistsAtPath:p]) {
2688       app_path = p;
2689       break;
2690     }
2691   }
2692
2693   if (! app_path)
2694     app_path = [workspace fullPathForApplication:updater];
2695
2696   if (app_path && [app_path hasPrefix:@"/Volumes/XScreenSaver "])
2697     app_path = 0;  // The DMG version will not do.
2698
2699   if (!app_path) {
2700     NSLog(@"Unable to find %@", updater);
2701     return;
2702   }
2703
2704   NSError *err = nil;
2705   if (! [workspace launchApplicationAtURL:[NSURL fileURLWithPath:app_path]
2706                    options:(NSWorkspaceLaunchWithoutAddingToRecents |
2707                             NSWorkspaceLaunchWithoutActivation |
2708                             NSWorkspaceLaunchAndHide)
2709                    configuration:nil
2710                    error:&err]) {
2711     NSLog(@"Unable to launch %@: %@", app_path, err);
2712   }
2713
2714 # endif // !USE_IPHONE
2715 }
2716
2717
2718 @end
2719
2720 /* Utility functions...
2721  */
2722
2723 static PrefsReader *
2724 get_prefsReader (Display *dpy)
2725 {
2726   XScreenSaverView *view = jwxyz_window_view (XRootWindow (dpy, 0));
2727   if (!view) return 0;
2728   return [view prefsReader];
2729 }
2730
2731
2732 char *
2733 get_string_resource (Display *dpy, char *name, char *class)
2734 {
2735   return [get_prefsReader(dpy) getStringResource:name];
2736 }
2737
2738 Bool
2739 get_boolean_resource (Display *dpy, char *name, char *class)
2740 {
2741   return [get_prefsReader(dpy) getBooleanResource:name];
2742 }
2743
2744 int
2745 get_integer_resource (Display *dpy, char *name, char *class)
2746 {
2747   return [get_prefsReader(dpy) getIntegerResource:name];
2748 }
2749
2750 double
2751 get_float_resource (Display *dpy, char *name, char *class)
2752 {
2753   return [get_prefsReader(dpy) getFloatResource:name];
2754 }