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