From http://www.jwz.org/xscreensaver/xscreensaver-5.33.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 (xdpy, xwindow, fpst, xdata);
1704
1705   gettimeofday (&tv, 0);
1706   now = tv.tv_sec + (tv.tv_usec / 1000000.0);
1707   next_frame_time = now + (delay / 1000000.0);
1708
1709   [self drawBackbuffer];
1710
1711 # ifdef USE_IPHONE      // Allow savers on the iPhone to run full-tilt.
1712   if (delay < [self animationTimeInterval])
1713     [self setAnimationTimeInterval:(delay / 1000000.0)];
1714 # endif
1715
1716 # ifdef DO_GC_HACKERY
1717   /* Current theory is that the 10.6 garbage collector sucks in the
1718      following way:
1719
1720      It only does a collection when a threshold of outstanding
1721      collectable allocations has been surpassed.  However, CoreGraphics
1722      creates lots of small collectable allocations that contain pointers
1723      to very large non-collectable allocations: a small CG object that's
1724      collectable referencing large malloc'd allocations (non-collectable)
1725      containing bitmap data.  So the large allocation doesn't get freed
1726      until GC collects the small allocation, which triggers its finalizer
1727      to run which frees the large allocation.  So GC is deciding that it
1728      doesn't really need to run, even though the process has gotten
1729      enormous.  GC eventually runs once pageouts have happened, but by
1730      then it's too late, and the machine's resident set has been
1731      sodomized.
1732
1733      So, we force an exhaustive garbage collection in this process
1734      approximately every 5 seconds whether the system thinks it needs 
1735      one or not.
1736   */
1737   {
1738     static int tick = 0;
1739     if (++tick > 5*30) {
1740       tick = 0;
1741       objc_collect (OBJC_EXHAUSTIVE_COLLECTION);
1742     }
1743   }
1744 # endif // DO_GC_HACKERY
1745
1746 # ifdef USE_IPHONE
1747   }
1748   @catch (NSException *e) {
1749     [self handleException: e];
1750   }
1751 # endif // USE_IPHONE
1752 }
1753
1754
1755 #ifndef USE_BACKBUFFER
1756
1757 - (void) animateOneFrame
1758 {
1759   [self render_x11];
1760   jwxyz_flush_context(xdpy);
1761 }
1762
1763 #else  // USE_BACKBUFFER
1764
1765 - (void) animateOneFrame
1766 {
1767   // Render X11 into the backing store bitmap...
1768
1769   NSAssert (backbuffer, @"no back buffer");
1770
1771 # ifdef USE_IPHONE
1772   UIGraphicsPushContext (backbuffer);
1773 # endif
1774
1775   [self render_x11];
1776
1777 # if defined USE_IPHONE && defined USE_BACKBUFFER
1778   UIGraphicsPopContext();
1779 # endif
1780 }
1781
1782 #endif // USE_BACKBUFFER
1783
1784
1785
1786 - (void) setFrame:(NSRect) newRect
1787 {
1788   [super setFrame:newRect];
1789
1790   if (xwindow)     // inform Xlib that the window has changed now.
1791     [self resize_x11];
1792 }
1793
1794
1795 # ifndef USE_IPHONE  // Doesn't exist on iOS
1796 - (void) setFrameSize:(NSSize) newSize
1797 {
1798   [super setFrameSize:newSize];
1799   if (xwindow)
1800     [self resize_x11];
1801 }
1802 # endif // !USE_IPHONE
1803
1804
1805 +(BOOL) performGammaFade
1806 {
1807   return YES;
1808 }
1809
1810 - (BOOL) hasConfigureSheet
1811 {
1812   return YES;
1813 }
1814
1815 + (NSString *) decompressXML: (NSData *)data
1816 {
1817   if (! data) return 0;
1818   BOOL compressed_p = !!strncmp ((const char *) data.bytes, "<?xml", 5);
1819
1820   // If it's not already XML, decompress it.
1821   NSAssert (compressed_p, @"xml isn't compressed");
1822   if (compressed_p) {
1823     NSMutableData *data2 = 0;
1824     int ret = -1;
1825     z_stream zs;
1826     memset (&zs, 0, sizeof(zs));
1827     ret = inflateInit2 (&zs, 16 + MAX_WBITS);
1828     if (ret == Z_OK) {
1829       UInt32 usize = * (UInt32 *) (data.bytes + data.length - 4);
1830       data2 = [NSMutableData dataWithLength: usize];
1831       zs.next_in   = (Bytef *) data.bytes;
1832       zs.avail_in  = (uint) data.length;
1833       zs.next_out  = (Bytef *) data2.bytes;
1834       zs.avail_out = (uint) data2.length;
1835       ret = inflate (&zs, Z_FINISH);
1836       inflateEnd (&zs);
1837     }
1838     if (ret == Z_OK || ret == Z_STREAM_END)
1839       data = data2;
1840     else
1841       NSAssert2 (0, @"gunzip error: %d: %s",
1842                  ret, (zs.msg ? zs.msg : "<null>"));
1843   }
1844
1845   return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
1846 }
1847
1848
1849 #ifndef USE_IPHONE
1850 - (NSWindow *) configureSheet
1851 #else
1852 - (UIViewController *) configureView
1853 #endif
1854 {
1855   NSBundle *bundle = [NSBundle bundleForClass:[self class]];
1856   NSString *file = [NSString stringWithCString:xsft->progclass
1857                                       encoding:NSISOLatin1StringEncoding];
1858   file = [file lowercaseString];
1859   NSString *path = [bundle pathForResource:file ofType:@"xml"];
1860   if (!path) {
1861     NSLog (@"%@.xml does not exist in the application bundle: %@/",
1862            file, [bundle resourcePath]);
1863     return nil;
1864   }
1865   
1866 # ifdef USE_IPHONE
1867   UIViewController *sheet;
1868 # else  // !USE_IPHONE
1869   NSWindow *sheet;
1870 # endif // !USE_IPHONE
1871
1872   NSData *xmld = [NSData dataWithContentsOfFile:path];
1873   NSString *xml = [[self class] decompressXML: xmld];
1874   sheet = [[XScreenSaverConfigSheet alloc]
1875             initWithXML:[xml dataUsingEncoding:NSUTF8StringEncoding]
1876                 options:xsft->options
1877              controller:[prefsReader userDefaultsController]
1878        globalController:[prefsReader globalDefaultsController]
1879                defaults:[prefsReader defaultOptions]];
1880
1881   // #### am I expected to retain this, or not? wtf.
1882   //      I thought not, but if I don't do this, we (sometimes) crash.
1883   // #### Analyze says "potential leak of an object stored into sheet"
1884   // [sheet retain];
1885
1886   return sheet;
1887 }
1888
1889
1890 - (NSUserDefaultsController *) userDefaultsController
1891 {
1892   return [prefsReader userDefaultsController];
1893 }
1894
1895
1896 /* Announce our willingness to accept keyboard input.
1897  */
1898 - (BOOL)acceptsFirstResponder
1899 {
1900   return YES;
1901 }
1902
1903
1904 - (void) beep
1905 {
1906 # ifndef USE_IPHONE
1907   NSBeep();
1908 # else // USE_IPHONE 
1909
1910   // There's no way to play a standard system alert sound!
1911   // We'd have to include our own WAV for that.
1912   //
1913   // Or we could vibrate:
1914   // #import <AudioToolbox/AudioToolbox.h>
1915   // AudioServicesPlaySystemSound (kSystemSoundID_Vibrate);
1916   //
1917   // Instead, just flash the screen white, then fade.
1918   //
1919   UIView *v = [[UIView alloc] initWithFrame: [self frame]]; 
1920   [v setBackgroundColor: [UIColor whiteColor]];
1921   [[self window] addSubview:v];
1922   [UIView animateWithDuration: 0.1
1923           animations:^{ [v setAlpha: 0.0]; }
1924           completion:^(BOOL finished) { [v removeFromSuperview]; } ];
1925
1926 # endif  // USE_IPHONE
1927 }
1928
1929
1930 /* Send an XEvent to the hack.  Returns YES if it was handled.
1931  */
1932 - (BOOL) sendEvent: (XEvent *) e
1933 {
1934   if (!initted_p || ![self isAnimating]) // no event handling unless running.
1935     return NO;
1936
1937   [self lockFocus];
1938   [self prepareContext];
1939   BOOL result = xsft->event_cb (xdpy, xwindow, xdata, e);
1940   [self unlockFocus];
1941   return result;
1942 }
1943
1944
1945 #ifndef USE_IPHONE
1946
1947 /* Convert an NSEvent into an XEvent, and pass it along.
1948    Returns YES if it was handled.
1949  */
1950 - (BOOL) convertEvent: (NSEvent *) e
1951             type: (int) type
1952 {
1953   XEvent xe;
1954   memset (&xe, 0, sizeof(xe));
1955   
1956   int state = 0;
1957   
1958   int flags = [e modifierFlags];
1959   if (flags & NSAlphaShiftKeyMask) state |= LockMask;
1960   if (flags & NSShiftKeyMask)      state |= ShiftMask;
1961   if (flags & NSControlKeyMask)    state |= ControlMask;
1962   if (flags & NSAlternateKeyMask)  state |= Mod1Mask;
1963   if (flags & NSCommandKeyMask)    state |= Mod2Mask;
1964   
1965   NSPoint p = [[[e window] contentView] convertPoint:[e locationInWindow]
1966                                             toView:self];
1967 # ifdef USE_IPHONE
1968   double s = [self hackedContentScaleFactor];
1969 # else
1970   int s = 1;
1971 # endif
1972   int x = s * p.x;
1973   int y = s * ([self bounds].size.height - p.y);
1974
1975   xe.xany.type = type;
1976   switch (type) {
1977     case ButtonPress:
1978     case ButtonRelease:
1979       xe.xbutton.x = x;
1980       xe.xbutton.y = y;
1981       xe.xbutton.state = state;
1982       if ([e type] == NSScrollWheel)
1983         xe.xbutton.button = ([e deltaY] > 0 ? Button4 :
1984                              [e deltaY] < 0 ? Button5 :
1985                              [e deltaX] > 0 ? Button6 :
1986                              [e deltaX] < 0 ? Button7 :
1987                              0);
1988       else
1989         xe.xbutton.button = [e buttonNumber] + 1;
1990       break;
1991     case MotionNotify:
1992       xe.xmotion.x = x;
1993       xe.xmotion.y = y;
1994       xe.xmotion.state = state;
1995       break;
1996     case KeyPress:
1997     case KeyRelease:
1998       {
1999         NSString *ns = (([e type] == NSFlagsChanged) ? 0 :
2000                         [e charactersIgnoringModifiers]);
2001         KeySym k = 0;
2002
2003         if (!ns || [ns length] == 0)                    // dead key
2004           {
2005             // Cocoa hides the difference between left and right keys.
2006             // Also we only get KeyPress events for these, no KeyRelease
2007             // (unless we hack the mod state manually.  Bleh.)
2008             //
2009             if      (flags & NSAlphaShiftKeyMask)   k = XK_Caps_Lock;
2010             else if (flags & NSShiftKeyMask)        k = XK_Shift_L;
2011             else if (flags & NSControlKeyMask)      k = XK_Control_L;
2012             else if (flags & NSAlternateKeyMask)    k = XK_Alt_L;
2013             else if (flags & NSCommandKeyMask)      k = XK_Meta_L;
2014           }
2015         else if ([ns length] == 1)                      // real key
2016           {
2017             switch ([ns characterAtIndex:0]) {
2018             case NSLeftArrowFunctionKey:  k = XK_Left;      break;
2019             case NSRightArrowFunctionKey: k = XK_Right;     break;
2020             case NSUpArrowFunctionKey:    k = XK_Up;        break;
2021             case NSDownArrowFunctionKey:  k = XK_Down;      break;
2022             case NSPageUpFunctionKey:     k = XK_Page_Up;   break;
2023             case NSPageDownFunctionKey:   k = XK_Page_Down; break;
2024             case NSHomeFunctionKey:       k = XK_Home;      break;
2025             case NSPrevFunctionKey:       k = XK_Prior;     break;
2026             case NSNextFunctionKey:       k = XK_Next;      break;
2027             case NSBeginFunctionKey:      k = XK_Begin;     break;
2028             case NSEndFunctionKey:        k = XK_End;       break;
2029             case NSF1FunctionKey:         k = XK_F1;        break;
2030             case NSF2FunctionKey:         k = XK_F2;        break;
2031             case NSF3FunctionKey:         k = XK_F3;        break;
2032             case NSF4FunctionKey:         k = XK_F4;        break;
2033             case NSF5FunctionKey:         k = XK_F5;        break;
2034             case NSF6FunctionKey:         k = XK_F6;        break;
2035             case NSF7FunctionKey:         k = XK_F7;        break;
2036             case NSF8FunctionKey:         k = XK_F8;        break;
2037             case NSF9FunctionKey:         k = XK_F9;        break;
2038             case NSF10FunctionKey:        k = XK_F10;       break;
2039             case NSF11FunctionKey:        k = XK_F11;       break;
2040             case NSF12FunctionKey:        k = XK_F12;       break;
2041             default:
2042               {
2043                 const char *s =
2044                   [ns cStringUsingEncoding:NSISOLatin1StringEncoding];
2045                 k = (s && *s ? *s : 0);
2046               }
2047               break;
2048             }
2049           }
2050
2051         if (! k) return YES;   // E.g., "KeyRelease XK_Shift_L"
2052
2053         xe.xkey.keycode = k;
2054         xe.xkey.state = state;
2055         break;
2056       }
2057     default:
2058       NSAssert1 (0, @"unknown X11 event type: %d", type);
2059       break;
2060   }
2061
2062   return [self sendEvent: &xe];
2063 }
2064
2065
2066 - (void) mouseDown: (NSEvent *) e
2067 {
2068   if (! [self convertEvent:e type:ButtonPress])
2069     [super mouseDown:e];
2070 }
2071
2072 - (void) mouseUp: (NSEvent *) e
2073 {
2074   if (! [self convertEvent:e type:ButtonRelease])
2075     [super mouseUp:e];
2076 }
2077
2078 - (void) otherMouseDown: (NSEvent *) e
2079 {
2080   if (! [self convertEvent:e type:ButtonPress])
2081     [super otherMouseDown:e];
2082 }
2083
2084 - (void) otherMouseUp: (NSEvent *) e
2085 {
2086   if (! [self convertEvent:e type:ButtonRelease])
2087     [super otherMouseUp:e];
2088 }
2089
2090 - (void) mouseMoved: (NSEvent *) e
2091 {
2092   if (! [self convertEvent:e type:MotionNotify])
2093     [super mouseMoved:e];
2094 }
2095
2096 - (void) mouseDragged: (NSEvent *) e
2097 {
2098   if (! [self convertEvent:e type:MotionNotify])
2099     [super mouseDragged:e];
2100 }
2101
2102 - (void) otherMouseDragged: (NSEvent *) e
2103 {
2104   if (! [self convertEvent:e type:MotionNotify])
2105     [super otherMouseDragged:e];
2106 }
2107
2108 - (void) scrollWheel: (NSEvent *) e
2109 {
2110   if (! [self convertEvent:e type:ButtonPress])
2111     [super scrollWheel:e];
2112 }
2113
2114 - (void) keyDown: (NSEvent *) e
2115 {
2116   if (! [self convertEvent:e type:KeyPress])
2117     [super keyDown:e];
2118 }
2119
2120 - (void) keyUp: (NSEvent *) e
2121 {
2122   if (! [self convertEvent:e type:KeyRelease])
2123     [super keyUp:e];
2124 }
2125
2126 - (void) flagsChanged: (NSEvent *) e
2127 {
2128   if (! [self convertEvent:e type:KeyPress])
2129     [super flagsChanged:e];
2130 }
2131
2132
2133 - (NSOpenGLPixelFormat *) getGLPixelFormat
2134 {
2135   NSAssert (prefsReader, @"no prefsReader for getGLPixelFormat");
2136
2137   NSOpenGLPixelFormatAttribute attrs[40];
2138   int i = 0;
2139   attrs[i++] = NSOpenGLPFAColorSize; attrs[i++] = 24;
2140
2141   if (double_buffered_p)
2142     attrs[i++] = NSOpenGLPFADoubleBuffer;
2143
2144   attrs[i] = 0;
2145
2146   return [[NSOpenGLPixelFormat alloc] initWithAttributes:attrs];
2147 }
2148
2149 #else  // USE_IPHONE
2150
2151
2152 - (void) stopAndClose:(Bool)relaunch_p
2153 {
2154   if ([self isAnimating])
2155     [self stopAnimation];
2156
2157   /* Need to make the SaverListController be the firstResponder again
2158      so that it can continue to receive its own shake events.  I
2159      suppose that this abstraction-breakage means that I'm adding
2160      XScreenSaverView to the UINavigationController wrong...
2161    */
2162 //  UIViewController *v = [[self window] rootViewController];
2163 //  if ([v isKindOfClass: [UINavigationController class]]) {
2164 //    UINavigationController *n = (UINavigationController *) v;
2165 //    [[n topViewController] becomeFirstResponder];
2166 //  }
2167   [self resignFirstResponder];
2168
2169   if (relaunch_p) {   // Fake a shake on the SaverListController.
2170     [_delegate didShake:self];
2171   } else {      // Not launching another, animate our return to the list.
2172 # if TARGET_IPHONE_SIMULATOR
2173     NSLog (@"fading back to saver list");
2174 # endif
2175     [_delegate wantsFadeOut:self];
2176   }
2177 }
2178
2179
2180 /* Whether the shape of the X11 Window should be changed to HxW when the
2181    device is in a landscape orientation.  X11 hacks want this, but OpenGL
2182    hacks do not.
2183  */
2184 - (BOOL)reshapeRotatedWindow
2185 {
2186   return YES;
2187 }
2188
2189
2190 /* Called after the device's orientation has changed.
2191    
2192    Rotation is complicated: the UI, X11 and OpenGL work in 3 different ways.
2193
2194    The UI (list of savers, preferences panels) is rotated by the system,
2195    because its UIWindow is under a UINavigationController that does
2196    automatic rotation, using Core Animation.
2197
2198    The savers are under a different UIWindow and a UINavigationController
2199    that does not do automatic rotation.
2200
2201    We have to do it this way because using Core Animation on an EAGLContext
2202    causes the OpenGL pipeline used on both X11 and GL savers to fall back on
2203    software rendering and performance goes to hell.
2204
2205    During and after rotation, the size/shape of the X11 window changes,
2206    and ConfigureNotify events are generated.
2207
2208    X11 code (jwxyz) continues to draw into the (reshaped) backbuffer, which is
2209    rendered onto a rotating OpenGL quad.
2210
2211    GL code always recieves a portrait-oriented X11 Window whose size never
2212    changes.  The GL COLOR_BUFFER is displayed on the hardware directly and
2213    unrotated, so the GL hacks themselves are responsible for rotating the
2214    GL scene to match current_device_rotation().
2215
2216    Touch events are converted to mouse clicks, and those XEvent coordinates
2217    are reported in the coordinate system currently in use by the X11 window.
2218    Again, GL must convert those.
2219  */
2220 - (void)didRotate:(NSNotification *)notification
2221 {
2222   UIDeviceOrientation current = [[UIDevice currentDevice] orientation];
2223
2224   /* Sometimes UIDevice doesn't know the proper orientation, or the device is
2225      face up/face down, so in those cases fall back to the status bar
2226      orientation. The SaverViewController tries to set the status bar to the
2227      proper orientation before it creates the XScreenSaverView; see
2228      _storedOrientation in SaverViewController.
2229    */
2230   if (current == UIDeviceOrientationUnknown ||
2231     current == UIDeviceOrientationFaceUp ||
2232     current == UIDeviceOrientationFaceDown) {
2233     /* Mind the differences between UIInterfaceOrientation and
2234        UIDeviceOrientaiton:
2235        1. UIInterfaceOrientation does not include FaceUp and FaceDown.
2236        2. LandscapeLeft and LandscapeRight are swapped between the two. But
2237           converting between device and interface orientation doesn't need to
2238           take this into account, because (from the UIInterfaceOrientation
2239           description): "rotating the device requires rotating the content in
2240           the opposite direction."
2241          */
2242     current = [UIApplication sharedApplication].statusBarOrientation;
2243   }
2244
2245   /* On the iPad (but not iPhone 3GS, or the simulator) sometimes we get
2246      an orientation change event with an unknown orientation.  Those seem
2247      to always be immediately followed by another orientation change with
2248      a *real* orientation change, so let's try just ignoring those bogus
2249      ones and hoping that the real one comes in shortly...
2250    */
2251   if (current == UIDeviceOrientationUnknown)
2252     return;
2253
2254   if (rotation_ratio >= 0) return;      // in the midst of rotation animation
2255   if (orientation == current) return;   // no change
2256
2257   new_orientation = current;            // current animation target
2258   rotation_ratio = 0;                   // start animating
2259   rot_start_time = double_time();
2260
2261   switch (orientation) {
2262   case UIDeviceOrientationLandscapeLeft:      angle_from = 90;  break;
2263   case UIDeviceOrientationLandscapeRight:     angle_from = 270; break;
2264   case UIDeviceOrientationPortraitUpsideDown: angle_from = 180; break;
2265   default:                                    angle_from = 0;   break;
2266   }
2267
2268   switch (new_orientation) {
2269   case UIDeviceOrientationLandscapeLeft:      angle_to = 90;  break;
2270   case UIDeviceOrientationLandscapeRight:     angle_to = 270; break;
2271   case UIDeviceOrientationPortraitUpsideDown: angle_to = 180; break;
2272   default:                                    angle_to = 0;   break;
2273   }
2274
2275   switch (orientation) {
2276   case UIDeviceOrientationLandscapeRight:       // from landscape
2277   case UIDeviceOrientationLandscapeLeft:
2278     rot_from.width  = initial_bounds.height;
2279     rot_from.height = initial_bounds.width;
2280     break;
2281   default:                                      // from portrait
2282     rot_from.width  = initial_bounds.width;
2283     rot_from.height = initial_bounds.height;
2284     break;
2285   }
2286
2287   switch (new_orientation) {
2288   case UIDeviceOrientationLandscapeRight:       // to landscape
2289   case UIDeviceOrientationLandscapeLeft:
2290     rot_to.width  = initial_bounds.height;
2291     rot_to.height = initial_bounds.width;
2292     break;
2293   default:                                      // to portrait
2294     rot_to.width  = initial_bounds.width;
2295     rot_to.height = initial_bounds.height;
2296     break;
2297   }
2298
2299 # if TARGET_IPHONE_SIMULATOR
2300   NSLog (@"%srotation begun: %s %d -> %s %d; %d x %d",
2301          initted_p ? "" : "initial ",
2302          orientname(orientation), (int) rot_current_angle,
2303          orientname(new_orientation), (int) angle_to,
2304          (int) rot_current_size.width, (int) rot_current_size.height);
2305 # endif
2306
2307   // Even though the status bar isn't on the screen, this still does two things:
2308   // 1. It fixes the orientation of the iOS simulator.
2309   // 2. It places the iOS notification center on the expected edge.
2310   // 3. It prevents the notification center from causing rotation events.
2311   [[UIApplication sharedApplication] setStatusBarOrientation:new_orientation
2312                                                     animated:NO];
2313
2314  if (! initted_p) {
2315    // If we've done a rotation but the saver hasn't been initialized yet,
2316    // don't bother going through an X11 resize, but just do it now.
2317    rot_start_time = 0;  // dawn of time
2318    [self hackRotation];
2319  }
2320 }
2321
2322
2323 /* We distinguish between taps and drags.
2324
2325    - Drags/pans (down, motion, up) are sent to the saver to handle.
2326    - Single-taps exit the saver.
2327    - Double-taps are sent to the saver as a "Space" keypress.
2328    - Swipes (really, two-finger drags/pans) send Up/Down/Left/RightArrow keys.
2329
2330    This means a saver cannot respond to a single-tap.  Only a few try to.
2331  */
2332
2333 - (void)initGestures
2334 {
2335   UITapGestureRecognizer *dtap = [[UITapGestureRecognizer alloc]
2336                                    initWithTarget:self
2337                                    action:@selector(handleDoubleTap)];
2338   dtap.numberOfTapsRequired = 2;
2339   dtap.numberOfTouchesRequired = 1;
2340
2341   UITapGestureRecognizer *stap = [[UITapGestureRecognizer alloc]
2342                                    initWithTarget:self
2343                                    action:@selector(handleTap)];
2344   stap.numberOfTapsRequired = 1;
2345   stap.numberOfTouchesRequired = 1;
2346  
2347   UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc]
2348                                   initWithTarget:self
2349                                   action:@selector(handlePan:)];
2350   pan.maximumNumberOfTouches = 1;
2351   pan.minimumNumberOfTouches = 1;
2352  
2353   // I couldn't get Swipe to work, but using a second Pan recognizer works.
2354   UIPanGestureRecognizer *pan2 = [[UIPanGestureRecognizer alloc]
2355                                    initWithTarget:self
2356                                    action:@selector(handlePan2:)];
2357   pan2.maximumNumberOfTouches = 2;
2358   pan2.minimumNumberOfTouches = 2;
2359
2360   // Also handle long-touch, and treat that the same as Pan.
2361   // Without this, panning doesn't start until there's motion, so the trick
2362   // of holding down your finger to freeze the scene doesn't work.
2363   //
2364   UILongPressGestureRecognizer *hold = [[UILongPressGestureRecognizer alloc]
2365                                          initWithTarget:self
2366                                          action:@selector(handleLongPress:)];
2367   hold.numberOfTapsRequired = 0;
2368   hold.numberOfTouchesRequired = 1;
2369   hold.minimumPressDuration = 0.25;   /* 1/4th second */
2370
2371   [stap requireGestureRecognizerToFail: dtap];
2372   [stap requireGestureRecognizerToFail: hold];
2373   [dtap requireGestureRecognizerToFail: hold];
2374   [pan  requireGestureRecognizerToFail: hold];
2375
2376   [self setMultipleTouchEnabled:YES];
2377
2378   [self addGestureRecognizer: dtap];
2379   [self addGestureRecognizer: stap];
2380   [self addGestureRecognizer: pan];
2381   [self addGestureRecognizer: pan2];
2382   [self addGestureRecognizer: hold];
2383
2384   [dtap release];
2385   [stap release];
2386   [pan  release];
2387   [pan2 release];
2388   [hold release];
2389 }
2390
2391
2392 /* Given a mouse (touch) coordinate in unrotated, unscaled view coordinates,
2393    convert it to what X11 and OpenGL expect.
2394
2395    Getting this crap right is tricky, given the confusion of the various
2396    scale factors, so here's a checklist that I think covers all of the X11
2397    and OpenGL cases. For each of these: rotate to all 4 orientations;
2398    ensure the mouse tracks properly to all 4 corners.
2399
2400    Test it in Xcode 6, because Xcode 5.0.2 can't run the iPhone6+ simulator.
2401
2402    Test hacks must cover:
2403      X11 ignoreRotation = true
2404      X11 ignoreRotation = false
2405      OpenGL (rotation is handled manually, so they never ignoreRotation)
2406
2407    Test devices must cover:
2408      contentScaleFactor = 1, hackedContentScaleFactor = 1 (iPad 2)
2409      contentScaleFactor = 2, hackedContentScaleFactor = 1 (iPad Retina Air)
2410      contentScaleFactor = 2, hackedContentScaleFactor = 2 (iPhone 5 5s 6 6+)
2411
2412      iPad 2:    768x1024 / 1 = 768x1024
2413      iPad Air: 1536x2048 / 2 = 768x1024 (iPad Retina is identical)
2414      iPhone 4s:  640x960 / 2 = 320x480
2415      iPhone 5:  640x1136 / 2 = 320x568 (iPhone 5s and iPhone 6 are identical)
2416      iPhone 6+: 640x1136 / 2 = 320x568 (nativeBounds 960x1704 nativeScale 3)
2417    
2418    Tests:
2419                       iPad2 iPadAir iPhone4s iPhone5 iPhone6+
2420      Attraction X  yes  Y       Y       Y       Y       Y
2421      Fireworkx  X  no   Y       Y       Y       Y       Y
2422      Carousel   GL yes  Y       Y       Y       Y       Y
2423      Voronoi    GL no   Y       Y       Y       Y       Y
2424  */
2425 - (void) convertMouse:(int)rot x:(int*)x y:(int *)y
2426 {
2427   int xx = *x, yy = *y;
2428
2429 # if TARGET_IPHONE_SIMULATOR
2430   {
2431     XWindowAttributes xgwa;
2432     XGetWindowAttributes (xdpy, xwindow, &xgwa);
2433     NSLog (@"TOUCH %4d, %-4d in %4d x %-4d  ig=%d rr=%d cs=%.0f hcs=%.0f\n",
2434            *x, *y, 
2435            xgwa.width, xgwa.height,
2436            ignore_rotation_p, [self reshapeRotatedWindow],
2437            [self contentScaleFactor],
2438            [self hackedContentScaleFactor]);
2439   }
2440 # endif // TARGET_IPHONE_SIMULATOR
2441
2442   if (!ignore_rotation_p && [self reshapeRotatedWindow]) {
2443     //
2444     // For X11 hacks with ignoreRotation == false, we need to rotate the
2445     // coordinates to match the unrotated X11 window.  We do not do this
2446     // for GL hacks, or for X11 hacks with ignoreRotation == true.
2447     //
2448     int w = [self frame].size.width;
2449     int h = [self frame].size.height;
2450     int swap;
2451     switch (orientation) {
2452     case UIDeviceOrientationLandscapeRight:
2453       swap = xx; xx = h-yy; yy = swap;
2454       break;
2455     case UIDeviceOrientationLandscapeLeft:
2456       swap = xx; xx = yy; yy = w-swap;
2457       break;
2458     case UIDeviceOrientationPortraitUpsideDown: 
2459       xx = w-xx; yy = h-yy;
2460     default:
2461       break;
2462     }
2463   }
2464
2465   double s = [self hackedContentScaleFactor];
2466   *x = xx * s;
2467   *y = yy * s;
2468
2469 # if TARGET_IPHONE_SIMULATOR || !defined __OPTIMIZE__
2470   {
2471     XWindowAttributes xgwa;
2472     XGetWindowAttributes (xdpy, xwindow, &xgwa);
2473     NSLog (@"touch %4d, %-4d in %4d x %-4d  ig=%d rr=%d cs=%.0f hcs=%.0f\n",
2474            *x, *y, 
2475            xgwa.width, xgwa.height,
2476            ignore_rotation_p, [self reshapeRotatedWindow],
2477            [self contentScaleFactor],
2478            [self hackedContentScaleFactor]);
2479     if (*x < 0 || *y < 0 || *x > xgwa.width || *y > xgwa.height)
2480       abort();
2481   }
2482 # endif // TARGET_IPHONE_SIMULATOR
2483 }
2484
2485
2486 /* Single click exits saver.
2487  */
2488 - (void) handleTap
2489 {
2490   [self stopAndClose:NO];
2491 }
2492
2493
2494 /* Double click sends Space KeyPress.
2495  */
2496 - (void) handleDoubleTap
2497 {
2498   if (!xsft->event_cb || !xwindow) return;
2499
2500   XEvent xe;
2501   memset (&xe, 0, sizeof(xe));
2502   xe.xkey.keycode = ' ';
2503   xe.xany.type = KeyPress;
2504   BOOL ok1 = [self sendEvent: &xe];
2505   xe.xany.type = KeyRelease;
2506   BOOL ok2 = [self sendEvent: &xe];
2507   if (!(ok1 || ok2))
2508     [self beep];
2509 }
2510
2511
2512 /* Drag with one finger down: send MotionNotify.
2513  */
2514 - (void) handlePan:(UIGestureRecognizer *)sender
2515 {
2516   if (!xsft->event_cb || !xwindow) return;
2517
2518   XEvent xe;
2519   memset (&xe, 0, sizeof(xe));
2520
2521   CGPoint p = [sender locationInView:self];  // this is in points, not pixels
2522   int x = p.x;
2523   int y = p.y;
2524   [self convertMouse: rot_current_angle x:&x y:&y];
2525   jwxyz_mouse_moved (xdpy, xwindow, x, y);
2526
2527   switch (sender.state) {
2528   case UIGestureRecognizerStateBegan:
2529     xe.xany.type = ButtonPress;
2530     xe.xbutton.button = 1;
2531     xe.xbutton.x = x;
2532     xe.xbutton.y = y;
2533     break;
2534
2535   case UIGestureRecognizerStateEnded:
2536     xe.xany.type = ButtonRelease;
2537     xe.xbutton.button = 1;
2538     xe.xbutton.x = x;
2539     xe.xbutton.y = y;
2540     break;
2541
2542   case UIGestureRecognizerStateChanged:
2543     xe.xany.type = MotionNotify;
2544     xe.xmotion.x = x;
2545     xe.xmotion.y = y;
2546     break;
2547
2548   default:
2549     break;
2550   }
2551
2552   BOOL ok = [self sendEvent: &xe];
2553   if (!ok && xe.xany.type == ButtonRelease)
2554     [self beep];
2555 }
2556
2557
2558 /* Hold one finger down: assume we're about to start dragging.
2559    Treat the same as Pan.
2560  */
2561 - (void) handleLongPress:(UIGestureRecognizer *)sender
2562 {
2563   [self handlePan:sender];
2564 }
2565
2566
2567
2568 /* Drag with 2 fingers down: send arrow keys.
2569  */
2570 - (void) handlePan2:(UIPanGestureRecognizer *)sender
2571 {
2572   if (!xsft->event_cb || !xwindow) return;
2573
2574   if (sender.state != UIGestureRecognizerStateEnded)
2575     return;
2576
2577   XEvent xe;
2578   memset (&xe, 0, sizeof(xe));
2579
2580   CGPoint p = [sender locationInView:self];  // this is in points, not pixels
2581   int x = p.x;
2582   int y = p.y;
2583   [self convertMouse: rot_current_angle x:&x y:&y];
2584
2585   if (abs(x) > abs(y))
2586     xe.xkey.keycode = (x > 0 ? XK_Right : XK_Left);
2587   else
2588     xe.xkey.keycode = (y > 0 ? XK_Down : XK_Up);
2589
2590   BOOL ok1 = [self sendEvent: &xe];
2591   xe.xany.type = KeyRelease;
2592   BOOL ok2 = [self sendEvent: &xe];
2593   if (!(ok1 || ok2))
2594     [self beep];
2595 }
2596
2597
2598 /* We need this to respond to "shake" gestures
2599  */
2600 - (BOOL)canBecomeFirstResponder
2601 {
2602   return YES;
2603 }
2604
2605 - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
2606 {
2607 }
2608
2609
2610 - (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event
2611 {
2612 }
2613
2614 /* Shake means exit and launch a new saver.
2615  */
2616 - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
2617 {
2618   [self stopAndClose:YES];
2619 }
2620
2621
2622 - (void)setScreenLocked:(BOOL)locked
2623 {
2624   if (screenLocked == locked) return;
2625   screenLocked = locked;
2626   if (locked) {
2627     if ([self isAnimating])
2628       [self stopAnimation];
2629   } else {
2630     if (! [self isAnimating])
2631       [self startAnimation];
2632   }
2633 }
2634
2635 - (NSDictionary *)getGLProperties
2636 {
2637   return [NSDictionary dictionaryWithObjectsAndKeys:
2638           kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat,
2639           nil];
2640 }
2641
2642 - (void)addExtraRenderbuffers:(CGSize)size
2643 {
2644   // No extra renderbuffers are needed for 2D screenhacks.
2645 }
2646
2647 #endif // USE_IPHONE
2648
2649
2650 - (void) checkForUpdates
2651 {
2652 # ifndef USE_IPHONE
2653   // We only check once at startup, even if there are multiple screens,
2654   // and even if this saver is running for many days.
2655   // (Uh, except this doesn't work because this static isn't shared,
2656   // even if we make it an exported global. Not sure why. Oh well.)
2657   static BOOL checked_p = NO;
2658   if (checked_p) return;
2659   checked_p = YES;
2660
2661   // If it's off, don't bother running the updater.  Otherwise, the
2662   // updater will decide if it's time to hit the network.
2663   if (! get_boolean_resource (xdpy,
2664                               SUSUEnableAutomaticChecksKey,
2665                               SUSUEnableAutomaticChecksKey))
2666     return;
2667
2668   NSString *updater = @"XScreenSaverUpdater.app";
2669
2670   // There may be multiple copies of the updater: e.g., one in /Applications
2671   // and one in the mounted installer DMG!  It's important that we run the
2672   // one from the disk and not the DMG, so search for the right one.
2673   //
2674   NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
2675   NSBundle *bundle = [NSBundle bundleForClass:[self class]];
2676   NSArray *search =
2677     @[[[bundle bundlePath] stringByDeletingLastPathComponent],
2678       [@"~/Library/Screen Savers" stringByExpandingTildeInPath],
2679       @"/Library/Screen Savers",
2680       @"/System/Library/Screen Savers",
2681       @"/Applications",
2682       @"/Applications/Utilities"];
2683   NSString *app_path = nil;
2684   for (NSString *dir in search) {
2685     NSString *p = [dir stringByAppendingPathComponent:updater];
2686     if ([[NSFileManager defaultManager] fileExistsAtPath:p]) {
2687       app_path = p;
2688       break;
2689     }
2690   }
2691
2692   if (! app_path)
2693     app_path = [workspace fullPathForApplication:updater];
2694
2695   if (app_path && [app_path hasPrefix:@"/Volumes/XScreenSaver "])
2696     app_path = 0;  // The DMG version will not do.
2697
2698   if (!app_path) {
2699     NSLog(@"Unable to find %@", updater);
2700     return;
2701   }
2702
2703   NSError *err = nil;
2704   if (! [workspace launchApplicationAtURL:[NSURL fileURLWithPath:app_path]
2705                    options:(NSWorkspaceLaunchWithoutAddingToRecents |
2706                             NSWorkspaceLaunchWithoutActivation |
2707                             NSWorkspaceLaunchAndHide)
2708                    configuration:nil
2709                    error:&err]) {
2710     NSLog(@"Unable to launch %@: %@", app_path, err);
2711   }
2712
2713 # endif // !USE_IPHONE
2714 }
2715
2716
2717 @end
2718
2719 /* Utility functions...
2720  */
2721
2722 static PrefsReader *
2723 get_prefsReader (Display *dpy)
2724 {
2725   XScreenSaverView *view = jwxyz_window_view (XRootWindow (dpy, 0));
2726   if (!view) return 0;
2727   return [view prefsReader];
2728 }
2729
2730
2731 char *
2732 get_string_resource (Display *dpy, char *name, char *class)
2733 {
2734   return [get_prefsReader(dpy) getStringResource:name];
2735 }
2736
2737 Bool
2738 get_boolean_resource (Display *dpy, char *name, char *class)
2739 {
2740   return [get_prefsReader(dpy) getBooleanResource:name];
2741 }
2742
2743 int
2744 get_integer_resource (Display *dpy, char *name, char *class)
2745 {
2746   return [get_prefsReader(dpy) getIntegerResource:name];
2747 }
2748
2749 double
2750 get_float_resource (Display *dpy, char *name, char *class)
2751 {
2752   return [get_prefsReader(dpy) getFloatResource:name];
2753 }