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