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