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"
25 /* Garbage collection only exists if we are being compiled against the
26 10.6 SDK or newer, not if we are building against the 10.4 SDK.
28 #ifndef MAC_OS_X_VERSION_10_6
29 # define MAC_OS_X_VERSION_10_6 1060 /* undefined in 10.4 SDK, grr */
31 #if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6 /* 10.6 SDK */
32 # import <objc/objc-auto.h>
33 # define DO_GC_HACKERY
36 extern struct xscreensaver_function_table *xscreensaver_function_table;
38 /* Global variables used by the screen savers
41 const char *progclass;
47 /* Stub definition of the superclass, for iPhone.
49 @implementation ScreenSaverView
51 NSTimeInterval anim_interval;
56 - (id)initWithFrame:(NSRect)frame isPreview:(BOOL)isPreview {
57 self = [super initWithFrame:frame];
59 anim_interval = 1.0/30;
62 - (NSTimeInterval)animationTimeInterval { return anim_interval; }
63 - (void)setAnimationTimeInterval:(NSTimeInterval)i { anim_interval = i; }
64 - (BOOL)hasConfigureSheet { return NO; }
65 - (NSWindow *)configureSheet { return nil; }
66 - (NSView *)configureView { return nil; }
67 - (BOOL)isPreview { return NO; }
68 - (BOOL)isAnimating { return animating_p; }
69 - (void)animateOneFrame { }
71 - (void)startAnimation {
72 if (animating_p) return;
74 anim_timer = [NSTimer scheduledTimerWithTimeInterval: anim_interval
76 selector:@selector(animateOneFrame)
81 - (void)stopAnimation {
83 [anim_timer invalidate];
90 # endif // !USE_IPHONE
94 @interface XScreenSaverView (Private)
95 - (void) stopAndClose;
96 - (void) stopAndClose:(Bool)relaunch;
99 @implementation XScreenSaverView
101 // Given a lower-cased saver name, returns the function table for it.
102 // If no name, guess the name from the class's bundle name.
104 - (struct xscreensaver_function_table *) findFunctionTable:(NSString *)name
106 NSBundle *nsb = [NSBundle bundleForClass:[self class]];
107 NSAssert1 (nsb, @"no bundle for class %@", [self class]);
109 NSString *path = [nsb bundlePath];
110 CFURLRef url = CFURLCreateWithFileSystemPath(kCFAllocatorDefault,
112 kCFURLPOSIXPathStyle,
114 CFBundleRef cfb = CFBundleCreate (kCFAllocatorDefault, url);
116 NSAssert1 (cfb, @"no CFBundle for \"%@\"", path);
119 name = [[path lastPathComponent] stringByDeletingPathExtension];
121 NSString *table_name = [[[name lowercaseString]
122 stringByReplacingOccurrencesOfString:@" "
124 stringByAppendingString:
125 @"_xscreensaver_function_table"];
126 void *addr = CFBundleGetDataPointerForName (cfb, (CFStringRef) table_name);
130 NSLog (@"no symbol \"%@\" for \"%@\"", table_name, path);
132 return (struct xscreensaver_function_table *) addr;
136 // Add the "Contents/Resources/" subdirectory of this screen saver's .bundle
137 // to $PATH for the benefit of savers that include helper shell scripts.
139 - (void) setShellPath
141 NSBundle *nsb = [NSBundle bundleForClass:[self class]];
142 NSAssert1 (nsb, @"no bundle for class %@", [self class]);
144 NSString *nsdir = [nsb resourcePath];
145 NSAssert1 (nsdir, @"no resourcePath for class %@", [self class]);
146 const char *dir = [nsdir cStringUsingEncoding:NSUTF8StringEncoding];
147 const char *opath = getenv ("PATH");
148 if (!opath) opath = "/bin"; // $PATH is unset when running under Shark!
149 char *npath = (char *) malloc (strlen (opath) + strlen (dir) + 30);
150 strcpy (npath, "PATH=");
153 strcat (npath, opath);
154 if (putenv (npath)) {
156 NSAssert1 (0, @"putenv \"%s\" failed", npath);
159 /* Don't free (npath) -- MacOS's putenv() does not copy it. */
163 // set an $XSCREENSAVER_CLASSPATH variable so that included shell scripts
164 // (e.g., "xscreensaver-text") know how to look up resources.
166 - (void) setResourcesEnv:(NSString *) name
168 NSBundle *nsb = [NSBundle bundleForClass:[self class]];
169 NSAssert1 (nsb, @"no bundle for class %@", [self class]);
171 const char *s = [name cStringUsingEncoding:NSUTF8StringEncoding];
172 char *env = (char *) malloc (strlen (s) + 40);
173 strcpy (env, "XSCREENSAVER_CLASSPATH=");
177 NSAssert1 (0, @"putenv \"%s\" failed", env);
179 /* Don't free (env) -- MacOS's putenv() does not copy it. */
184 add_default_options (const XrmOptionDescRec *opts,
185 const char * const *defs,
186 XrmOptionDescRec **opts_ret,
187 const char ***defs_ret)
189 /* These aren't "real" command-line options (there are no actual command-line
190 options in the Cocoa version); but this is the somewhat kludgey way that
191 the <xscreensaver-text /> and <xscreensaver-image /> tags in the
192 ../hacks/config/\*.xml files communicate with the preferences database.
194 static const XrmOptionDescRec default_options [] = {
195 { "-text-mode", ".textMode", XrmoptionSepArg, 0 },
196 { "-text-literal", ".textLiteral", XrmoptionSepArg, 0 },
197 { "-text-file", ".textFile", XrmoptionSepArg, 0 },
198 { "-text-url", ".textURL", XrmoptionSepArg, 0 },
199 { "-text-program", ".textProgram", XrmoptionSepArg, 0 },
200 { "-grab-desktop", ".grabDesktopImages", XrmoptionNoArg, "True" },
201 { "-no-grab-desktop", ".grabDesktopImages", XrmoptionNoArg, "False"},
202 { "-choose-random-images", ".chooseRandomImages",XrmoptionNoArg, "True" },
203 { "-no-choose-random-images",".chooseRandomImages",XrmoptionNoArg, "False"},
204 { "-image-directory", ".imageDirectory", XrmoptionSepArg, 0 },
205 { "-fps", ".doFPS", XrmoptionNoArg, "True" },
206 { "-no-fps", ".doFPS", XrmoptionNoArg, "False"},
209 static const char *default_defaults [] = {
211 ".doubleBuffer: True",
212 ".multiSample: False",
220 ".textURL: http://twitter.com/statuses/public_timeline.atom",
222 ".grabDesktopImages: yes",
224 ".chooseRandomImages: no",
226 ".chooseRandomImages: yes",
228 ".imageDirectory: ~/Pictures",
234 for (i = 0; default_options[i].option; i++)
236 for (i = 0; opts[i].option; i++)
239 XrmOptionDescRec *opts2 = (XrmOptionDescRec *)
240 calloc (count + 1, sizeof (*opts2));
244 while (default_options[j].option) {
245 opts2[i] = default_options[j];
249 while (opts[j].option) {
260 for (i = 0; default_defaults[i]; i++)
262 for (i = 0; defs[i]; i++)
265 const char **defs2 = (const char **) calloc (count + 1, sizeof (*defs2));
269 while (default_defaults[j]) {
270 defs2[i] = default_defaults[j];
284 /* Returns the current time in seconds as a double.
290 # ifdef GETTIMEOFDAY_TWO_ARGS
292 gettimeofday(&now, &tzp);
297 return (now.tv_sec + ((double) now.tv_usec * 0.000001));
302 - (id) initWithFrame:(NSRect)frame
303 saverName:(NSString *)saverName
304 isPreview:(BOOL)isPreview
307 rot_current_size = frame.size; // needs to be early, because
308 rot_from = rot_current_size; // [self setFrame] is called by
309 rot_to = rot_current_size; // [super initWithFrame].
313 if (! (self = [super initWithFrame:frame isPreview:isPreview]))
316 xsft = [self findFunctionTable: saverName];
325 [self setMultipleTouchEnabled:YES];
326 orientation = UIDeviceOrientationUnknown;
327 [self didRotate:nil];
328 # endif // USE_IPHONE
332 xsft->setup_cb (xsft, xsft->setup_arg);
334 /* The plist files for these preferences show up in
335 $HOME/Library/Preferences/ByHost/ in a file named like
336 "org.jwz.xscreensaver.<SAVERNAME>.<NUMBERS>.plist"
338 NSString *name = [NSString stringWithCString:xsft->progclass
339 encoding:NSISOLatin1StringEncoding];
340 name = [@"org.jwz.xscreensaver." stringByAppendingString:name];
341 [self setResourcesEnv:name];
344 XrmOptionDescRec *opts = 0;
345 const char **defs = 0;
346 add_default_options (xsft->options, xsft->defaults, &opts, &defs);
347 prefsReader = [[PrefsReader alloc]
348 initWithName:name xrmKeys:opts defaults:defs];
350 // free (opts); // bah, we need these! #### leak!
351 xsft->options = opts;
353 progname = progclass = xsft->progclass;
358 [self createBackbuffer];
360 // So we can tell when we're docked.
361 [UIDevice currentDevice].batteryMonitoringEnabled = YES;
362 # endif // USE_IPHONE
367 - (id) initWithFrame:(NSRect)frame isPreview:(BOOL)p
369 return [self initWithFrame:frame saverName:0 isPreview:p];
375 NSAssert(![self isAnimating], @"still animating");
376 NSAssert(!xdata, @"xdata not yet freed");
378 jwxyz_free_display (xdpy);
382 CGContextRelease (backbuffer);
385 [prefsReader release];
393 - (PrefsReader *) prefsReader
400 - (void) lockFocus { }
401 - (void) unlockFocus { }
407 /* A few seconds after the saver launches, we store the "wasRunning"
408 preference. This is so that if the saver is crashing at startup,
409 we don't launch it again next time, getting stuck in a crash loop.
411 - (void) allSystemsGo: (NSTimer *) timer
413 NSAssert (timer == crash_timer, @"crash timer screwed up");
416 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
417 [prefs setBool:YES forKey:@"wasRunning"];
423 - (void) startAnimation
425 NSAssert(![self isAnimating], @"already animating");
426 NSAssert(!initted_p && !xdata, @"already initialized");
427 [super startAnimation];
428 /* We can't draw on the window from this method, so we actually do the
429 initialization of the screen saver (xsft->init_cb) in the first call
430 to animateOneFrame() instead.
435 [crash_timer invalidate];
437 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
438 [prefs removeObjectForKey:@"wasRunning"];
441 crash_timer = [NSTimer scheduledTimerWithTimeInterval: 5
443 selector:@selector(allSystemsGo:)
446 # endif // USE_IPHONE
448 // Never automatically turn the screen off if we are docked,
449 // and an animation is running.
452 [UIApplication sharedApplication].idleTimerDisabled =
453 ([UIDevice currentDevice].batteryState != UIDeviceBatteryStateUnplugged);
458 - (void)stopAnimation
460 NSAssert([self isAnimating], @"not animating");
464 [self lockFocus]; // in case something tries to draw from here
465 [self prepareContext];
467 /* I considered just not even calling the free callback at all...
468 But webcollage-cocoa needs it, to kill the inferior webcollage
469 processes (since the screen saver framework never generates a
470 SIGPIPE for them...) Instead, I turned off the free call in
471 xlockmore.c, which is where all of the bogus calls are anyway.
473 xsft->free_cb (xdpy, xwindow, xdata);
476 // setup_p = NO; // #### wait, do we need this?
483 [crash_timer invalidate];
485 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
486 [prefs removeObjectForKey:@"wasRunning"];
488 # endif // USE_IPHONE
490 [super stopAnimation];
492 // When an animation is no longer running (e.g., looking at the list)
493 // then it's ok to power off the screen when docked.
496 [UIApplication sharedApplication].idleTimerDisabled = NO;
501 /* Hook for the XScreenSaverGLView subclass
503 - (void) prepareContext
507 /* Hook for the XScreenSaverGLView subclass
509 - (void) resizeContext
515 screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
517 fps_compute (fpst, 0, -1);
523 /* Create a bitmap context into which we render everything.
525 - (void) createBackbuffer
527 CGContextRef ob = backbuffer;
528 NSSize osize = backbuffer_size;
530 CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB();
531 double s = self.contentScaleFactor;
532 backbuffer_size.width = (int) (s * rot_current_size.width);
533 backbuffer_size.height = (int) (s * rot_current_size.height);
534 backbuffer = CGBitmapContextCreate (NULL,
535 backbuffer_size.width,
536 backbuffer_size.height,
538 backbuffer_size.width * 4,
540 kCGImageAlphaPremultipliedLast);
541 NSAssert (backbuffer, @"unable to allocate back buffer");
542 CGColorSpaceRelease (cs);
545 CGContextSetGrayFillColor (backbuffer, 0, 1);
546 CGRect r = CGRectZero;
547 r.size = backbuffer_size;
548 CGContextFillRect (backbuffer, r);
551 // Restore old bits, as much as possible, to the X11 upper left origin.
554 rect.origin.y = (backbuffer_size.height - osize.height);
556 CGImageRef img = CGBitmapContextCreateImage (ob);
557 CGContextDrawImage (backbuffer, rect, img);
558 CGImageRelease (img);
559 CGContextRelease (ob);
563 static GLfloat _global_rot_current_angle_kludge;
565 double current_device_rotation (void)
567 return -_global_rot_current_angle_kludge;
571 - (void) hackRotation
573 if (rotation_ratio >= 0) { // in the midst of a rotation animation
575 # define CLAMP180(N) while (N < 0) N += 360; while (N > 180) N -= 360
576 GLfloat f = angle_from;
577 GLfloat t = angle_to;
580 GLfloat dist = -(t-f);
583 // Intermediate angle.
584 rot_current_angle = f - rotation_ratio * dist;
586 // Intermediate frame size.
587 rot_current_size.width = rot_from.width +
588 rotation_ratio * (rot_to.width - rot_from.width);
589 rot_current_size.height = rot_from.height +
590 rotation_ratio * (rot_to.height - rot_from.height);
592 // Tick animation. Complete rotation in 1/6th sec.
593 double now = double_time();
594 double duration = 1/6.0;
595 rotation_ratio = 1 - ((rot_start_time + duration - now) / duration);
597 if (rotation_ratio > 1) { // Done animating.
598 orientation = new_orientation;
599 rot_current_angle = angle_to;
600 rot_current_size = rot_to;
603 // Check orientation again in case we rotated again while rotating:
604 // this is a no-op if nothing has changed.
605 [self didRotate:nil];
607 } else { // Not animating a rotation.
608 rot_current_angle = angle_to;
609 rot_current_size = rot_to;
612 CLAMP180(rot_current_angle);
613 _global_rot_current_angle_kludge = rot_current_angle;
617 double s = self.contentScaleFactor;
618 if (((int) backbuffer_size.width != (int) (s * rot_current_size.width) ||
619 (int) backbuffer_size.height != (int) (s * rot_current_size.height))
620 /* && rotation_ratio == -1*/)
621 [self setFrame:[self frame]];
625 - (void)alertView:(UIAlertView *)av clickedButtonAtIndex:(NSInteger)i
627 if (i == 0) exit (-1); // Cancel
628 [self stopAndClose]; // Keep going
631 - (void) handleException: (NSException *)e
633 NSLog (@"Caught exception: %@", e);
634 [[[UIAlertView alloc] initWithTitle:
635 [NSString stringWithFormat: @"%s crashed!",
638 [NSString stringWithFormat:
639 @"The error message was:"
641 "If it keeps crashing, try "
642 "resetting its options.",
645 cancelButtonTitle: @"Exit"
646 otherButtonTitles: @"Keep going", nil]
648 [self stopAnimation];
659 if (orientation == UIDeviceOrientationUnknown)
660 [self didRotate:nil];
668 NSAssert (backbuffer, @"no back buffer");
669 xdpy = jwxyz_make_display (self, backbuffer);
671 xdpy = jwxyz_make_display (self, 0);
673 xwindow = XRootWindow (xdpy, 0);
676 jwxyz_window_resized (xdpy, xwindow,
678 backbuffer_size.width, backbuffer_size.height,
681 NSRect r = [self frame];
682 jwxyz_window_resized (xdpy, xwindow,
683 r.origin.x, r.origin.y,
684 r.size.width, r.size.height,
692 xsft->setup_cb (xsft, xsft->setup_arg);
696 NSAssert(!xdata, @"xdata already initialized");
701 XSetWindowBackground (xdpy, xwindow,
702 get_pixel_resource (xdpy, 0,
703 "background", "Background"));
704 XClearWindow (xdpy, xwindow);
707 [[self window] setAcceptsMouseMovedEvents:YES];
710 /* In MacOS 10.5, this enables "QuartzGL", meaning that the Quartz
711 drawing primitives will run on the GPU instead of the CPU.
712 It seems like it might make things worse rather than better,
713 though... Plus it makes us binary-incompatible with 10.4.
715 # if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5
716 [[self window] setPreferredBackingLocation:
717 NSWindowBackingLocationVideoMemory];
721 /* Kludge: even though the init_cb functions are declared to take 2 args,
722 actually call them with 3, for the benefit of xlockmore_init() and
725 void *(*init_cb) (Display *, Window, void *) =
726 (void *(*) (Display *, Window, void *)) xsft->init_cb;
728 xdata = init_cb (xdpy, xwindow, xsft->setup_arg);
730 if (get_boolean_resource (xdpy, "doFPS", "DoFPS")) {
731 fpst = fps_init (xdpy, xwindow);
732 if (! xsft->fps_cb) xsft->fps_cb = screenhack_do_fps;
737 /* I don't understand why we have to do this *every frame*, but we do,
738 or else the cursor comes back on.
741 if (![self isPreview])
742 [NSCursor setHiddenUntilMouseMoves:YES];
748 /* This is just a guess, but the -fps code wants to know how long
749 we were sleeping between frames.
751 long usecs = 1000000 * [self animationTimeInterval];
752 usecs -= 200; // caller apparently sleeps for slightly less sometimes...
753 if (usecs < 0) usecs = 0;
754 fps_slept (fpst, usecs);
758 /* It turns out that [ScreenSaverView setAnimationTimeInterval] does nothing.
759 This is bad, because some of the screen hacks want to delay for long
760 periods (like 5 seconds or a minute!) between frames, and running them
761 all at 60 FPS is no good.
763 So, we don't use setAnimationTimeInterval, and just let the framework call
764 us whenever. But, we only invoke the screen hack's "draw frame" method
765 when enough time has expired.
767 This means two extra calls to gettimeofday() per frame. For fast-cycling
768 screen savers, that might actually slow them down. Oh well.
770 #### Also, we do not run the draw callback faster than the system's
771 animationTimeInterval, so if any savers are pickier about timing
772 than that, this may slow them down too much. If that's a problem,
773 then we could call draw_cb in a loop here (with usleep) until the
774 next call would put us past animationTimeInterval... But a better
775 approach would probably be to just change the saver to not do that.
778 gettimeofday (&tv, 0);
779 double now = tv.tv_sec + (tv.tv_usec / 1000000.0);
780 if (now < next_frame_time) return;
782 [self prepareContext];
785 // We do this here instead of in setFrame so that all the
786 // Xlib drawing takes place under the animation timer.
787 [self resizeContext];
794 r.size.width = backbuffer_size.width;
795 r.size.height = backbuffer_size.height;
796 # endif // USE_IPHONE
798 xsft->reshape_cb (xdpy, xwindow, xdata, r.size.width, r.size.height);
802 // Run any XtAppAddInput callbacks now.
803 // (Note that XtAppAddTimeOut callbacks have already been run by
804 // the Cocoa event loop.)
806 jwxyz_sources_run (display_sources_data (xdpy));
812 NSDisableScreenUpdates();
814 unsigned long delay = xsft->draw_cb (xdpy, xwindow, xdata);
815 if (fpst) xsft->fps_cb (xdpy, xwindow, fpst, xdata);
817 NSEnableScreenUpdates();
820 gettimeofday (&tv, 0);
821 now = tv.tv_sec + (tv.tv_usec / 1000000.0);
822 next_frame_time = now + (delay / 1000000.0);
824 # ifdef USE_IPHONE // Allow savers on the iPhone to run full-tilt.
825 if (delay < [self animationTimeInterval])
826 [self setAnimationTimeInterval:(delay / 1000000.0)];
829 # ifdef DO_GC_HACKERY
830 /* Current theory is that the 10.6 garbage collector sucks in the
833 It only does a collection when a threshold of outstanding
834 collectable allocations has been surpassed. However, CoreGraphics
835 creates lots of small collectable allocations that contain pointers
836 to very large non-collectable allocations: a small CG object that's
837 collectable referencing large malloc'd allocations (non-collectable)
838 containing bitmap data. So the large allocation doesn't get freed
839 until GC collects the small allocation, which triggers its finalizer
840 to run which frees the large allocation. So GC is deciding that it
841 doesn't really need to run, even though the process has gotten
842 enormous. GC eventually runs once pageouts have happened, but by
843 then it's too late, and the machine's resident set has been
846 So, we force an exhaustive garbage collection in this process
847 approximately every 5 seconds whether the system thinks it needs
854 objc_collect (OBJC_EXHAUSTIVE_COLLECTION);
857 # endif // DO_GC_HACKERY
861 @catch (NSException *e) {
862 [self handleException: e];
864 # endif // USE_IPHONE
868 /* On MacOS: drawRect does nothing, and animateOneFrame renders.
869 On iOS GL: drawRect does nothing, and animateOneFrame renders.
870 On iOS X11: drawRect renders, and animateOneFrame marks the view dirty.
874 - (void)drawRect:(NSRect)rect
876 if (xwindow) // clear to the X window's bg color, not necessarily black.
877 XClearWindow (xdpy, xwindow);
879 [super drawRect:rect]; // early: black.
882 - (void) animateOneFrame
889 - (void)drawRect:(NSRect)rect
891 // Render X11 into the backing store bitmap...
893 NSAssert (backbuffer, @"no back buffer");
894 UIGraphicsPushContext (backbuffer);
896 UIGraphicsPopContext();
898 // Then copy that bitmap to the screen.
900 CGContextRef cgc = UIGraphicsGetCurrentContext();
902 // Mask it to only update the parts that are exposed.
903 // CGContextClipToRect (cgc, rect);
905 double s = self.contentScaleFactor;
906 CGRect frame = [self frame];
909 target.size.width = backbuffer_size.width;
910 target.size.height = backbuffer_size.height;
911 target.origin.x = (s * frame.size.width - target.size.width) / 2;
912 target.origin.y = (s * frame.size.height - target.size.height) / 2;
914 target.origin.x /= s;
915 target.origin.y /= s;
916 target.size.width /= s;
917 target.size.height /= s;
919 CGAffineTransform t = CGAffineTransformIdentity;
921 // Rotate around center
922 float cx = frame.size.width / 2;
923 float cy = frame.size.height / 2;
924 t = CGAffineTransformTranslate (t, cx, cy);
925 t = CGAffineTransformRotate (t, -rot_current_angle / (180.0 / M_PI));
926 t = CGAffineTransformTranslate (t, -cx, -cy);
929 t = CGAffineTransformConcat (t,
930 CGAffineTransformMake ( 1, 0, 0,
931 -1, 0, frame.size.height));
933 // Clear background (visible in corners of screen during rotation)
934 if (rotation_ratio != -1) {
935 CGContextSetGrayFillColor (cgc, 0, 1);
936 CGContextFillRect (cgc, frame);
939 CGContextConcatCTM (cgc, t);
941 // Copy the backbuffer to the screen.
942 // Note that CGContextDrawImage measures in "points", not "pixels".
943 CGImageRef img = CGBitmapContextCreateImage (backbuffer);
944 CGContextDrawImage (cgc, target, img);
945 CGImageRelease (img);
948 - (void) animateOneFrame
950 [self setNeedsDisplay];
953 #endif // !USE_IPHONE
957 - (void) setFrame:(NSRect) newRect
959 [super setFrame:newRect];
962 [self createBackbuffer];
965 resized_p = YES; // The reshape_cb runs in render_x11
966 if (xwindow) { // inform Xlib that the window has changed now.
968 NSAssert (backbuffer, @"no back buffer");
969 // The backbuffer is the rotated size, and so is the xwindow.
970 jwxyz_window_resized (xdpy, xwindow,
972 backbuffer_size.width, backbuffer_size.height,
975 jwxyz_window_resized (xdpy, xwindow,
976 newRect.origin.x, newRect.origin.y,
977 newRect.size.width, newRect.size.height,
984 # ifndef USE_IPHONE // Doesn't exist on iOS
985 - (void) setFrameSize:(NSSize) newSize
987 [super setFrameSize:newSize];
990 jwxyz_window_resized (xdpy, xwindow,
991 [self frame].origin.x,
992 [self frame].origin.y,
993 newSize.width, newSize.height,
994 0); // backbuffer only on iPhone
996 # endif // !USE_IPHONE
999 +(BOOL) performGammaFade
1004 - (BOOL) hasConfigureSheet
1010 - (NSWindow *) configureSheet
1012 - (UIViewController *) configureView
1015 NSBundle *bundle = [NSBundle bundleForClass:[self class]];
1016 NSString *file = [NSString stringWithCString:xsft->progclass
1017 encoding:NSISOLatin1StringEncoding];
1018 file = [file lowercaseString];
1019 NSString *path = [bundle pathForResource:file ofType:@"xml"];
1021 NSLog (@"%@.xml does not exist in the application bundle: %@/",
1022 file, [bundle resourcePath]);
1027 UIViewController *sheet;
1028 # else // !USE_IPHONE
1030 # endif // !USE_IPHONE
1032 sheet = [[XScreenSaverConfigSheet alloc]
1033 initWithXMLFile:path
1034 options:xsft->options
1035 controller:[prefsReader userDefaultsController]
1036 defaults:[prefsReader defaultOptions]];
1038 // #### am I expected to retain this, or not? wtf.
1039 // I thought not, but if I don't do this, we (sometimes) crash.
1046 - (NSUserDefaultsController *) userDefaultsController
1048 return [prefsReader userDefaultsController];
1052 /* Announce our willingness to accept keyboard input.
1054 - (BOOL)acceptsFirstResponder
1062 /* Convert an NSEvent into an XEvent, and pass it along.
1063 Returns YES if it was handled.
1065 - (BOOL) doEvent: (NSEvent *) e
1068 if (![self isPreview] || // no event handling if actually screen-saving!
1069 ![self isAnimating] ||
1074 memset (&xe, 0, sizeof(xe));
1078 int flags = [e modifierFlags];
1079 if (flags & NSAlphaShiftKeyMask) state |= LockMask;
1080 if (flags & NSShiftKeyMask) state |= ShiftMask;
1081 if (flags & NSControlKeyMask) state |= ControlMask;
1082 if (flags & NSAlternateKeyMask) state |= Mod1Mask;
1083 if (flags & NSCommandKeyMask) state |= Mod2Mask;
1085 NSPoint p = [[[e window] contentView] convertPoint:[e locationInWindow]
1088 double s = self.contentScaleFactor;
1093 int y = s * ([self frame].size.height - p.y);
1095 xe.xany.type = type;
1101 xe.xbutton.state = state;
1102 if ([e type] == NSScrollWheel)
1103 xe.xbutton.button = ([e deltaY] > 0 ? Button4 :
1104 [e deltaY] < 0 ? Button5 :
1105 [e deltaX] > 0 ? Button6 :
1106 [e deltaX] < 0 ? Button7 :
1109 xe.xbutton.button = [e buttonNumber] + 1;
1114 xe.xmotion.state = state;
1119 NSString *ns = (([e type] == NSFlagsChanged) ? 0 :
1120 [e charactersIgnoringModifiers]);
1123 if (!ns || [ns length] == 0) // dead key
1125 // Cocoa hides the difference between left and right keys.
1126 // Also we only get KeyPress events for these, no KeyRelease
1127 // (unless we hack the mod state manually. Bleh.)
1129 if (flags & NSAlphaShiftKeyMask) k = XK_Caps_Lock;
1130 else if (flags & NSShiftKeyMask) k = XK_Shift_L;
1131 else if (flags & NSControlKeyMask) k = XK_Control_L;
1132 else if (flags & NSAlternateKeyMask) k = XK_Alt_L;
1133 else if (flags & NSCommandKeyMask) k = XK_Meta_L;
1135 else if ([ns length] == 1) // real key
1137 switch ([ns characterAtIndex:0]) {
1138 case NSLeftArrowFunctionKey: k = XK_Left; break;
1139 case NSRightArrowFunctionKey: k = XK_Right; break;
1140 case NSUpArrowFunctionKey: k = XK_Up; break;
1141 case NSDownArrowFunctionKey: k = XK_Down; break;
1142 case NSPageUpFunctionKey: k = XK_Page_Up; break;
1143 case NSPageDownFunctionKey: k = XK_Page_Down; break;
1144 case NSHomeFunctionKey: k = XK_Home; break;
1145 case NSPrevFunctionKey: k = XK_Prior; break;
1146 case NSNextFunctionKey: k = XK_Next; break;
1147 case NSBeginFunctionKey: k = XK_Begin; break;
1148 case NSEndFunctionKey: k = XK_End; break;
1152 [ns cStringUsingEncoding:NSISOLatin1StringEncoding];
1153 k = (s && *s ? *s : 0);
1159 xe.xkey.keycode = k;
1160 xe.xkey.state = state;
1164 NSAssert (0, @"unknown X11 event type: %d", type);
1169 [self prepareContext];
1170 BOOL result = xsft->event_cb (xdpy, xwindow, xdata, &xe);
1176 - (void) mouseDown: (NSEvent *) e
1178 if (! [self doEvent:e type:ButtonPress])
1179 [super mouseDown:e];
1182 - (void) mouseUp: (NSEvent *) e
1184 if (! [self doEvent:e type:ButtonRelease])
1188 - (void) otherMouseDown: (NSEvent *) e
1190 if (! [self doEvent:e type:ButtonPress])
1191 [super otherMouseDown:e];
1194 - (void) otherMouseUp: (NSEvent *) e
1196 if (! [self doEvent:e type:ButtonRelease])
1197 [super otherMouseUp:e];
1200 - (void) mouseMoved: (NSEvent *) e
1202 if (! [self doEvent:e type:MotionNotify])
1203 [super mouseMoved:e];
1206 - (void) mouseDragged: (NSEvent *) e
1208 if (! [self doEvent:e type:MotionNotify])
1209 [super mouseDragged:e];
1212 - (void) otherMouseDragged: (NSEvent *) e
1214 if (! [self doEvent:e type:MotionNotify])
1215 [super otherMouseDragged:e];
1218 - (void) scrollWheel: (NSEvent *) e
1220 if (! [self doEvent:e type:ButtonPress])
1221 [super scrollWheel:e];
1224 - (void) keyDown: (NSEvent *) e
1226 if (! [self doEvent:e type:KeyPress])
1230 - (void) keyUp: (NSEvent *) e
1232 if (! [self doEvent:e type:KeyRelease])
1236 - (void) flagsChanged: (NSEvent *) e
1238 if (! [self doEvent:e type:KeyPress])
1239 [super flagsChanged:e];
1245 - (void) stopAndClose
1247 if ([self isAnimating])
1248 [self stopAnimation];
1250 /* Need to make the SaverListController be the firstResponder again
1251 so that it can continue to receive its own shake events. I
1252 suppose that this abstraction-breakage means that I'm adding
1253 XScreenSaverView to the UINavigationController wrong...
1255 UIViewController *v = [[self window] rootViewController];
1256 if ([v isKindOfClass: [UINavigationController class]]) {
1257 UINavigationController *n = (UINavigationController *) v;
1258 [[n topViewController] becomeFirstResponder];
1261 // [self removeFromSuperview];
1262 [UIView animateWithDuration: 0.5
1263 animations:^{ self.alpha = 0.0; }
1264 completion:^(BOOL finished) {
1265 [self removeFromSuperview];
1271 - (void) stopAndClose:(Bool)relaunch_p
1273 [self stopAndClose];
1275 if (relaunch_p) { // Fake a shake on the SaverListController.
1276 UIViewController *v = [[self window] rootViewController];
1277 if ([v isKindOfClass: [UINavigationController class]]) {
1278 UINavigationController *n = (UINavigationController *) v;
1279 [[n topViewController] motionEnded: UIEventSubtypeMotionShake
1286 /* Called after the device's orientation has changed.
1288 Note: we could include a subclass of UIViewController which
1289 contains a shouldAutorotateToInterfaceOrientation method that
1290 returns YES, in which case Core Animation would auto-rotate our
1291 View for us in response to rotation events... but, that interacts
1292 badly with the EAGLContext -- if you introduce Core Animation into
1293 the path, the OpenGL pipeline probably falls back on software
1294 rendering and performance goes to hell. Also, the scaling and
1295 rotation that Core Animation does interacts incorrectly with the GL
1298 So, we have to hack the rotation animation manually, in the GL world.
1300 Possibly XScreenSaverView should use Core Animation, and
1301 XScreenSaverGLView should override that.
1303 - (void)didRotate:(NSNotification *)notification
1305 UIDeviceOrientation current = [[UIDevice currentDevice] orientation];
1307 /* If the simulator starts up in the rotated position, sometimes
1308 the UIDevice says we're in Portrait when we're not -- but it
1309 turns out that the UINavigationController knows what's up!
1310 So get it from there.
1312 if (current == UIDeviceOrientationUnknown) {
1313 switch ([[[self window] rootViewController] interfaceOrientation]) {
1314 case UIInterfaceOrientationPortrait:
1315 current = UIDeviceOrientationPortrait;
1317 case UIInterfaceOrientationPortraitUpsideDown:
1318 current = UIDeviceOrientationPortraitUpsideDown;
1320 case UIInterfaceOrientationLandscapeLeft: // It's opposite day
1321 current = UIDeviceOrientationLandscapeRight;
1323 case UIInterfaceOrientationLandscapeRight:
1324 current = UIDeviceOrientationLandscapeLeft;
1331 /* On the iPad (but not iPhone 3GS, or the simulator) sometimes we get
1332 an orientation change event with an unknown orientation. Those seem
1333 to always be immediately followed by another orientation change with
1334 a *real* orientation change, so let's try just ignoring those bogus
1335 ones and hoping that the real one comes in shortly...
1337 if (current == UIDeviceOrientationUnknown)
1340 if (rotation_ratio >= 0) return; // in the midst of rotation animation
1341 if (orientation == current) return; // no change
1343 // When transitioning to FaceUp or FaceDown, pretend there was no change.
1344 if (current == UIDeviceOrientationFaceUp ||
1345 current == UIDeviceOrientationFaceDown)
1348 new_orientation = current; // current animation target
1349 rotation_ratio = 0; // start animating
1350 rot_start_time = double_time();
1352 switch (orientation) {
1353 case UIDeviceOrientationLandscapeLeft: angle_from = 90; break;
1354 case UIDeviceOrientationLandscapeRight: angle_from = 270; break;
1355 case UIDeviceOrientationPortraitUpsideDown: angle_from = 180; break;
1356 default: angle_from = 0; break;
1359 switch (new_orientation) {
1360 case UIDeviceOrientationLandscapeLeft: angle_to = 90; break;
1361 case UIDeviceOrientationLandscapeRight: angle_to = 270; break;
1362 case UIDeviceOrientationPortraitUpsideDown: angle_to = 180; break;
1363 default: angle_to = 0; break;
1366 NSRect ff = [self frame];
1368 switch (orientation) {
1369 case UIDeviceOrientationLandscapeRight: // from landscape
1370 case UIDeviceOrientationLandscapeLeft:
1371 rot_from.width = ff.size.height;
1372 rot_from.height = ff.size.width;
1374 default: // from portrait
1375 rot_from.width = ff.size.width;
1376 rot_from.height = ff.size.height;
1380 switch (new_orientation) {
1381 case UIDeviceOrientationLandscapeRight: // to landscape
1382 case UIDeviceOrientationLandscapeLeft:
1383 rot_to.width = ff.size.height;
1384 rot_to.height = ff.size.width;
1386 default: // to portrait
1387 rot_to.width = ff.size.width;
1388 rot_to.height = ff.size.height;
1393 // If we've done a rotation but the saver hasn't been initialized yet,
1394 // don't bother going through an X11 resize, but just do it now.
1395 rot_start_time = 0; // dawn of time
1396 [self hackRotation];
1401 /* I believe we can't use UIGestureRecognizer for tracking touches
1402 because UIPanGestureRecognizer doesn't give us enough detail in its
1405 In the simulator, multi-touch sequences look like this:
1407 touchesBegan [touchA, touchB]
1408 touchesEnd [touchA, touchB]
1410 But on real devices, sometimes you get that, but sometimes you get:
1412 touchesBegan [touchA, touchB]
1418 touchesBegan [touchA]
1419 touchesBegan [touchB]
1423 So the only way to properly detect a "pinch" gesture is to remember
1424 the start-point of each touch as it comes in; and the end-point of
1425 each touch as those come in; and only process the gesture once the
1426 number of touchEnds matches the number of touchBegins.
1430 rotate_mouse (int *x, int *y, int w, int h, int rot)
1432 int ox = *x, oy = *y;
1433 if (rot > 45 && rot < 135) { *x = oy; *y = w-ox; }
1434 else if (rot < -45 && rot > -135) { *x = h-oy; *y = ox; }
1435 else if (rot > 135 || rot < -135) { *x = w-ox; *y = h-oy; }
1439 #if 0 // AudioToolbox/AudioToolbox.h
1442 // There's no way to play a standard system alert sound!
1443 // We'd have to include our own WAV for that. Eh, fuck it.
1444 AudioServicesPlaySystemSound (kSystemSoundID_Vibrate);
1445 # if TARGET_IPHONE_SIMULATOR
1446 NSLog(@"BEEP"); // The sim doesn't vibrate.
1452 /* We distinguish between taps and drags.
1453 - Drags (down, motion, up) are sent to the saver to handle.
1454 - Single-taps exit the saver.
1455 This means a saver cannot respond to a single-tap. Only a few try to.
1458 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
1462 if (xsft->event_cb && xwindow) {
1463 double s = self.contentScaleFactor;
1465 memset (&xe, 0, sizeof(xe));
1467 int w = s * [self frame].size.width;
1468 int h = s * [self frame].size.height;
1469 for (UITouch *touch in touches) {
1470 CGPoint p = [touch locationInView:self];
1471 xe.xany.type = ButtonPress;
1472 xe.xbutton.button = i + 1;
1473 xe.xbutton.button = i + 1;
1474 xe.xbutton.x = s * p.x;
1475 xe.xbutton.y = s * p.y;
1476 rotate_mouse (&xe.xbutton.x, &xe.xbutton.y, w, h, rot_current_angle);
1477 jwxyz_mouse_moved (xdpy, xwindow, xe.xbutton.x, xe.xbutton.y);
1479 // Ignore return code: don't care whether the hack handled it.
1480 xsft->event_cb (xdpy, xwindow, xdata, &xe);
1482 // Remember when/where this was, to determine tap versus drag or hold.
1483 tap_time = double_time();
1487 break; // No pinches: only look at the first touch.
1493 - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
1495 if (xsft->event_cb && xwindow) {
1496 double s = self.contentScaleFactor;
1498 memset (&xe, 0, sizeof(xe));
1500 int w = s * [self frame].size.width;
1501 int h = s * [self frame].size.height;
1502 for (UITouch *touch in touches) {
1503 CGPoint p = [touch locationInView:self];
1505 // If the ButtonRelease came less than half a second after ButtonPress,
1506 // and didn't move far, then this was a tap, not a drag or a hold.
1507 // Interpret it as "exit".
1509 double dist = sqrt (((p.x - tap_point.x) * (p.x - tap_point.x)) +
1510 ((p.y - tap_point.y) * (p.y - tap_point.y)));
1511 if (tap_time + 0.5 >= double_time() && dist < 20) {
1512 [self stopAndClose];
1516 xe.xany.type = ButtonRelease;
1517 xe.xbutton.button = i + 1;
1518 xe.xbutton.x = s * p.x;
1519 xe.xbutton.y = s * p.y;
1520 rotate_mouse (&xe.xbutton.x, &xe.xbutton.y, w, h, rot_current_angle);
1521 jwxyz_mouse_moved (xdpy, xwindow, xe.xbutton.x, xe.xbutton.y);
1522 xsft->event_cb (xdpy, xwindow, xdata, &xe);
1524 break; // No pinches: only look at the first touch.
1530 - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
1532 if (xsft->event_cb && xwindow) {
1533 double s = self.contentScaleFactor;
1535 memset (&xe, 0, sizeof(xe));
1537 int w = s * [self frame].size.width;
1538 int h = s * [self frame].size.height;
1539 for (UITouch *touch in touches) {
1540 CGPoint p = [touch locationInView:self];
1541 xe.xany.type = MotionNotify;
1542 xe.xmotion.x = s * p.x;
1543 xe.xmotion.y = s * p.y;
1544 rotate_mouse (&xe.xbutton.x, &xe.xbutton.y, w, h, rot_current_angle);
1545 jwxyz_mouse_moved (xdpy, xwindow, xe.xmotion.x, xe.xmotion.y);
1546 xsft->event_cb (xdpy, xwindow, xdata, &xe);
1548 break; // No pinches: only look at the first touch.
1554 /* We need this to respond to "shake" gestures
1556 - (BOOL)canBecomeFirstResponder
1561 - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
1566 - (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event
1570 /* Shake means exit and launch a new saver.
1572 - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
1574 [self stopAndClose:YES];
1578 - (void)setScreenLocked:(BOOL)locked
1580 if (screenLocked == locked) return;
1581 screenLocked = locked;
1583 if ([self isAnimating])
1584 [self stopAnimation];
1586 if (! [self isAnimating])
1587 [self startAnimation];
1592 #endif // USE_IPHONE
1597 /* Utility functions...
1600 static PrefsReader *
1601 get_prefsReader (Display *dpy)
1603 XScreenSaverView *view = jwxyz_window_view (XRootWindow (dpy, 0));
1604 if (!view) return 0;
1605 return [view prefsReader];
1610 get_string_resource (Display *dpy, char *name, char *class)
1612 return [get_prefsReader(dpy) getStringResource:name];
1616 get_boolean_resource (Display *dpy, char *name, char *class)
1618 return [get_prefsReader(dpy) getBooleanResource:name];
1622 get_integer_resource (Display *dpy, char *name, char *class)
1624 return [get_prefsReader(dpy) getIntegerResource:name];
1628 get_float_resource (Display *dpy, char *name, char *class)
1630 return [get_prefsReader(dpy) getFloatResource:name];