From http://www.jwz.org/xscreensaver/xscreensaver-5.23.tar.gz
[xscreensaver] / OSX / XScreenSaverView.m.orig
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 # ifndef USE_IPHONE
395   [self setLayer: [CALayer layer]];
396   self.layer.delegate = self;
397   self.layer.opaque = YES;
398   [self setWantsLayer: YES];
399 # endif
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 # endif
420
421   [prefsReader release];
422
423   // xsft
424   // fpst
425
426   [super dealloc];
427 }
428
429 - (PrefsReader *) prefsReader
430 {
431   return prefsReader;
432 }
433
434
435 #ifdef USE_IPHONE
436 - (void) lockFocus { }
437 - (void) unlockFocus { }
438 #endif // USE_IPHONE
439
440
441
442 # ifdef USE_IPHONE
443 /* A few seconds after the saver launches, we store the "wasRunning"
444    preference.  This is so that if the saver is crashing at startup,
445    we don't launch it again next time, getting stuck in a crash loop.
446  */
447 - (void) allSystemsGo: (NSTimer *) timer
448 {
449   NSAssert (timer == crash_timer, @"crash timer screwed up");
450   crash_timer = 0;
451
452   NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
453   [prefs setBool:YES forKey:@"wasRunning"];
454   [prefs synchronize];
455 }
456 #endif // USE_IPHONE
457
458
459 - (void) startAnimation
460 {
461   NSAssert(![self isAnimating], @"already animating");
462   NSAssert(!initted_p && !xdata, @"already initialized");
463   [super startAnimation];
464   /* We can't draw on the window from this method, so we actually do the
465      initialization of the screen saver (xsft->init_cb) in the first call
466      to animateOneFrame() instead.
467    */
468
469 # ifdef USE_IPHONE
470   if (crash_timer)
471     [crash_timer invalidate];
472
473   NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
474   [prefs removeObjectForKey:@"wasRunning"];
475   [prefs synchronize];
476
477   crash_timer = [NSTimer scheduledTimerWithTimeInterval: 5
478                          target:self
479                          selector:@selector(allSystemsGo:)
480                          userInfo:nil
481                          repeats:NO];
482
483 # endif // USE_IPHONE
484
485   // Never automatically turn the screen off if we are docked,
486   // and an animation is running.
487   //
488 # ifdef USE_IPHONE
489   [UIApplication sharedApplication].idleTimerDisabled =
490     ([UIDevice currentDevice].batteryState != UIDeviceBatteryStateUnplugged);
491 # endif
492 }
493
494
495 - (void)stopAnimation
496 {
497   NSAssert([self isAnimating], @"not animating");
498
499   if (initted_p) {
500
501     [self lockFocus];       // in case something tries to draw from here
502     [self prepareContext];
503
504     /* I considered just not even calling the free callback at all...
505        But webcollage-cocoa needs it, to kill the inferior webcollage
506        processes (since the screen saver framework never generates a
507        SIGPIPE for them...)  Instead, I turned off the free call in
508        xlockmore.c, which is where all of the bogus calls are anyway.
509      */
510     xsft->free_cb (xdpy, xwindow, xdata);
511     [self unlockFocus];
512
513 //  setup_p = NO; // #### wait, do we need this?
514     initted_p = NO;
515     xdata = 0;
516   }
517
518 # ifdef USE_IPHONE
519   if (crash_timer)
520     [crash_timer invalidate];
521   crash_timer = 0;
522   NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
523   [prefs removeObjectForKey:@"wasRunning"];
524   [prefs synchronize];
525 # endif // USE_IPHONE
526
527   [super stopAnimation];
528
529   // When an animation is no longer running (e.g., looking at the list)
530   // then it's ok to power off the screen when docked.
531   //
532 # ifdef USE_IPHONE
533   [UIApplication sharedApplication].idleTimerDisabled = NO;
534 # endif
535 }
536
537
538 /* Hook for the XScreenSaverGLView subclass
539  */
540 - (void) prepareContext
541 {
542 }
543
544 /* Hook for the XScreenSaverGLView subclass
545  */
546 - (void) resizeContext
547 {
548 }
549
550
551 static void
552 screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
553 {
554   fps_compute (fpst, 0, -1);
555   fps_draw (fpst);
556 }
557
558
559 #ifdef USE_IPHONE
560
561 /* On iPhones with Retina displays, we can draw the savers in "real"
562    pixels, and that works great.  The 320x480 "point" screen is really
563    a 640x960 *pixel* screen.  However, Retina iPads have 768x1024
564    point screens which are 1536x2048 pixels, and apparently that's
565    enough pixels that copying those bits to the screen is slow.  Like,
566    drops us from 15fps to 7fps.  So, on Retina iPads, we don't draw in
567    real pixels.  This will probably make the savers look better
568    anyway, since that's a higher resolution than most desktop monitors
569    have even today.  (This is only true for X11 programs, not GL 
570    programs.  Those are fine at full rez.)
571
572    This method is overridden in XScreenSaverGLView, since this kludge
573    isn't necessary for GL programs, being resolution independent by
574    nature.
575  */
576 - (CGFloat) hackedContentScaleFactor
577 {
578   GLfloat s = [self contentScaleFactor];
579   if (initial_bounds.width  >= 1024 ||
580       initial_bounds.height >= 1024)
581     s = 1;
582   return s;
583 }
584
585
586 static GLfloat _global_rot_current_angle_kludge;
587
588 double current_device_rotation (void)
589 {
590   return -_global_rot_current_angle_kludge;
591 }
592
593
594 - (void) hackRotation
595 {
596   if (rotation_ratio >= 0) {    // in the midst of a rotation animation
597
598 #   define CLAMP180(N) while (N < 0) N += 360; while (N > 180) N -= 360
599     GLfloat f = angle_from;
600     GLfloat t = angle_to;
601     CLAMP180(f);
602     CLAMP180(t);
603     GLfloat dist = -(t-f);
604     CLAMP180(dist);
605
606     // Intermediate angle.
607     rot_current_angle = f - rotation_ratio * dist;
608
609     // Intermediate frame size.
610     rot_current_size.width = rot_from.width + 
611       rotation_ratio * (rot_to.width - rot_from.width);
612     rot_current_size.height = rot_from.height + 
613       rotation_ratio * (rot_to.height - rot_from.height);
614
615     // Tick animation.  Complete rotation in 1/6th sec.
616     double now = double_time();
617     double duration = 1/6.0;
618     rotation_ratio = 1 - ((rot_start_time + duration - now) / duration);
619
620     if (rotation_ratio > 1) {   // Done animating.
621       orientation = new_orientation;
622       rot_current_angle = angle_to;
623       rot_current_size = rot_to;
624       rotation_ratio = -1;
625
626       // Check orientation again in case we rotated again while rotating:
627       // this is a no-op if nothing has changed.
628       [self didRotate:nil];
629     }
630   } else {                      // Not animating a rotation.
631     rot_current_angle = angle_to;
632     rot_current_size = rot_to;
633   }
634
635   CLAMP180(rot_current_angle);
636   _global_rot_current_angle_kludge = rot_current_angle;
637
638 #   undef CLAMP180
639
640   double s = [self hackedContentScaleFactor];
641   if (!ignore_rotation_p &&
642       /* rotation_ratio && */
643       ((int) backbuffer_size.width  != (int) (s * rot_current_size.width) ||
644        (int) backbuffer_size.height != (int) (s * rot_current_size.height)))
645     [self resize_x11];
646 }
647
648
649 - (void)alertView:(UIAlertView *)av clickedButtonAtIndex:(NSInteger)i
650 {
651   if (i == 0) exit (-1);        // Cancel
652   [self stopAndClose:NO];       // Keep going
653 }
654
655 - (void) handleException: (NSException *)e
656 {
657   NSLog (@"Caught exception: %@", e);
658   [[[UIAlertView alloc] initWithTitle:
659                           [NSString stringWithFormat: @"%s crashed!",
660                                     xsft->progclass]
661                         message:
662                           [NSString stringWithFormat:
663                                       @"The error message was:"
664                                     "\n\n%@\n\n"
665                                     "If it keeps crashing, try "
666                                     "resetting its options.",
667                                     e]
668                         delegate: self
669                         cancelButtonTitle: @"Exit"
670                         otherButtonTitles: @"Keep going", nil]
671     show];
672   [self stopAnimation];
673 }
674
675 #endif // USE_IPHONE
676
677
678 #ifdef USE_BACKBUFFER
679
680 /* Create a bitmap context into which we render everything.
681    If the desired size has changed, re-created it.
682  */
683 - (void) createBackbuffer
684 {
685 # ifdef USE_IPHONE
686   double s = [self hackedContentScaleFactor];
687   CGSize rotsize = ignore_rotation_p ? initial_bounds : rot_current_size;
688   int new_w = s * rotsize.width;
689   int new_h = s * rotsize.height;
690 # else
691   int new_w = [self bounds].size.width;
692   int new_h = [self bounds].size.height;
693 # endif
694
695   if (backbuffer &&
696       backbuffer_size.width  == new_w &&
697       backbuffer_size.height == new_h)
698     return;
699
700   CGSize osize = backbuffer_size;
701   CGContextRef ob = backbuffer;
702
703   backbuffer_size.width  = new_w;
704   backbuffer_size.height = new_h;
705
706   CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB();
707   backbuffer = CGBitmapContextCreate (NULL,
708                                       backbuffer_size.width,
709                                       backbuffer_size.height,
710                                       8, 
711                                       backbuffer_size.width * 4,
712                                       cs,
713                                       // kCGImageAlphaPremultipliedLast
714                                       (kCGImageAlphaNoneSkipFirst |
715                                        kCGBitmapByteOrder32Host)
716                                       );
717   CGColorSpaceRelease (cs);
718   NSAssert (backbuffer, @"unable to allocate back buffer");
719
720   // Clear it.
721   CGRect r;
722   r.origin.x = r.origin.y = 0;
723   r.size = backbuffer_size;
724   CGContextSetGrayFillColor (backbuffer, 0, 1);
725   CGContextFillRect (backbuffer, r);
726
727   if (ob) {
728     // Restore old bits, as much as possible, to the X11 upper left origin.
729     CGRect rect;
730     rect.origin.x = 0;
731     rect.origin.y = (backbuffer_size.height - osize.height);
732     rect.size  = osize;
733     CGImageRef img = CGBitmapContextCreateImage (ob);
734     CGContextDrawImage (backbuffer, rect, img);
735     CGImageRelease (img);
736     CGContextRelease (ob);
737   }
738 }
739
740 #endif // USE_BACKBUFFER
741
742
743 /* Inform X11 that the size of our window has changed.
744  */
745 - (void) resize_x11
746 {
747   if (!xwindow) return;  // early
748
749 # ifdef USE_BACKBUFFER
750   [self createBackbuffer];
751   jwxyz_window_resized (xdpy, xwindow,
752                         0, 0,
753                         backbuffer_size.width, backbuffer_size.height,
754                         backbuffer);
755 # else   // !USE_BACKBUFFER
756   NSRect r = [self frame];              // ignoring rotation is closer
757   r.size = [self bounds].size;          // to what XGetGeometry expects.
758   jwxyz_window_resized (xdpy, xwindow,
759                         r.origin.x, r.origin.y,
760                         r.size.width, r.size.height,
761                         0);
762 # endif  // !USE_BACKBUFFER
763
764   // Next time render_x11 is called, run the saver's reshape_cb.
765   resized_p = YES;
766 }
767
768
769 - (void) render_x11
770 {
771 # ifdef USE_IPHONE
772   @try {
773
774   if (orientation == UIDeviceOrientationUnknown)
775     [self didRotate:nil];
776   [self hackRotation];
777 # endif
778
779   if (!initted_p) {
780
781     if (! xdpy) {
782 # ifdef USE_BACKBUFFER
783       NSAssert (backbuffer, @"no back buffer");
784       xdpy = jwxyz_make_display (self, backbuffer);
785 # else
786       xdpy = jwxyz_make_display (self, 0);
787 # endif
788       xwindow = XRootWindow (xdpy, 0);
789
790 # ifdef USE_IPHONE
791       /* Some X11 hacks (fluidballs) want to ignore all rotation events. */
792       ignore_rotation_p =
793         get_boolean_resource (xdpy, "ignoreRotation", "IgnoreRotation");
794 # endif // USE_IPHONE
795
796       [self resize_x11];
797     }
798
799     if (!setup_p) {
800       setup_p = YES;
801       if (xsft->setup_cb)
802         xsft->setup_cb (xsft, xsft->setup_arg);
803     }
804     initted_p = YES;
805     resized_p = NO;
806     NSAssert(!xdata, @"xdata already initialized");
807
808
809 # undef ya_rand_init
810     ya_rand_init (0);
811     
812     XSetWindowBackground (xdpy, xwindow,
813                           get_pixel_resource (xdpy, 0,
814                                               "background", "Background"));
815     XClearWindow (xdpy, xwindow);
816     
817 # ifndef USE_IPHONE
818     [[self window] setAcceptsMouseMovedEvents:YES];
819 # endif
820
821     /* In MacOS 10.5, this enables "QuartzGL", meaning that the Quartz
822        drawing primitives will run on the GPU instead of the CPU.
823        It seems like it might make things worse rather than better,
824        though...  Plus it makes us binary-incompatible with 10.4.
825
826 # if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5
827     [[self window] setPreferredBackingLocation:
828                      NSWindowBackingLocationVideoMemory];
829 # endif
830      */
831
832     /* Kludge: even though the init_cb functions are declared to take 2 args,
833       actually call them with 3, for the benefit of xlockmore_init() and
834       xlockmore_setup().
835       */
836     void *(*init_cb) (Display *, Window, void *) = 
837       (void *(*) (Display *, Window, void *)) xsft->init_cb;
838     
839     xdata = init_cb (xdpy, xwindow, xsft->setup_arg);
840
841     if (get_boolean_resource (xdpy, "doFPS", "DoFPS")) {
842       fpst = fps_init (xdpy, xwindow);
843       if (! xsft->fps_cb) xsft->fps_cb = screenhack_do_fps;
844     }
845   }
846
847
848   /* I don't understand why we have to do this *every frame*, but we do,
849      or else the cursor comes back on.
850    */
851 # ifndef USE_IPHONE
852   if (![self isPreview])
853     [NSCursor setHiddenUntilMouseMoves:YES];
854 # endif
855
856
857   if (fpst)
858     {
859       /* This is just a guess, but the -fps code wants to know how long
860          we were sleeping between frames.
861        */
862       long usecs = 1000000 * [self animationTimeInterval];
863       usecs -= 200;  // caller apparently sleeps for slightly less sometimes...
864       if (usecs < 0) usecs = 0;
865       fps_slept (fpst, usecs);
866     }
867
868
869   /* It turns out that [ScreenSaverView setAnimationTimeInterval] does nothing.
870      This is bad, because some of the screen hacks want to delay for long 
871      periods (like 5 seconds or a minute!) between frames, and running them
872      all at 60 FPS is no good.
873   
874      So, we don't use setAnimationTimeInterval, and just let the framework call
875      us whenever.  But, we only invoke the screen hack's "draw frame" method
876      when enough time has expired.
877   
878      This means two extra calls to gettimeofday() per frame.  For fast-cycling
879      screen savers, that might actually slow them down.  Oh well.
880
881      #### Also, we do not run the draw callback faster than the system's
882           animationTimeInterval, so if any savers are pickier about timing
883           than that, this may slow them down too much.  If that's a problem,
884           then we could call draw_cb in a loop here (with usleep) until the
885           next call would put us past animationTimeInterval...  But a better
886           approach would probably be to just change the saver to not do that.
887    */
888   struct timeval tv;
889   gettimeofday (&tv, 0);
890   double now = tv.tv_sec + (tv.tv_usec / 1000000.0);
891   if (now < next_frame_time) return;
892   
893   [self prepareContext];
894
895   if (resized_p) {
896     // We do this here instead of in setFrame so that all the
897     // Xlib drawing takes place under the animation timer.
898     [self resizeContext];
899     NSRect r;
900 # ifndef USE_BACKBUFFER
901     r = [self bounds];
902 # else  // USE_BACKBUFFER
903     r.origin.x = 0;
904     r.origin.y = 0;
905     r.size.width  = backbuffer_size.width;
906     r.size.height = backbuffer_size.height;
907 # endif // USE_BACKBUFFER
908
909     xsft->reshape_cb (xdpy, xwindow, xdata, r.size.width, r.size.height);
910     resized_p = NO;
911   }
912
913   // Run any XtAppAddInput callbacks now.
914   // (Note that XtAppAddTimeOut callbacks have already been run by
915   // the Cocoa event loop.)
916   //
917   jwxyz_sources_run (display_sources_data (xdpy));
918
919
920   // And finally:
921   //
922 # ifndef USE_IPHONE
923   NSDisableScreenUpdates();
924 # endif
925   unsigned long delay = xsft->draw_cb (xdpy, xwindow, xdata);
926   if (fpst) xsft->fps_cb (xdpy, xwindow, fpst, xdata);
927 # ifndef USE_IPHONE
928   NSEnableScreenUpdates();
929 # endif
930
931   gettimeofday (&tv, 0);
932   now = tv.tv_sec + (tv.tv_usec / 1000000.0);
933   next_frame_time = now + (delay / 1000000.0);
934
935 # ifdef USE_IPHONE      // Allow savers on the iPhone to run full-tilt.
936   if (delay < [self animationTimeInterval])
937     [self setAnimationTimeInterval:(delay / 1000000.0)];
938 # endif
939
940 # ifdef DO_GC_HACKERY
941   /* Current theory is that the 10.6 garbage collector sucks in the
942      following way:
943
944      It only does a collection when a threshold of outstanding
945      collectable allocations has been surpassed.  However, CoreGraphics
946      creates lots of small collectable allocations that contain pointers
947      to very large non-collectable allocations: a small CG object that's
948      collectable referencing large malloc'd allocations (non-collectable)
949      containing bitmap data.  So the large allocation doesn't get freed
950      until GC collects the small allocation, which triggers its finalizer
951      to run which frees the large allocation.  So GC is deciding that it
952      doesn't really need to run, even though the process has gotten
953      enormous.  GC eventually runs once pageouts have happened, but by
954      then it's too late, and the machine's resident set has been
955      sodomized.
956
957      So, we force an exhaustive garbage collection in this process
958      approximately every 5 seconds whether the system thinks it needs 
959      one or not.
960   */
961   {
962     static int tick = 0;
963     if (++tick > 5*30) {
964       tick = 0;
965       objc_collect (OBJC_EXHAUSTIVE_COLLECTION);
966     }
967   }
968 # endif // DO_GC_HACKERY
969
970 # ifdef USE_IPHONE
971   }
972   @catch (NSException *e) {
973     [self handleException: e];
974   }
975 # endif // USE_IPHONE
976 }
977
978
979 /* drawRect always does nothing, and animateOneFrame renders bits to the
980    screen.  This is (now) true of both X11 and GL on both MacOS and iOS.
981  */
982
983 - (void)drawRect:(NSRect)rect
984 {
985   if (xwindow)    // clear to the X window's bg color, not necessarily black.
986     XClearWindow (xdpy, xwindow);
987   else
988     [super drawRect:rect];    // early: black.
989 }
990
991
992 #ifndef USE_BACKBUFFER
993
994 - (void) animateOneFrame
995 {
996   [self render_x11];
997 }
998
999 #else  // USE_BACKBUFFER
1000
1001 - (void) animateOneFrame
1002 {
1003   // Render X11 into the backing store bitmap...
1004
1005   NSAssert (backbuffer, @"no back buffer");
1006
1007 # ifdef USE_IPHONE
1008   UIGraphicsPushContext (backbuffer);
1009 # endif
1010
1011   [self render_x11];
1012
1013 # ifdef USE_IPHONE
1014   UIGraphicsPopContext();
1015 # endif
1016
1017 # ifdef USE_IPHONE
1018   // Then compute the transformations for rotation.
1019   double hs = [self hackedContentScaleFactor];
1020   double s = [self contentScaleFactor];
1021
1022   // The rotation origin for layer.affineTransform is in the center already.
1023   CGAffineTransform t = ignore_rotation_p ?
1024     CGAffineTransformIdentity :
1025     CGAffineTransformMakeRotation (rot_current_angle / (180.0 / M_PI));
1026
1027   CGFloat f = s / hs;
1028   self.layer.affineTransform = CGAffineTransformScale(t, f, f);
1029
1030   CGRect bounds;
1031   bounds.origin.x = 0;
1032   bounds.origin.y = 0;
1033   bounds.size.width = backbuffer_size.width / s;
1034   bounds.size.height = backbuffer_size.height / s;
1035   self.layer.bounds = bounds;
1036 # endif // USE_IPHONE
1037  
1038   [self.layer setNeedsDisplay];
1039 }
1040
1041 - (void) drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
1042 {
1043   // This "isn't safe" if NULL is passed to CGBitmapCreateContext before iOS 4.
1044   char *dest_data = (char *)CGBitmapContextGetData (ctx);
1045
1046   // The CGContext here is normally upside-down on iOS.
1047   if (dest_data &&
1048       CGBitmapContextGetBitmapInfo (ctx) ==
1049         (kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Host)
1050 #ifdef USE_IPHONE
1051       && CGContextGetCTM (ctx).d < 0
1052 #endif
1053       )
1054   {
1055     size_t dest_height = CGBitmapContextGetHeight (ctx);
1056     size_t dest_bpr = CGBitmapContextGetBytesPerRow (ctx);
1057     size_t src_height = CGBitmapContextGetHeight (backbuffer);
1058     size_t src_bpr = CGBitmapContextGetBytesPerRow (backbuffer);
1059     char *src_data = (char *)CGBitmapContextGetData (backbuffer);
1060
1061     size_t height = src_height < dest_height ? src_height : dest_height;
1062     
1063     if (src_bpr == dest_bpr) {
1064       // iPad 1: 4.0 ms, iPad 2: 6.7 ms
1065       memcpy (dest_data, src_data, src_bpr * height);
1066     } else {
1067       // iPad 1: 4.6 ms, iPad 2: 7.2 ms
1068       size_t bpr = src_bpr < dest_bpr ? src_bpr : dest_bpr;
1069       while (height) {
1070         memcpy (dest_data, src_data, bpr);
1071         --height;
1072         src_data += src_bpr;
1073         dest_data += dest_bpr;
1074       }
1075     }
1076   } else {
1077
1078     // iPad 1: 9.6 ms, iPad 2: 12.1 ms
1079
1080 #ifdef USE_IPHONE
1081     CGContextScaleCTM (ctx, 1, -1);
1082     CGFloat s = [self contentScaleFactor];
1083     CGFloat hs = [self hackedContentScaleFactor];
1084     CGContextTranslateCTM (ctx, 0, -backbuffer_size.height * hs / s);
1085 #endif
1086     
1087     CGImageRef img = CGBitmapContextCreateImage (backbuffer);
1088     CGContextDrawImage (ctx, self.layer.bounds, img);
1089     CGImageRelease (img);
1090   }
1091 }
1092
1093 #endif // !USE_BACKBUFFER
1094
1095
1096
1097 - (void) setFrame:(NSRect) newRect
1098 {
1099   [super setFrame:newRect];
1100
1101   if (xwindow)     // inform Xlib that the window has changed now.
1102     [self resize_x11];
1103 }
1104
1105
1106 # ifndef USE_IPHONE  // Doesn't exist on iOS
1107 - (void) setFrameSize:(NSSize) newSize
1108 {
1109   [super setFrameSize:newSize];
1110   if (xwindow)
1111     [self resize_x11];
1112 }
1113 # endif // !USE_IPHONE
1114
1115
1116 +(BOOL) performGammaFade
1117 {
1118   return YES;
1119 }
1120
1121 - (BOOL) hasConfigureSheet
1122 {
1123   return YES;
1124 }
1125
1126 + (NSString *) decompressXML: (NSData *)data
1127 {
1128   if (! data) return 0;
1129   BOOL compressed_p = !!strncmp ((const char *) data.bytes, "<?xml", 5);
1130
1131   // If it's not already XML, decompress it.
1132   NSAssert (compressed_p, @"xml isn't compressed");
1133   if (compressed_p) {
1134     NSMutableData *data2 = 0;
1135     int ret = -1;
1136     z_stream zs;
1137     memset (&zs, 0, sizeof(zs));
1138     ret = inflateInit2 (&zs, 16 + MAX_WBITS);
1139     if (ret == Z_OK) {
1140       UInt32 usize = * (UInt32 *) (data.bytes + data.length - 4);
1141       data2 = [NSMutableData dataWithLength: usize];
1142       zs.next_in   = (Bytef *) data.bytes;
1143       zs.avail_in  = data.length;
1144       zs.next_out  = (Bytef *) data2.bytes;
1145       zs.avail_out = data2.length;
1146       ret = inflate (&zs, Z_FINISH);
1147       inflateEnd (&zs);
1148     }
1149     if (ret == Z_OK || ret == Z_STREAM_END)
1150       data = data2;
1151     else
1152       NSAssert2 (0, @"gunzip error: %d: %s",
1153                  ret, (zs.msg ? zs.msg : "<null>"));
1154   }
1155
1156   return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
1157 }
1158
1159
1160 #ifndef USE_IPHONE
1161 - (NSWindow *) configureSheet
1162 #else
1163 - (UIViewController *) configureView
1164 #endif
1165 {
1166   NSBundle *bundle = [NSBundle bundleForClass:[self class]];
1167   NSString *file = [NSString stringWithCString:xsft->progclass
1168                                       encoding:NSISOLatin1StringEncoding];
1169   file = [file lowercaseString];
1170   NSString *path = [bundle pathForResource:file ofType:@"xml"];
1171   if (!path) {
1172     NSLog (@"%@.xml does not exist in the application bundle: %@/",
1173            file, [bundle resourcePath]);
1174     return nil;
1175   }
1176   
1177 # ifdef USE_IPHONE
1178   UIViewController *sheet;
1179 # else  // !USE_IPHONE
1180   NSWindow *sheet;
1181 # endif // !USE_IPHONE
1182
1183   NSData *xmld = [NSData dataWithContentsOfFile:path];
1184   NSString *xml = [[self class] decompressXML: xmld];
1185   sheet = [[XScreenSaverConfigSheet alloc]
1186             initWithXML:[xml dataUsingEncoding:NSUTF8StringEncoding]
1187                 options:xsft->options
1188              controller:[prefsReader userDefaultsController]
1189                defaults:[prefsReader defaultOptions]];
1190
1191   // #### am I expected to retain this, or not? wtf.
1192   //      I thought not, but if I don't do this, we (sometimes) crash.
1193   // #### Analyze says "potential leak of an object stored into sheet"
1194   [sheet retain];
1195
1196   return sheet;
1197 }
1198
1199
1200 - (NSUserDefaultsController *) userDefaultsController
1201 {
1202   return [prefsReader userDefaultsController];
1203 }
1204
1205
1206 /* Announce our willingness to accept keyboard input.
1207 */
1208 - (BOOL)acceptsFirstResponder
1209 {
1210   return YES;
1211 }
1212
1213
1214 #ifndef USE_IPHONE
1215
1216 /* Convert an NSEvent into an XEvent, and pass it along.
1217    Returns YES if it was handled.
1218  */
1219 - (BOOL) doEvent: (NSEvent *) e
1220             type: (int) type
1221 {
1222   if (![self isPreview] ||     // no event handling if actually screen-saving!
1223       ![self isAnimating] ||
1224       !initted_p)
1225     return NO;
1226
1227   XEvent xe;
1228   memset (&xe, 0, sizeof(xe));
1229   
1230   int state = 0;
1231   
1232   int flags = [e modifierFlags];
1233   if (flags & NSAlphaShiftKeyMask) state |= LockMask;
1234   if (flags & NSShiftKeyMask)      state |= ShiftMask;
1235   if (flags & NSControlKeyMask)    state |= ControlMask;
1236   if (flags & NSAlternateKeyMask)  state |= Mod1Mask;
1237   if (flags & NSCommandKeyMask)    state |= Mod2Mask;
1238   
1239   NSPoint p = [[[e window] contentView] convertPoint:[e locationInWindow]
1240                                             toView:self];
1241 # ifdef USE_IPHONE
1242   double s = [self hackedContentScaleFactor];
1243 # else
1244   int s = 1;
1245 # endif
1246   int x = s * p.x;
1247   int y = s * ([self bounds].size.height - p.y);
1248
1249   xe.xany.type = type;
1250   switch (type) {
1251     case ButtonPress:
1252     case ButtonRelease:
1253       xe.xbutton.x = x;
1254       xe.xbutton.y = y;
1255       xe.xbutton.state = state;
1256       if ([e type] == NSScrollWheel)
1257         xe.xbutton.button = ([e deltaY] > 0 ? Button4 :
1258                              [e deltaY] < 0 ? Button5 :
1259                              [e deltaX] > 0 ? Button6 :
1260                              [e deltaX] < 0 ? Button7 :
1261                              0);
1262       else
1263         xe.xbutton.button = [e buttonNumber] + 1;
1264       break;
1265     case MotionNotify:
1266       xe.xmotion.x = x;
1267       xe.xmotion.y = y;
1268       xe.xmotion.state = state;
1269       break;
1270     case KeyPress:
1271     case KeyRelease:
1272       {
1273         NSString *ns = (([e type] == NSFlagsChanged) ? 0 :
1274                         [e charactersIgnoringModifiers]);
1275         KeySym k = 0;
1276
1277         if (!ns || [ns length] == 0)                    // dead key
1278           {
1279             // Cocoa hides the difference between left and right keys.
1280             // Also we only get KeyPress events for these, no KeyRelease
1281             // (unless we hack the mod state manually.  Bleh.)
1282             //
1283             if      (flags & NSAlphaShiftKeyMask)   k = XK_Caps_Lock;
1284             else if (flags & NSShiftKeyMask)        k = XK_Shift_L;
1285             else if (flags & NSControlKeyMask)      k = XK_Control_L;
1286             else if (flags & NSAlternateKeyMask)    k = XK_Alt_L;
1287             else if (flags & NSCommandKeyMask)      k = XK_Meta_L;
1288           }
1289         else if ([ns length] == 1)                      // real key
1290           {
1291             switch ([ns characterAtIndex:0]) {
1292             case NSLeftArrowFunctionKey:  k = XK_Left;      break;
1293             case NSRightArrowFunctionKey: k = XK_Right;     break;
1294             case NSUpArrowFunctionKey:    k = XK_Up;        break;
1295             case NSDownArrowFunctionKey:  k = XK_Down;      break;
1296             case NSPageUpFunctionKey:     k = XK_Page_Up;   break;
1297             case NSPageDownFunctionKey:   k = XK_Page_Down; break;
1298             case NSHomeFunctionKey:       k = XK_Home;      break;
1299             case NSPrevFunctionKey:       k = XK_Prior;     break;
1300             case NSNextFunctionKey:       k = XK_Next;      break;
1301             case NSBeginFunctionKey:      k = XK_Begin;     break;
1302             case NSEndFunctionKey:        k = XK_End;       break;
1303             default:
1304               {
1305                 const char *s =
1306                   [ns cStringUsingEncoding:NSISOLatin1StringEncoding];
1307                 k = (s && *s ? *s : 0);
1308               }
1309               break;
1310             }
1311           }
1312
1313         if (! k) return YES;   // E.g., "KeyRelease XK_Shift_L"
1314
1315         xe.xkey.keycode = k;
1316         xe.xkey.state = state;
1317         break;
1318       }
1319     default:
1320       NSAssert1 (0, @"unknown X11 event type: %d", type);
1321       break;
1322   }
1323
1324   [self lockFocus];
1325   [self prepareContext];
1326   BOOL result = xsft->event_cb (xdpy, xwindow, xdata, &xe);
1327   [self unlockFocus];
1328   return result;
1329 }
1330
1331
1332 - (void) mouseDown: (NSEvent *) e
1333 {
1334   if (! [self doEvent:e type:ButtonPress])
1335     [super mouseDown:e];
1336 }
1337
1338 - (void) mouseUp: (NSEvent *) e
1339 {
1340   if (! [self doEvent:e type:ButtonRelease])
1341     [super mouseUp:e];
1342 }
1343
1344 - (void) otherMouseDown: (NSEvent *) e
1345 {
1346   if (! [self doEvent:e type:ButtonPress])
1347     [super otherMouseDown:e];
1348 }
1349
1350 - (void) otherMouseUp: (NSEvent *) e
1351 {
1352   if (! [self doEvent:e type:ButtonRelease])
1353     [super otherMouseUp:e];
1354 }
1355
1356 - (void) mouseMoved: (NSEvent *) e
1357 {
1358   if (! [self doEvent:e type:MotionNotify])
1359     [super mouseMoved:e];
1360 }
1361
1362 - (void) mouseDragged: (NSEvent *) e
1363 {
1364   if (! [self doEvent:e type:MotionNotify])
1365     [super mouseDragged:e];
1366 }
1367
1368 - (void) otherMouseDragged: (NSEvent *) e
1369 {
1370   if (! [self doEvent:e type:MotionNotify])
1371     [super otherMouseDragged:e];
1372 }
1373
1374 - (void) scrollWheel: (NSEvent *) e
1375 {
1376   if (! [self doEvent:e type:ButtonPress])
1377     [super scrollWheel:e];
1378 }
1379
1380 - (void) keyDown: (NSEvent *) e
1381 {
1382   if (! [self doEvent:e type:KeyPress])
1383     [super keyDown:e];
1384 }
1385
1386 - (void) keyUp: (NSEvent *) e
1387 {
1388   if (! [self doEvent:e type:KeyRelease])
1389     [super keyUp:e];
1390 }
1391
1392 - (void) flagsChanged: (NSEvent *) e
1393 {
1394   if (! [self doEvent:e type:KeyPress])
1395     [super flagsChanged:e];
1396 }
1397
1398 #else  // USE_IPHONE
1399
1400
1401 - (void) stopAndClose:(Bool)relaunch_p
1402 {
1403   if ([self isAnimating])
1404     [self stopAnimation];
1405
1406   /* Need to make the SaverListController be the firstResponder again
1407      so that it can continue to receive its own shake events.  I
1408      suppose that this abstraction-breakage means that I'm adding
1409      XScreenSaverView to the UINavigationController wrong...
1410    */
1411   UIViewController *v = [[self window] rootViewController];
1412   if ([v isKindOfClass: [UINavigationController class]]) {
1413     UINavigationController *n = (UINavigationController *) v;
1414     [[n topViewController] becomeFirstResponder];
1415   }
1416
1417   UIView *fader = [self superview];  // the "backgroundView" view is our parent
1418
1419   if (relaunch_p) {   // Fake a shake on the SaverListController.
1420     // Why is [self window] sometimes null here?
1421     UIWindow *w = [[UIApplication sharedApplication] keyWindow];
1422     UIViewController *v = [w rootViewController];
1423     if ([v isKindOfClass: [UINavigationController class]]) {
1424       UINavigationController *n = (UINavigationController *) v;
1425       [[n topViewController] motionEnded: UIEventSubtypeMotionShake
1426                                withEvent: nil];
1427     }
1428   } else {      // Not launching another, animate our return to the list.
1429     [UIView animateWithDuration: 0.5
1430             animations:^{ fader.alpha = 0.0; }
1431             completion:^(BOOL finished) {
1432                [fader removeFromSuperview];
1433                fader.alpha = 1.0;
1434             }];
1435   }
1436 }
1437
1438
1439 /* Called after the device's orientation has changed.
1440
1441    Note: we could include a subclass of UIViewController which
1442    contains a shouldAutorotateToInterfaceOrientation method that
1443    returns YES, in which case Core Animation would auto-rotate our
1444    View for us in response to rotation events... but, that interacts
1445    badly with the EAGLContext -- if you introduce Core Animation into
1446    the path, the OpenGL pipeline probably falls back on software
1447    rendering and performance goes to hell.  Also, the scaling and
1448    rotation that Core Animation does interacts incorrectly with the GL
1449    context anyway.
1450
1451    So, we have to hack the rotation animation manually, in the GL world.
1452
1453    Possibly XScreenSaverView should use Core Animation, and 
1454    XScreenSaverGLView should override that.
1455 */
1456 - (void)didRotate:(NSNotification *)notification
1457 {
1458   UIDeviceOrientation current = [[UIDevice currentDevice] orientation];
1459
1460   /* If the simulator starts up in the rotated position, sometimes
1461      the UIDevice says we're in Portrait when we're not -- but it
1462      turns out that the UINavigationController knows what's up!
1463      So get it from there.
1464    */
1465   if (current == UIDeviceOrientationUnknown) {
1466     switch ([[[self window] rootViewController] interfaceOrientation]) {
1467     case UIInterfaceOrientationPortrait:
1468       current = UIDeviceOrientationPortrait;
1469       break;
1470     case UIInterfaceOrientationPortraitUpsideDown:
1471       current = UIDeviceOrientationPortraitUpsideDown;
1472       break;
1473     case UIInterfaceOrientationLandscapeLeft:           // It's opposite day
1474       current = UIDeviceOrientationLandscapeRight;
1475       break;
1476     case UIInterfaceOrientationLandscapeRight:
1477       current = UIDeviceOrientationLandscapeLeft;
1478       break;
1479     default:
1480       break;
1481     }
1482   }
1483
1484   /* On the iPad (but not iPhone 3GS, or the simulator) sometimes we get
1485      an orientation change event with an unknown orientation.  Those seem
1486      to always be immediately followed by another orientation change with
1487      a *real* orientation change, so let's try just ignoring those bogus
1488      ones and hoping that the real one comes in shortly...
1489    */
1490   if (current == UIDeviceOrientationUnknown)
1491     return;
1492
1493   if (rotation_ratio >= 0) return;      // in the midst of rotation animation
1494   if (orientation == current) return;   // no change
1495
1496   // When transitioning to FaceUp or FaceDown, pretend there was no change.
1497   if (current == UIDeviceOrientationFaceUp ||
1498       current == UIDeviceOrientationFaceDown)
1499     return;
1500
1501   new_orientation = current;            // current animation target
1502   rotation_ratio = 0;                   // start animating
1503   rot_start_time = double_time();
1504
1505   switch (orientation) {
1506   case UIDeviceOrientationLandscapeLeft:      angle_from = 90;  break;
1507   case UIDeviceOrientationLandscapeRight:     angle_from = 270; break;
1508   case UIDeviceOrientationPortraitUpsideDown: angle_from = 180; break;
1509   default:                                    angle_from = 0;   break;
1510   }
1511
1512   switch (new_orientation) {
1513   case UIDeviceOrientationLandscapeLeft:      angle_to = 90;  break;
1514   case UIDeviceOrientationLandscapeRight:     angle_to = 270; break;
1515   case UIDeviceOrientationPortraitUpsideDown: angle_to = 180; break;
1516   default:                                    angle_to = 0;   break;
1517   }
1518
1519   switch (orientation) {
1520   case UIDeviceOrientationLandscapeRight:       // from landscape
1521   case UIDeviceOrientationLandscapeLeft:
1522     rot_from.width  = initial_bounds.height;
1523     rot_from.height = initial_bounds.width;
1524     break;
1525   default:                                      // from portrait
1526     rot_from.width  = initial_bounds.width;
1527     rot_from.height = initial_bounds.height;
1528     break;
1529   }
1530
1531   switch (new_orientation) {
1532   case UIDeviceOrientationLandscapeRight:       // to landscape
1533   case UIDeviceOrientationLandscapeLeft:
1534     rot_to.width  = initial_bounds.height;
1535     rot_to.height = initial_bounds.width;
1536     break;
1537   default:                                      // to portrait
1538     rot_to.width  = initial_bounds.width;
1539     rot_to.height = initial_bounds.height;
1540     break;
1541   }
1542
1543  if (! initted_p) {
1544    // If we've done a rotation but the saver hasn't been initialized yet,
1545    // don't bother going through an X11 resize, but just do it now.
1546    rot_start_time = 0;  // dawn of time
1547    [self hackRotation];
1548  }
1549 }
1550
1551
1552 /* I believe we can't use UIGestureRecognizer for tracking touches
1553    because UIPanGestureRecognizer doesn't give us enough detail in its
1554    callbacks.
1555
1556    Currently we don't handle multi-touches (just the first touch) but
1557    I'm leaving this comment here for future reference:
1558
1559    In the simulator, multi-touch sequences look like this:
1560
1561      touchesBegan [touchA, touchB]
1562      touchesEnd [touchA, touchB]
1563
1564    But on real devices, sometimes you get that, but sometimes you get:
1565
1566      touchesBegan [touchA, touchB]
1567      touchesEnd [touchB]
1568      touchesEnd [touchA]
1569
1570    Or even
1571
1572      touchesBegan [touchA]
1573      touchesBegan [touchB]
1574      touchesEnd [touchA]
1575      touchesEnd [touchB]
1576
1577    So the only way to properly detect a "pinch" gesture is to remember
1578    the start-point of each touch as it comes in; and the end-point of
1579    each touch as those come in; and only process the gesture once the
1580    number of touchEnds matches the number of touchBegins.
1581  */
1582
1583 - (void) rotateMouse:(int)rot x:(int*)x y:(int *)y w:(int)w h:(int)h
1584 {
1585   // This is a no-op unless contentScaleFactor != hackedContentScaleFactor.
1586   // Currently, this is the iPad Retina only.
1587   CGRect frame = [self bounds];         // Scale.
1588   double s = [self hackedContentScaleFactor];
1589   *x *= (backbuffer_size.width  / frame.size.width)  / s;
1590   *y *= (backbuffer_size.height / frame.size.height) / s;
1591 }
1592
1593
1594 #if 0  // AudioToolbox/AudioToolbox.h
1595 - (void) beep
1596 {
1597   // There's no way to play a standard system alert sound!
1598   // We'd have to include our own WAV for that.  Eh, fuck it.
1599   AudioServicesPlaySystemSound (kSystemSoundID_Vibrate);
1600 # if TARGET_IPHONE_SIMULATOR
1601   NSLog(@"BEEP");  // The sim doesn't vibrate.
1602 # endif
1603 }
1604 #endif
1605
1606
1607 /* We distinguish between taps and drags.
1608    - Drags (down, motion, up) are sent to the saver to handle.
1609    - Single-taps exit the saver.
1610    This means a saver cannot respond to a single-tap.  Only a few try to.
1611  */
1612
1613 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
1614 {
1615   // If they are trying to pinch, just do nothing.
1616   if ([[event allTouches] count] > 1)
1617     return;
1618
1619   tap_time = 0;
1620
1621   if (xsft->event_cb && xwindow) {
1622     double s = [self hackedContentScaleFactor];
1623     XEvent xe;
1624     memset (&xe, 0, sizeof(xe));
1625     int i = 0;
1626     // #### 'frame' here or 'bounds'?
1627     int w = s * [self frame].size.width;
1628     int h = s * [self frame].size.height;
1629     for (UITouch *touch in touches) {
1630       CGPoint p = [touch locationInView:self];
1631       xe.xany.type = ButtonPress;
1632       xe.xbutton.button = i + 1;
1633       xe.xbutton.button = i + 1;
1634       xe.xbutton.x      = s * p.x;
1635       xe.xbutton.y      = s * p.y;
1636       [self rotateMouse: rot_current_angle
1637             x: &xe.xbutton.x y: &xe.xbutton.y w: w h: h];
1638       jwxyz_mouse_moved (xdpy, xwindow, xe.xbutton.x, xe.xbutton.y);
1639
1640       // Ignore return code: don't care whether the hack handled it.
1641       xsft->event_cb (xdpy, xwindow, xdata, &xe);
1642
1643       // Remember when/where this was, to determine tap versus drag or hold.
1644       tap_time = double_time();
1645       tap_point = p;
1646
1647       i++;
1648       break;  // No pinches: only look at the first touch.
1649     }
1650   }
1651 }
1652
1653
1654 - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
1655 {
1656   // If they are trying to pinch, just do nothing.
1657   if ([[event allTouches] count] > 1)
1658     return;
1659
1660   if (xsft->event_cb && xwindow) {
1661     double s = [self hackedContentScaleFactor];
1662     XEvent xe;
1663     memset (&xe, 0, sizeof(xe));
1664     int i = 0;
1665     // #### 'frame' here or 'bounds'?
1666     int w = s * [self frame].size.width;
1667     int h = s * [self frame].size.height;
1668     for (UITouch *touch in touches) {
1669       CGPoint p = [touch locationInView:self];
1670
1671       // If the ButtonRelease came less than half a second after ButtonPress,
1672       // and didn't move far, then this was a tap, not a drag or a hold.
1673       // Interpret it as "exit".
1674       //
1675       double dist = sqrt (((p.x - tap_point.x) * (p.x - tap_point.x)) +
1676                           ((p.y - tap_point.y) * (p.y - tap_point.y)));
1677       if (tap_time + 0.5 >= double_time() && dist < 20) {
1678         [self stopAndClose:NO];
1679         return;
1680       }
1681
1682       xe.xany.type      = ButtonRelease;
1683       xe.xbutton.button = i + 1;
1684       xe.xbutton.x      = s * p.x;
1685       xe.xbutton.y      = s * p.y;
1686       [self rotateMouse: rot_current_angle
1687             x: &xe.xbutton.x y: &xe.xbutton.y w: w h: h];
1688       jwxyz_mouse_moved (xdpy, xwindow, xe.xbutton.x, xe.xbutton.y);
1689       xsft->event_cb (xdpy, xwindow, xdata, &xe);
1690       i++;
1691       break;  // No pinches: only look at the first touch.
1692     }
1693   }
1694 }
1695
1696
1697 - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
1698 {
1699   // If they are trying to pinch, just do nothing.
1700   if ([[event allTouches] count] > 1)
1701     return;
1702
1703   if (xsft->event_cb && xwindow) {
1704     double s = [self hackedContentScaleFactor];
1705     XEvent xe;
1706     memset (&xe, 0, sizeof(xe));
1707     int i = 0;
1708     // #### 'frame' here or 'bounds'?
1709     int w = s * [self frame].size.width;
1710     int h = s * [self frame].size.height;
1711     for (UITouch *touch in touches) {
1712       CGPoint p = [touch locationInView:self];
1713       xe.xany.type      = MotionNotify;
1714       xe.xmotion.x      = s * p.x;
1715       xe.xmotion.y      = s * p.y;
1716       [self rotateMouse: rot_current_angle
1717             x: &xe.xbutton.x y: &xe.xbutton.y w: w h: h];
1718       jwxyz_mouse_moved (xdpy, xwindow, xe.xmotion.x, xe.xmotion.y);
1719       xsft->event_cb (xdpy, xwindow, xdata, &xe);
1720       i++;
1721       break;  // No pinches: only look at the first touch.
1722     }
1723   }
1724 }
1725
1726
1727 /* We need this to respond to "shake" gestures
1728  */
1729 - (BOOL)canBecomeFirstResponder
1730 {
1731   return YES;
1732 }
1733
1734 - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
1735 {
1736 }
1737
1738
1739 - (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event
1740 {
1741 }
1742
1743 /* Shake means exit and launch a new saver.
1744  */
1745 - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
1746 {
1747   [self stopAndClose:YES];
1748 }
1749
1750
1751 - (void)setScreenLocked:(BOOL)locked
1752 {
1753   if (screenLocked == locked) return;
1754   screenLocked = locked;
1755   if (locked) {
1756     if ([self isAnimating])
1757       [self stopAnimation];
1758   } else {
1759     if (! [self isAnimating])
1760       [self startAnimation];
1761   }
1762 }
1763
1764
1765 #endif // USE_IPHONE
1766
1767
1768 @end
1769
1770 /* Utility functions...
1771  */
1772
1773 static PrefsReader *
1774 get_prefsReader (Display *dpy)
1775 {
1776   XScreenSaverView *view = jwxyz_window_view (XRootWindow (dpy, 0));
1777   if (!view) return 0;
1778   return [view prefsReader];
1779 }
1780
1781
1782 char *
1783 get_string_resource (Display *dpy, char *name, char *class)
1784 {
1785   return [get_prefsReader(dpy) getStringResource:name];
1786 }
1787
1788 Bool
1789 get_boolean_resource (Display *dpy, char *name, char *class)
1790 {
1791   return [get_prefsReader(dpy) getBooleanResource:name];
1792 }
1793
1794 int
1795 get_integer_resource (Display *dpy, char *name, char *class)
1796 {
1797   return [get_prefsReader(dpy) getIntegerResource:name];
1798 }
1799
1800 double
1801 get_float_resource (Display *dpy, char *name, char *class)
1802 {
1803   return [get_prefsReader(dpy) getFloatResource:name];
1804 }