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>
19 #import "XScreenSaverView.h"
20 #import "XScreenSaverConfigSheet.h"
21 #import "screenhackI.h"
22 #import "xlockmoreI.h"
23 #import "jwxyz-timers.h"
26 /* Garbage collection only exists if we are being compiled against the
27 10.6 SDK or newer, not if we are building against the 10.4 SDK.
29 #ifndef MAC_OS_X_VERSION_10_6
30 # define MAC_OS_X_VERSION_10_6 1060 /* undefined in 10.4 SDK, grr */
32 #if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6 /* 10.6 SDK */
33 # import <objc/objc-auto.h>
34 # define DO_GC_HACKERY
37 extern struct xscreensaver_function_table *xscreensaver_function_table;
39 /* Global variables used by the screen savers
42 const char *progclass;
48 extern NSDictionary *make_function_table_dict(void); // ios-function-table.m
50 /* Stub definition of the superclass, for iPhone.
52 @implementation ScreenSaverView
54 NSTimeInterval anim_interval;
59 - (id)initWithFrame:(NSRect)frame isPreview:(BOOL)isPreview {
60 self = [super initWithFrame:frame];
62 anim_interval = 1.0/30;
65 - (NSTimeInterval)animationTimeInterval { return anim_interval; }
66 - (void)setAnimationTimeInterval:(NSTimeInterval)i { anim_interval = i; }
67 - (BOOL)hasConfigureSheet { return NO; }
68 - (NSWindow *)configureSheet { return nil; }
69 - (NSView *)configureView { return nil; }
70 - (BOOL)isPreview { return NO; }
71 - (BOOL)isAnimating { return animating_p; }
72 - (void)animateOneFrame { }
74 - (void)startAnimation {
75 if (animating_p) return;
77 anim_timer = [NSTimer scheduledTimerWithTimeInterval: anim_interval
79 selector:@selector(animateOneFrame)
84 - (void)stopAnimation {
86 [anim_timer invalidate];
93 # endif // !USE_IPHONE
97 @interface XScreenSaverView (Private)
98 - (void) stopAndClose:(Bool)relaunch;
101 @implementation XScreenSaverView
103 // Given a lower-cased saver name, returns the function table for it.
104 // If no name, guess the name from the class's bundle name.
106 - (struct xscreensaver_function_table *) findFunctionTable:(NSString *)name
108 NSBundle *nsb = [NSBundle bundleForClass:[self class]];
109 NSAssert1 (nsb, @"no bundle for class %@", [self class]);
111 NSString *path = [nsb bundlePath];
112 CFURLRef url = CFURLCreateWithFileSystemPath(kCFAllocatorDefault,
114 kCFURLPOSIXPathStyle,
116 CFBundleRef cfb = CFBundleCreate (kCFAllocatorDefault, url);
118 NSAssert1 (cfb, @"no CFBundle for \"%@\"", path);
119 // #### Analyze says "Potential leak of an object stored into cfb"
122 name = [[path lastPathComponent] stringByDeletingPathExtension];
124 name = [[name lowercaseString]
125 stringByReplacingOccurrencesOfString:@" "
129 // CFBundleGetDataPointerForName doesn't work in "Archive" builds.
130 // I'm guessing that symbol-stripping is mandatory. Fuck.
131 NSString *table_name = [name stringByAppendingString:
132 @"_xscreensaver_function_table"];
133 void *addr = CFBundleGetDataPointerForName (cfb, (CFStringRef) table_name);
137 NSLog (@"no symbol \"%@\" for \"%@\"", table_name, path);
140 // Remember: any time you add a new saver to the iOS app,
141 // manually run "make ios-function-table.m"!
142 if (! function_tables)
143 function_tables = [make_function_table_dict() retain];
144 NSValue *v = [function_tables objectForKey: name];
145 void *addr = v ? [v pointerValue] : 0;
146 # endif // USE_IPHONE
148 return (struct xscreensaver_function_table *) addr;
152 // Add the "Contents/Resources/" subdirectory of this screen saver's .bundle
153 // to $PATH for the benefit of savers that include helper shell scripts.
155 - (void) setShellPath
157 NSBundle *nsb = [NSBundle bundleForClass:[self class]];
158 NSAssert1 (nsb, @"no bundle for class %@", [self class]);
160 NSString *nsdir = [nsb resourcePath];
161 NSAssert1 (nsdir, @"no resourcePath for class %@", [self class]);
162 const char *dir = [nsdir cStringUsingEncoding:NSUTF8StringEncoding];
163 const char *opath = getenv ("PATH");
164 if (!opath) opath = "/bin"; // $PATH is unset when running under Shark!
165 char *npath = (char *) malloc (strlen (opath) + strlen (dir) + 30);
166 strcpy (npath, "PATH=");
169 strcat (npath, opath);
170 if (putenv (npath)) {
172 NSAssert1 (0, @"putenv \"%s\" failed", npath);
175 /* Don't free (npath) -- MacOS's putenv() does not copy it. */
179 // set an $XSCREENSAVER_CLASSPATH variable so that included shell scripts
180 // (e.g., "xscreensaver-text") know how to look up resources.
182 - (void) setResourcesEnv:(NSString *) name
184 NSBundle *nsb = [NSBundle bundleForClass:[self class]];
185 NSAssert1 (nsb, @"no bundle for class %@", [self class]);
187 const char *s = [name cStringUsingEncoding:NSUTF8StringEncoding];
188 char *env = (char *) malloc (strlen (s) + 40);
189 strcpy (env, "XSCREENSAVER_CLASSPATH=");
193 NSAssert1 (0, @"putenv \"%s\" failed", env);
195 /* Don't free (env) -- MacOS's putenv() does not copy it. */
200 add_default_options (const XrmOptionDescRec *opts,
201 const char * const *defs,
202 XrmOptionDescRec **opts_ret,
203 const char ***defs_ret)
205 /* These aren't "real" command-line options (there are no actual command-line
206 options in the Cocoa version); but this is the somewhat kludgey way that
207 the <xscreensaver-text /> and <xscreensaver-image /> tags in the
208 ../hacks/config/\*.xml files communicate with the preferences database.
210 static const XrmOptionDescRec default_options [] = {
211 { "-text-mode", ".textMode", XrmoptionSepArg, 0 },
212 { "-text-literal", ".textLiteral", XrmoptionSepArg, 0 },
213 { "-text-file", ".textFile", XrmoptionSepArg, 0 },
214 { "-text-url", ".textURL", XrmoptionSepArg, 0 },
215 { "-text-program", ".textProgram", XrmoptionSepArg, 0 },
216 { "-grab-desktop", ".grabDesktopImages", XrmoptionNoArg, "True" },
217 { "-no-grab-desktop", ".grabDesktopImages", XrmoptionNoArg, "False"},
218 { "-choose-random-images", ".chooseRandomImages",XrmoptionNoArg, "True" },
219 { "-no-choose-random-images",".chooseRandomImages",XrmoptionNoArg, "False"},
220 { "-image-directory", ".imageDirectory", XrmoptionSepArg, 0 },
221 { "-fps", ".doFPS", XrmoptionNoArg, "True" },
222 { "-no-fps", ".doFPS", XrmoptionNoArg, "False"},
225 static const char *default_defaults [] = {
227 ".doubleBuffer: True",
228 ".multiSample: False",
236 ".textURL: http://en.wikipedia.org/w/index.php?title=Special:NewPages&feed=rss",
238 ".grabDesktopImages: yes",
240 ".chooseRandomImages: no",
242 ".chooseRandomImages: yes",
244 ".imageDirectory: ~/Pictures",
250 for (i = 0; default_options[i].option; i++)
252 for (i = 0; opts[i].option; i++)
255 XrmOptionDescRec *opts2 = (XrmOptionDescRec *)
256 calloc (count + 1, sizeof (*opts2));
260 while (default_options[j].option) {
261 opts2[i] = default_options[j];
265 while (opts[j].option) {
276 for (i = 0; default_defaults[i]; i++)
278 for (i = 0; defs[i]; i++)
281 const char **defs2 = (const char **) calloc (count + 1, sizeof (*defs2));
285 while (default_defaults[j]) {
286 defs2[i] = default_defaults[j];
300 /* Returns the current time in seconds as a double.
306 # ifdef GETTIMEOFDAY_TWO_ARGS
308 gettimeofday(&now, &tzp);
313 return (now.tv_sec + ((double) now.tv_usec * 0.000001));
318 - (id) initWithFrame:(NSRect)frame
319 saverName:(NSString *)saverName
320 isPreview:(BOOL)isPreview
323 rot_current_size = frame.size; // needs to be early, because
324 rot_from = rot_current_size; // [self setFrame] is called by
325 rot_to = rot_current_size; // [super initWithFrame].
329 if (! (self = [super initWithFrame:frame isPreview:isPreview]))
332 xsft = [self findFunctionTable: saverName];
341 [self setMultipleTouchEnabled:YES];
342 orientation = UIDeviceOrientationUnknown;
343 [self didRotate:nil];
344 # endif // USE_IPHONE
348 xsft->setup_cb (xsft, xsft->setup_arg);
350 /* The plist files for these preferences show up in
351 $HOME/Library/Preferences/ByHost/ in a file named like
352 "org.jwz.xscreensaver.<SAVERNAME>.<NUMBERS>.plist"
354 NSString *name = [NSString stringWithCString:xsft->progclass
355 encoding:NSISOLatin1StringEncoding];
356 name = [@"org.jwz.xscreensaver." stringByAppendingString:name];
357 [self setResourcesEnv:name];
360 XrmOptionDescRec *opts = 0;
361 const char **defs = 0;
362 add_default_options (xsft->options, xsft->defaults, &opts, &defs);
363 prefsReader = [[PrefsReader alloc]
364 initWithName:name xrmKeys:opts defaults:defs];
366 // free (opts); // bah, we need these! #### leak!
367 xsft->options = opts;
369 progname = progclass = xsft->progclass;
373 # ifdef USE_BACKBUFFER
374 [self createBackbuffer];
379 // So we can tell when we're docked.
380 [UIDevice currentDevice].batteryMonitoringEnabled = YES;
381 # endif // USE_IPHONE
389 [self setLayer: [CALayer layer]];
390 [self setWantsLayer: YES];
395 - (id) initWithFrame:(NSRect)frame isPreview:(BOOL)p
397 return [self initWithFrame:frame saverName:0 isPreview:p];
403 NSAssert(![self isAnimating], @"still animating");
404 NSAssert(!xdata, @"xdata not yet freed");
406 jwxyz_free_display (xdpy);
408 # ifdef USE_BACKBUFFER
410 CGContextRelease (backbuffer);
413 [prefsReader release];
421 - (PrefsReader *) prefsReader
428 - (void) lockFocus { }
429 - (void) unlockFocus { }
435 /* A few seconds after the saver launches, we store the "wasRunning"
436 preference. This is so that if the saver is crashing at startup,
437 we don't launch it again next time, getting stuck in a crash loop.
439 - (void) allSystemsGo: (NSTimer *) timer
441 NSAssert (timer == crash_timer, @"crash timer screwed up");
444 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
445 [prefs setBool:YES forKey:@"wasRunning"];
451 - (void) startAnimation
453 NSAssert(![self isAnimating], @"already animating");
454 NSAssert(!initted_p && !xdata, @"already initialized");
455 [super startAnimation];
456 /* We can't draw on the window from this method, so we actually do the
457 initialization of the screen saver (xsft->init_cb) in the first call
458 to animateOneFrame() instead.
463 [crash_timer invalidate];
465 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
466 [prefs removeObjectForKey:@"wasRunning"];
469 crash_timer = [NSTimer scheduledTimerWithTimeInterval: 5
471 selector:@selector(allSystemsGo:)
475 # endif // USE_IPHONE
477 // Never automatically turn the screen off if we are docked,
478 // and an animation is running.
481 [UIApplication sharedApplication].idleTimerDisabled =
482 ([UIDevice currentDevice].batteryState != UIDeviceBatteryStateUnplugged);
487 - (void)stopAnimation
489 NSAssert([self isAnimating], @"not animating");
493 [self lockFocus]; // in case something tries to draw from here
494 [self prepareContext];
496 /* I considered just not even calling the free callback at all...
497 But webcollage-cocoa needs it, to kill the inferior webcollage
498 processes (since the screen saver framework never generates a
499 SIGPIPE for them...) Instead, I turned off the free call in
500 xlockmore.c, which is where all of the bogus calls are anyway.
502 xsft->free_cb (xdpy, xwindow, xdata);
505 // setup_p = NO; // #### wait, do we need this?
512 [crash_timer invalidate];
514 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
515 [prefs removeObjectForKey:@"wasRunning"];
517 # endif // USE_IPHONE
519 [super stopAnimation];
521 // When an animation is no longer running (e.g., looking at the list)
522 // then it's ok to power off the screen when docked.
525 [UIApplication sharedApplication].idleTimerDisabled = NO;
530 /* Hook for the XScreenSaverGLView subclass
532 - (void) prepareContext
536 /* Hook for the XScreenSaverGLView subclass
538 - (void) resizeContext
544 screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
546 fps_compute (fpst, 0, -1);
553 /* On iPhones with Retina displays, we can draw the savers in "real"
554 pixels, and that works great. The 320x480 "point" screen is really
555 a 640x960 *pixel* screen. However, Retina iPads have 768x1024
556 point screens which are 1536x2048 pixels, and apparently that's
557 enough pixels that copying those bits to the screen is slow. Like,
558 drops us from 15fps to 7fps. So, on Retina iPads, we don't draw in
559 real pixels. This will probably make the savers look better
560 anyway, since that's a higher resolution than most desktop monitors
561 have even today. (This is only true for X11 programs, not GL
562 programs. Those are fine at full rez.)
564 This method is overridden in XScreenSaverGLView, since this kludge
565 isn't necessary for GL programs, being resolution independent by
568 - (CGFloat) hackedContentScaleFactor
570 GLfloat s = [self contentScaleFactor];
571 CGRect frame = [self bounds];
572 if (frame.size.width >= 1024 ||
573 frame.size.height >= 1024)
579 static GLfloat _global_rot_current_angle_kludge;
581 double current_device_rotation (void)
583 return -_global_rot_current_angle_kludge;
587 - (void) hackRotation
589 if (rotation_ratio >= 0) { // in the midst of a rotation animation
591 # define CLAMP180(N) while (N < 0) N += 360; while (N > 180) N -= 360
592 GLfloat f = angle_from;
593 GLfloat t = angle_to;
596 GLfloat dist = -(t-f);
599 // Intermediate angle.
600 rot_current_angle = f - rotation_ratio * dist;
602 // Intermediate frame size.
603 rot_current_size.width = rot_from.width +
604 rotation_ratio * (rot_to.width - rot_from.width);
605 rot_current_size.height = rot_from.height +
606 rotation_ratio * (rot_to.height - rot_from.height);
608 // Tick animation. Complete rotation in 1/6th sec.
609 double now = double_time();
610 double duration = 1/6.0;
611 rotation_ratio = 1 - ((rot_start_time + duration - now) / duration);
613 if (rotation_ratio > 1) { // Done animating.
614 orientation = new_orientation;
615 rot_current_angle = angle_to;
616 rot_current_size = rot_to;
619 // Check orientation again in case we rotated again while rotating:
620 // this is a no-op if nothing has changed.
621 [self didRotate:nil];
623 } else { // Not animating a rotation.
624 rot_current_angle = angle_to;
625 rot_current_size = rot_to;
628 CLAMP180(rot_current_angle);
629 _global_rot_current_angle_kludge = rot_current_angle;
633 double s = [self hackedContentScaleFactor];
634 if (!ignore_rotation_p &&
635 /* rotation_ratio && */
636 ((int) backbuffer_size.width != (int) (s * rot_current_size.width) ||
637 (int) backbuffer_size.height != (int) (s * rot_current_size.height)))
642 - (void)alertView:(UIAlertView *)av clickedButtonAtIndex:(NSInteger)i
644 if (i == 0) exit (-1); // Cancel
645 [self stopAndClose:NO]; // Keep going
648 - (void) handleException: (NSException *)e
650 NSLog (@"Caught exception: %@", e);
651 [[[UIAlertView alloc] initWithTitle:
652 [NSString stringWithFormat: @"%s crashed!",
655 [NSString stringWithFormat:
656 @"The error message was:"
658 "If it keeps crashing, try "
659 "resetting its options.",
662 cancelButtonTitle: @"Exit"
663 otherButtonTitles: @"Keep going", nil]
665 [self stopAnimation];
671 #ifdef USE_BACKBUFFER
673 /* Create a bitmap context into which we render everything.
674 If the desired size has changed, re-created it.
676 - (void) createBackbuffer
679 double s = [self hackedContentScaleFactor];
680 int new_w = s * rot_current_size.width;
681 int new_h = s * rot_current_size.height;
683 int new_w = [self bounds].size.width;
684 int new_h = [self bounds].size.height;
688 backbuffer_size.width == new_w &&
689 backbuffer_size.height == new_h)
692 CGSize osize = backbuffer_size;
693 CGContextRef ob = backbuffer;
695 backbuffer_size.width = new_w;
696 backbuffer_size.height = new_h;
698 CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB();
699 backbuffer = CGBitmapContextCreate (NULL,
700 backbuffer_size.width,
701 backbuffer_size.height,
703 backbuffer_size.width * 4,
705 kCGImageAlphaPremultipliedLast);
706 CGColorSpaceRelease (cs);
707 NSAssert (backbuffer, @"unable to allocate back buffer");
711 r.origin.x = r.origin.y = 0;
712 r.size = backbuffer_size;
713 CGContextSetGrayFillColor (backbuffer, 0, 1);
714 CGContextFillRect (backbuffer, r);
717 // Restore old bits, as much as possible, to the X11 upper left origin.
720 rect.origin.y = (backbuffer_size.height - osize.height);
722 CGImageRef img = CGBitmapContextCreateImage (ob);
723 CGContextDrawImage (backbuffer, rect, img);
724 CGImageRelease (img);
725 CGContextRelease (ob);
729 #endif // USE_BACKBUFFER
732 /* Inform X11 that the size of our window has changed.
736 if (!xwindow) return; // early
738 # ifdef USE_BACKBUFFER
739 [self createBackbuffer];
740 jwxyz_window_resized (xdpy, xwindow,
742 backbuffer_size.width, backbuffer_size.height,
744 # else // !USE_BACKBUFFER
745 NSRect r = [self frame]; // ignoring rotation is closer
746 r.size = [self bounds].size; // to what XGetGeometry expects.
747 jwxyz_window_resized (xdpy, xwindow,
748 r.origin.x, r.origin.y,
749 r.size.width, r.size.height,
751 # endif // !USE_BACKBUFFER
753 // Next time render_x11 is called, run the saver's reshape_cb.
763 if (orientation == UIDeviceOrientationUnknown)
764 [self didRotate:nil];
771 # ifdef USE_BACKBUFFER
772 NSAssert (backbuffer, @"no back buffer");
773 xdpy = jwxyz_make_display (self, backbuffer);
775 xdpy = jwxyz_make_display (self, 0);
777 xwindow = XRootWindow (xdpy, 0);
784 xsft->setup_cb (xsft, xsft->setup_arg);
788 NSAssert(!xdata, @"xdata already initialized");
791 /* Some X11 hacks (fluidballs) want to ignore all rotation events. */
793 get_boolean_resource (xdpy, "ignoreRotation", "IgnoreRotation");
794 # endif // USE_IPHONE
800 XSetWindowBackground (xdpy, xwindow,
801 get_pixel_resource (xdpy, 0,
802 "background", "Background"));
803 XClearWindow (xdpy, xwindow);
806 [[self window] setAcceptsMouseMovedEvents:YES];
809 /* In MacOS 10.5, this enables "QuartzGL", meaning that the Quartz
810 drawing primitives will run on the GPU instead of the CPU.
811 It seems like it might make things worse rather than better,
812 though... Plus it makes us binary-incompatible with 10.4.
814 # if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5
815 [[self window] setPreferredBackingLocation:
816 NSWindowBackingLocationVideoMemory];
820 /* Kludge: even though the init_cb functions are declared to take 2 args,
821 actually call them with 3, for the benefit of xlockmore_init() and
824 void *(*init_cb) (Display *, Window, void *) =
825 (void *(*) (Display *, Window, void *)) xsft->init_cb;
827 xdata = init_cb (xdpy, xwindow, xsft->setup_arg);
829 if (get_boolean_resource (xdpy, "doFPS", "DoFPS")) {
830 fpst = fps_init (xdpy, xwindow);
831 if (! xsft->fps_cb) xsft->fps_cb = screenhack_do_fps;
836 /* I don't understand why we have to do this *every frame*, but we do,
837 or else the cursor comes back on.
840 if (![self isPreview])
841 [NSCursor setHiddenUntilMouseMoves:YES];
847 /* This is just a guess, but the -fps code wants to know how long
848 we were sleeping between frames.
850 long usecs = 1000000 * [self animationTimeInterval];
851 usecs -= 200; // caller apparently sleeps for slightly less sometimes...
852 if (usecs < 0) usecs = 0;
853 fps_slept (fpst, usecs);
857 /* It turns out that [ScreenSaverView setAnimationTimeInterval] does nothing.
858 This is bad, because some of the screen hacks want to delay for long
859 periods (like 5 seconds or a minute!) between frames, and running them
860 all at 60 FPS is no good.
862 So, we don't use setAnimationTimeInterval, and just let the framework call
863 us whenever. But, we only invoke the screen hack's "draw frame" method
864 when enough time has expired.
866 This means two extra calls to gettimeofday() per frame. For fast-cycling
867 screen savers, that might actually slow them down. Oh well.
869 #### Also, we do not run the draw callback faster than the system's
870 animationTimeInterval, so if any savers are pickier about timing
871 than that, this may slow them down too much. If that's a problem,
872 then we could call draw_cb in a loop here (with usleep) until the
873 next call would put us past animationTimeInterval... But a better
874 approach would probably be to just change the saver to not do that.
877 gettimeofday (&tv, 0);
878 double now = tv.tv_sec + (tv.tv_usec / 1000000.0);
879 if (now < next_frame_time) return;
881 [self prepareContext];
884 // We do this here instead of in setFrame so that all the
885 // Xlib drawing takes place under the animation timer.
886 [self resizeContext];
888 # ifndef USE_BACKBUFFER
890 # else // USE_BACKBUFFER
893 r.size.width = backbuffer_size.width;
894 r.size.height = backbuffer_size.height;
895 # endif // USE_BACKBUFFER
897 xsft->reshape_cb (xdpy, xwindow, xdata, r.size.width, r.size.height);
901 // Run any XtAppAddInput callbacks now.
902 // (Note that XtAppAddTimeOut callbacks have already been run by
903 // the Cocoa event loop.)
905 jwxyz_sources_run (display_sources_data (xdpy));
911 NSDisableScreenUpdates();
913 unsigned long delay = xsft->draw_cb (xdpy, xwindow, xdata);
914 if (fpst) xsft->fps_cb (xdpy, xwindow, fpst, xdata);
916 NSEnableScreenUpdates();
919 gettimeofday (&tv, 0);
920 now = tv.tv_sec + (tv.tv_usec / 1000000.0);
921 next_frame_time = now + (delay / 1000000.0);
923 # ifdef USE_IPHONE // Allow savers on the iPhone to run full-tilt.
924 if (delay < [self animationTimeInterval])
925 [self setAnimationTimeInterval:(delay / 1000000.0)];
928 # ifdef DO_GC_HACKERY
929 /* Current theory is that the 10.6 garbage collector sucks in the
932 It only does a collection when a threshold of outstanding
933 collectable allocations has been surpassed. However, CoreGraphics
934 creates lots of small collectable allocations that contain pointers
935 to very large non-collectable allocations: a small CG object that's
936 collectable referencing large malloc'd allocations (non-collectable)
937 containing bitmap data. So the large allocation doesn't get freed
938 until GC collects the small allocation, which triggers its finalizer
939 to run which frees the large allocation. So GC is deciding that it
940 doesn't really need to run, even though the process has gotten
941 enormous. GC eventually runs once pageouts have happened, but by
942 then it's too late, and the machine's resident set has been
945 So, we force an exhaustive garbage collection in this process
946 approximately every 5 seconds whether the system thinks it needs
953 objc_collect (OBJC_EXHAUSTIVE_COLLECTION);
956 # endif // DO_GC_HACKERY
960 @catch (NSException *e) {
961 [self handleException: e];
963 # endif // USE_IPHONE
967 /* drawRect always does nothing, and animateOneFrame renders bits to the
968 screen. This is (now) true of both X11 and GL on both MacOS and iOS.
971 - (void)drawRect:(NSRect)rect
973 if (xwindow) // clear to the X window's bg color, not necessarily black.
974 XClearWindow (xdpy, xwindow);
976 [super drawRect:rect]; // early: black.
980 #ifndef USE_BACKBUFFER
982 - (void) animateOneFrame
987 #else // USE_BACKBUFFER
989 - (void) animateOneFrame
991 // Render X11 into the backing store bitmap...
993 NSAssert (backbuffer, @"no back buffer");
996 UIGraphicsPushContext (backbuffer);
1002 UIGraphicsPopContext();
1006 // Then compute the transformations for rotation.
1008 if (!ignore_rotation_p) {
1009 // The rotation origin for layer.affineTransform is in the center already.
1010 CGAffineTransform t =
1011 CGAffineTransformMakeRotation (rot_current_angle / (180.0 / M_PI));
1013 // Correct the aspect ratio.
1014 CGRect frame = [self bounds];
1015 double s = [self hackedContentScaleFactor];
1016 t = CGAffineTransformScale(t,
1017 backbuffer_size.width / (s * frame.size.width),
1018 backbuffer_size.height / (s * frame.size.height));
1019 self.layer.affineTransform = t;
1021 # endif // USE_IPHONE
1023 // Then copy that bitmap to the screen, by just stuffing it into
1024 // the layer. The superclass drawRect method will handle the rest.
1026 CGImageRef img = CGBitmapContextCreateImage (backbuffer);
1027 self.layer.contents = (id)img;
1028 CGImageRelease (img);
1031 #endif // !USE_BACKBUFFER
1035 - (void) setFrame:(NSRect) newRect
1037 [super setFrame:newRect];
1039 if (xwindow) // inform Xlib that the window has changed now.
1044 # ifndef USE_IPHONE // Doesn't exist on iOS
1045 - (void) setFrameSize:(NSSize) newSize
1047 [super setFrameSize:newSize];
1051 # endif // !USE_IPHONE
1054 +(BOOL) performGammaFade
1059 - (BOOL) hasConfigureSheet
1065 - (NSWindow *) configureSheet
1067 - (UIViewController *) configureView
1070 NSBundle *bundle = [NSBundle bundleForClass:[self class]];
1071 NSString *file = [NSString stringWithCString:xsft->progclass
1072 encoding:NSISOLatin1StringEncoding];
1073 file = [file lowercaseString];
1074 NSString *path = [bundle pathForResource:file ofType:@"xml"];
1076 NSLog (@"%@.xml does not exist in the application bundle: %@/",
1077 file, [bundle resourcePath]);
1082 UIViewController *sheet;
1083 # else // !USE_IPHONE
1085 # endif // !USE_IPHONE
1087 sheet = [[XScreenSaverConfigSheet alloc]
1088 initWithXMLFile:path
1089 options:xsft->options
1090 controller:[prefsReader userDefaultsController]
1091 defaults:[prefsReader defaultOptions]];
1093 // #### am I expected to retain this, or not? wtf.
1094 // I thought not, but if I don't do this, we (sometimes) crash.
1095 // #### Analyze says "potential leak of an object stored into sheet"
1102 - (NSUserDefaultsController *) userDefaultsController
1104 return [prefsReader userDefaultsController];
1108 /* Announce our willingness to accept keyboard input.
1110 - (BOOL)acceptsFirstResponder
1118 /* Convert an NSEvent into an XEvent, and pass it along.
1119 Returns YES if it was handled.
1121 - (BOOL) doEvent: (NSEvent *) e
1124 if (![self isPreview] || // no event handling if actually screen-saving!
1125 ![self isAnimating] ||
1130 memset (&xe, 0, sizeof(xe));
1134 int flags = [e modifierFlags];
1135 if (flags & NSAlphaShiftKeyMask) state |= LockMask;
1136 if (flags & NSShiftKeyMask) state |= ShiftMask;
1137 if (flags & NSControlKeyMask) state |= ControlMask;
1138 if (flags & NSAlternateKeyMask) state |= Mod1Mask;
1139 if (flags & NSCommandKeyMask) state |= Mod2Mask;
1141 NSPoint p = [[[e window] contentView] convertPoint:[e locationInWindow]
1144 double s = [self hackedContentScaleFactor];
1149 int y = s * ([self bounds].size.height - p.y);
1151 xe.xany.type = type;
1157 xe.xbutton.state = state;
1158 if ([e type] == NSScrollWheel)
1159 xe.xbutton.button = ([e deltaY] > 0 ? Button4 :
1160 [e deltaY] < 0 ? Button5 :
1161 [e deltaX] > 0 ? Button6 :
1162 [e deltaX] < 0 ? Button7 :
1165 xe.xbutton.button = [e buttonNumber] + 1;
1170 xe.xmotion.state = state;
1175 NSString *ns = (([e type] == NSFlagsChanged) ? 0 :
1176 [e charactersIgnoringModifiers]);
1179 if (!ns || [ns length] == 0) // dead key
1181 // Cocoa hides the difference between left and right keys.
1182 // Also we only get KeyPress events for these, no KeyRelease
1183 // (unless we hack the mod state manually. Bleh.)
1185 if (flags & NSAlphaShiftKeyMask) k = XK_Caps_Lock;
1186 else if (flags & NSShiftKeyMask) k = XK_Shift_L;
1187 else if (flags & NSControlKeyMask) k = XK_Control_L;
1188 else if (flags & NSAlternateKeyMask) k = XK_Alt_L;
1189 else if (flags & NSCommandKeyMask) k = XK_Meta_L;
1191 else if ([ns length] == 1) // real key
1193 switch ([ns characterAtIndex:0]) {
1194 case NSLeftArrowFunctionKey: k = XK_Left; break;
1195 case NSRightArrowFunctionKey: k = XK_Right; break;
1196 case NSUpArrowFunctionKey: k = XK_Up; break;
1197 case NSDownArrowFunctionKey: k = XK_Down; break;
1198 case NSPageUpFunctionKey: k = XK_Page_Up; break;
1199 case NSPageDownFunctionKey: k = XK_Page_Down; break;
1200 case NSHomeFunctionKey: k = XK_Home; break;
1201 case NSPrevFunctionKey: k = XK_Prior; break;
1202 case NSNextFunctionKey: k = XK_Next; break;
1203 case NSBeginFunctionKey: k = XK_Begin; break;
1204 case NSEndFunctionKey: k = XK_End; break;
1208 [ns cStringUsingEncoding:NSISOLatin1StringEncoding];
1209 k = (s && *s ? *s : 0);
1215 if (! k) return YES; // E.g., "KeyRelease XK_Shift_L"
1217 xe.xkey.keycode = k;
1218 xe.xkey.state = state;
1222 NSAssert1 (0, @"unknown X11 event type: %d", type);
1227 [self prepareContext];
1228 BOOL result = xsft->event_cb (xdpy, xwindow, xdata, &xe);
1234 - (void) mouseDown: (NSEvent *) e
1236 if (! [self doEvent:e type:ButtonPress])
1237 [super mouseDown:e];
1240 - (void) mouseUp: (NSEvent *) e
1242 if (! [self doEvent:e type:ButtonRelease])
1246 - (void) otherMouseDown: (NSEvent *) e
1248 if (! [self doEvent:e type:ButtonPress])
1249 [super otherMouseDown:e];
1252 - (void) otherMouseUp: (NSEvent *) e
1254 if (! [self doEvent:e type:ButtonRelease])
1255 [super otherMouseUp:e];
1258 - (void) mouseMoved: (NSEvent *) e
1260 if (! [self doEvent:e type:MotionNotify])
1261 [super mouseMoved:e];
1264 - (void) mouseDragged: (NSEvent *) e
1266 if (! [self doEvent:e type:MotionNotify])
1267 [super mouseDragged:e];
1270 - (void) otherMouseDragged: (NSEvent *) e
1272 if (! [self doEvent:e type:MotionNotify])
1273 [super otherMouseDragged:e];
1276 - (void) scrollWheel: (NSEvent *) e
1278 if (! [self doEvent:e type:ButtonPress])
1279 [super scrollWheel:e];
1282 - (void) keyDown: (NSEvent *) e
1284 if (! [self doEvent:e type:KeyPress])
1288 - (void) keyUp: (NSEvent *) e
1290 if (! [self doEvent:e type:KeyRelease])
1294 - (void) flagsChanged: (NSEvent *) e
1296 if (! [self doEvent:e type:KeyPress])
1297 [super flagsChanged:e];
1303 - (void) stopAndClose:(Bool)relaunch_p
1305 if ([self isAnimating])
1306 [self stopAnimation];
1308 /* Need to make the SaverListController be the firstResponder again
1309 so that it can continue to receive its own shake events. I
1310 suppose that this abstraction-breakage means that I'm adding
1311 XScreenSaverView to the UINavigationController wrong...
1313 UIViewController *v = [[self window] rootViewController];
1314 if ([v isKindOfClass: [UINavigationController class]]) {
1315 UINavigationController *n = (UINavigationController *) v;
1316 [[n topViewController] becomeFirstResponder];
1319 UIView *fader = [self superview]; // the "backgroundView" view is our parent
1321 if (relaunch_p) { // Fake a shake on the SaverListController.
1322 // Why is [self window] sometimes null here?
1323 UIWindow *w = [[UIApplication sharedApplication] keyWindow];
1324 UIViewController *v = [w rootViewController];
1325 if ([v isKindOfClass: [UINavigationController class]]) {
1326 UINavigationController *n = (UINavigationController *) v;
1327 [[n topViewController] motionEnded: UIEventSubtypeMotionShake
1330 } else { // Not launching another, animate our return to the list.
1331 [UIView animateWithDuration: 0.5
1332 animations:^{ fader.alpha = 0.0; }
1333 completion:^(BOOL finished) {
1334 [fader removeFromSuperview];
1341 /* Called after the device's orientation has changed.
1343 Note: we could include a subclass of UIViewController which
1344 contains a shouldAutorotateToInterfaceOrientation method that
1345 returns YES, in which case Core Animation would auto-rotate our
1346 View for us in response to rotation events... but, that interacts
1347 badly with the EAGLContext -- if you introduce Core Animation into
1348 the path, the OpenGL pipeline probably falls back on software
1349 rendering and performance goes to hell. Also, the scaling and
1350 rotation that Core Animation does interacts incorrectly with the GL
1353 So, we have to hack the rotation animation manually, in the GL world.
1355 Possibly XScreenSaverView should use Core Animation, and
1356 XScreenSaverGLView should override that.
1358 - (void)didRotate:(NSNotification *)notification
1360 UIDeviceOrientation current = [[UIDevice currentDevice] orientation];
1362 /* If the simulator starts up in the rotated position, sometimes
1363 the UIDevice says we're in Portrait when we're not -- but it
1364 turns out that the UINavigationController knows what's up!
1365 So get it from there.
1367 if (current == UIDeviceOrientationUnknown) {
1368 switch ([[[self window] rootViewController] interfaceOrientation]) {
1369 case UIInterfaceOrientationPortrait:
1370 current = UIDeviceOrientationPortrait;
1372 case UIInterfaceOrientationPortraitUpsideDown:
1373 current = UIDeviceOrientationPortraitUpsideDown;
1375 case UIInterfaceOrientationLandscapeLeft: // It's opposite day
1376 current = UIDeviceOrientationLandscapeRight;
1378 case UIInterfaceOrientationLandscapeRight:
1379 current = UIDeviceOrientationLandscapeLeft;
1386 /* On the iPad (but not iPhone 3GS, or the simulator) sometimes we get
1387 an orientation change event with an unknown orientation. Those seem
1388 to always be immediately followed by another orientation change with
1389 a *real* orientation change, so let's try just ignoring those bogus
1390 ones and hoping that the real one comes in shortly...
1392 if (current == UIDeviceOrientationUnknown)
1395 if (rotation_ratio >= 0) return; // in the midst of rotation animation
1396 if (orientation == current) return; // no change
1398 // When transitioning to FaceUp or FaceDown, pretend there was no change.
1399 if (current == UIDeviceOrientationFaceUp ||
1400 current == UIDeviceOrientationFaceDown)
1403 new_orientation = current; // current animation target
1404 rotation_ratio = 0; // start animating
1405 rot_start_time = double_time();
1407 switch (orientation) {
1408 case UIDeviceOrientationLandscapeLeft: angle_from = 90; break;
1409 case UIDeviceOrientationLandscapeRight: angle_from = 270; break;
1410 case UIDeviceOrientationPortraitUpsideDown: angle_from = 180; break;
1411 default: angle_from = 0; break;
1414 switch (new_orientation) {
1415 case UIDeviceOrientationLandscapeLeft: angle_to = 90; break;
1416 case UIDeviceOrientationLandscapeRight: angle_to = 270; break;
1417 case UIDeviceOrientationPortraitUpsideDown: angle_to = 180; break;
1418 default: angle_to = 0; break;
1421 NSRect ff = [self bounds];
1423 switch (orientation) {
1424 case UIDeviceOrientationLandscapeRight: // from landscape
1425 case UIDeviceOrientationLandscapeLeft:
1426 rot_from.width = ff.size.height;
1427 rot_from.height = ff.size.width;
1429 default: // from portrait
1430 rot_from.width = ff.size.width;
1431 rot_from.height = ff.size.height;
1435 switch (new_orientation) {
1436 case UIDeviceOrientationLandscapeRight: // to landscape
1437 case UIDeviceOrientationLandscapeLeft:
1438 rot_to.width = ff.size.height;
1439 rot_to.height = ff.size.width;
1441 default: // to portrait
1442 rot_to.width = ff.size.width;
1443 rot_to.height = ff.size.height;
1448 // If we've done a rotation but the saver hasn't been initialized yet,
1449 // don't bother going through an X11 resize, but just do it now.
1450 rot_start_time = 0; // dawn of time
1451 [self hackRotation];
1456 /* I believe we can't use UIGestureRecognizer for tracking touches
1457 because UIPanGestureRecognizer doesn't give us enough detail in its
1460 Currently we don't handle multi-touches (just the first touch) but
1461 I'm leaving this comment here for future reference:
1463 In the simulator, multi-touch sequences look like this:
1465 touchesBegan [touchA, touchB]
1466 touchesEnd [touchA, touchB]
1468 But on real devices, sometimes you get that, but sometimes you get:
1470 touchesBegan [touchA, touchB]
1476 touchesBegan [touchA]
1477 touchesBegan [touchB]
1481 So the only way to properly detect a "pinch" gesture is to remember
1482 the start-point of each touch as it comes in; and the end-point of
1483 each touch as those come in; and only process the gesture once the
1484 number of touchEnds matches the number of touchBegins.
1487 - (void) rotateMouse:(int)rot x:(int*)x y:(int *)y w:(int)w h:(int)h
1489 CGRect frame = [self bounds]; // Correct aspect ratio and scale.
1490 double s = [self hackedContentScaleFactor];
1491 *x *= (backbuffer_size.width / frame.size.width) / s;
1492 *y *= (backbuffer_size.height / frame.size.height) / s;
1496 #if 0 // AudioToolbox/AudioToolbox.h
1499 // There's no way to play a standard system alert sound!
1500 // We'd have to include our own WAV for that. Eh, fuck it.
1501 AudioServicesPlaySystemSound (kSystemSoundID_Vibrate);
1502 # if TARGET_IPHONE_SIMULATOR
1503 NSLog(@"BEEP"); // The sim doesn't vibrate.
1509 /* We distinguish between taps and drags.
1510 - Drags (down, motion, up) are sent to the saver to handle.
1511 - Single-taps exit the saver.
1512 This means a saver cannot respond to a single-tap. Only a few try to.
1515 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
1517 // If they are trying to pinch, just do nothing.
1518 if ([[event allTouches] count] > 1)
1523 if (xsft->event_cb && xwindow) {
1524 double s = [self hackedContentScaleFactor];
1526 memset (&xe, 0, sizeof(xe));
1528 // #### 'frame' here or 'bounds'?
1529 int w = s * [self frame].size.width;
1530 int h = s * [self frame].size.height;
1531 for (UITouch *touch in touches) {
1532 CGPoint p = [touch locationInView:self];
1533 xe.xany.type = ButtonPress;
1534 xe.xbutton.button = i + 1;
1535 xe.xbutton.button = i + 1;
1536 xe.xbutton.x = s * p.x;
1537 xe.xbutton.y = s * p.y;
1538 [self rotateMouse: rot_current_angle
1539 x: &xe.xbutton.x y: &xe.xbutton.y w: w h: h];
1540 jwxyz_mouse_moved (xdpy, xwindow, xe.xbutton.x, xe.xbutton.y);
1542 // Ignore return code: don't care whether the hack handled it.
1543 xsft->event_cb (xdpy, xwindow, xdata, &xe);
1545 // Remember when/where this was, to determine tap versus drag or hold.
1546 tap_time = double_time();
1550 break; // No pinches: only look at the first touch.
1556 - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
1558 // If they are trying to pinch, just do nothing.
1559 if ([[event allTouches] count] > 1)
1562 if (xsft->event_cb && xwindow) {
1563 double s = [self hackedContentScaleFactor];
1565 memset (&xe, 0, sizeof(xe));
1567 // #### 'frame' here or 'bounds'?
1568 int w = s * [self frame].size.width;
1569 int h = s * [self frame].size.height;
1570 for (UITouch *touch in touches) {
1571 CGPoint p = [touch locationInView:self];
1573 // If the ButtonRelease came less than half a second after ButtonPress,
1574 // and didn't move far, then this was a tap, not a drag or a hold.
1575 // Interpret it as "exit".
1577 double dist = sqrt (((p.x - tap_point.x) * (p.x - tap_point.x)) +
1578 ((p.y - tap_point.y) * (p.y - tap_point.y)));
1579 if (tap_time + 0.5 >= double_time() && dist < 20) {
1580 [self stopAndClose:NO];
1584 xe.xany.type = ButtonRelease;
1585 xe.xbutton.button = i + 1;
1586 xe.xbutton.x = s * p.x;
1587 xe.xbutton.y = s * p.y;
1588 [self rotateMouse: rot_current_angle
1589 x: &xe.xbutton.x y: &xe.xbutton.y w: w h: h];
1590 jwxyz_mouse_moved (xdpy, xwindow, xe.xbutton.x, xe.xbutton.y);
1591 xsft->event_cb (xdpy, xwindow, xdata, &xe);
1593 break; // No pinches: only look at the first touch.
1599 - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
1601 // If they are trying to pinch, just do nothing.
1602 if ([[event allTouches] count] > 1)
1605 if (xsft->event_cb && xwindow) {
1606 double s = [self hackedContentScaleFactor];
1608 memset (&xe, 0, sizeof(xe));
1610 // #### 'frame' here or 'bounds'?
1611 int w = s * [self frame].size.width;
1612 int h = s * [self frame].size.height;
1613 for (UITouch *touch in touches) {
1614 CGPoint p = [touch locationInView:self];
1615 xe.xany.type = MotionNotify;
1616 xe.xmotion.x = s * p.x;
1617 xe.xmotion.y = s * p.y;
1618 [self rotateMouse: rot_current_angle
1619 x: &xe.xbutton.x y: &xe.xbutton.y w: w h: h];
1620 jwxyz_mouse_moved (xdpy, xwindow, xe.xmotion.x, xe.xmotion.y);
1621 xsft->event_cb (xdpy, xwindow, xdata, &xe);
1623 break; // No pinches: only look at the first touch.
1629 /* We need this to respond to "shake" gestures
1631 - (BOOL)canBecomeFirstResponder
1636 - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
1641 - (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event
1645 /* Shake means exit and launch a new saver.
1647 - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
1649 [self stopAndClose:YES];
1653 - (void)setScreenLocked:(BOOL)locked
1655 if (screenLocked == locked) return;
1656 screenLocked = locked;
1658 if ([self isAnimating])
1659 [self stopAnimation];
1661 if (! [self isAnimating])
1662 [self startAnimation];
1667 #endif // USE_IPHONE
1672 /* Utility functions...
1675 static PrefsReader *
1676 get_prefsReader (Display *dpy)
1678 XScreenSaverView *view = jwxyz_window_view (XRootWindow (dpy, 0));
1679 if (!view) return 0;
1680 return [view prefsReader];
1685 get_string_resource (Display *dpy, char *name, char *class)
1687 return [get_prefsReader(dpy) getStringResource:name];
1691 get_boolean_resource (Display *dpy, char *name, char *class)
1693 return [get_prefsReader(dpy) getBooleanResource:name];
1697 get_integer_resource (Display *dpy, char *name, char *class)
1699 return [get_prefsReader(dpy) getIntegerResource:name];
1703 get_float_resource (Display *dpy, char *name, char *class)
1705 return [get_prefsReader(dpy) getFloatResource:name];