1 /* xscreensaver, Copyright (c) 2006-2013 Jamie Zawinski <jwz@jwz.org>
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
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.
18 #import <QuartzCore/QuartzCore.h>
20 #import "XScreenSaverView.h"
21 #import "XScreenSaverConfigSheet.h"
22 #import "screenhackI.h"
23 #import "xlockmoreI.h"
24 #import "jwxyz-timers.h"
27 /* Garbage collection only exists if we are being compiled against the
28 10.6 SDK or newer, not if we are building against the 10.4 SDK.
30 #ifndef MAC_OS_X_VERSION_10_6
31 # define MAC_OS_X_VERSION_10_6 1060 /* undefined in 10.4 SDK, grr */
33 #if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6 /* 10.6 SDK */
34 # import <objc/objc-auto.h>
35 # define DO_GC_HACKERY
38 extern struct xscreensaver_function_table *xscreensaver_function_table;
40 /* Global variables used by the screen savers
43 const char *progclass;
49 extern NSDictionary *make_function_table_dict(void); // ios-function-table.m
51 /* Stub definition of the superclass, for iPhone.
53 @implementation ScreenSaverView
55 NSTimeInterval anim_interval;
60 - (id)initWithFrame:(NSRect)frame isPreview:(BOOL)isPreview {
61 self = [super initWithFrame:frame];
63 anim_interval = 1.0/30;
66 - (NSTimeInterval)animationTimeInterval { return anim_interval; }
67 - (void)setAnimationTimeInterval:(NSTimeInterval)i { anim_interval = i; }
68 - (BOOL)hasConfigureSheet { return NO; }
69 - (NSWindow *)configureSheet { return nil; }
70 - (NSView *)configureView { return nil; }
71 - (BOOL)isPreview { return NO; }
72 - (BOOL)isAnimating { return animating_p; }
73 - (void)animateOneFrame { }
75 - (void)startAnimation {
76 if (animating_p) return;
78 anim_timer = [NSTimer scheduledTimerWithTimeInterval: anim_interval
80 selector:@selector(animateOneFrame)
85 - (void)stopAnimation {
87 [anim_timer invalidate];
94 # endif // !USE_IPHONE
98 @interface XScreenSaverView (Private)
99 - (void) stopAndClose:(Bool)relaunch;
102 @implementation XScreenSaverView
104 // Given a lower-cased saver name, returns the function table for it.
105 // If no name, guess the name from the class's bundle name.
107 - (struct xscreensaver_function_table *) findFunctionTable:(NSString *)name
109 NSBundle *nsb = [NSBundle bundleForClass:[self class]];
110 NSAssert1 (nsb, @"no bundle for class %@", [self class]);
112 NSString *path = [nsb bundlePath];
113 CFURLRef url = CFURLCreateWithFileSystemPath(kCFAllocatorDefault,
115 kCFURLPOSIXPathStyle,
117 CFBundleRef cfb = CFBundleCreate (kCFAllocatorDefault, url);
119 NSAssert1 (cfb, @"no CFBundle for \"%@\"", path);
120 // #### Analyze says "Potential leak of an object stored into cfb"
123 name = [[path lastPathComponent] stringByDeletingPathExtension];
125 name = [[name lowercaseString]
126 stringByReplacingOccurrencesOfString:@" "
130 // CFBundleGetDataPointerForName doesn't work in "Archive" builds.
131 // I'm guessing that symbol-stripping is mandatory. Fuck.
132 NSString *table_name = [name stringByAppendingString:
133 @"_xscreensaver_function_table"];
134 void *addr = CFBundleGetDataPointerForName (cfb, (CFStringRef) table_name);
138 NSLog (@"no symbol \"%@\" for \"%@\"", table_name, path);
141 // Remember: any time you add a new saver to the iOS app,
142 // manually run "make ios-function-table.m"!
143 if (! function_tables)
144 function_tables = [make_function_table_dict() retain];
145 NSValue *v = [function_tables objectForKey: name];
146 void *addr = v ? [v pointerValue] : 0;
147 # endif // USE_IPHONE
149 return (struct xscreensaver_function_table *) addr;
153 // Add the "Contents/Resources/" subdirectory of this screen saver's .bundle
154 // to $PATH for the benefit of savers that include helper shell scripts.
156 - (void) setShellPath
158 NSBundle *nsb = [NSBundle bundleForClass:[self class]];
159 NSAssert1 (nsb, @"no bundle for class %@", [self class]);
161 NSString *nsdir = [nsb resourcePath];
162 NSAssert1 (nsdir, @"no resourcePath for class %@", [self class]);
163 const char *dir = [nsdir cStringUsingEncoding:NSUTF8StringEncoding];
164 const char *opath = getenv ("PATH");
165 if (!opath) opath = "/bin"; // $PATH is unset when running under Shark!
166 char *npath = (char *) malloc (strlen (opath) + strlen (dir) + 30);
167 strcpy (npath, "PATH=");
170 strcat (npath, opath);
171 if (putenv (npath)) {
173 NSAssert1 (0, @"putenv \"%s\" failed", npath);
176 /* Don't free (npath) -- MacOS's putenv() does not copy it. */
180 // set an $XSCREENSAVER_CLASSPATH variable so that included shell scripts
181 // (e.g., "xscreensaver-text") know how to look up resources.
183 - (void) setResourcesEnv:(NSString *) name
185 NSBundle *nsb = [NSBundle bundleForClass:[self class]];
186 NSAssert1 (nsb, @"no bundle for class %@", [self class]);
188 const char *s = [name cStringUsingEncoding:NSUTF8StringEncoding];
189 char *env = (char *) malloc (strlen (s) + 40);
190 strcpy (env, "XSCREENSAVER_CLASSPATH=");
194 NSAssert1 (0, @"putenv \"%s\" failed", env);
196 /* Don't free (env) -- MacOS's putenv() does not copy it. */
201 add_default_options (const XrmOptionDescRec *opts,
202 const char * const *defs,
203 XrmOptionDescRec **opts_ret,
204 const char ***defs_ret)
206 /* These aren't "real" command-line options (there are no actual command-line
207 options in the Cocoa version); but this is the somewhat kludgey way that
208 the <xscreensaver-text /> and <xscreensaver-image /> tags in the
209 ../hacks/config/\*.xml files communicate with the preferences database.
211 static const XrmOptionDescRec default_options [] = {
212 { "-text-mode", ".textMode", XrmoptionSepArg, 0 },
213 { "-text-literal", ".textLiteral", XrmoptionSepArg, 0 },
214 { "-text-file", ".textFile", XrmoptionSepArg, 0 },
215 { "-text-url", ".textURL", XrmoptionSepArg, 0 },
216 { "-text-program", ".textProgram", XrmoptionSepArg, 0 },
217 { "-grab-desktop", ".grabDesktopImages", XrmoptionNoArg, "True" },
218 { "-no-grab-desktop", ".grabDesktopImages", XrmoptionNoArg, "False"},
219 { "-choose-random-images", ".chooseRandomImages",XrmoptionNoArg, "True" },
220 { "-no-choose-random-images",".chooseRandomImages",XrmoptionNoArg, "False"},
221 { "-image-directory", ".imageDirectory", XrmoptionSepArg, 0 },
222 { "-fps", ".doFPS", XrmoptionNoArg, "True" },
223 { "-no-fps", ".doFPS", XrmoptionNoArg, "False"},
224 { "-foreground", ".foreground", XrmoptionSepArg, 0 },
225 { "-fg", ".foreground", XrmoptionSepArg, 0 },
226 { "-background", ".background", XrmoptionSepArg, 0 },
227 { "-bg", ".background", XrmoptionSepArg, 0 },
230 static const char *default_defaults [] = {
232 ".doubleBuffer: True",
233 ".multiSample: False",
241 ".textURL: http://en.wikipedia.org/w/index.php?title=Special:NewPages&feed=rss",
243 ".grabDesktopImages: yes",
245 ".chooseRandomImages: no",
247 ".chooseRandomImages: yes",
249 ".imageDirectory: ~/Pictures",
255 for (i = 0; default_options[i].option; i++)
257 for (i = 0; opts[i].option; i++)
260 XrmOptionDescRec *opts2 = (XrmOptionDescRec *)
261 calloc (count + 1, sizeof (*opts2));
265 while (default_options[j].option) {
266 opts2[i] = default_options[j];
270 while (opts[j].option) {
281 for (i = 0; default_defaults[i]; i++)
283 for (i = 0; defs[i]; i++)
286 const char **defs2 = (const char **) calloc (count + 1, sizeof (*defs2));
290 while (default_defaults[j]) {
291 defs2[i] = default_defaults[j];
305 /* Returns the current time in seconds as a double.
311 # ifdef GETTIMEOFDAY_TWO_ARGS
313 gettimeofday(&now, &tzp);
318 return (now.tv_sec + ((double) now.tv_usec * 0.000001));
323 - (id) initWithFrame:(NSRect)frame
324 saverName:(NSString *)saverName
325 isPreview:(BOOL)isPreview
328 initial_bounds = frame.size;
329 rot_current_size = frame.size; // needs to be early, because
330 rot_from = rot_current_size; // [self setFrame] is called by
331 rot_to = rot_current_size; // [super initWithFrame].
335 if (! (self = [super initWithFrame:frame isPreview:isPreview]))
338 xsft = [self findFunctionTable: saverName];
347 [self setMultipleTouchEnabled:YES];
348 orientation = UIDeviceOrientationUnknown;
349 [self didRotate:nil];
350 # endif // USE_IPHONE
354 xsft->setup_cb (xsft, xsft->setup_arg);
356 /* The plist files for these preferences show up in
357 $HOME/Library/Preferences/ByHost/ in a file named like
358 "org.jwz.xscreensaver.<SAVERNAME>.<NUMBERS>.plist"
360 NSString *name = [NSString stringWithCString:xsft->progclass
361 encoding:NSISOLatin1StringEncoding];
362 name = [@"org.jwz.xscreensaver." stringByAppendingString:name];
363 [self setResourcesEnv:name];
366 XrmOptionDescRec *opts = 0;
367 const char **defs = 0;
368 add_default_options (xsft->options, xsft->defaults, &opts, &defs);
369 prefsReader = [[PrefsReader alloc]
370 initWithName:name xrmKeys:opts defaults:defs];
372 // free (opts); // bah, we need these! #### leak!
373 xsft->options = opts;
375 progname = progclass = xsft->progclass;
379 # ifdef USE_BACKBUFFER
380 [self createBackbuffer];
385 // So we can tell when we're docked.
386 [UIDevice currentDevice].batteryMonitoringEnabled = YES;
387 # endif // USE_IPHONE
394 # if !defined(USE_IPHONE) && defined(USE_CALAYER)
395 [self setLayer: [CALayer layer]];
396 self.layer.delegate = self;
397 self.layer.opaque = YES;
398 [self setWantsLayer: YES];
399 # endif // !USE_IPHONE && USE_CALAYER
403 - (id) initWithFrame:(NSRect)frame isPreview:(BOOL)p
405 return [self initWithFrame:frame saverName:0 isPreview:p];
411 NSAssert(![self isAnimating], @"still animating");
412 NSAssert(!xdata, @"xdata not yet freed");
414 jwxyz_free_display (xdpy);
416 # ifdef USE_BACKBUFFER
418 CGContextRelease (backbuffer);
421 CGColorSpaceRelease (colorspace);
425 CGContextRelease (window_ctx);
426 # endif // !USE_CALAYER
428 # endif // USE_BACKBUFFER
430 [prefsReader release];
438 - (PrefsReader *) prefsReader
445 - (void) lockFocus { }
446 - (void) unlockFocus { }
452 /* A few seconds after the saver launches, we store the "wasRunning"
453 preference. This is so that if the saver is crashing at startup,
454 we don't launch it again next time, getting stuck in a crash loop.
456 - (void) allSystemsGo: (NSTimer *) timer
458 NSAssert (timer == crash_timer, @"crash timer screwed up");
461 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
462 [prefs setBool:YES forKey:@"wasRunning"];
468 - (void) startAnimation
470 NSAssert(![self isAnimating], @"already animating");
471 NSAssert(!initted_p && !xdata, @"already initialized");
472 [super startAnimation];
473 /* We can't draw on the window from this method, so we actually do the
474 initialization of the screen saver (xsft->init_cb) in the first call
475 to animateOneFrame() instead.
480 [crash_timer invalidate];
482 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
483 [prefs removeObjectForKey:@"wasRunning"];
486 crash_timer = [NSTimer scheduledTimerWithTimeInterval: 5
488 selector:@selector(allSystemsGo:)
492 # endif // USE_IPHONE
494 // Never automatically turn the screen off if we are docked,
495 // and an animation is running.
498 [UIApplication sharedApplication].idleTimerDisabled =
499 ([UIDevice currentDevice].batteryState != UIDeviceBatteryStateUnplugged);
504 - (void)stopAnimation
506 NSAssert([self isAnimating], @"not animating");
510 [self lockFocus]; // in case something tries to draw from here
511 [self prepareContext];
513 /* I considered just not even calling the free callback at all...
514 But webcollage-cocoa needs it, to kill the inferior webcollage
515 processes (since the screen saver framework never generates a
516 SIGPIPE for them...) Instead, I turned off the free call in
517 xlockmore.c, which is where all of the bogus calls are anyway.
519 xsft->free_cb (xdpy, xwindow, xdata);
522 // setup_p = NO; // #### wait, do we need this?
529 [crash_timer invalidate];
531 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
532 [prefs removeObjectForKey:@"wasRunning"];
534 # endif // USE_IPHONE
536 [super stopAnimation];
538 // When an animation is no longer running (e.g., looking at the list)
539 // then it's ok to power off the screen when docked.
542 [UIApplication sharedApplication].idleTimerDisabled = NO;
547 /* Hook for the XScreenSaverGLView subclass
549 - (void) prepareContext
553 /* Hook for the XScreenSaverGLView subclass
555 - (void) resizeContext
561 screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
563 fps_compute (fpst, 0, -1);
570 /* On iPhones with Retina displays, we can draw the savers in "real"
571 pixels, and that works great. The 320x480 "point" screen is really
572 a 640x960 *pixel* screen. However, Retina iPads have 768x1024
573 point screens which are 1536x2048 pixels, and apparently that's
574 enough pixels that copying those bits to the screen is slow. Like,
575 drops us from 15fps to 7fps. So, on Retina iPads, we don't draw in
576 real pixels. This will probably make the savers look better
577 anyway, since that's a higher resolution than most desktop monitors
578 have even today. (This is only true for X11 programs, not GL
579 programs. Those are fine at full rez.)
581 This method is overridden in XScreenSaverGLView, since this kludge
582 isn't necessary for GL programs, being resolution independent by
585 - (CGFloat) hackedContentScaleFactor
587 GLfloat s = [self contentScaleFactor];
588 if (initial_bounds.width >= 1024 ||
589 initial_bounds.height >= 1024)
595 static GLfloat _global_rot_current_angle_kludge;
597 double current_device_rotation (void)
599 return -_global_rot_current_angle_kludge;
603 - (void) hackRotation
605 if (rotation_ratio >= 0) { // in the midst of a rotation animation
607 # define CLAMP180(N) while (N < 0) N += 360; while (N > 180) N -= 360
608 GLfloat f = angle_from;
609 GLfloat t = angle_to;
612 GLfloat dist = -(t-f);
615 // Intermediate angle.
616 rot_current_angle = f - rotation_ratio * dist;
618 // Intermediate frame size.
619 rot_current_size.width = rot_from.width +
620 rotation_ratio * (rot_to.width - rot_from.width);
621 rot_current_size.height = rot_from.height +
622 rotation_ratio * (rot_to.height - rot_from.height);
624 // Tick animation. Complete rotation in 1/6th sec.
625 double now = double_time();
626 double duration = 1/6.0;
627 rotation_ratio = 1 - ((rot_start_time + duration - now) / duration);
629 if (rotation_ratio > 1) { // Done animating.
630 orientation = new_orientation;
631 rot_current_angle = angle_to;
632 rot_current_size = rot_to;
635 // Check orientation again in case we rotated again while rotating:
636 // this is a no-op if nothing has changed.
637 [self didRotate:nil];
639 } else { // Not animating a rotation.
640 rot_current_angle = angle_to;
641 rot_current_size = rot_to;
644 CLAMP180(rot_current_angle);
645 _global_rot_current_angle_kludge = rot_current_angle;
649 double s = [self hackedContentScaleFactor];
650 if (!ignore_rotation_p &&
651 /* rotation_ratio && */
652 ((int) backbuffer_size.width != (int) (s * rot_current_size.width) ||
653 (int) backbuffer_size.height != (int) (s * rot_current_size.height)))
658 - (void)alertView:(UIAlertView *)av clickedButtonAtIndex:(NSInteger)i
660 if (i == 0) exit (-1); // Cancel
661 [self stopAndClose:NO]; // Keep going
664 - (void) handleException: (NSException *)e
666 NSLog (@"Caught exception: %@", e);
667 [[[UIAlertView alloc] initWithTitle:
668 [NSString stringWithFormat: @"%s crashed!",
671 [NSString stringWithFormat:
672 @"The error message was:"
674 "If it keeps crashing, try "
675 "resetting its options.",
678 cancelButtonTitle: @"Exit"
679 otherButtonTitles: @"Keep going", nil]
681 [self stopAnimation];
687 #ifdef USE_BACKBUFFER
689 /* Create a bitmap context into which we render everything.
690 If the desired size has changed, re-created it.
692 - (void) createBackbuffer
695 double s = [self hackedContentScaleFactor];
696 CGSize rotsize = ignore_rotation_p ? initial_bounds : rot_current_size;
697 int new_w = s * rotsize.width;
698 int new_h = s * rotsize.height;
700 int new_w = [self bounds].size.width;
701 int new_h = [self bounds].size.height;
704 // Colorspaces and CGContexts only happen with non-GL hacks.
706 CGColorSpaceRelease (colorspace);
709 CGContextRelease (window_ctx);
712 NSWindow *window = [self window];
714 if (window && xdpy) {
718 // TODO: This was borrowed from jwxyz_window_resized, and should
719 // probably be refactored.
721 // Figure out which screen the window is currently on.
722 CGDirectDisplayID cgdpy = 0;
726 // TODO: XTranslateCoordinates is returning (0,1200) on my system.
728 // In any case, those weren't valid coordinates for CGGetDisplaysWithPoint.
729 // XTranslateCoordinates (xdpy, xwindow, NULL, 0, 0, &wx, &wy, NULL);
734 p0 = [window convertBaseToScreen:p0];
735 CGPoint p = {p0.x, p0.y};
737 CGGetDisplaysWithPoint (p, 1, &cgdpy, &n);
738 NSAssert (cgdpy, @"unable to find CGDisplay");
742 // Figure out this screen's colorspace, and use that for every CGImage.
744 CMProfileRef profile = 0;
746 // CMGetProfileByAVID is deprecated as of OS X 10.6, but there's no
747 // documented replacement as of OS X 10.9.
748 // http://lists.apple.com/archives/colorsync-dev/2012/Nov/msg00001.html
749 CMGetProfileByAVID ((CMDisplayIDType) cgdpy, &profile);
750 NSAssert (profile, @"unable to find colorspace profile");
751 colorspace = CGColorSpaceCreateWithPlatformColorSpace (profile);
752 NSAssert (colorspace, @"unable to find colorspace");
754 # else // USE_CALAYER
755 // Was apparently faster until 10.9.
756 colorspace = CGColorSpaceCreateDeviceRGB ();
757 # endif // USE_CALAYER
760 window_ctx = [[window graphicsContext] graphicsPort];
761 CGContextRetain (window_ctx);
762 # endif // !USE_CALAYER
768 # endif // !USE_CALAYER
769 colorspace = CGColorSpaceCreateDeviceRGB();
773 backbuffer_size.width == new_w &&
774 backbuffer_size.height == new_h)
777 CGSize osize = backbuffer_size;
778 CGContextRef ob = backbuffer;
780 backbuffer_size.width = new_w;
781 backbuffer_size.height = new_h;
783 backbuffer = CGBitmapContextCreate (NULL,
784 backbuffer_size.width,
785 backbuffer_size.height,
787 backbuffer_size.width * 4,
789 // kCGImageAlphaPremultipliedLast
790 (kCGImageAlphaNoneSkipFirst |
791 kCGBitmapByteOrder32Host)
793 NSAssert (backbuffer, @"unable to allocate back buffer");
797 r.origin.x = r.origin.y = 0;
798 r.size = backbuffer_size;
799 CGContextSetGrayFillColor (backbuffer, 0, 1);
800 CGContextFillRect (backbuffer, r);
803 // Restore old bits, as much as possible, to the X11 upper left origin.
806 rect.origin.y = (backbuffer_size.height - osize.height);
808 CGImageRef img = CGBitmapContextCreateImage (ob);
809 CGContextDrawImage (backbuffer, rect, img);
810 CGImageRelease (img);
811 CGContextRelease (ob);
815 #endif // USE_BACKBUFFER
818 /* Inform X11 that the size of our window has changed.
822 if (!xwindow) return; // early
824 # ifdef USE_BACKBUFFER
825 [self createBackbuffer];
826 jwxyz_window_resized (xdpy, xwindow,
828 backbuffer_size.width, backbuffer_size.height,
830 # else // !USE_BACKBUFFER
831 NSRect r = [self frame]; // ignoring rotation is closer
832 r.size = [self bounds].size; // to what XGetGeometry expects.
833 jwxyz_window_resized (xdpy, xwindow,
834 r.origin.x, r.origin.y,
835 r.size.width, r.size.height,
837 # endif // !USE_BACKBUFFER
839 // Next time render_x11 is called, run the saver's reshape_cb.
849 if (orientation == UIDeviceOrientationUnknown)
850 [self didRotate:nil];
857 # ifdef USE_BACKBUFFER
858 NSAssert (backbuffer, @"no back buffer");
859 xdpy = jwxyz_make_display (self, backbuffer);
861 xdpy = jwxyz_make_display (self, 0);
863 xwindow = XRootWindow (xdpy, 0);
866 /* Some X11 hacks (fluidballs) want to ignore all rotation events. */
868 get_boolean_resource (xdpy, "ignoreRotation", "IgnoreRotation");
869 # endif // USE_IPHONE
877 xsft->setup_cb (xsft, xsft->setup_arg);
881 NSAssert(!xdata, @"xdata already initialized");
887 XSetWindowBackground (xdpy, xwindow,
888 get_pixel_resource (xdpy, 0,
889 "background", "Background"));
890 XClearWindow (xdpy, xwindow);
893 [[self window] setAcceptsMouseMovedEvents:YES];
896 /* In MacOS 10.5, this enables "QuartzGL", meaning that the Quartz
897 drawing primitives will run on the GPU instead of the CPU.
898 It seems like it might make things worse rather than better,
899 though... Plus it makes us binary-incompatible with 10.4.
901 # if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5
902 [[self window] setPreferredBackingLocation:
903 NSWindowBackingLocationVideoMemory];
907 /* Kludge: even though the init_cb functions are declared to take 2 args,
908 actually call them with 3, for the benefit of xlockmore_init() and
911 void *(*init_cb) (Display *, Window, void *) =
912 (void *(*) (Display *, Window, void *)) xsft->init_cb;
914 xdata = init_cb (xdpy, xwindow, xsft->setup_arg);
916 if (get_boolean_resource (xdpy, "doFPS", "DoFPS")) {
917 fpst = fps_init (xdpy, xwindow);
918 if (! xsft->fps_cb) xsft->fps_cb = screenhack_do_fps;
923 /* I don't understand why we have to do this *every frame*, but we do,
924 or else the cursor comes back on.
927 if (![self isPreview])
928 [NSCursor setHiddenUntilMouseMoves:YES];
934 /* This is just a guess, but the -fps code wants to know how long
935 we were sleeping between frames.
937 long usecs = 1000000 * [self animationTimeInterval];
938 usecs -= 200; // caller apparently sleeps for slightly less sometimes...
939 if (usecs < 0) usecs = 0;
940 fps_slept (fpst, usecs);
944 /* It turns out that [ScreenSaverView setAnimationTimeInterval] does nothing.
945 This is bad, because some of the screen hacks want to delay for long
946 periods (like 5 seconds or a minute!) between frames, and running them
947 all at 60 FPS is no good.
949 So, we don't use setAnimationTimeInterval, and just let the framework call
950 us whenever. But, we only invoke the screen hack's "draw frame" method
951 when enough time has expired.
953 This means two extra calls to gettimeofday() per frame. For fast-cycling
954 screen savers, that might actually slow them down. Oh well.
956 #### Also, we do not run the draw callback faster than the system's
957 animationTimeInterval, so if any savers are pickier about timing
958 than that, this may slow them down too much. If that's a problem,
959 then we could call draw_cb in a loop here (with usleep) until the
960 next call would put us past animationTimeInterval... But a better
961 approach would probably be to just change the saver to not do that.
964 gettimeofday (&tv, 0);
965 double now = tv.tv_sec + (tv.tv_usec / 1000000.0);
966 if (now < next_frame_time) return;
968 [self prepareContext];
971 // We do this here instead of in setFrame so that all the
972 // Xlib drawing takes place under the animation timer.
973 [self resizeContext];
975 # ifndef USE_BACKBUFFER
977 # else // USE_BACKBUFFER
980 r.size.width = backbuffer_size.width;
981 r.size.height = backbuffer_size.height;
982 # endif // USE_BACKBUFFER
984 xsft->reshape_cb (xdpy, xwindow, xdata, r.size.width, r.size.height);
988 // Run any XtAppAddInput callbacks now.
989 // (Note that XtAppAddTimeOut callbacks have already been run by
990 // the Cocoa event loop.)
992 jwxyz_sources_run (display_sources_data (xdpy));
998 NSDisableScreenUpdates();
1000 unsigned long delay = xsft->draw_cb (xdpy, xwindow, xdata);
1001 if (fpst) xsft->fps_cb (xdpy, xwindow, fpst, xdata);
1003 NSEnableScreenUpdates();
1006 gettimeofday (&tv, 0);
1007 now = tv.tv_sec + (tv.tv_usec / 1000000.0);
1008 next_frame_time = now + (delay / 1000000.0);
1010 # ifdef USE_IPHONE // Allow savers on the iPhone to run full-tilt.
1011 if (delay < [self animationTimeInterval])
1012 [self setAnimationTimeInterval:(delay / 1000000.0)];
1015 # ifdef DO_GC_HACKERY
1016 /* Current theory is that the 10.6 garbage collector sucks in the
1019 It only does a collection when a threshold of outstanding
1020 collectable allocations has been surpassed. However, CoreGraphics
1021 creates lots of small collectable allocations that contain pointers
1022 to very large non-collectable allocations: a small CG object that's
1023 collectable referencing large malloc'd allocations (non-collectable)
1024 containing bitmap data. So the large allocation doesn't get freed
1025 until GC collects the small allocation, which triggers its finalizer
1026 to run which frees the large allocation. So GC is deciding that it
1027 doesn't really need to run, even though the process has gotten
1028 enormous. GC eventually runs once pageouts have happened, but by
1029 then it's too late, and the machine's resident set has been
1032 So, we force an exhaustive garbage collection in this process
1033 approximately every 5 seconds whether the system thinks it needs
1037 static int tick = 0;
1038 if (++tick > 5*30) {
1040 objc_collect (OBJC_EXHAUSTIVE_COLLECTION);
1043 # endif // DO_GC_HACKERY
1047 @catch (NSException *e) {
1048 [self handleException: e];
1050 # endif // USE_IPHONE
1054 /* drawRect always does nothing, and animateOneFrame renders bits to the
1055 screen. This is (now) true of both X11 and GL on both MacOS and iOS.
1058 - (void)drawRect:(NSRect)rect
1060 if (xwindow) // clear to the X window's bg color, not necessarily black.
1061 XClearWindow (xdpy, xwindow);
1063 [super drawRect:rect]; // early: black.
1067 #ifndef USE_BACKBUFFER
1069 - (void) animateOneFrame
1072 jwxyz_flush_context(xdpy);
1075 #else // USE_BACKBUFFER
1077 - (void) animateOneFrame
1079 // Render X11 into the backing store bitmap...
1081 NSAssert (backbuffer, @"no back buffer");
1084 UIGraphicsPushContext (backbuffer);
1090 UIGraphicsPopContext();
1094 // Then compute the transformations for rotation.
1095 double hs = [self hackedContentScaleFactor];
1096 double s = [self contentScaleFactor];
1098 // The rotation origin for layer.affineTransform is in the center already.
1099 CGAffineTransform t = ignore_rotation_p ?
1100 CGAffineTransformIdentity :
1101 CGAffineTransformMakeRotation (rot_current_angle / (180.0 / M_PI));
1104 self.layer.affineTransform = CGAffineTransformScale(t, f, f);
1107 bounds.origin.x = 0;
1108 bounds.origin.y = 0;
1109 bounds.size.width = backbuffer_size.width / s;
1110 bounds.size.height = backbuffer_size.height / s;
1111 self.layer.bounds = bounds;
1112 # endif // USE_IPHONE
1115 [self.layer setNeedsDisplay];
1116 # else // !USE_CALAYER
1118 w = CGBitmapContextGetWidth (backbuffer),
1119 h = CGBitmapContextGetHeight (backbuffer);
1121 size_t bpl = CGBitmapContextGetBytesPerRow (backbuffer);
1122 CGDataProviderRef prov = CGDataProviderCreateWithData (NULL,
1123 CGBitmapContextGetData(backbuffer),
1128 CGImageRef img = CGImageCreate (w, h,
1130 CGBitmapContextGetBytesPerRow(backbuffer),
1132 CGBitmapContextGetBitmapInfo(backbuffer),
1134 kCGRenderingIntentDefault);
1136 CGDataProviderRelease (prov);
1141 rect.size = backbuffer_size;
1142 CGContextDrawImage (window_ctx, rect, img);
1144 CGImageRelease (img);
1146 CGContextFlush (window_ctx);
1147 # endif // !USE_CALAYER
1152 - (void) drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
1154 // This "isn't safe" if NULL is passed to CGBitmapCreateContext before iOS 4.
1155 char *dest_data = (char *)CGBitmapContextGetData (ctx);
1157 // The CGContext here is normally upside-down on iOS.
1159 CGBitmapContextGetBitmapInfo (ctx) ==
1160 (kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Host)
1162 && CGContextGetCTM (ctx).d < 0
1163 # endif // USE_IPHONE
1166 size_t dest_height = CGBitmapContextGetHeight (ctx);
1167 size_t dest_bpr = CGBitmapContextGetBytesPerRow (ctx);
1168 size_t src_height = CGBitmapContextGetHeight (backbuffer);
1169 size_t src_bpr = CGBitmapContextGetBytesPerRow (backbuffer);
1170 char *src_data = (char *)CGBitmapContextGetData (backbuffer);
1172 size_t height = src_height < dest_height ? src_height : dest_height;
1174 if (src_bpr == dest_bpr) {
1175 // iPad 1: 4.0 ms, iPad 2: 6.7 ms
1176 memcpy (dest_data, src_data, src_bpr * height);
1178 // iPad 1: 4.6 ms, iPad 2: 7.2 ms
1179 size_t bpr = src_bpr < dest_bpr ? src_bpr : dest_bpr;
1181 memcpy (dest_data, src_data, bpr);
1183 src_data += src_bpr;
1184 dest_data += dest_bpr;
1189 // iPad 1: 9.6 ms, iPad 2: 12.1 ms
1192 CGContextScaleCTM (ctx, 1, -1);
1193 CGFloat s = [self contentScaleFactor];
1194 CGFloat hs = [self hackedContentScaleFactor];
1195 CGContextTranslateCTM (ctx, 0, -backbuffer_size.height * hs / s);
1196 # endif // USE_IPHONE
1198 CGImageRef img = CGBitmapContextCreateImage (backbuffer);
1199 CGContextDrawImage (ctx, self.layer.bounds, img);
1200 CGImageRelease (img);
1203 # endif // USE_CALAYER
1205 #endif // USE_BACKBUFFER
1209 - (void) setFrame:(NSRect) newRect
1211 [super setFrame:newRect];
1213 if (xwindow) // inform Xlib that the window has changed now.
1218 # ifndef USE_IPHONE // Doesn't exist on iOS
1219 - (void) setFrameSize:(NSSize) newSize
1221 [super setFrameSize:newSize];
1225 # endif // !USE_IPHONE
1228 +(BOOL) performGammaFade
1233 - (BOOL) hasConfigureSheet
1238 + (NSString *) decompressXML: (NSData *)data
1240 if (! data) return 0;
1241 BOOL compressed_p = !!strncmp ((const char *) data.bytes, "<?xml", 5);
1243 // If it's not already XML, decompress it.
1244 NSAssert (compressed_p, @"xml isn't compressed");
1246 NSMutableData *data2 = 0;
1249 memset (&zs, 0, sizeof(zs));
1250 ret = inflateInit2 (&zs, 16 + MAX_WBITS);
1252 UInt32 usize = * (UInt32 *) (data.bytes + data.length - 4);
1253 data2 = [NSMutableData dataWithLength: usize];
1254 zs.next_in = (Bytef *) data.bytes;
1255 zs.avail_in = data.length;
1256 zs.next_out = (Bytef *) data2.bytes;
1257 zs.avail_out = data2.length;
1258 ret = inflate (&zs, Z_FINISH);
1261 if (ret == Z_OK || ret == Z_STREAM_END)
1264 NSAssert2 (0, @"gunzip error: %d: %s",
1265 ret, (zs.msg ? zs.msg : "<null>"));
1268 return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
1273 - (NSWindow *) configureSheet
1275 - (UIViewController *) configureView
1278 NSBundle *bundle = [NSBundle bundleForClass:[self class]];
1279 NSString *file = [NSString stringWithCString:xsft->progclass
1280 encoding:NSISOLatin1StringEncoding];
1281 file = [file lowercaseString];
1282 NSString *path = [bundle pathForResource:file ofType:@"xml"];
1284 NSLog (@"%@.xml does not exist in the application bundle: %@/",
1285 file, [bundle resourcePath]);
1290 UIViewController *sheet;
1291 # else // !USE_IPHONE
1293 # endif // !USE_IPHONE
1295 NSData *xmld = [NSData dataWithContentsOfFile:path];
1296 NSString *xml = [[self class] decompressXML: xmld];
1297 sheet = [[XScreenSaverConfigSheet alloc]
1298 initWithXML:[xml dataUsingEncoding:NSUTF8StringEncoding]
1299 options:xsft->options
1300 controller:[prefsReader userDefaultsController]
1301 defaults:[prefsReader defaultOptions]];
1303 // #### am I expected to retain this, or not? wtf.
1304 // I thought not, but if I don't do this, we (sometimes) crash.
1305 // #### Analyze says "potential leak of an object stored into sheet"
1312 - (NSUserDefaultsController *) userDefaultsController
1314 return [prefsReader userDefaultsController];
1318 /* Announce our willingness to accept keyboard input.
1320 - (BOOL)acceptsFirstResponder
1328 /* Convert an NSEvent into an XEvent, and pass it along.
1329 Returns YES if it was handled.
1331 - (BOOL) doEvent: (NSEvent *) e
1334 if (![self isPreview] || // no event handling if actually screen-saving!
1335 ![self isAnimating] ||
1340 memset (&xe, 0, sizeof(xe));
1344 int flags = [e modifierFlags];
1345 if (flags & NSAlphaShiftKeyMask) state |= LockMask;
1346 if (flags & NSShiftKeyMask) state |= ShiftMask;
1347 if (flags & NSControlKeyMask) state |= ControlMask;
1348 if (flags & NSAlternateKeyMask) state |= Mod1Mask;
1349 if (flags & NSCommandKeyMask) state |= Mod2Mask;
1351 NSPoint p = [[[e window] contentView] convertPoint:[e locationInWindow]
1354 double s = [self hackedContentScaleFactor];
1359 int y = s * ([self bounds].size.height - p.y);
1361 xe.xany.type = type;
1367 xe.xbutton.state = state;
1368 if ([e type] == NSScrollWheel)
1369 xe.xbutton.button = ([e deltaY] > 0 ? Button4 :
1370 [e deltaY] < 0 ? Button5 :
1371 [e deltaX] > 0 ? Button6 :
1372 [e deltaX] < 0 ? Button7 :
1375 xe.xbutton.button = [e buttonNumber] + 1;
1380 xe.xmotion.state = state;
1385 NSString *ns = (([e type] == NSFlagsChanged) ? 0 :
1386 [e charactersIgnoringModifiers]);
1389 if (!ns || [ns length] == 0) // dead key
1391 // Cocoa hides the difference between left and right keys.
1392 // Also we only get KeyPress events for these, no KeyRelease
1393 // (unless we hack the mod state manually. Bleh.)
1395 if (flags & NSAlphaShiftKeyMask) k = XK_Caps_Lock;
1396 else if (flags & NSShiftKeyMask) k = XK_Shift_L;
1397 else if (flags & NSControlKeyMask) k = XK_Control_L;
1398 else if (flags & NSAlternateKeyMask) k = XK_Alt_L;
1399 else if (flags & NSCommandKeyMask) k = XK_Meta_L;
1401 else if ([ns length] == 1) // real key
1403 switch ([ns characterAtIndex:0]) {
1404 case NSLeftArrowFunctionKey: k = XK_Left; break;
1405 case NSRightArrowFunctionKey: k = XK_Right; break;
1406 case NSUpArrowFunctionKey: k = XK_Up; break;
1407 case NSDownArrowFunctionKey: k = XK_Down; break;
1408 case NSPageUpFunctionKey: k = XK_Page_Up; break;
1409 case NSPageDownFunctionKey: k = XK_Page_Down; break;
1410 case NSHomeFunctionKey: k = XK_Home; break;
1411 case NSPrevFunctionKey: k = XK_Prior; break;
1412 case NSNextFunctionKey: k = XK_Next; break;
1413 case NSBeginFunctionKey: k = XK_Begin; break;
1414 case NSEndFunctionKey: k = XK_End; break;
1418 [ns cStringUsingEncoding:NSISOLatin1StringEncoding];
1419 k = (s && *s ? *s : 0);
1425 if (! k) return YES; // E.g., "KeyRelease XK_Shift_L"
1427 xe.xkey.keycode = k;
1428 xe.xkey.state = state;
1432 NSAssert1 (0, @"unknown X11 event type: %d", type);
1437 [self prepareContext];
1438 BOOL result = xsft->event_cb (xdpy, xwindow, xdata, &xe);
1444 - (void) mouseDown: (NSEvent *) e
1446 if (! [self doEvent:e type:ButtonPress])
1447 [super mouseDown:e];
1450 - (void) mouseUp: (NSEvent *) e
1452 if (! [self doEvent:e type:ButtonRelease])
1456 - (void) otherMouseDown: (NSEvent *) e
1458 if (! [self doEvent:e type:ButtonPress])
1459 [super otherMouseDown:e];
1462 - (void) otherMouseUp: (NSEvent *) e
1464 if (! [self doEvent:e type:ButtonRelease])
1465 [super otherMouseUp:e];
1468 - (void) mouseMoved: (NSEvent *) e
1470 if (! [self doEvent:e type:MotionNotify])
1471 [super mouseMoved:e];
1474 - (void) mouseDragged: (NSEvent *) e
1476 if (! [self doEvent:e type:MotionNotify])
1477 [super mouseDragged:e];
1480 - (void) otherMouseDragged: (NSEvent *) e
1482 if (! [self doEvent:e type:MotionNotify])
1483 [super otherMouseDragged:e];
1486 - (void) scrollWheel: (NSEvent *) e
1488 if (! [self doEvent:e type:ButtonPress])
1489 [super scrollWheel:e];
1492 - (void) keyDown: (NSEvent *) e
1494 if (! [self doEvent:e type:KeyPress])
1498 - (void) keyUp: (NSEvent *) e
1500 if (! [self doEvent:e type:KeyRelease])
1504 - (void) flagsChanged: (NSEvent *) e
1506 if (! [self doEvent:e type:KeyPress])
1507 [super flagsChanged:e];
1513 - (void) stopAndClose:(Bool)relaunch_p
1515 if ([self isAnimating])
1516 [self stopAnimation];
1518 /* Need to make the SaverListController be the firstResponder again
1519 so that it can continue to receive its own shake events. I
1520 suppose that this abstraction-breakage means that I'm adding
1521 XScreenSaverView to the UINavigationController wrong...
1523 UIViewController *v = [[self window] rootViewController];
1524 if ([v isKindOfClass: [UINavigationController class]]) {
1525 UINavigationController *n = (UINavigationController *) v;
1526 [[n topViewController] becomeFirstResponder];
1529 UIView *fader = [self superview]; // the "backgroundView" view is our parent
1531 if (relaunch_p) { // Fake a shake on the SaverListController.
1532 // Why is [self window] sometimes null here?
1533 UIWindow *w = [[UIApplication sharedApplication] keyWindow];
1534 UIViewController *v = [w rootViewController];
1535 if ([v isKindOfClass: [UINavigationController class]]) {
1536 UINavigationController *n = (UINavigationController *) v;
1537 [[n topViewController] motionEnded: UIEventSubtypeMotionShake
1540 } else { // Not launching another, animate our return to the list.
1541 [UIView animateWithDuration: 0.5
1542 animations:^{ fader.alpha = 0.0; }
1543 completion:^(BOOL finished) {
1544 [fader removeFromSuperview];
1551 /* Called after the device's orientation has changed.
1553 Note: we could include a subclass of UIViewController which
1554 contains a shouldAutorotateToInterfaceOrientation method that
1555 returns YES, in which case Core Animation would auto-rotate our
1556 View for us in response to rotation events... but, that interacts
1557 badly with the EAGLContext -- if you introduce Core Animation into
1558 the path, the OpenGL pipeline probably falls back on software
1559 rendering and performance goes to hell. Also, the scaling and
1560 rotation that Core Animation does interacts incorrectly with the GL
1563 So, we have to hack the rotation animation manually, in the GL world.
1565 Possibly XScreenSaverView should use Core Animation, and
1566 XScreenSaverGLView should override that.
1568 - (void)didRotate:(NSNotification *)notification
1570 UIDeviceOrientation current = [[UIDevice currentDevice] orientation];
1572 /* If the simulator starts up in the rotated position, sometimes
1573 the UIDevice says we're in Portrait when we're not -- but it
1574 turns out that the UINavigationController knows what's up!
1575 So get it from there.
1577 if (current == UIDeviceOrientationUnknown) {
1578 switch ([[[self window] rootViewController] interfaceOrientation]) {
1579 case UIInterfaceOrientationPortrait:
1580 current = UIDeviceOrientationPortrait;
1582 case UIInterfaceOrientationPortraitUpsideDown:
1583 current = UIDeviceOrientationPortraitUpsideDown;
1585 case UIInterfaceOrientationLandscapeLeft: // It's opposite day
1586 current = UIDeviceOrientationLandscapeRight;
1588 case UIInterfaceOrientationLandscapeRight:
1589 current = UIDeviceOrientationLandscapeLeft;
1596 /* On the iPad (but not iPhone 3GS, or the simulator) sometimes we get
1597 an orientation change event with an unknown orientation. Those seem
1598 to always be immediately followed by another orientation change with
1599 a *real* orientation change, so let's try just ignoring those bogus
1600 ones and hoping that the real one comes in shortly...
1602 if (current == UIDeviceOrientationUnknown)
1605 if (rotation_ratio >= 0) return; // in the midst of rotation animation
1606 if (orientation == current) return; // no change
1608 // When transitioning to FaceUp or FaceDown, pretend there was no change.
1609 if (current == UIDeviceOrientationFaceUp ||
1610 current == UIDeviceOrientationFaceDown)
1613 new_orientation = current; // current animation target
1614 rotation_ratio = 0; // start animating
1615 rot_start_time = double_time();
1617 switch (orientation) {
1618 case UIDeviceOrientationLandscapeLeft: angle_from = 90; break;
1619 case UIDeviceOrientationLandscapeRight: angle_from = 270; break;
1620 case UIDeviceOrientationPortraitUpsideDown: angle_from = 180; break;
1621 default: angle_from = 0; break;
1624 switch (new_orientation) {
1625 case UIDeviceOrientationLandscapeLeft: angle_to = 90; break;
1626 case UIDeviceOrientationLandscapeRight: angle_to = 270; break;
1627 case UIDeviceOrientationPortraitUpsideDown: angle_to = 180; break;
1628 default: angle_to = 0; break;
1631 switch (orientation) {
1632 case UIDeviceOrientationLandscapeRight: // from landscape
1633 case UIDeviceOrientationLandscapeLeft:
1634 rot_from.width = initial_bounds.height;
1635 rot_from.height = initial_bounds.width;
1637 default: // from portrait
1638 rot_from.width = initial_bounds.width;
1639 rot_from.height = initial_bounds.height;
1643 switch (new_orientation) {
1644 case UIDeviceOrientationLandscapeRight: // to landscape
1645 case UIDeviceOrientationLandscapeLeft:
1646 rot_to.width = initial_bounds.height;
1647 rot_to.height = initial_bounds.width;
1649 default: // to portrait
1650 rot_to.width = initial_bounds.width;
1651 rot_to.height = initial_bounds.height;
1656 // If we've done a rotation but the saver hasn't been initialized yet,
1657 // don't bother going through an X11 resize, but just do it now.
1658 rot_start_time = 0; // dawn of time
1659 [self hackRotation];
1664 /* I believe we can't use UIGestureRecognizer for tracking touches
1665 because UIPanGestureRecognizer doesn't give us enough detail in its
1668 Currently we don't handle multi-touches (just the first touch) but
1669 I'm leaving this comment here for future reference:
1671 In the simulator, multi-touch sequences look like this:
1673 touchesBegan [touchA, touchB]
1674 touchesEnd [touchA, touchB]
1676 But on real devices, sometimes you get that, but sometimes you get:
1678 touchesBegan [touchA, touchB]
1684 touchesBegan [touchA]
1685 touchesBegan [touchB]
1689 So the only way to properly detect a "pinch" gesture is to remember
1690 the start-point of each touch as it comes in; and the end-point of
1691 each touch as those come in; and only process the gesture once the
1692 number of touchEnds matches the number of touchBegins.
1695 - (void) rotateMouse:(int)rot x:(int*)x y:(int *)y w:(int)w h:(int)h
1697 // This is a no-op unless contentScaleFactor != hackedContentScaleFactor.
1698 // Currently, this is the iPad Retina only.
1699 CGRect frame = [self bounds]; // Scale.
1700 double s = [self hackedContentScaleFactor];
1701 *x *= (backbuffer_size.width / frame.size.width) / s;
1702 *y *= (backbuffer_size.height / frame.size.height) / s;
1706 #if 0 // AudioToolbox/AudioToolbox.h
1709 // There's no way to play a standard system alert sound!
1710 // We'd have to include our own WAV for that. Eh, fuck it.
1711 AudioServicesPlaySystemSound (kSystemSoundID_Vibrate);
1712 # if TARGET_IPHONE_SIMULATOR
1713 NSLog(@"BEEP"); // The sim doesn't vibrate.
1719 /* We distinguish between taps and drags.
1720 - Drags (down, motion, up) are sent to the saver to handle.
1721 - Single-taps exit the saver.
1722 This means a saver cannot respond to a single-tap. Only a few try to.
1725 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
1727 // If they are trying to pinch, just do nothing.
1728 if ([[event allTouches] count] > 1)
1733 if (xsft->event_cb && xwindow) {
1734 double s = [self hackedContentScaleFactor];
1736 memset (&xe, 0, sizeof(xe));
1738 // #### 'frame' here or 'bounds'?
1739 int w = s * [self frame].size.width;
1740 int h = s * [self frame].size.height;
1741 for (UITouch *touch in touches) {
1742 CGPoint p = [touch locationInView:self];
1743 xe.xany.type = ButtonPress;
1744 xe.xbutton.button = i + 1;
1745 xe.xbutton.button = i + 1;
1746 xe.xbutton.x = s * p.x;
1747 xe.xbutton.y = s * p.y;
1748 [self rotateMouse: rot_current_angle
1749 x: &xe.xbutton.x y: &xe.xbutton.y w: w h: h];
1750 jwxyz_mouse_moved (xdpy, xwindow, xe.xbutton.x, xe.xbutton.y);
1752 // Ignore return code: don't care whether the hack handled it.
1753 xsft->event_cb (xdpy, xwindow, xdata, &xe);
1755 // Remember when/where this was, to determine tap versus drag or hold.
1756 tap_time = double_time();
1760 break; // No pinches: only look at the first touch.
1766 - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
1768 // If they are trying to pinch, just do nothing.
1769 if ([[event allTouches] count] > 1)
1772 if (xsft->event_cb && xwindow) {
1773 double s = [self hackedContentScaleFactor];
1775 memset (&xe, 0, sizeof(xe));
1777 // #### 'frame' here or 'bounds'?
1778 int w = s * [self frame].size.width;
1779 int h = s * [self frame].size.height;
1780 for (UITouch *touch in touches) {
1781 CGPoint p = [touch locationInView:self];
1783 // If the ButtonRelease came less than half a second after ButtonPress,
1784 // and didn't move far, then this was a tap, not a drag or a hold.
1785 // Interpret it as "exit".
1787 double dist = sqrt (((p.x - tap_point.x) * (p.x - tap_point.x)) +
1788 ((p.y - tap_point.y) * (p.y - tap_point.y)));
1789 if (tap_time + 0.5 >= double_time() && dist < 20) {
1790 [self stopAndClose:NO];
1794 xe.xany.type = ButtonRelease;
1795 xe.xbutton.button = i + 1;
1796 xe.xbutton.x = s * p.x;
1797 xe.xbutton.y = s * p.y;
1798 [self rotateMouse: rot_current_angle
1799 x: &xe.xbutton.x y: &xe.xbutton.y w: w h: h];
1800 jwxyz_mouse_moved (xdpy, xwindow, xe.xbutton.x, xe.xbutton.y);
1801 xsft->event_cb (xdpy, xwindow, xdata, &xe);
1803 break; // No pinches: only look at the first touch.
1809 - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
1811 // If they are trying to pinch, just do nothing.
1812 if ([[event allTouches] count] > 1)
1815 if (xsft->event_cb && xwindow) {
1816 double s = [self hackedContentScaleFactor];
1818 memset (&xe, 0, sizeof(xe));
1820 // #### 'frame' here or 'bounds'?
1821 int w = s * [self frame].size.width;
1822 int h = s * [self frame].size.height;
1823 for (UITouch *touch in touches) {
1824 CGPoint p = [touch locationInView:self];
1825 xe.xany.type = MotionNotify;
1826 xe.xmotion.x = s * p.x;
1827 xe.xmotion.y = s * p.y;
1828 [self rotateMouse: rot_current_angle
1829 x: &xe.xbutton.x y: &xe.xbutton.y w: w h: h];
1830 jwxyz_mouse_moved (xdpy, xwindow, xe.xmotion.x, xe.xmotion.y);
1831 xsft->event_cb (xdpy, xwindow, xdata, &xe);
1833 break; // No pinches: only look at the first touch.
1839 /* We need this to respond to "shake" gestures
1841 - (BOOL)canBecomeFirstResponder
1846 - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
1851 - (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event
1855 /* Shake means exit and launch a new saver.
1857 - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
1859 [self stopAndClose:YES];
1863 - (void)setScreenLocked:(BOOL)locked
1865 if (screenLocked == locked) return;
1866 screenLocked = locked;
1868 if ([self isAnimating])
1869 [self stopAnimation];
1871 if (! [self isAnimating])
1872 [self startAnimation];
1877 #endif // USE_IPHONE
1882 /* Utility functions...
1885 static PrefsReader *
1886 get_prefsReader (Display *dpy)
1888 XScreenSaverView *view = jwxyz_window_view (XRootWindow (dpy, 0));
1889 if (!view) return 0;
1890 return [view prefsReader];
1895 get_string_resource (Display *dpy, char *name, char *class)
1897 return [get_prefsReader(dpy) getStringResource:name];
1901 get_boolean_resource (Display *dpy, char *name, char *class)
1903 return [get_prefsReader(dpy) getBooleanResource:name];
1907 get_integer_resource (Display *dpy, char *name, char *class)
1909 return [get_prefsReader(dpy) getIntegerResource:name];
1913 get_float_resource (Display *dpy, char *name, char *class)
1915 return [get_prefsReader(dpy) getFloatResource:name];