ba87a89d633a1f3ec45dbcfb1335228f935d179f
[xscreensaver] / OSX / XScreenSaverView.m
1 /* xscreensaver, Copyright (c) 2006-2013 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 <zlib.h>
20 #import "XScreenSaverView.h"
21 #import "XScreenSaverConfigSheet.h"
22 #import "screenhackI.h"
23 #import "xlockmoreI.h"
24 #import "jwxyz-timers.h"
25
26
27 /* Garbage collection only exists if we are being compiled against the 
28    10.6 SDK or newer, not if we are building against the 10.4 SDK.
29  */
30 #ifndef  MAC_OS_X_VERSION_10_6
31 # define MAC_OS_X_VERSION_10_6 1060  /* undefined in 10.4 SDK, grr */
32 #endif
33 #if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6  /* 10.6 SDK */
34 # import <objc/objc-auto.h>
35 # define DO_GC_HACKERY
36 #endif
37
38 extern struct xscreensaver_function_table *xscreensaver_function_table;
39
40 /* Global variables used by the screen savers
41  */
42 const char *progname;
43 const char *progclass;
44 int mono_p = 0;
45
46
47 # ifdef USE_IPHONE
48
49 extern NSDictionary *make_function_table_dict(void);  // ios-function-table.m
50
51 /* Stub definition of the superclass, for iPhone.
52  */
53 @implementation ScreenSaverView
54 {
55   NSTimeInterval anim_interval;
56   Bool animating_p;
57   NSTimer *anim_timer;
58 }
59
60 - (id)initWithFrame:(NSRect)frame isPreview:(BOOL)isPreview {
61   self = [super initWithFrame:frame];
62   if (! self) return 0;
63   anim_interval = 1.0/30;
64   return self;
65 }
66 - (NSTimeInterval)animationTimeInterval { return anim_interval; }
67 - (void)setAnimationTimeInterval:(NSTimeInterval)i { anim_interval = i; }
68 - (BOOL)hasConfigureSheet { return NO; }
69 - (NSWindow *)configureSheet { return nil; }
70 - (NSView *)configureView { return nil; }
71 - (BOOL)isPreview { return NO; }
72 - (BOOL)isAnimating { return animating_p; }
73 - (void)animateOneFrame { }
74
75 - (void)startAnimation {
76   if (animating_p) return;
77   animating_p = YES;
78   anim_timer = [NSTimer scheduledTimerWithTimeInterval: anim_interval
79                         target:self
80                         selector:@selector(animateOneFrame)
81                         userInfo:nil
82                         repeats:YES];
83 }
84
85 - (void)stopAnimation {
86   if (anim_timer) {
87     [anim_timer invalidate];
88     anim_timer = 0;
89   }
90   animating_p = NO;
91 }
92 @end
93
94 # endif // !USE_IPHONE
95
96
97
98 @interface XScreenSaverView (Private)
99 - (void) stopAndClose:(Bool)relaunch;
100 @end
101
102 @implementation XScreenSaverView
103
104 // Given a lower-cased saver name, returns the function table for it.
105 // If no name, guess the name from the class's bundle name.
106 //
107 - (struct xscreensaver_function_table *) findFunctionTable:(NSString *)name
108 {
109   NSBundle *nsb = [NSBundle bundleForClass:[self class]];
110   NSAssert1 (nsb, @"no bundle for class %@", [self class]);
111
112   NSString *path = [nsb bundlePath];
113   CFURLRef url = CFURLCreateWithFileSystemPath(kCFAllocatorDefault,
114                                                (CFStringRef) path,
115                                                kCFURLPOSIXPathStyle,
116                                                true);
117   CFBundleRef cfb = CFBundleCreate (kCFAllocatorDefault, url);
118   CFRelease (url);
119   NSAssert1 (cfb, @"no CFBundle for \"%@\"", path);
120   // #### Analyze says "Potential leak of an object stored into cfb"
121   
122   if (! name)
123     name = [[path lastPathComponent] stringByDeletingPathExtension];
124
125   name = [[name lowercaseString]
126            stringByReplacingOccurrencesOfString:@" "
127            withString:@""];
128
129 # ifndef USE_IPHONE
130   // CFBundleGetDataPointerForName doesn't work in "Archive" builds.
131   // I'm guessing that symbol-stripping is mandatory.  Fuck.
132   NSString *table_name = [name stringByAppendingString:
133                                  @"_xscreensaver_function_table"];
134   void *addr = CFBundleGetDataPointerForName (cfb, (CFStringRef) table_name);
135   CFRelease (cfb);
136
137   if (! addr)
138     NSLog (@"no symbol \"%@\" for \"%@\"", table_name, path);
139
140 # else  // USE_IPHONE
141   // Remember: any time you add a new saver to the iOS app,
142   // manually run "make ios-function-table.m"!
143   if (! function_tables)
144     function_tables = [make_function_table_dict() retain];
145   NSValue *v = [function_tables objectForKey: name];
146   void *addr = v ? [v pointerValue] : 0;
147 # endif // USE_IPHONE
148
149   return (struct xscreensaver_function_table *) addr;
150 }
151
152
153 // Add the "Contents/Resources/" subdirectory of this screen saver's .bundle
154 // to $PATH for the benefit of savers that include helper shell scripts.
155 //
156 - (void) setShellPath
157 {
158   NSBundle *nsb = [NSBundle bundleForClass:[self class]];
159   NSAssert1 (nsb, @"no bundle for class %@", [self class]);
160   
161   NSString *nsdir = [nsb resourcePath];
162   NSAssert1 (nsdir, @"no resourcePath for class %@", [self class]);
163   const char *dir = [nsdir cStringUsingEncoding:NSUTF8StringEncoding];
164   const char *opath = getenv ("PATH");
165   if (!opath) opath = "/bin"; // $PATH is unset when running under Shark!
166   char *npath = (char *) malloc (strlen (opath) + strlen (dir) + 30);
167   strcpy (npath, "PATH=");
168   strcat (npath, dir);
169   strcat (npath, ":");
170   strcat (npath, opath);
171   if (putenv (npath)) {
172     perror ("putenv");
173     NSAssert1 (0, @"putenv \"%s\" failed", npath);
174   }
175
176   /* Don't free (npath) -- MacOS's putenv() does not copy it. */
177 }
178
179
180 // set an $XSCREENSAVER_CLASSPATH variable so that included shell scripts
181 // (e.g., "xscreensaver-text") know how to look up resources.
182 //
183 - (void) setResourcesEnv:(NSString *) name
184 {
185   NSBundle *nsb = [NSBundle bundleForClass:[self class]];
186   NSAssert1 (nsb, @"no bundle for class %@", [self class]);
187   
188   const char *s = [name cStringUsingEncoding:NSUTF8StringEncoding];
189   char *env = (char *) malloc (strlen (s) + 40);
190   strcpy (env, "XSCREENSAVER_CLASSPATH=");
191   strcat (env, s);
192   if (putenv (env)) {
193     perror ("putenv");
194     NSAssert1 (0, @"putenv \"%s\" failed", env);
195   }
196   /* Don't free (env) -- MacOS's putenv() does not copy it. */
197 }
198
199
200 static void
201 add_default_options (const XrmOptionDescRec *opts,
202                      const char * const *defs,
203                      XrmOptionDescRec **opts_ret,
204                      const char ***defs_ret)
205 {
206   /* These aren't "real" command-line options (there are no actual command-line
207      options in the Cocoa version); but this is the somewhat kludgey way that
208      the <xscreensaver-text /> and <xscreensaver-image /> tags in the
209      ../hacks/config/\*.xml files communicate with the preferences database.
210   */
211   static const XrmOptionDescRec default_options [] = {
212     { "-text-mode",              ".textMode",          XrmoptionSepArg, 0 },
213     { "-text-literal",           ".textLiteral",       XrmoptionSepArg, 0 },
214     { "-text-file",              ".textFile",          XrmoptionSepArg, 0 },
215     { "-text-url",               ".textURL",           XrmoptionSepArg, 0 },
216     { "-text-program",           ".textProgram",       XrmoptionSepArg, 0 },
217     { "-grab-desktop",           ".grabDesktopImages", XrmoptionNoArg, "True" },
218     { "-no-grab-desktop",        ".grabDesktopImages", XrmoptionNoArg, "False"},
219     { "-choose-random-images",   ".chooseRandomImages",XrmoptionNoArg, "True" },
220     { "-no-choose-random-images",".chooseRandomImages",XrmoptionNoArg, "False"},
221     { "-image-directory",        ".imageDirectory",    XrmoptionSepArg, 0 },
222     { "-fps",                    ".doFPS",             XrmoptionNoArg, "True" },
223     { "-no-fps",                 ".doFPS",             XrmoptionNoArg, "False"},
224     { "-foreground",             ".foreground",        XrmoptionSepArg, 0 },
225     { "-fg",                     ".foreground",        XrmoptionSepArg, 0 },
226     { "-background",             ".background",        XrmoptionSepArg, 0 },
227     { "-bg",                     ".background",        XrmoptionSepArg, 0 },
228     { 0, 0, 0, 0 }
229   };
230   static const char *default_defaults [] = {
231     ".doFPS:              False",
232     ".doubleBuffer:       True",
233     ".multiSample:        False",
234 # ifndef USE_IPHONE
235     ".textMode:           date",
236 # else
237     ".textMode:           url",
238 # endif
239  // ".textLiteral:        ",
240  // ".textFile:           ",
241     ".textURL:            http://en.wikipedia.org/w/index.php?title=Special:NewPages&feed=rss",
242  // ".textProgram:        ",
243     ".grabDesktopImages:  yes",
244 # ifndef USE_IPHONE
245     ".chooseRandomImages: no",
246 # else
247     ".chooseRandomImages: yes",
248 # endif
249     ".imageDirectory:     ~/Pictures",
250     ".relaunchDelay:      2",
251     0
252   };
253
254   int count = 0, i, j;
255   for (i = 0; default_options[i].option; i++)
256     count++;
257   for (i = 0; opts[i].option; i++)
258     count++;
259
260   XrmOptionDescRec *opts2 = (XrmOptionDescRec *) 
261     calloc (count + 1, sizeof (*opts2));
262
263   i = 0;
264   j = 0;
265   while (default_options[j].option) {
266     opts2[i] = default_options[j];
267     i++, j++;
268   }
269   j = 0;
270   while (opts[j].option) {
271     opts2[i] = opts[j];
272     i++, j++;
273   }
274
275   *opts_ret = opts2;
276
277
278   /* now the defaults
279    */
280   count = 0;
281   for (i = 0; default_defaults[i]; i++)
282     count++;
283   for (i = 0; defs[i]; i++)
284     count++;
285
286   const char **defs2 = (const char **) calloc (count + 1, sizeof (*defs2));
287
288   i = 0;
289   j = 0;
290   while (default_defaults[j]) {
291     defs2[i] = default_defaults[j];
292     i++, j++;
293   }
294   j = 0;
295   while (defs[j]) {
296     defs2[i] = defs[j];
297     i++, j++;
298   }
299
300   *defs_ret = defs2;
301 }
302
303
304 #ifdef USE_IPHONE
305 /* Returns the current time in seconds as a double.
306  */
307 static double
308 double_time (void)
309 {
310   struct timeval now;
311 # ifdef GETTIMEOFDAY_TWO_ARGS
312   struct timezone tzp;
313   gettimeofday(&now, &tzp);
314 # else
315   gettimeofday(&now);
316 # endif
317
318   return (now.tv_sec + ((double) now.tv_usec * 0.000001));
319 }
320 #endif // USE_IPHONE
321
322
323 - (id) initWithFrame:(NSRect)frame
324            saverName:(NSString *)saverName
325            isPreview:(BOOL)isPreview
326 {
327 # ifdef USE_IPHONE
328   initial_bounds = frame.size;
329   rot_current_size = frame.size;        // needs to be early, because
330   rot_from = rot_current_size;          // [self setFrame] is called by
331   rot_to = rot_current_size;            // [super initWithFrame].
332   rotation_ratio = -1;
333 # endif
334
335   if (! (self = [super initWithFrame:frame isPreview:isPreview]))
336     return 0;
337   
338   xsft = [self findFunctionTable: saverName];
339   if (! xsft) {
340     [self release];
341     return 0;
342   }
343
344   [self setShellPath];
345
346 # ifdef USE_IPHONE
347   [self setMultipleTouchEnabled:YES];
348   orientation = UIDeviceOrientationUnknown;
349   [self didRotate:nil];
350 # endif // USE_IPHONE
351
352   setup_p = YES;
353   if (xsft->setup_cb)
354     xsft->setup_cb (xsft, xsft->setup_arg);
355
356   /* The plist files for these preferences show up in
357      $HOME/Library/Preferences/ByHost/ in a file named like
358      "org.jwz.xscreensaver.<SAVERNAME>.<NUMBERS>.plist"
359    */
360   NSString *name = [NSString stringWithCString:xsft->progclass
361                              encoding:NSISOLatin1StringEncoding];
362   name = [@"org.jwz.xscreensaver." stringByAppendingString:name];
363   [self setResourcesEnv:name];
364
365   
366   XrmOptionDescRec *opts = 0;
367   const char **defs = 0;
368   add_default_options (xsft->options, xsft->defaults, &opts, &defs);
369   prefsReader = [[PrefsReader alloc]
370                   initWithName:name xrmKeys:opts defaults:defs];
371   free (defs);
372   // free (opts);  // bah, we need these! #### leak!
373   xsft->options = opts;
374   
375   progname = progclass = xsft->progclass;
376
377   next_frame_time = 0;
378   
379 # ifdef USE_BACKBUFFER
380   [self createBackbuffer];
381   [self initLayer];
382 # endif
383
384 # ifdef USE_IPHONE
385   // So we can tell when we're docked.
386   [UIDevice currentDevice].batteryMonitoringEnabled = YES;
387 # endif // USE_IPHONE
388
389   return self;
390 }
391
392 - (void) initLayer
393 {
394 # if !defined(USE_IPHONE) && defined(USE_CALAYER)
395   [self setLayer: [CALayer layer]];
396   self.layer.delegate = self;
397   self.layer.opaque = YES;
398   [self setWantsLayer: YES];
399 # endif  // !USE_IPHONE && USE_CALAYER
400 }
401
402
403 - (id) initWithFrame:(NSRect)frame isPreview:(BOOL)p
404 {
405   return [self initWithFrame:frame saverName:0 isPreview:p];
406 }
407
408
409 - (void) dealloc
410 {
411   NSAssert(![self isAnimating], @"still animating");
412   NSAssert(!xdata, @"xdata not yet freed");
413   if (xdpy)
414     jwxyz_free_display (xdpy);
415
416 # ifdef USE_BACKBUFFER
417   if (backbuffer)
418     CGContextRelease (backbuffer);
419
420   if (colorspace)
421     CGColorSpaceRelease (colorspace);
422
423 #  ifndef USE_CALAYER
424   if (window_ctx)
425     CGContextRelease (window_ctx);
426 #  endif // !USE_CALAYER
427
428 # endif // USE_BACKBUFFER
429
430   [prefsReader release];
431
432   // xsft
433   // fpst
434
435   [super dealloc];
436 }
437
438 - (PrefsReader *) prefsReader
439 {
440   return prefsReader;
441 }
442
443
444 #ifdef USE_IPHONE
445 - (void) lockFocus { }
446 - (void) unlockFocus { }
447 #endif // USE_IPHONE
448
449
450
451 # ifdef USE_IPHONE
452 /* A few seconds after the saver launches, we store the "wasRunning"
453    preference.  This is so that if the saver is crashing at startup,
454    we don't launch it again next time, getting stuck in a crash loop.
455  */
456 - (void) allSystemsGo: (NSTimer *) timer
457 {
458   NSAssert (timer == crash_timer, @"crash timer screwed up");
459   crash_timer = 0;
460
461   NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
462   [prefs setBool:YES forKey:@"wasRunning"];
463   [prefs synchronize];
464 }
465 #endif // USE_IPHONE
466
467
468 - (void) startAnimation
469 {
470   NSAssert(![self isAnimating], @"already animating");
471   NSAssert(!initted_p && !xdata, @"already initialized");
472   [super startAnimation];
473   /* We can't draw on the window from this method, so we actually do the
474      initialization of the screen saver (xsft->init_cb) in the first call
475      to animateOneFrame() instead.
476    */
477
478 # ifdef USE_IPHONE
479   if (crash_timer)
480     [crash_timer invalidate];
481
482   NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
483   [prefs removeObjectForKey:@"wasRunning"];
484   [prefs synchronize];
485
486   crash_timer = [NSTimer scheduledTimerWithTimeInterval: 5
487                          target:self
488                          selector:@selector(allSystemsGo:)
489                          userInfo:nil
490                          repeats:NO];
491
492 # endif // USE_IPHONE
493
494   // Never automatically turn the screen off if we are docked,
495   // and an animation is running.
496   //
497 # ifdef USE_IPHONE
498   [UIApplication sharedApplication].idleTimerDisabled =
499     ([UIDevice currentDevice].batteryState != UIDeviceBatteryStateUnplugged);
500 # endif
501 }
502
503
504 - (void)stopAnimation
505 {
506   NSAssert([self isAnimating], @"not animating");
507
508   if (initted_p) {
509
510     [self lockFocus];       // in case something tries to draw from here
511     [self prepareContext];
512
513     /* I considered just not even calling the free callback at all...
514        But webcollage-cocoa needs it, to kill the inferior webcollage
515        processes (since the screen saver framework never generates a
516        SIGPIPE for them...)  Instead, I turned off the free call in
517        xlockmore.c, which is where all of the bogus calls are anyway.
518      */
519     xsft->free_cb (xdpy, xwindow, xdata);
520     [self unlockFocus];
521
522 //  setup_p = NO; // #### wait, do we need this?
523     initted_p = NO;
524     xdata = 0;
525   }
526
527 # ifdef USE_IPHONE
528   if (crash_timer)
529     [crash_timer invalidate];
530   crash_timer = 0;
531   NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
532   [prefs removeObjectForKey:@"wasRunning"];
533   [prefs synchronize];
534 # endif // USE_IPHONE
535
536   [super stopAnimation];
537
538   // When an animation is no longer running (e.g., looking at the list)
539   // then it's ok to power off the screen when docked.
540   //
541 # ifdef USE_IPHONE
542   [UIApplication sharedApplication].idleTimerDisabled = NO;
543 # endif
544 }
545
546
547 /* Hook for the XScreenSaverGLView subclass
548  */
549 - (void) prepareContext
550 {
551 }
552
553 /* Hook for the XScreenSaverGLView subclass
554  */
555 - (void) resizeContext
556 {
557 }
558
559
560 static void
561 screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
562 {
563   fps_compute (fpst, 0, -1);
564   fps_draw (fpst);
565 }
566
567
568 #ifdef USE_IPHONE
569
570 /* On iPhones with Retina displays, we can draw the savers in "real"
571    pixels, and that works great.  The 320x480 "point" screen is really
572    a 640x960 *pixel* screen.  However, Retina iPads have 768x1024
573    point screens which are 1536x2048 pixels, and apparently that's
574    enough pixels that copying those bits to the screen is slow.  Like,
575    drops us from 15fps to 7fps.  So, on Retina iPads, we don't draw in
576    real pixels.  This will probably make the savers look better
577    anyway, since that's a higher resolution than most desktop monitors
578    have even today.  (This is only true for X11 programs, not GL 
579    programs.  Those are fine at full rez.)
580
581    This method is overridden in XScreenSaverGLView, since this kludge
582    isn't necessary for GL programs, being resolution independent by
583    nature.
584  */
585 - (CGFloat) hackedContentScaleFactor
586 {
587   GLfloat s = [self contentScaleFactor];
588   if (initial_bounds.width  >= 1024 ||
589       initial_bounds.height >= 1024)
590     s = 1;
591   return s;
592 }
593
594
595 static GLfloat _global_rot_current_angle_kludge;
596
597 double current_device_rotation (void)
598 {
599   return -_global_rot_current_angle_kludge;
600 }
601
602
603 - (void) hackRotation
604 {
605   if (rotation_ratio >= 0) {    // in the midst of a rotation animation
606
607 #   define CLAMP180(N) while (N < 0) N += 360; while (N > 180) N -= 360
608     GLfloat f = angle_from;
609     GLfloat t = angle_to;
610     CLAMP180(f);
611     CLAMP180(t);
612     GLfloat dist = -(t-f);
613     CLAMP180(dist);
614
615     // Intermediate angle.
616     rot_current_angle = f - rotation_ratio * dist;
617
618     // Intermediate frame size.
619     rot_current_size.width = rot_from.width + 
620       rotation_ratio * (rot_to.width - rot_from.width);
621     rot_current_size.height = rot_from.height + 
622       rotation_ratio * (rot_to.height - rot_from.height);
623
624     // Tick animation.  Complete rotation in 1/6th sec.
625     double now = double_time();
626     double duration = 1/6.0;
627     rotation_ratio = 1 - ((rot_start_time + duration - now) / duration);
628
629     if (rotation_ratio > 1) {   // Done animating.
630       orientation = new_orientation;
631       rot_current_angle = angle_to;
632       rot_current_size = rot_to;
633       rotation_ratio = -1;
634
635       // Check orientation again in case we rotated again while rotating:
636       // this is a no-op if nothing has changed.
637       [self didRotate:nil];
638     }
639   } else {                      // Not animating a rotation.
640     rot_current_angle = angle_to;
641     rot_current_size = rot_to;
642   }
643
644   CLAMP180(rot_current_angle);
645   _global_rot_current_angle_kludge = rot_current_angle;
646
647 #   undef CLAMP180
648
649   double s = [self hackedContentScaleFactor];
650   if (!ignore_rotation_p &&
651       /* rotation_ratio && */
652       ((int) backbuffer_size.width  != (int) (s * rot_current_size.width) ||
653        (int) backbuffer_size.height != (int) (s * rot_current_size.height)))
654     [self resize_x11];
655 }
656
657
658 - (void)alertView:(UIAlertView *)av clickedButtonAtIndex:(NSInteger)i
659 {
660   if (i == 0) exit (-1);        // Cancel
661   [self stopAndClose:NO];       // Keep going
662 }
663
664 - (void) handleException: (NSException *)e
665 {
666   NSLog (@"Caught exception: %@", e);
667   [[[UIAlertView alloc] initWithTitle:
668                           [NSString stringWithFormat: @"%s crashed!",
669                                     xsft->progclass]
670                         message:
671                           [NSString stringWithFormat:
672                                       @"The error message was:"
673                                     "\n\n%@\n\n"
674                                     "If it keeps crashing, try "
675                                     "resetting its options.",
676                                     e]
677                         delegate: self
678                         cancelButtonTitle: @"Exit"
679                         otherButtonTitles: @"Keep going", nil]
680     show];
681   [self stopAnimation];
682 }
683
684 #endif // USE_IPHONE
685
686
687 #ifdef USE_BACKBUFFER
688
689 /* Create a bitmap context into which we render everything.
690    If the desired size has changed, re-created it.
691  */
692 - (void) createBackbuffer
693 {
694 # ifdef USE_IPHONE
695   double s = [self hackedContentScaleFactor];
696   CGSize rotsize = ignore_rotation_p ? initial_bounds : rot_current_size;
697   int new_w = s * rotsize.width;
698   int new_h = s * rotsize.height;
699 # else
700   int new_w = [self bounds].size.width;
701   int new_h = [self bounds].size.height;
702 # endif
703         
704   // Colorspaces and CGContexts only happen with non-GL hacks.
705   if (colorspace)
706     CGColorSpaceRelease (colorspace);
707 # ifndef USE_CALAYER
708   if (window_ctx)
709     CGContextRelease (window_ctx);
710 # endif
711         
712   NSWindow *window = [self window];
713
714   if (window && xdpy) {
715     [self lockFocus];
716
717 # ifndef USE_CALAYER
718     // TODO: This was borrowed from jwxyz_window_resized, and should
719     // probably be refactored.
720           
721     // Figure out which screen the window is currently on.
722     CGDirectDisplayID cgdpy = 0;
723
724     {
725 //    int wx, wy;
726 //    TODO: XTranslateCoordinates is returning (0,1200) on my system.
727 //    Is this right?
728 //    In any case, those weren't valid coordinates for CGGetDisplaysWithPoint.
729 //    XTranslateCoordinates (xdpy, xwindow, NULL, 0, 0, &wx, &wy, NULL);
730 //    p.x = wx;
731 //    p.y = wy;
732
733       NSPoint p0 = {0, 0};
734       p0 = [window convertBaseToScreen:p0];
735       CGPoint p = {p0.x, p0.y};
736       CGDisplayCount n;
737       CGGetDisplaysWithPoint (p, 1, &cgdpy, &n);
738       NSAssert (cgdpy, @"unable to find CGDisplay");
739     }
740
741     {
742       // Figure out this screen's colorspace, and use that for every CGImage.
743       //
744       CMProfileRef profile = 0;
745
746       // CMGetProfileByAVID is deprecated as of OS X 10.6, but there's no
747       // documented replacement as of OS X 10.9.
748       // http://lists.apple.com/archives/colorsync-dev/2012/Nov/msg00001.html
749       CMGetProfileByAVID ((CMDisplayIDType) cgdpy, &profile);
750       NSAssert (profile, @"unable to find colorspace profile");
751       colorspace = CGColorSpaceCreateWithPlatformColorSpace (profile);
752       NSAssert (colorspace, @"unable to find colorspace");
753     }
754 # else  // USE_CALAYER
755     // Was apparently faster until 10.9.
756     colorspace = CGColorSpaceCreateDeviceRGB ();
757 # endif // USE_CALAYER
758
759 # ifndef USE_CALAYER
760     window_ctx = [[window graphicsContext] graphicsPort];
761     CGContextRetain (window_ctx);
762 # endif // !USE_CALAYER
763           
764     [self unlockFocus];
765   } else {
766 # ifndef USE_CALAYER
767     window_ctx = NULL;
768 # endif // !USE_CALAYER
769     colorspace = CGColorSpaceCreateDeviceRGB();
770   }
771
772   if (backbuffer &&
773       backbuffer_size.width  == new_w &&
774       backbuffer_size.height == new_h)
775     return;
776
777   CGSize osize = backbuffer_size;
778   CGContextRef ob = backbuffer;
779
780   backbuffer_size.width  = new_w;
781   backbuffer_size.height = new_h;
782
783   backbuffer = CGBitmapContextCreate (NULL,
784                                       backbuffer_size.width,
785                                       backbuffer_size.height,
786                                       8, 
787                                       backbuffer_size.width * 4,
788                                       colorspace,
789                                       // kCGImageAlphaPremultipliedLast
790                                       (kCGImageAlphaNoneSkipFirst |
791                                        kCGBitmapByteOrder32Host)
792                                       );
793   NSAssert (backbuffer, @"unable to allocate back buffer");
794
795   // Clear it.
796   CGRect r;
797   r.origin.x = r.origin.y = 0;
798   r.size = backbuffer_size;
799   CGContextSetGrayFillColor (backbuffer, 0, 1);
800   CGContextFillRect (backbuffer, r);
801
802   if (ob) {
803     // Restore old bits, as much as possible, to the X11 upper left origin.
804     CGRect rect;
805     rect.origin.x = 0;
806     rect.origin.y = (backbuffer_size.height - osize.height);
807     rect.size  = osize;
808     CGImageRef img = CGBitmapContextCreateImage (ob);
809     CGContextDrawImage (backbuffer, rect, img);
810     CGImageRelease (img);
811     CGContextRelease (ob);
812   }
813 }
814
815 #endif // USE_BACKBUFFER
816
817
818 /* Inform X11 that the size of our window has changed.
819  */
820 - (void) resize_x11
821 {
822   if (!xwindow) return;  // early
823
824 # ifdef USE_BACKBUFFER
825   [self createBackbuffer];
826   jwxyz_window_resized (xdpy, xwindow,
827                         0, 0,
828                         backbuffer_size.width, backbuffer_size.height,
829                         backbuffer);
830 # else   // !USE_BACKBUFFER
831   NSRect r = [self frame];              // ignoring rotation is closer
832   r.size = [self bounds].size;          // to what XGetGeometry expects.
833   jwxyz_window_resized (xdpy, xwindow,
834                         r.origin.x, r.origin.y,
835                         r.size.width, r.size.height,
836                         0);
837 # endif  // !USE_BACKBUFFER
838
839   // Next time render_x11 is called, run the saver's reshape_cb.
840   resized_p = YES;
841 }
842
843
844 - (void) render_x11
845 {
846 # ifdef USE_IPHONE
847   @try {
848
849   if (orientation == UIDeviceOrientationUnknown)
850     [self didRotate:nil];
851   [self hackRotation];
852 # endif
853
854   if (!initted_p) {
855
856     if (! xdpy) {
857 # ifdef USE_BACKBUFFER
858       NSAssert (backbuffer, @"no back buffer");
859       xdpy = jwxyz_make_display (self, backbuffer);
860 # else
861       xdpy = jwxyz_make_display (self, 0);
862 # endif
863       xwindow = XRootWindow (xdpy, 0);
864
865 # ifdef USE_IPHONE
866       /* Some X11 hacks (fluidballs) want to ignore all rotation events. */
867       ignore_rotation_p =
868         get_boolean_resource (xdpy, "ignoreRotation", "IgnoreRotation");
869 # endif // USE_IPHONE
870
871       [self resize_x11];
872     }
873
874     if (!setup_p) {
875       setup_p = YES;
876       if (xsft->setup_cb)
877         xsft->setup_cb (xsft, xsft->setup_arg);
878     }
879     initted_p = YES;
880     resized_p = NO;
881     NSAssert(!xdata, @"xdata already initialized");
882
883
884 # undef ya_rand_init
885     ya_rand_init (0);
886     
887     XSetWindowBackground (xdpy, xwindow,
888                           get_pixel_resource (xdpy, 0,
889                                               "background", "Background"));
890     XClearWindow (xdpy, xwindow);
891     
892 # ifndef USE_IPHONE
893     [[self window] setAcceptsMouseMovedEvents:YES];
894 # endif
895
896     /* In MacOS 10.5, this enables "QuartzGL", meaning that the Quartz
897        drawing primitives will run on the GPU instead of the CPU.
898        It seems like it might make things worse rather than better,
899        though...  Plus it makes us binary-incompatible with 10.4.
900
901 # if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5
902     [[self window] setPreferredBackingLocation:
903                      NSWindowBackingLocationVideoMemory];
904 # endif
905      */
906
907     /* Kludge: even though the init_cb functions are declared to take 2 args,
908       actually call them with 3, for the benefit of xlockmore_init() and
909       xlockmore_setup().
910       */
911     void *(*init_cb) (Display *, Window, void *) = 
912       (void *(*) (Display *, Window, void *)) xsft->init_cb;
913     
914     xdata = init_cb (xdpy, xwindow, xsft->setup_arg);
915
916     if (get_boolean_resource (xdpy, "doFPS", "DoFPS")) {
917       fpst = fps_init (xdpy, xwindow);
918       if (! xsft->fps_cb) xsft->fps_cb = screenhack_do_fps;
919     }
920   }
921
922
923   /* I don't understand why we have to do this *every frame*, but we do,
924      or else the cursor comes back on.
925    */
926 # ifndef USE_IPHONE
927   if (![self isPreview])
928     [NSCursor setHiddenUntilMouseMoves:YES];
929 # endif
930
931
932   if (fpst)
933     {
934       /* This is just a guess, but the -fps code wants to know how long
935          we were sleeping between frames.
936        */
937       long usecs = 1000000 * [self animationTimeInterval];
938       usecs -= 200;  // caller apparently sleeps for slightly less sometimes...
939       if (usecs < 0) usecs = 0;
940       fps_slept (fpst, usecs);
941     }
942
943
944   /* It turns out that [ScreenSaverView setAnimationTimeInterval] does nothing.
945      This is bad, because some of the screen hacks want to delay for long 
946      periods (like 5 seconds or a minute!) between frames, and running them
947      all at 60 FPS is no good.
948   
949      So, we don't use setAnimationTimeInterval, and just let the framework call
950      us whenever.  But, we only invoke the screen hack's "draw frame" method
951      when enough time has expired.
952   
953      This means two extra calls to gettimeofday() per frame.  For fast-cycling
954      screen savers, that might actually slow them down.  Oh well.
955
956      #### Also, we do not run the draw callback faster than the system's
957           animationTimeInterval, so if any savers are pickier about timing
958           than that, this may slow them down too much.  If that's a problem,
959           then we could call draw_cb in a loop here (with usleep) until the
960           next call would put us past animationTimeInterval...  But a better
961           approach would probably be to just change the saver to not do that.
962    */
963   struct timeval tv;
964   gettimeofday (&tv, 0);
965   double now = tv.tv_sec + (tv.tv_usec / 1000000.0);
966   if (now < next_frame_time) return;
967   
968   [self prepareContext];
969
970   if (resized_p) {
971     // We do this here instead of in setFrame so that all the
972     // Xlib drawing takes place under the animation timer.
973     [self resizeContext];
974     NSRect r;
975 # ifndef USE_BACKBUFFER
976     r = [self bounds];
977 # else  // USE_BACKBUFFER
978     r.origin.x = 0;
979     r.origin.y = 0;
980     r.size.width  = backbuffer_size.width;
981     r.size.height = backbuffer_size.height;
982 # endif // USE_BACKBUFFER
983
984     xsft->reshape_cb (xdpy, xwindow, xdata, r.size.width, r.size.height);
985     resized_p = NO;
986   }
987
988   // Run any XtAppAddInput callbacks now.
989   // (Note that XtAppAddTimeOut callbacks have already been run by
990   // the Cocoa event loop.)
991   //
992   jwxyz_sources_run (display_sources_data (xdpy));
993
994
995   // And finally:
996   //
997 # ifndef USE_IPHONE
998   NSDisableScreenUpdates();
999 # endif
1000   unsigned long delay = xsft->draw_cb (xdpy, xwindow, xdata);
1001   if (fpst) xsft->fps_cb (xdpy, xwindow, fpst, xdata);
1002 # ifndef USE_IPHONE
1003   NSEnableScreenUpdates();
1004 # endif
1005
1006   gettimeofday (&tv, 0);
1007   now = tv.tv_sec + (tv.tv_usec / 1000000.0);
1008   next_frame_time = now + (delay / 1000000.0);
1009
1010 # ifdef USE_IPHONE      // Allow savers on the iPhone to run full-tilt.
1011   if (delay < [self animationTimeInterval])
1012     [self setAnimationTimeInterval:(delay / 1000000.0)];
1013 # endif
1014
1015 # ifdef DO_GC_HACKERY
1016   /* Current theory is that the 10.6 garbage collector sucks in the
1017      following way:
1018
1019      It only does a collection when a threshold of outstanding
1020      collectable allocations has been surpassed.  However, CoreGraphics
1021      creates lots of small collectable allocations that contain pointers
1022      to very large non-collectable allocations: a small CG object that's
1023      collectable referencing large malloc'd allocations (non-collectable)
1024      containing bitmap data.  So the large allocation doesn't get freed
1025      until GC collects the small allocation, which triggers its finalizer
1026      to run which frees the large allocation.  So GC is deciding that it
1027      doesn't really need to run, even though the process has gotten
1028      enormous.  GC eventually runs once pageouts have happened, but by
1029      then it's too late, and the machine's resident set has been
1030      sodomized.
1031
1032      So, we force an exhaustive garbage collection in this process
1033      approximately every 5 seconds whether the system thinks it needs 
1034      one or not.
1035   */
1036   {
1037     static int tick = 0;
1038     if (++tick > 5*30) {
1039       tick = 0;
1040       objc_collect (OBJC_EXHAUSTIVE_COLLECTION);
1041     }
1042   }
1043 # endif // DO_GC_HACKERY
1044
1045 # ifdef USE_IPHONE
1046   }
1047   @catch (NSException *e) {
1048     [self handleException: e];
1049   }
1050 # endif // USE_IPHONE
1051 }
1052
1053
1054 /* drawRect always does nothing, and animateOneFrame renders bits to the
1055    screen.  This is (now) true of both X11 and GL on both MacOS and iOS.
1056  */
1057
1058 - (void)drawRect:(NSRect)rect
1059 {
1060   if (xwindow)    // clear to the X window's bg color, not necessarily black.
1061     XClearWindow (xdpy, xwindow);
1062   else
1063     [super drawRect:rect];    // early: black.
1064 }
1065
1066
1067 #ifndef USE_BACKBUFFER
1068
1069 - (void) animateOneFrame
1070 {
1071   [self render_x11];
1072   jwxyz_flush_context(xdpy);
1073 }
1074
1075 #else  // USE_BACKBUFFER
1076
1077 - (void) animateOneFrame
1078 {
1079   // Render X11 into the backing store bitmap...
1080
1081   NSAssert (backbuffer, @"no back buffer");
1082
1083 # ifdef USE_IPHONE
1084   UIGraphicsPushContext (backbuffer);
1085 # endif
1086
1087   [self render_x11];
1088
1089 # ifdef USE_IPHONE
1090   UIGraphicsPopContext();
1091 # endif
1092
1093 # ifdef USE_IPHONE
1094   // Then compute the transformations for rotation.
1095   double hs = [self hackedContentScaleFactor];
1096   double s = [self contentScaleFactor];
1097
1098   // The rotation origin for layer.affineTransform is in the center already.
1099   CGAffineTransform t = ignore_rotation_p ?
1100     CGAffineTransformIdentity :
1101     CGAffineTransformMakeRotation (rot_current_angle / (180.0 / M_PI));
1102
1103   CGFloat f = s / hs;
1104   self.layer.affineTransform = CGAffineTransformScale(t, f, f);
1105
1106   CGRect bounds;
1107   bounds.origin.x = 0;
1108   bounds.origin.y = 0;
1109   bounds.size.width = backbuffer_size.width / s;
1110   bounds.size.height = backbuffer_size.height / s;
1111   self.layer.bounds = bounds;
1112 # endif // USE_IPHONE
1113  
1114 # ifdef USE_CALAYER
1115   [self.layer setNeedsDisplay];
1116 # else // !USE_CALAYER
1117   size_t
1118     w = CGBitmapContextGetWidth (backbuffer),
1119     h = CGBitmapContextGetHeight (backbuffer);
1120   
1121   size_t bpl = CGBitmapContextGetBytesPerRow (backbuffer);
1122   CGDataProviderRef prov = CGDataProviderCreateWithData (NULL,
1123                                             CGBitmapContextGetData(backbuffer),
1124                                                          bpl * h,
1125                                                          NULL);
1126
1127
1128   CGImageRef img = CGImageCreate (w, h,
1129                                   8, 32,
1130                                   CGBitmapContextGetBytesPerRow(backbuffer),
1131                                   colorspace,
1132                                   CGBitmapContextGetBitmapInfo(backbuffer),
1133                                   prov, NULL, NO,
1134                                   kCGRenderingIntentDefault);
1135
1136   CGDataProviderRelease (prov);
1137   
1138   CGRect rect;
1139   rect.origin.x = 0;
1140   rect.origin.y = 0;
1141   rect.size = backbuffer_size;
1142   CGContextDrawImage (window_ctx, rect, img);
1143   
1144   CGImageRelease (img);
1145
1146   CGContextFlush (window_ctx);
1147 # endif // !USE_CALAYER
1148 }
1149
1150 # ifdef USE_CALAYER
1151
1152 - (void) drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
1153 {
1154   // This "isn't safe" if NULL is passed to CGBitmapCreateContext before iOS 4.
1155   char *dest_data = (char *)CGBitmapContextGetData (ctx);
1156
1157   // The CGContext here is normally upside-down on iOS.
1158   if (dest_data &&
1159       CGBitmapContextGetBitmapInfo (ctx) ==
1160         (kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Host)
1161 #  ifdef USE_IPHONE
1162       && CGContextGetCTM (ctx).d < 0
1163 #  endif // USE_IPHONE
1164       )
1165   {
1166     size_t dest_height = CGBitmapContextGetHeight (ctx);
1167     size_t dest_bpr = CGBitmapContextGetBytesPerRow (ctx);
1168     size_t src_height = CGBitmapContextGetHeight (backbuffer);
1169     size_t src_bpr = CGBitmapContextGetBytesPerRow (backbuffer);
1170     char *src_data = (char *)CGBitmapContextGetData (backbuffer);
1171
1172     size_t height = src_height < dest_height ? src_height : dest_height;
1173     
1174     if (src_bpr == dest_bpr) {
1175       // iPad 1: 4.0 ms, iPad 2: 6.7 ms
1176       memcpy (dest_data, src_data, src_bpr * height);
1177     } else {
1178       // iPad 1: 4.6 ms, iPad 2: 7.2 ms
1179       size_t bpr = src_bpr < dest_bpr ? src_bpr : dest_bpr;
1180       while (height) {
1181         memcpy (dest_data, src_data, bpr);
1182         --height;
1183         src_data += src_bpr;
1184         dest_data += dest_bpr;
1185       }
1186     }
1187   } else {
1188
1189     // iPad 1: 9.6 ms, iPad 2: 12.1 ms
1190
1191 #  ifdef USE_IPHONE
1192     CGContextScaleCTM (ctx, 1, -1);
1193     CGFloat s = [self contentScaleFactor];
1194     CGFloat hs = [self hackedContentScaleFactor];
1195     CGContextTranslateCTM (ctx, 0, -backbuffer_size.height * hs / s);
1196 #  endif // USE_IPHONE
1197     
1198     CGImageRef img = CGBitmapContextCreateImage (backbuffer);
1199     CGContextDrawImage (ctx, self.layer.bounds, img);
1200     CGImageRelease (img);
1201   }
1202 }
1203 # endif  // USE_CALAYER
1204
1205 #endif // USE_BACKBUFFER
1206
1207
1208
1209 - (void) setFrame:(NSRect) newRect
1210 {
1211   [super setFrame:newRect];
1212
1213   if (xwindow)     // inform Xlib that the window has changed now.
1214     [self resize_x11];
1215 }
1216
1217
1218 # ifndef USE_IPHONE  // Doesn't exist on iOS
1219 - (void) setFrameSize:(NSSize) newSize
1220 {
1221   [super setFrameSize:newSize];
1222   if (xwindow)
1223     [self resize_x11];
1224 }
1225 # endif // !USE_IPHONE
1226
1227
1228 +(BOOL) performGammaFade
1229 {
1230   return YES;
1231 }
1232
1233 - (BOOL) hasConfigureSheet
1234 {
1235   return YES;
1236 }
1237
1238 + (NSString *) decompressXML: (NSData *)data
1239 {
1240   if (! data) return 0;
1241   BOOL compressed_p = !!strncmp ((const char *) data.bytes, "<?xml", 5);
1242
1243   // If it's not already XML, decompress it.
1244   NSAssert (compressed_p, @"xml isn't compressed");
1245   if (compressed_p) {
1246     NSMutableData *data2 = 0;
1247     int ret = -1;
1248     z_stream zs;
1249     memset (&zs, 0, sizeof(zs));
1250     ret = inflateInit2 (&zs, 16 + MAX_WBITS);
1251     if (ret == Z_OK) {
1252       UInt32 usize = * (UInt32 *) (data.bytes + data.length - 4);
1253       data2 = [NSMutableData dataWithLength: usize];
1254       zs.next_in   = (Bytef *) data.bytes;
1255       zs.avail_in  = data.length;
1256       zs.next_out  = (Bytef *) data2.bytes;
1257       zs.avail_out = data2.length;
1258       ret = inflate (&zs, Z_FINISH);
1259       inflateEnd (&zs);
1260     }
1261     if (ret == Z_OK || ret == Z_STREAM_END)
1262       data = data2;
1263     else
1264       NSAssert2 (0, @"gunzip error: %d: %s",
1265                  ret, (zs.msg ? zs.msg : "<null>"));
1266   }
1267
1268   return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
1269 }
1270
1271
1272 #ifndef USE_IPHONE
1273 - (NSWindow *) configureSheet
1274 #else
1275 - (UIViewController *) configureView
1276 #endif
1277 {
1278   NSBundle *bundle = [NSBundle bundleForClass:[self class]];
1279   NSString *file = [NSString stringWithCString:xsft->progclass
1280                                       encoding:NSISOLatin1StringEncoding];
1281   file = [file lowercaseString];
1282   NSString *path = [bundle pathForResource:file ofType:@"xml"];
1283   if (!path) {
1284     NSLog (@"%@.xml does not exist in the application bundle: %@/",
1285            file, [bundle resourcePath]);
1286     return nil;
1287   }
1288   
1289 # ifdef USE_IPHONE
1290   UIViewController *sheet;
1291 # else  // !USE_IPHONE
1292   NSWindow *sheet;
1293 # endif // !USE_IPHONE
1294
1295   NSData *xmld = [NSData dataWithContentsOfFile:path];
1296   NSString *xml = [[self class] decompressXML: xmld];
1297   sheet = [[XScreenSaverConfigSheet alloc]
1298             initWithXML:[xml dataUsingEncoding:NSUTF8StringEncoding]
1299                 options:xsft->options
1300              controller:[prefsReader userDefaultsController]
1301                defaults:[prefsReader defaultOptions]];
1302
1303   // #### am I expected to retain this, or not? wtf.
1304   //      I thought not, but if I don't do this, we (sometimes) crash.
1305   // #### Analyze says "potential leak of an object stored into sheet"
1306   [sheet retain];
1307
1308   return sheet;
1309 }
1310
1311
1312 - (NSUserDefaultsController *) userDefaultsController
1313 {
1314   return [prefsReader userDefaultsController];
1315 }
1316
1317
1318 /* Announce our willingness to accept keyboard input.
1319 */
1320 - (BOOL)acceptsFirstResponder
1321 {
1322   return YES;
1323 }
1324
1325
1326 #ifndef USE_IPHONE
1327
1328 /* Convert an NSEvent into an XEvent, and pass it along.
1329    Returns YES if it was handled.
1330  */
1331 - (BOOL) doEvent: (NSEvent *) e
1332             type: (int) type
1333 {
1334   if (![self isPreview] ||     // no event handling if actually screen-saving!
1335       ![self isAnimating] ||
1336       !initted_p)
1337     return NO;
1338
1339   XEvent xe;
1340   memset (&xe, 0, sizeof(xe));
1341   
1342   int state = 0;
1343   
1344   int flags = [e modifierFlags];
1345   if (flags & NSAlphaShiftKeyMask) state |= LockMask;
1346   if (flags & NSShiftKeyMask)      state |= ShiftMask;
1347   if (flags & NSControlKeyMask)    state |= ControlMask;
1348   if (flags & NSAlternateKeyMask)  state |= Mod1Mask;
1349   if (flags & NSCommandKeyMask)    state |= Mod2Mask;
1350   
1351   NSPoint p = [[[e window] contentView] convertPoint:[e locationInWindow]
1352                                             toView:self];
1353 # ifdef USE_IPHONE
1354   double s = [self hackedContentScaleFactor];
1355 # else
1356   int s = 1;
1357 # endif
1358   int x = s * p.x;
1359   int y = s * ([self bounds].size.height - p.y);
1360
1361   xe.xany.type = type;
1362   switch (type) {
1363     case ButtonPress:
1364     case ButtonRelease:
1365       xe.xbutton.x = x;
1366       xe.xbutton.y = y;
1367       xe.xbutton.state = state;
1368       if ([e type] == NSScrollWheel)
1369         xe.xbutton.button = ([e deltaY] > 0 ? Button4 :
1370                              [e deltaY] < 0 ? Button5 :
1371                              [e deltaX] > 0 ? Button6 :
1372                              [e deltaX] < 0 ? Button7 :
1373                              0);
1374       else
1375         xe.xbutton.button = [e buttonNumber] + 1;
1376       break;
1377     case MotionNotify:
1378       xe.xmotion.x = x;
1379       xe.xmotion.y = y;
1380       xe.xmotion.state = state;
1381       break;
1382     case KeyPress:
1383     case KeyRelease:
1384       {
1385         NSString *ns = (([e type] == NSFlagsChanged) ? 0 :
1386                         [e charactersIgnoringModifiers]);
1387         KeySym k = 0;
1388
1389         if (!ns || [ns length] == 0)                    // dead key
1390           {
1391             // Cocoa hides the difference between left and right keys.
1392             // Also we only get KeyPress events for these, no KeyRelease
1393             // (unless we hack the mod state manually.  Bleh.)
1394             //
1395             if      (flags & NSAlphaShiftKeyMask)   k = XK_Caps_Lock;
1396             else if (flags & NSShiftKeyMask)        k = XK_Shift_L;
1397             else if (flags & NSControlKeyMask)      k = XK_Control_L;
1398             else if (flags & NSAlternateKeyMask)    k = XK_Alt_L;
1399             else if (flags & NSCommandKeyMask)      k = XK_Meta_L;
1400           }
1401         else if ([ns length] == 1)                      // real key
1402           {
1403             switch ([ns characterAtIndex:0]) {
1404             case NSLeftArrowFunctionKey:  k = XK_Left;      break;
1405             case NSRightArrowFunctionKey: k = XK_Right;     break;
1406             case NSUpArrowFunctionKey:    k = XK_Up;        break;
1407             case NSDownArrowFunctionKey:  k = XK_Down;      break;
1408             case NSPageUpFunctionKey:     k = XK_Page_Up;   break;
1409             case NSPageDownFunctionKey:   k = XK_Page_Down; break;
1410             case NSHomeFunctionKey:       k = XK_Home;      break;
1411             case NSPrevFunctionKey:       k = XK_Prior;     break;
1412             case NSNextFunctionKey:       k = XK_Next;      break;
1413             case NSBeginFunctionKey:      k = XK_Begin;     break;
1414             case NSEndFunctionKey:        k = XK_End;       break;
1415             default:
1416               {
1417                 const char *s =
1418                   [ns cStringUsingEncoding:NSISOLatin1StringEncoding];
1419                 k = (s && *s ? *s : 0);
1420               }
1421               break;
1422             }
1423           }
1424
1425         if (! k) return YES;   // E.g., "KeyRelease XK_Shift_L"
1426
1427         xe.xkey.keycode = k;
1428         xe.xkey.state = state;
1429         break;
1430       }
1431     default:
1432       NSAssert1 (0, @"unknown X11 event type: %d", type);
1433       break;
1434   }
1435
1436   [self lockFocus];
1437   [self prepareContext];
1438   BOOL result = xsft->event_cb (xdpy, xwindow, xdata, &xe);
1439   [self unlockFocus];
1440   return result;
1441 }
1442
1443
1444 - (void) mouseDown: (NSEvent *) e
1445 {
1446   if (! [self doEvent:e type:ButtonPress])
1447     [super mouseDown:e];
1448 }
1449
1450 - (void) mouseUp: (NSEvent *) e
1451 {
1452   if (! [self doEvent:e type:ButtonRelease])
1453     [super mouseUp:e];
1454 }
1455
1456 - (void) otherMouseDown: (NSEvent *) e
1457 {
1458   if (! [self doEvent:e type:ButtonPress])
1459     [super otherMouseDown:e];
1460 }
1461
1462 - (void) otherMouseUp: (NSEvent *) e
1463 {
1464   if (! [self doEvent:e type:ButtonRelease])
1465     [super otherMouseUp:e];
1466 }
1467
1468 - (void) mouseMoved: (NSEvent *) e
1469 {
1470   if (! [self doEvent:e type:MotionNotify])
1471     [super mouseMoved:e];
1472 }
1473
1474 - (void) mouseDragged: (NSEvent *) e
1475 {
1476   if (! [self doEvent:e type:MotionNotify])
1477     [super mouseDragged:e];
1478 }
1479
1480 - (void) otherMouseDragged: (NSEvent *) e
1481 {
1482   if (! [self doEvent:e type:MotionNotify])
1483     [super otherMouseDragged:e];
1484 }
1485
1486 - (void) scrollWheel: (NSEvent *) e
1487 {
1488   if (! [self doEvent:e type:ButtonPress])
1489     [super scrollWheel:e];
1490 }
1491
1492 - (void) keyDown: (NSEvent *) e
1493 {
1494   if (! [self doEvent:e type:KeyPress])
1495     [super keyDown:e];
1496 }
1497
1498 - (void) keyUp: (NSEvent *) e
1499 {
1500   if (! [self doEvent:e type:KeyRelease])
1501     [super keyUp:e];
1502 }
1503
1504 - (void) flagsChanged: (NSEvent *) e
1505 {
1506   if (! [self doEvent:e type:KeyPress])
1507     [super flagsChanged:e];
1508 }
1509
1510 #else  // USE_IPHONE
1511
1512
1513 - (void) stopAndClose:(Bool)relaunch_p
1514 {
1515   if ([self isAnimating])
1516     [self stopAnimation];
1517
1518   /* Need to make the SaverListController be the firstResponder again
1519      so that it can continue to receive its own shake events.  I
1520      suppose that this abstraction-breakage means that I'm adding
1521      XScreenSaverView to the UINavigationController wrong...
1522    */
1523   UIViewController *v = [[self window] rootViewController];
1524   if ([v isKindOfClass: [UINavigationController class]]) {
1525     UINavigationController *n = (UINavigationController *) v;
1526     [[n topViewController] becomeFirstResponder];
1527   }
1528
1529   UIView *fader = [self superview];  // the "backgroundView" view is our parent
1530
1531   if (relaunch_p) {   // Fake a shake on the SaverListController.
1532     // Why is [self window] sometimes null here?
1533     UIWindow *w = [[UIApplication sharedApplication] keyWindow];
1534     UIViewController *v = [w rootViewController];
1535     if ([v isKindOfClass: [UINavigationController class]]) {
1536       UINavigationController *n = (UINavigationController *) v;
1537       [[n topViewController] motionEnded: UIEventSubtypeMotionShake
1538                                withEvent: nil];
1539     }
1540   } else {      // Not launching another, animate our return to the list.
1541     [UIView animateWithDuration: 0.5
1542             animations:^{ fader.alpha = 0.0; }
1543             completion:^(BOOL finished) {
1544                [fader removeFromSuperview];
1545                fader.alpha = 1.0;
1546             }];
1547   }
1548 }
1549
1550
1551 /* Called after the device's orientation has changed.
1552
1553    Note: we could include a subclass of UIViewController which
1554    contains a shouldAutorotateToInterfaceOrientation method that
1555    returns YES, in which case Core Animation would auto-rotate our
1556    View for us in response to rotation events... but, that interacts
1557    badly with the EAGLContext -- if you introduce Core Animation into
1558    the path, the OpenGL pipeline probably falls back on software
1559    rendering and performance goes to hell.  Also, the scaling and
1560    rotation that Core Animation does interacts incorrectly with the GL
1561    context anyway.
1562
1563    So, we have to hack the rotation animation manually, in the GL world.
1564
1565    Possibly XScreenSaverView should use Core Animation, and 
1566    XScreenSaverGLView should override that.
1567 */
1568 - (void)didRotate:(NSNotification *)notification
1569 {
1570   UIDeviceOrientation current = [[UIDevice currentDevice] orientation];
1571
1572   /* If the simulator starts up in the rotated position, sometimes
1573      the UIDevice says we're in Portrait when we're not -- but it
1574      turns out that the UINavigationController knows what's up!
1575      So get it from there.
1576    */
1577   if (current == UIDeviceOrientationUnknown) {
1578     switch ([[[self window] rootViewController] interfaceOrientation]) {
1579     case UIInterfaceOrientationPortrait:
1580       current = UIDeviceOrientationPortrait;
1581       break;
1582     case UIInterfaceOrientationPortraitUpsideDown:
1583       current = UIDeviceOrientationPortraitUpsideDown;
1584       break;
1585     case UIInterfaceOrientationLandscapeLeft:           // It's opposite day
1586       current = UIDeviceOrientationLandscapeRight;
1587       break;
1588     case UIInterfaceOrientationLandscapeRight:
1589       current = UIDeviceOrientationLandscapeLeft;
1590       break;
1591     default:
1592       break;
1593     }
1594   }
1595
1596   /* On the iPad (but not iPhone 3GS, or the simulator) sometimes we get
1597      an orientation change event with an unknown orientation.  Those seem
1598      to always be immediately followed by another orientation change with
1599      a *real* orientation change, so let's try just ignoring those bogus
1600      ones and hoping that the real one comes in shortly...
1601    */
1602   if (current == UIDeviceOrientationUnknown)
1603     return;
1604
1605   if (rotation_ratio >= 0) return;      // in the midst of rotation animation
1606   if (orientation == current) return;   // no change
1607
1608   // When transitioning to FaceUp or FaceDown, pretend there was no change.
1609   if (current == UIDeviceOrientationFaceUp ||
1610       current == UIDeviceOrientationFaceDown)
1611     return;
1612
1613   new_orientation = current;            // current animation target
1614   rotation_ratio = 0;                   // start animating
1615   rot_start_time = double_time();
1616
1617   switch (orientation) {
1618   case UIDeviceOrientationLandscapeLeft:      angle_from = 90;  break;
1619   case UIDeviceOrientationLandscapeRight:     angle_from = 270; break;
1620   case UIDeviceOrientationPortraitUpsideDown: angle_from = 180; break;
1621   default:                                    angle_from = 0;   break;
1622   }
1623
1624   switch (new_orientation) {
1625   case UIDeviceOrientationLandscapeLeft:      angle_to = 90;  break;
1626   case UIDeviceOrientationLandscapeRight:     angle_to = 270; break;
1627   case UIDeviceOrientationPortraitUpsideDown: angle_to = 180; break;
1628   default:                                    angle_to = 0;   break;
1629   }
1630
1631   switch (orientation) {
1632   case UIDeviceOrientationLandscapeRight:       // from landscape
1633   case UIDeviceOrientationLandscapeLeft:
1634     rot_from.width  = initial_bounds.height;
1635     rot_from.height = initial_bounds.width;
1636     break;
1637   default:                                      // from portrait
1638     rot_from.width  = initial_bounds.width;
1639     rot_from.height = initial_bounds.height;
1640     break;
1641   }
1642
1643   switch (new_orientation) {
1644   case UIDeviceOrientationLandscapeRight:       // to landscape
1645   case UIDeviceOrientationLandscapeLeft:
1646     rot_to.width  = initial_bounds.height;
1647     rot_to.height = initial_bounds.width;
1648     break;
1649   default:                                      // to portrait
1650     rot_to.width  = initial_bounds.width;
1651     rot_to.height = initial_bounds.height;
1652     break;
1653   }
1654
1655  if (! initted_p) {
1656    // If we've done a rotation but the saver hasn't been initialized yet,
1657    // don't bother going through an X11 resize, but just do it now.
1658    rot_start_time = 0;  // dawn of time
1659    [self hackRotation];
1660  }
1661 }
1662
1663
1664 /* I believe we can't use UIGestureRecognizer for tracking touches
1665    because UIPanGestureRecognizer doesn't give us enough detail in its
1666    callbacks.
1667
1668    Currently we don't handle multi-touches (just the first touch) but
1669    I'm leaving this comment here for future reference:
1670
1671    In the simulator, multi-touch sequences look like this:
1672
1673      touchesBegan [touchA, touchB]
1674      touchesEnd [touchA, touchB]
1675
1676    But on real devices, sometimes you get that, but sometimes you get:
1677
1678      touchesBegan [touchA, touchB]
1679      touchesEnd [touchB]
1680      touchesEnd [touchA]
1681
1682    Or even
1683
1684      touchesBegan [touchA]
1685      touchesBegan [touchB]
1686      touchesEnd [touchA]
1687      touchesEnd [touchB]
1688
1689    So the only way to properly detect a "pinch" gesture is to remember
1690    the start-point of each touch as it comes in; and the end-point of
1691    each touch as those come in; and only process the gesture once the
1692    number of touchEnds matches the number of touchBegins.
1693  */
1694
1695 - (void) rotateMouse:(int)rot x:(int*)x y:(int *)y w:(int)w h:(int)h
1696 {
1697   // This is a no-op unless contentScaleFactor != hackedContentScaleFactor.
1698   // Currently, this is the iPad Retina only.
1699   CGRect frame = [self bounds];         // Scale.
1700   double s = [self hackedContentScaleFactor];
1701   *x *= (backbuffer_size.width  / frame.size.width)  / s;
1702   *y *= (backbuffer_size.height / frame.size.height) / s;
1703 }
1704
1705
1706 #if 0  // AudioToolbox/AudioToolbox.h
1707 - (void) beep
1708 {
1709   // There's no way to play a standard system alert sound!
1710   // We'd have to include our own WAV for that.  Eh, fuck it.
1711   AudioServicesPlaySystemSound (kSystemSoundID_Vibrate);
1712 # if TARGET_IPHONE_SIMULATOR
1713   NSLog(@"BEEP");  // The sim doesn't vibrate.
1714 # endif
1715 }
1716 #endif
1717
1718
1719 /* We distinguish between taps and drags.
1720    - Drags (down, motion, up) are sent to the saver to handle.
1721    - Single-taps exit the saver.
1722    This means a saver cannot respond to a single-tap.  Only a few try to.
1723  */
1724
1725 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
1726 {
1727   // If they are trying to pinch, just do nothing.
1728   if ([[event allTouches] count] > 1)
1729     return;
1730
1731   tap_time = 0;
1732
1733   if (xsft->event_cb && xwindow) {
1734     double s = [self hackedContentScaleFactor];
1735     XEvent xe;
1736     memset (&xe, 0, sizeof(xe));
1737     int i = 0;
1738     // #### 'frame' here or 'bounds'?
1739     int w = s * [self frame].size.width;
1740     int h = s * [self frame].size.height;
1741     for (UITouch *touch in touches) {
1742       CGPoint p = [touch locationInView:self];
1743       xe.xany.type = ButtonPress;
1744       xe.xbutton.button = i + 1;
1745       xe.xbutton.button = i + 1;
1746       xe.xbutton.x      = s * p.x;
1747       xe.xbutton.y      = s * p.y;
1748       [self rotateMouse: rot_current_angle
1749             x: &xe.xbutton.x y: &xe.xbutton.y w: w h: h];
1750       jwxyz_mouse_moved (xdpy, xwindow, xe.xbutton.x, xe.xbutton.y);
1751
1752       // Ignore return code: don't care whether the hack handled it.
1753       xsft->event_cb (xdpy, xwindow, xdata, &xe);
1754
1755       // Remember when/where this was, to determine tap versus drag or hold.
1756       tap_time = double_time();
1757       tap_point = p;
1758
1759       i++;
1760       break;  // No pinches: only look at the first touch.
1761     }
1762   }
1763 }
1764
1765
1766 - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
1767 {
1768   // If they are trying to pinch, just do nothing.
1769   if ([[event allTouches] count] > 1)
1770     return;
1771
1772   if (xsft->event_cb && xwindow) {
1773     double s = [self hackedContentScaleFactor];
1774     XEvent xe;
1775     memset (&xe, 0, sizeof(xe));
1776     int i = 0;
1777     // #### 'frame' here or 'bounds'?
1778     int w = s * [self frame].size.width;
1779     int h = s * [self frame].size.height;
1780     for (UITouch *touch in touches) {
1781       CGPoint p = [touch locationInView:self];
1782
1783       // If the ButtonRelease came less than half a second after ButtonPress,
1784       // and didn't move far, then this was a tap, not a drag or a hold.
1785       // Interpret it as "exit".
1786       //
1787       double dist = sqrt (((p.x - tap_point.x) * (p.x - tap_point.x)) +
1788                           ((p.y - tap_point.y) * (p.y - tap_point.y)));
1789       if (tap_time + 0.5 >= double_time() && dist < 20) {
1790         [self stopAndClose:NO];
1791         return;
1792       }
1793
1794       xe.xany.type      = ButtonRelease;
1795       xe.xbutton.button = i + 1;
1796       xe.xbutton.x      = s * p.x;
1797       xe.xbutton.y      = s * p.y;
1798       [self rotateMouse: rot_current_angle
1799             x: &xe.xbutton.x y: &xe.xbutton.y w: w h: h];
1800       jwxyz_mouse_moved (xdpy, xwindow, xe.xbutton.x, xe.xbutton.y);
1801       xsft->event_cb (xdpy, xwindow, xdata, &xe);
1802       i++;
1803       break;  // No pinches: only look at the first touch.
1804     }
1805   }
1806 }
1807
1808
1809 - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
1810 {
1811   // If they are trying to pinch, just do nothing.
1812   if ([[event allTouches] count] > 1)
1813     return;
1814
1815   if (xsft->event_cb && xwindow) {
1816     double s = [self hackedContentScaleFactor];
1817     XEvent xe;
1818     memset (&xe, 0, sizeof(xe));
1819     int i = 0;
1820     // #### 'frame' here or 'bounds'?
1821     int w = s * [self frame].size.width;
1822     int h = s * [self frame].size.height;
1823     for (UITouch *touch in touches) {
1824       CGPoint p = [touch locationInView:self];
1825       xe.xany.type      = MotionNotify;
1826       xe.xmotion.x      = s * p.x;
1827       xe.xmotion.y      = s * p.y;
1828       [self rotateMouse: rot_current_angle
1829             x: &xe.xbutton.x y: &xe.xbutton.y w: w h: h];
1830       jwxyz_mouse_moved (xdpy, xwindow, xe.xmotion.x, xe.xmotion.y);
1831       xsft->event_cb (xdpy, xwindow, xdata, &xe);
1832       i++;
1833       break;  // No pinches: only look at the first touch.
1834     }
1835   }
1836 }
1837
1838
1839 /* We need this to respond to "shake" gestures
1840  */
1841 - (BOOL)canBecomeFirstResponder
1842 {
1843   return YES;
1844 }
1845
1846 - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
1847 {
1848 }
1849
1850
1851 - (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event
1852 {
1853 }
1854
1855 /* Shake means exit and launch a new saver.
1856  */
1857 - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
1858 {
1859   [self stopAndClose:YES];
1860 }
1861
1862
1863 - (void)setScreenLocked:(BOOL)locked
1864 {
1865   if (screenLocked == locked) return;
1866   screenLocked = locked;
1867   if (locked) {
1868     if ([self isAnimating])
1869       [self stopAnimation];
1870   } else {
1871     if (! [self isAnimating])
1872       [self startAnimation];
1873   }
1874 }
1875
1876
1877 #endif // USE_IPHONE
1878
1879
1880 @end
1881
1882 /* Utility functions...
1883  */
1884
1885 static PrefsReader *
1886 get_prefsReader (Display *dpy)
1887 {
1888   XScreenSaverView *view = jwxyz_window_view (XRootWindow (dpy, 0));
1889   if (!view) return 0;
1890   return [view prefsReader];
1891 }
1892
1893
1894 char *
1895 get_string_resource (Display *dpy, char *name, char *class)
1896 {
1897   return [get_prefsReader(dpy) getStringResource:name];
1898 }
1899
1900 Bool
1901 get_boolean_resource (Display *dpy, char *name, char *class)
1902 {
1903   return [get_prefsReader(dpy) getBooleanResource:name];
1904 }
1905
1906 int
1907 get_integer_resource (Display *dpy, char *name, char *class)
1908 {
1909   return [get_prefsReader(dpy) getIntegerResource:name];
1910 }
1911
1912 double
1913 get_float_resource (Display *dpy, char *name, char *class)
1914 {
1915   return [get_prefsReader(dpy) getFloatResource:name];
1916 }