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