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