1 /* xscreensaver, Copyright (c) 2006-2012 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 # include "ios_function_tables.h"
27 static NSDictionary *function_tables = 0;
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.
34 #ifndef MAC_OS_X_VERSION_10_6
35 # define MAC_OS_X_VERSION_10_6 1060 /* undefined in 10.4 SDK, grr */
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
42 extern struct xscreensaver_function_table *xscreensaver_function_table;
44 /* Global variables used by the screen savers
47 const char *progclass;
53 /* Stub definition of the superclass, for iPhone.
55 @implementation ScreenSaverView
57 NSTimeInterval anim_interval;
62 - (id)initWithFrame:(NSRect)frame isPreview:(BOOL)isPreview {
63 self = [super initWithFrame:frame];
65 anim_interval = 1.0/30;
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 { }
77 - (void)startAnimation {
78 if (animating_p) return;
80 anim_timer = [NSTimer scheduledTimerWithTimeInterval: anim_interval
82 selector:@selector(animateOneFrame)
87 - (void)stopAnimation {
89 [anim_timer invalidate];
96 # endif // !USE_IPHONE
100 @interface XScreenSaverView (Private)
101 - (void) stopAndClose;
102 - (void) stopAndClose:(Bool)relaunch;
105 @implementation XScreenSaverView
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.
110 - (struct xscreensaver_function_table *) findFunctionTable:(NSString *)name
112 NSBundle *nsb = [NSBundle bundleForClass:[self class]];
113 NSAssert1 (nsb, @"no bundle for class %@", [self class]);
115 NSString *path = [nsb bundlePath];
116 CFURLRef url = CFURLCreateWithFileSystemPath(kCFAllocatorDefault,
118 kCFURLPOSIXPathStyle,
120 CFBundleRef cfb = CFBundleCreate (kCFAllocatorDefault, url);
122 NSAssert1 (cfb, @"no CFBundle for \"%@\"", path);
125 name = [[path lastPathComponent] stringByDeletingPathExtension];
127 name = [[name lowercaseString]
128 stringByReplacingOccurrencesOfString:@" "
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);
140 NSLog (@"no symbol \"%@\" for \"%@\"", table_name, path);
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
151 return (struct xscreensaver_function_table *) addr;
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.
158 - (void) setShellPath
160 NSBundle *nsb = [NSBundle bundleForClass:[self class]];
161 NSAssert1 (nsb, @"no bundle for class %@", [self class]);
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=");
172 strcat (npath, opath);
173 if (putenv (npath)) {
175 NSAssert1 (0, @"putenv \"%s\" failed", npath);
178 /* Don't free (npath) -- MacOS's putenv() does not copy it. */
182 // set an $XSCREENSAVER_CLASSPATH variable so that included shell scripts
183 // (e.g., "xscreensaver-text") know how to look up resources.
185 - (void) setResourcesEnv:(NSString *) name
187 NSBundle *nsb = [NSBundle bundleForClass:[self class]];
188 NSAssert1 (nsb, @"no bundle for class %@", [self class]);
190 const char *s = [name cStringUsingEncoding:NSUTF8StringEncoding];
191 char *env = (char *) malloc (strlen (s) + 40);
192 strcpy (env, "XSCREENSAVER_CLASSPATH=");
196 NSAssert1 (0, @"putenv \"%s\" failed", env);
198 /* Don't free (env) -- MacOS's putenv() does not copy it. */
203 add_default_options (const XrmOptionDescRec *opts,
204 const char * const *defs,
205 XrmOptionDescRec **opts_ret,
206 const char ***defs_ret)
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.
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"},
228 static const char *default_defaults [] = {
230 ".doubleBuffer: True",
231 ".multiSample: False",
239 ".textURL: http://twitter.com/statuses/public_timeline.atom",
241 ".grabDesktopImages: yes",
243 ".chooseRandomImages: no",
245 ".chooseRandomImages: yes",
247 ".imageDirectory: ~/Pictures",
253 for (i = 0; default_options[i].option; i++)
255 for (i = 0; opts[i].option; i++)
258 XrmOptionDescRec *opts2 = (XrmOptionDescRec *)
259 calloc (count + 1, sizeof (*opts2));
263 while (default_options[j].option) {
264 opts2[i] = default_options[j];
268 while (opts[j].option) {
279 for (i = 0; default_defaults[i]; i++)
281 for (i = 0; defs[i]; i++)
284 const char **defs2 = (const char **) calloc (count + 1, sizeof (*defs2));
288 while (default_defaults[j]) {
289 defs2[i] = default_defaults[j];
303 /* Returns the current time in seconds as a double.
309 # ifdef GETTIMEOFDAY_TWO_ARGS
311 gettimeofday(&now, &tzp);
316 return (now.tv_sec + ((double) now.tv_usec * 0.000001));
321 - (id) initWithFrame:(NSRect)frame
322 saverName:(NSString *)saverName
323 isPreview:(BOOL)isPreview
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].
332 if (! (self = [super initWithFrame:frame isPreview:isPreview]))
335 xsft = [self findFunctionTable: saverName];
344 [self setMultipleTouchEnabled:YES];
345 orientation = UIDeviceOrientationUnknown;
346 [self didRotate:nil];
347 # endif // USE_IPHONE
351 xsft->setup_cb (xsft, xsft->setup_arg);
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"
357 NSString *name = [NSString stringWithCString:xsft->progclass
358 encoding:NSISOLatin1StringEncoding];
359 name = [@"org.jwz.xscreensaver." stringByAppendingString:name];
360 [self setResourcesEnv:name];
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];
369 // free (opts); // bah, we need these! #### leak!
370 xsft->options = opts;
372 progname = progclass = xsft->progclass;
376 # ifdef USE_BACKBUFFER
377 [self createBackbuffer];
382 // So we can tell when we're docked.
383 [UIDevice currentDevice].batteryMonitoringEnabled = YES;
384 # endif // USE_IPHONE
392 [self setLayer: [CALayer layer]];
393 [self setWantsLayer: YES];
398 - (id) initWithFrame:(NSRect)frame isPreview:(BOOL)p
400 return [self initWithFrame:frame saverName:0 isPreview:p];
406 NSAssert(![self isAnimating], @"still animating");
407 NSAssert(!xdata, @"xdata not yet freed");
409 jwxyz_free_display (xdpy);
411 # ifdef USE_BACKBUFFER
413 CGContextRelease (backbuffer);
416 [prefsReader release];
424 - (PrefsReader *) prefsReader
431 - (void) lockFocus { }
432 - (void) unlockFocus { }
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.
442 - (void) allSystemsGo: (NSTimer *) timer
444 NSAssert (timer == crash_timer, @"crash timer screwed up");
447 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
448 [prefs setBool:YES forKey:@"wasRunning"];
454 - (void) startAnimation
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.
466 [crash_timer invalidate];
468 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
469 [prefs removeObjectForKey:@"wasRunning"];
472 crash_timer = [NSTimer scheduledTimerWithTimeInterval: 5
474 selector:@selector(allSystemsGo:)
477 # endif // USE_IPHONE
479 // Never automatically turn the screen off if we are docked,
480 // and an animation is running.
483 [UIApplication sharedApplication].idleTimerDisabled =
484 ([UIDevice currentDevice].batteryState != UIDeviceBatteryStateUnplugged);
489 - (void)stopAnimation
491 NSAssert([self isAnimating], @"not animating");
495 [self lockFocus]; // in case something tries to draw from here
496 [self prepareContext];
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.
504 xsft->free_cb (xdpy, xwindow, xdata);
507 // setup_p = NO; // #### wait, do we need this?
514 [crash_timer invalidate];
516 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
517 [prefs removeObjectForKey:@"wasRunning"];
519 # endif // USE_IPHONE
521 [super stopAnimation];
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.
527 [UIApplication sharedApplication].idleTimerDisabled = NO;
532 /* Hook for the XScreenSaverGLView subclass
534 - (void) prepareContext
538 /* Hook for the XScreenSaverGLView subclass
540 - (void) resizeContext
546 screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
548 fps_compute (fpst, 0, -1);
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.)
566 - (CGFloat) hackedContentScaleFactor
568 GLfloat s = [self contentScaleFactor];
569 CGRect frame = [self bounds];
570 if (frame.size.width >= 1024 ||
571 frame.size.height >= 1024)
577 static GLfloat _global_rot_current_angle_kludge;
579 double current_device_rotation (void)
581 return -_global_rot_current_angle_kludge;
585 - (void) hackRotation
587 if (rotation_ratio >= 0) { // in the midst of a rotation animation
589 # define CLAMP180(N) while (N < 0) N += 360; while (N > 180) N -= 360
590 GLfloat f = angle_from;
591 GLfloat t = angle_to;
594 GLfloat dist = -(t-f);
597 // Intermediate angle.
598 rot_current_angle = f - rotation_ratio * dist;
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);
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);
611 if (rotation_ratio > 1) { // Done animating.
612 orientation = new_orientation;
613 rot_current_angle = angle_to;
614 rot_current_size = rot_to;
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];
621 } else { // Not animating a rotation.
622 rot_current_angle = angle_to;
623 rot_current_size = rot_to;
626 CLAMP180(rot_current_angle);
627 _global_rot_current_angle_kludge = rot_current_angle;
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*/)
639 - (void)alertView:(UIAlertView *)av clickedButtonAtIndex:(NSInteger)i
641 if (i == 0) exit (-1); // Cancel
642 [self stopAndClose]; // Keep going
645 - (void) handleException: (NSException *)e
647 NSLog (@"Caught exception: %@", e);
648 [[[UIAlertView alloc] initWithTitle:
649 [NSString stringWithFormat: @"%s crashed!",
652 [NSString stringWithFormat:
653 @"The error message was:"
655 "If it keeps crashing, try "
656 "resetting its options.",
659 cancelButtonTitle: @"Exit"
660 otherButtonTitles: @"Keep going", nil]
662 [self stopAnimation];
668 #ifdef USE_BACKBUFFER
670 /* Create a bitmap context into which we render everything.
671 If the desired size has changed, re-created it.
673 - (void) createBackbuffer
676 double s = [self hackedContentScaleFactor];
677 int new_w = s * rot_current_size.width;
678 int new_h = s * rot_current_size.height;
680 int new_w = [self bounds].size.width;
681 int new_h = [self bounds].size.height;
685 backbuffer_size.width == new_w &&
686 backbuffer_size.height == new_h)
689 CGSize osize = backbuffer_size;
690 CGContextRef ob = backbuffer;
692 backbuffer_size.width = new_w;
693 backbuffer_size.height = new_h;
695 CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB();
696 backbuffer = CGBitmapContextCreate (NULL,
697 backbuffer_size.width,
698 backbuffer_size.height,
700 backbuffer_size.width * 4,
702 kCGImageAlphaPremultipliedLast);
703 CGColorSpaceRelease (cs);
704 NSAssert (backbuffer, @"unable to allocate back buffer");
708 r.origin.x = r.origin.y = 0;
709 r.size = backbuffer_size;
710 CGContextSetGrayFillColor (backbuffer, 0, 1);
711 CGContextFillRect (backbuffer, r);
714 // Restore old bits, as much as possible, to the X11 upper left origin.
717 rect.origin.y = (backbuffer_size.height - osize.height);
719 CGImageRef img = CGBitmapContextCreateImage (ob);
720 CGContextDrawImage (backbuffer, rect, img);
721 CGImageRelease (img);
722 CGContextRelease (ob);
726 #endif // USE_BACKBUFFER
729 /* Inform X11 that the size of our window has changed.
733 if (!xwindow) return; // early
735 # ifdef USE_BACKBUFFER
736 [self createBackbuffer];
737 jwxyz_window_resized (xdpy, xwindow,
739 backbuffer_size.width, backbuffer_size.height,
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,
748 # endif // !USE_BACKBUFFER
750 // Next time render_x11 is called, run the saver's reshape_cb.
760 if (orientation == UIDeviceOrientationUnknown)
761 [self didRotate:nil];
768 # ifdef USE_BACKBUFFER
769 NSAssert (backbuffer, @"no back buffer");
770 xdpy = jwxyz_make_display (self, backbuffer);
772 xdpy = jwxyz_make_display (self, 0);
774 xwindow = XRootWindow (xdpy, 0);
781 xsft->setup_cb (xsft, xsft->setup_arg);
785 NSAssert(!xdata, @"xdata already initialized");
790 XSetWindowBackground (xdpy, xwindow,
791 get_pixel_resource (xdpy, 0,
792 "background", "Background"));
793 XClearWindow (xdpy, xwindow);
796 [[self window] setAcceptsMouseMovedEvents:YES];
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.
804 # if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5
805 [[self window] setPreferredBackingLocation:
806 NSWindowBackingLocationVideoMemory];
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
814 void *(*init_cb) (Display *, Window, void *) =
815 (void *(*) (Display *, Window, void *)) xsft->init_cb;
817 xdata = init_cb (xdpy, xwindow, xsft->setup_arg);
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;
826 /* I don't understand why we have to do this *every frame*, but we do,
827 or else the cursor comes back on.
830 if (![self isPreview])
831 [NSCursor setHiddenUntilMouseMoves:YES];
837 /* This is just a guess, but the -fps code wants to know how long
838 we were sleeping between frames.
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);
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.
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.
856 This means two extra calls to gettimeofday() per frame. For fast-cycling
857 screen savers, that might actually slow them down. Oh well.
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.
867 gettimeofday (&tv, 0);
868 double now = tv.tv_sec + (tv.tv_usec / 1000000.0);
869 if (now < next_frame_time) return;
871 [self prepareContext];
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];
878 # ifndef USE_BACKBUFFER
880 # else // USE_BACKBUFFER
883 r.size.width = backbuffer_size.width;
884 r.size.height = backbuffer_size.height;
885 # endif // USE_BACKBUFFER
887 xsft->reshape_cb (xdpy, xwindow, xdata, r.size.width, r.size.height);
891 // Run any XtAppAddInput callbacks now.
892 // (Note that XtAppAddTimeOut callbacks have already been run by
893 // the Cocoa event loop.)
895 jwxyz_sources_run (display_sources_data (xdpy));
901 NSDisableScreenUpdates();
903 unsigned long delay = xsft->draw_cb (xdpy, xwindow, xdata);
904 if (fpst) xsft->fps_cb (xdpy, xwindow, fpst, xdata);
906 NSEnableScreenUpdates();
909 gettimeofday (&tv, 0);
910 now = tv.tv_sec + (tv.tv_usec / 1000000.0);
911 next_frame_time = now + (delay / 1000000.0);
913 # ifdef USE_IPHONE // Allow savers on the iPhone to run full-tilt.
914 if (delay < [self animationTimeInterval])
915 [self setAnimationTimeInterval:(delay / 1000000.0)];
918 # ifdef DO_GC_HACKERY
919 /* Current theory is that the 10.6 garbage collector sucks in the
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
935 So, we force an exhaustive garbage collection in this process
936 approximately every 5 seconds whether the system thinks it needs
943 objc_collect (OBJC_EXHAUSTIVE_COLLECTION);
946 # endif // DO_GC_HACKERY
950 @catch (NSException *e) {
951 [self handleException: e];
953 # endif // USE_IPHONE
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.
961 - (void)drawRect:(NSRect)rect
963 if (xwindow) // clear to the X window's bg color, not necessarily black.
964 XClearWindow (xdpy, xwindow);
966 [super drawRect:rect]; // early: black.
970 #ifndef USE_BACKBUFFER
972 - (void) animateOneFrame
977 #else // USE_BACKBUFFER
979 - (void) animateOneFrame
981 // Render X11 into the backing store bitmap...
983 NSAssert (backbuffer, @"no back buffer");
986 UIGraphicsPushContext (backbuffer);
992 UIGraphicsPopContext();
996 // Then compute the transformations for rotation.
998 // The rotation origin for layer.affineTransform is in the center already.
999 CGAffineTransform t =
1000 CGAffineTransformMakeRotation (rot_current_angle / (180.0 / M_PI));
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));
1009 self.layer.affineTransform = t;
1010 # endif // USE_IPHONE
1012 // Then copy that bitmap to the screen, by just stuffing it into
1013 // the layer. The superclass drawRect method will handle the rest.
1015 CGImageRef img = CGBitmapContextCreateImage (backbuffer);
1016 self.layer.contents = (id)img;
1017 CGImageRelease (img);
1020 #endif // !USE_BACKBUFFER
1024 - (void) setFrame:(NSRect) newRect
1026 [super setFrame:newRect];
1028 if (xwindow) // inform Xlib that the window has changed now.
1033 # ifndef USE_IPHONE // Doesn't exist on iOS
1034 - (void) setFrameSize:(NSSize) newSize
1036 [super setFrameSize:newSize];
1040 # endif // !USE_IPHONE
1043 +(BOOL) performGammaFade
1048 - (BOOL) hasConfigureSheet
1054 - (NSWindow *) configureSheet
1056 - (UIViewController *) configureView
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"];
1065 NSLog (@"%@.xml does not exist in the application bundle: %@/",
1066 file, [bundle resourcePath]);
1071 UIViewController *sheet;
1072 # else // !USE_IPHONE
1074 # endif // !USE_IPHONE
1076 sheet = [[XScreenSaverConfigSheet alloc]
1077 initWithXMLFile:path
1078 options:xsft->options
1079 controller:[prefsReader userDefaultsController]
1080 defaults:[prefsReader defaultOptions]];
1082 // #### am I expected to retain this, or not? wtf.
1083 // I thought not, but if I don't do this, we (sometimes) crash.
1090 - (NSUserDefaultsController *) userDefaultsController
1092 return [prefsReader userDefaultsController];
1096 /* Announce our willingness to accept keyboard input.
1098 - (BOOL)acceptsFirstResponder
1106 /* Convert an NSEvent into an XEvent, and pass it along.
1107 Returns YES if it was handled.
1109 - (BOOL) doEvent: (NSEvent *) e
1112 if (![self isPreview] || // no event handling if actually screen-saving!
1113 ![self isAnimating] ||
1118 memset (&xe, 0, sizeof(xe));
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;
1129 NSPoint p = [[[e window] contentView] convertPoint:[e locationInWindow]
1132 double s = [self hackedContentScaleFactor];
1137 int y = s * ([self bounds].size.height - p.y);
1139 xe.xany.type = type;
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 :
1153 xe.xbutton.button = [e buttonNumber] + 1;
1158 xe.xmotion.state = state;
1163 NSString *ns = (([e type] == NSFlagsChanged) ? 0 :
1164 [e charactersIgnoringModifiers]);
1167 if (!ns || [ns length] == 0) // dead key
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.)
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;
1179 else if ([ns length] == 1) // real key
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;
1196 [ns cStringUsingEncoding:NSISOLatin1StringEncoding];
1197 k = (s && *s ? *s : 0);
1203 if (! k) return YES; // E.g., "KeyRelease XK_Shift_L"
1205 xe.xkey.keycode = k;
1206 xe.xkey.state = state;
1210 NSAssert (0, @"unknown X11 event type: %d", type);
1215 [self prepareContext];
1216 BOOL result = xsft->event_cb (xdpy, xwindow, xdata, &xe);
1222 - (void) mouseDown: (NSEvent *) e
1224 if (! [self doEvent:e type:ButtonPress])
1225 [super mouseDown:e];
1228 - (void) mouseUp: (NSEvent *) e
1230 if (! [self doEvent:e type:ButtonRelease])
1234 - (void) otherMouseDown: (NSEvent *) e
1236 if (! [self doEvent:e type:ButtonPress])
1237 [super otherMouseDown:e];
1240 - (void) otherMouseUp: (NSEvent *) e
1242 if (! [self doEvent:e type:ButtonRelease])
1243 [super otherMouseUp:e];
1246 - (void) mouseMoved: (NSEvent *) e
1248 if (! [self doEvent:e type:MotionNotify])
1249 [super mouseMoved:e];
1252 - (void) mouseDragged: (NSEvent *) e
1254 if (! [self doEvent:e type:MotionNotify])
1255 [super mouseDragged:e];
1258 - (void) otherMouseDragged: (NSEvent *) e
1260 if (! [self doEvent:e type:MotionNotify])
1261 [super otherMouseDragged:e];
1264 - (void) scrollWheel: (NSEvent *) e
1266 if (! [self doEvent:e type:ButtonPress])
1267 [super scrollWheel:e];
1270 - (void) keyDown: (NSEvent *) e
1272 if (! [self doEvent:e type:KeyPress])
1276 - (void) keyUp: (NSEvent *) e
1278 if (! [self doEvent:e type:KeyRelease])
1282 - (void) flagsChanged: (NSEvent *) e
1284 if (! [self doEvent:e type:KeyPress])
1285 [super flagsChanged:e];
1291 - (void) stopAndClose
1293 if ([self isAnimating])
1294 [self stopAnimation];
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...
1301 UIViewController *v = [[self window] rootViewController];
1302 if ([v isKindOfClass: [UINavigationController class]]) {
1303 UINavigationController *n = (UINavigationController *) v;
1304 [[n topViewController] becomeFirstResponder];
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];
1317 - (void) stopAndClose:(Bool)relaunch_p
1319 [self stopAndClose];
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
1332 /* Called after the device's orientation has changed.
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
1344 So, we have to hack the rotation animation manually, in the GL world.
1346 Possibly XScreenSaverView should use Core Animation, and
1347 XScreenSaverGLView should override that.
1349 - (void)didRotate:(NSNotification *)notification
1351 UIDeviceOrientation current = [[UIDevice currentDevice] orientation];
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.
1358 if (current == UIDeviceOrientationUnknown) {
1359 switch ([[[self window] rootViewController] interfaceOrientation]) {
1360 case UIInterfaceOrientationPortrait:
1361 current = UIDeviceOrientationPortrait;
1363 case UIInterfaceOrientationPortraitUpsideDown:
1364 current = UIDeviceOrientationPortraitUpsideDown;
1366 case UIInterfaceOrientationLandscapeLeft: // It's opposite day
1367 current = UIDeviceOrientationLandscapeRight;
1369 case UIInterfaceOrientationLandscapeRight:
1370 current = UIDeviceOrientationLandscapeLeft;
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...
1383 if (current == UIDeviceOrientationUnknown)
1386 if (rotation_ratio >= 0) return; // in the midst of rotation animation
1387 if (orientation == current) return; // no change
1389 // When transitioning to FaceUp or FaceDown, pretend there was no change.
1390 if (current == UIDeviceOrientationFaceUp ||
1391 current == UIDeviceOrientationFaceDown)
1394 new_orientation = current; // current animation target
1395 rotation_ratio = 0; // start animating
1396 rot_start_time = double_time();
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;
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;
1412 NSRect ff = [self bounds];
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;
1420 default: // from portrait
1421 rot_from.width = ff.size.width;
1422 rot_from.height = ff.size.height;
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;
1432 default: // to portrait
1433 rot_to.width = ff.size.width;
1434 rot_to.height = ff.size.height;
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];
1447 /* I believe we can't use UIGestureRecognizer for tracking touches
1448 because UIPanGestureRecognizer doesn't give us enough detail in its
1451 Currently we don't handle multi-touches (just the first touch) but
1452 I'm leaving this comment here for future reference:
1454 In the simulator, multi-touch sequences look like this:
1456 touchesBegan [touchA, touchB]
1457 touchesEnd [touchA, touchB]
1459 But on real devices, sometimes you get that, but sometimes you get:
1461 touchesBegan [touchA, touchB]
1467 touchesBegan [touchA]
1468 touchesBegan [touchB]
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.
1478 - (void) rotateMouse:(int)rot x:(int*)x y:(int *)y w:(int)w h:(int)h
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;
1487 #if 0 // AudioToolbox/AudioToolbox.h
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.
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.
1506 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
1510 if (xsft->event_cb && xwindow) {
1511 double s = [self hackedContentScaleFactor];
1513 memset (&xe, 0, sizeof(xe));
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);
1529 // Ignore return code: don't care whether the hack handled it.
1530 xsft->event_cb (xdpy, xwindow, xdata, &xe);
1532 // Remember when/where this was, to determine tap versus drag or hold.
1533 tap_time = double_time();
1537 break; // No pinches: only look at the first touch.
1543 - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
1545 if (xsft->event_cb && xwindow) {
1546 double s = [self hackedContentScaleFactor];
1548 memset (&xe, 0, sizeof(xe));
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];
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".
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];
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);
1576 break; // No pinches: only look at the first touch.
1582 - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
1584 if (xsft->event_cb && xwindow) {
1585 double s = [self hackedContentScaleFactor];
1587 memset (&xe, 0, sizeof(xe));
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);
1602 break; // No pinches: only look at the first touch.
1608 /* We need this to respond to "shake" gestures
1610 - (BOOL)canBecomeFirstResponder
1615 - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
1620 - (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event
1624 /* Shake means exit and launch a new saver.
1626 - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
1628 [self stopAndClose:YES];
1632 - (void)setScreenLocked:(BOOL)locked
1634 if (screenLocked == locked) return;
1635 screenLocked = locked;
1637 if ([self isAnimating])
1638 [self stopAnimation];
1640 if (! [self isAnimating])
1641 [self startAnimation];
1646 #endif // USE_IPHONE
1651 /* Utility functions...
1654 static PrefsReader *
1655 get_prefsReader (Display *dpy)
1657 XScreenSaverView *view = jwxyz_window_view (XRootWindow (dpy, 0));
1658 if (!view) return 0;
1659 return [view prefsReader];
1664 get_string_resource (Display *dpy, char *name, char *class)
1666 return [get_prefsReader(dpy) getStringResource:name];
1670 get_boolean_resource (Display *dpy, char *name, char *class)
1672 return [get_prefsReader(dpy) getBooleanResource:name];
1676 get_integer_resource (Display *dpy, char *name, char *class)
1678 return [get_prefsReader(dpy) getIntegerResource:name];
1682 get_float_resource (Display *dpy, char *name, char *class)
1684 return [get_prefsReader(dpy) getFloatResource:name];