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