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