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