1 /* xscreensaver, Copyright (c) 2006-2014 Jamie Zawinski <jwz@jwz.org>
3 * Permission to use, copy, modify, distribute, and sell this software and its
4 * documentation for any purpose is hereby granted without fee, provided that
5 * the above copyright notice appear in all copies and that both that
6 * copyright notice and this permission notice appear in supporting
7 * documentation. No representations are made about the suitability of this
8 * software for any purpose. It is provided "as is" without express or
12 /* This is a subclass of Apple's ScreenSaverView that knows how to run
13 xscreensaver programs without X11 via the dark magic of the "jwxyz"
14 library. In xscreensaver terminology, this is the replacement for
15 the "screenhack.c" module.
18 #import <QuartzCore/QuartzCore.h>
20 #import "XScreenSaverView.h"
21 #import "XScreenSaverConfigSheet.h"
23 #import "screenhackI.h"
24 #import "xlockmoreI.h"
25 #import "jwxyz-timers.h"
28 /* Garbage collection only exists if we are being compiled against the
29 10.6 SDK or newer, not if we are building against the 10.4 SDK.
31 #ifndef MAC_OS_X_VERSION_10_6
32 # define MAC_OS_X_VERSION_10_6 1060 /* undefined in 10.4 SDK, grr */
34 #if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6 /* 10.6 SDK */
35 # import <objc/objc-auto.h>
36 # define DO_GC_HACKERY
39 extern struct xscreensaver_function_table *xscreensaver_function_table;
41 /* Global variables used by the screen savers
44 const char *progclass;
50 # define NSSizeToCGSize(x) (x)
52 extern NSDictionary *make_function_table_dict(void); // ios-function-table.m
54 /* Stub definition of the superclass, for iPhone.
56 @implementation ScreenSaverView
58 NSTimeInterval anim_interval;
63 - (id)initWithFrame:(NSRect)frame isPreview:(BOOL)isPreview {
64 self = [super initWithFrame:frame];
66 anim_interval = 1.0/30;
69 - (NSTimeInterval)animationTimeInterval { return anim_interval; }
70 - (void)setAnimationTimeInterval:(NSTimeInterval)i { anim_interval = i; }
71 - (BOOL)hasConfigureSheet { return NO; }
72 - (NSWindow *)configureSheet { return nil; }
73 - (NSView *)configureView { return nil; }
74 - (BOOL)isPreview { return NO; }
75 - (BOOL)isAnimating { return animating_p; }
76 - (void)animateOneFrame { }
78 - (void)startAnimation {
79 if (animating_p) return;
81 anim_timer = [NSTimer scheduledTimerWithTimeInterval: anim_interval
83 selector:@selector(animateOneFrame)
88 - (void)stopAnimation {
90 [anim_timer invalidate];
97 # endif // !USE_IPHONE
101 @interface XScreenSaverView (Private)
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);
123 // #### Analyze says "Potential leak of an object stored into cfb"
126 name = [[path lastPathComponent] stringByDeletingPathExtension];
128 name = [[name lowercaseString]
129 stringByReplacingOccurrencesOfString:@" "
133 // CFBundleGetDataPointerForName doesn't work in "Archive" builds.
134 // I'm guessing that symbol-stripping is mandatory. Fuck.
135 NSString *table_name = [name stringByAppendingString:
136 @"_xscreensaver_function_table"];
137 void *addr = CFBundleGetDataPointerForName (cfb, (CFStringRef) table_name);
141 NSLog (@"no symbol \"%@\" for \"%@\"", table_name, path);
144 // Depends on the auto-generated "ios-function-table.m" being up to date.
145 if (! function_tables)
146 function_tables = [make_function_table_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"},
226 { "-foreground", ".foreground", XrmoptionSepArg, 0 },
227 { "-fg", ".foreground", XrmoptionSepArg, 0 },
228 { "-background", ".background", XrmoptionSepArg, 0 },
229 { "-bg", ".background", XrmoptionSepArg, 0 },
232 // <xscreensaver-updater />
233 { "-" SUSUEnableAutomaticChecksKey,
234 "." SUSUEnableAutomaticChecksKey, XrmoptionNoArg, "True" },
235 { "-no-" SUSUEnableAutomaticChecksKey,
236 "." SUSUEnableAutomaticChecksKey, XrmoptionNoArg, "False" },
237 { "-" SUAutomaticallyUpdateKey,
238 "." SUAutomaticallyUpdateKey, XrmoptionNoArg, "True" },
239 { "-no-" SUAutomaticallyUpdateKey,
240 "." SUAutomaticallyUpdateKey, XrmoptionNoArg, "False" },
241 { "-" SUSendProfileInfoKey,
242 "." SUSendProfileInfoKey, XrmoptionNoArg,"True" },
243 { "-no-" SUSendProfileInfoKey,
244 "." SUSendProfileInfoKey, XrmoptionNoArg,"False"},
245 { "-" SUScheduledCheckIntervalKey,
246 "." SUScheduledCheckIntervalKey, XrmoptionSepArg, 0 },
247 # endif // !USE_IPHONE
251 static const char *default_defaults [] = {
253 ".doubleBuffer: True",
254 ".multiSample: False",
262 ".textURL: http://en.wikipedia.org/w/index.php?title=Special:NewPages&feed=rss",
264 ".grabDesktopImages: yes",
266 ".chooseRandomImages: no",
268 ".chooseRandomImages: yes",
270 ".imageDirectory: ~/Pictures",
272 ".texFontCacheSize: 30",
276 # define STR(S) STR1(S)
277 # define __objc_yes Yes
278 # define __objc_no No
279 "." SUSUEnableAutomaticChecksKey ": " STR(SUSUEnableAutomaticChecksDef),
280 "." SUAutomaticallyUpdateKey ": " STR(SUAutomaticallyUpdateDef),
281 "." SUSendProfileInfoKey ": " STR(SUSendProfileInfoDef),
282 "." SUScheduledCheckIntervalKey ": " STR(SUScheduledCheckIntervalDef),
287 # endif // USE_IPHONE
292 for (i = 0; default_options[i].option; i++)
294 for (i = 0; opts[i].option; i++)
297 XrmOptionDescRec *opts2 = (XrmOptionDescRec *)
298 calloc (count + 1, sizeof (*opts2));
302 while (default_options[j].option) {
303 opts2[i] = default_options[j];
307 while (opts[j].option) {
318 for (i = 0; default_defaults[i]; i++)
320 for (i = 0; defs[i]; i++)
323 const char **defs2 = (const char **) calloc (count + 1, sizeof (*defs2));
327 while (default_defaults[j]) {
328 defs2[i] = default_defaults[j];
342 /* Returns the current time in seconds as a double.
348 # ifdef GETTIMEOFDAY_TWO_ARGS
350 gettimeofday(&now, &tzp);
355 return (now.tv_sec + ((double) now.tv_usec * 0.000001));
359 #if TARGET_IPHONE_SIMULATOR
361 orientname(unsigned long o)
364 case UIDeviceOrientationUnknown: return "Unknown";
365 case UIDeviceOrientationPortrait: return "Portrait";
366 case UIDeviceOrientationPortraitUpsideDown: return "PortraitUpsideDown";
367 case UIDeviceOrientationLandscapeLeft: return "LandscapeLeft";
368 case UIDeviceOrientationLandscapeRight: return "LandscapeRight";
369 case UIDeviceOrientationFaceUp: return "FaceUp";
370 case UIDeviceOrientationFaceDown: return "FaceDown";
371 default: return "ERROR";
374 #endif // TARGET_IPHONE_SIMULATOR
377 - (id) initWithFrame:(NSRect)frame
378 saverName:(NSString *)saverName
379 isPreview:(BOOL)isPreview
381 if (! (self = [super initWithFrame:frame isPreview:isPreview]))
384 xsft = [self findFunctionTable: saverName];
394 xsft->setup_cb (xsft, xsft->setup_arg);
396 /* The plist files for these preferences show up in
397 $HOME/Library/Preferences/ByHost/ in a file named like
398 "org.jwz.xscreensaver.<SAVERNAME>.<NUMBERS>.plist"
400 NSString *name = [NSString stringWithCString:xsft->progclass
401 encoding:NSISOLatin1StringEncoding];
402 name = [@"org.jwz.xscreensaver." stringByAppendingString:name];
403 [self setResourcesEnv:name];
406 XrmOptionDescRec *opts = 0;
407 const char **defs = 0;
408 add_default_options (xsft->options, xsft->defaults, &opts, &defs);
409 prefsReader = [[PrefsReader alloc]
410 initWithName:name xrmKeys:opts defaults:defs];
412 // free (opts); // bah, we need these! #### leak!
413 xsft->options = opts;
415 progname = progclass = xsft->progclass;
420 double s = [self hackedContentScaleFactor];
425 CGSize bb_size; // pixels, not points
426 bb_size.width = s * frame.size.width;
427 bb_size.height = s * frame.size.height;
430 initial_bounds = rot_current_size = rot_from = rot_to = bb_size;
433 orientation = UIDeviceOrientationUnknown;
434 [self didRotate:nil];
437 // So we can tell when we're docked.
438 [UIDevice currentDevice].batteryMonitoringEnabled = YES;
439 # endif // USE_IPHONE
441 # ifdef USE_BACKBUFFER
442 [self createBackbuffer:bb_size];
451 # if !defined(USE_IPHONE) && defined(BACKBUFFER_CALAYER)
452 [self setLayer: [CALayer layer]];
453 self.layer.delegate = self;
454 self.layer.opaque = YES;
455 [self setWantsLayer: YES];
456 # endif // !USE_IPHONE && BACKBUFFER_CALAYER
460 - (id) initWithFrame:(NSRect)frame isPreview:(BOOL)p
462 return [self initWithFrame:frame saverName:0 isPreview:p];
468 NSAssert(![self isAnimating], @"still animating");
469 NSAssert(!xdata, @"xdata not yet freed");
470 NSAssert(!xdpy, @"xdpy not yet freed");
472 # ifdef USE_BACKBUFFER
474 CGContextRelease (backbuffer);
477 CGColorSpaceRelease (colorspace);
479 # ifdef BACKBUFFER_CGCONTEXT
481 CGContextRelease (window_ctx);
482 # endif // BACKBUFFER_CGCONTEXT
484 # endif // USE_BACKBUFFER
486 [prefsReader release];
494 - (PrefsReader *) prefsReader
501 - (void) lockFocus { }
502 - (void) unlockFocus { }
508 /* A few seconds after the saver launches, we store the "wasRunning"
509 preference. This is so that if the saver is crashing at startup,
510 we don't launch it again next time, getting stuck in a crash loop.
512 - (void) allSystemsGo: (NSTimer *) timer
514 NSAssert (timer == crash_timer, @"crash timer screwed up");
517 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
518 [prefs setBool:YES forKey:@"wasRunning"];
524 - (void) startAnimation
526 NSAssert(![self isAnimating], @"already animating");
527 NSAssert(!initted_p && !xdata, @"already initialized");
529 // See comment in render_x11() for why this value is important:
530 [self setAnimationTimeInterval: 1.0 / 120.0];
532 [super startAnimation];
533 /* We can't draw on the window from this method, so we actually do the
534 initialization of the screen saver (xsft->init_cb) in the first call
535 to animateOneFrame() instead.
540 [crash_timer invalidate];
542 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
543 [prefs removeObjectForKey:@"wasRunning"];
546 crash_timer = [NSTimer scheduledTimerWithTimeInterval: 5
548 selector:@selector(allSystemsGo:)
552 # endif // USE_IPHONE
554 // Never automatically turn the screen off if we are docked,
555 // and an animation is running.
558 [UIApplication sharedApplication].idleTimerDisabled =
559 ([UIDevice currentDevice].batteryState != UIDeviceBatteryStateUnplugged);
560 [[UIApplication sharedApplication]
561 setStatusBarHidden:YES withAnimation:UIStatusBarAnimationNone];
566 - (void)stopAnimation
568 NSAssert([self isAnimating], @"not animating");
572 [self lockFocus]; // in case something tries to draw from here
573 [self prepareContext];
575 /* I considered just not even calling the free callback at all...
576 But webcollage-cocoa needs it, to kill the inferior webcollage
577 processes (since the screen saver framework never generates a
578 SIGPIPE for them...) Instead, I turned off the free call in
579 xlockmore.c, which is where all of the bogus calls are anyway.
581 xsft->free_cb (xdpy, xwindow, xdata);
584 // xdpy must be freed before dealloc is called, because xdpy owns a
585 // circular reference to the parent XScreenSaverView.
586 jwxyz_free_display (xdpy);
590 // setup_p = NO; // #### wait, do we need this?
597 [crash_timer invalidate];
599 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
600 [prefs removeObjectForKey:@"wasRunning"];
602 # endif // USE_IPHONE
604 [super stopAnimation];
606 // When an animation is no longer running (e.g., looking at the list)
607 // then it's ok to power off the screen when docked.
610 [UIApplication sharedApplication].idleTimerDisabled = NO;
611 [[UIApplication sharedApplication]
612 setStatusBarHidden:NO withAnimation:UIStatusBarAnimationNone];
617 /* Hook for the XScreenSaverGLView subclass
619 - (void) prepareContext
623 /* Hook for the XScreenSaverGLView subclass
625 - (void) resizeContext
631 screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
633 fps_compute (fpst, 0, -1);
640 /* On iPhones with Retina displays, we can draw the savers in "real"
641 pixels, and that works great. The 320x480 "point" screen is really
642 a 640x960 *pixel* screen. However, Retina iPads have 768x1024
643 point screens which are 1536x2048 pixels, and apparently that's
644 enough pixels that copying those bits to the screen is slow. Like,
645 drops us from 15fps to 7fps. So, on Retina iPads, we don't draw in
646 real pixels. This will probably make the savers look better
647 anyway, since that's a higher resolution than most desktop monitors
648 have even today. (This is only true for X11 programs, not GL
649 programs. Those are fine at full rez.)
651 This method is overridden in XScreenSaverGLView, since this kludge
652 isn't necessary for GL programs, being resolution independent by
655 - (CGFloat) hackedContentScaleFactor
657 NSSize ssize = [[[UIScreen mainScreen] currentMode] size];
658 NSSize bsize = [self bounds].size;
660 // Ratio of screen size in pixels to view size in points.
661 GLfloat s = ((ssize.width > ssize.height ? ssize.width : ssize.height) /
662 (bsize.width > bsize.height ? bsize.width : bsize.height));
664 if (ssize.width >= 1024 && ssize.height >= 1024)
671 static GLfloat _global_rot_current_angle_kludge;
673 double current_device_rotation (void)
675 return -_global_rot_current_angle_kludge;
679 - (void) hackRotation
681 if (rotation_ratio >= 0) { // in the midst of a rotation animation
683 # define CLAMP180(N) while (N < 0) N += 360; while (N > 180) N -= 360
684 GLfloat f = angle_from;
685 GLfloat t = angle_to;
688 GLfloat dist = -(t-f);
691 // Intermediate angle.
692 rot_current_angle = f - rotation_ratio * dist;
694 // Intermediate frame size.
695 rot_current_size.width = rot_from.width +
696 rotation_ratio * (rot_to.width - rot_from.width);
697 rot_current_size.height = rot_from.height +
698 rotation_ratio * (rot_to.height - rot_from.height);
700 // Tick animation. Complete rotation in 1/6th sec.
701 double now = double_time();
702 double duration = 1/6.0;
703 rotation_ratio = 1 - ((rot_start_time + duration - now) / duration);
705 if (rotation_ratio > 1 || ignore_rotation_p) { // Done animating.
706 orientation = new_orientation;
707 rot_current_angle = angle_to;
708 rot_current_size = rot_to;
711 # if TARGET_IPHONE_SIMULATOR
712 NSLog (@"rotation ended: %s %d, %d x %d",
713 orientname(orientation), (int) rot_current_angle,
714 (int) rot_current_size.width, (int) rot_current_size.height);
717 // Check orientation again in case we rotated again while rotating:
718 // this is a no-op if nothing has changed.
719 [self didRotate:nil];
721 } else { // Not animating a rotation.
722 rot_current_angle = angle_to;
723 rot_current_size = rot_to;
726 CLAMP180(rot_current_angle);
727 _global_rot_current_angle_kludge = rot_current_angle;
731 CGSize rotsize = ((ignore_rotation_p || ![self reshapeRotatedWindow])
734 if ((int) backbuffer_size.width != (int) rotsize.width ||
735 (int) backbuffer_size.height != (int) rotsize.height)
740 - (void)alertView:(UIAlertView *)av clickedButtonAtIndex:(NSInteger)i
742 if (i == 0) exit (-1); // Cancel
743 [self stopAndClose:NO]; // Keep going
746 - (void) handleException: (NSException *)e
748 NSLog (@"Caught exception: %@", e);
749 [[[UIAlertView alloc] initWithTitle:
750 [NSString stringWithFormat: @"%s crashed!",
753 [NSString stringWithFormat:
754 @"The error message was:"
756 "If it keeps crashing, try "
757 "resetting its options.",
760 cancelButtonTitle: @"Exit"
761 otherButtonTitles: @"Keep going", nil]
763 [self stopAnimation];
769 #ifdef USE_BACKBUFFER
771 /* Create a bitmap context into which we render everything.
772 If the desired size has changed, re-created it.
773 new_size is in rotated pixels, not points: the same size
774 and shape as the X11 window as seen by the hacks.
776 - (void) createBackbuffer:(CGSize)new_size
778 // Colorspaces and CGContexts only happen with non-GL hacks.
780 CGColorSpaceRelease (colorspace);
781 # ifdef BACKBUFFER_CGCONTEXT
783 CGContextRelease (window_ctx);
786 NSWindow *window = [self window];
788 if (window && xdpy) {
791 # if defined(BACKBUFFER_CGCONTEXT)
792 // TODO: This was borrowed from jwxyz_window_resized, and should
793 // probably be refactored.
795 // Figure out which screen the window is currently on.
796 CGDirectDisplayID cgdpy = 0;
800 // TODO: XTranslateCoordinates is returning (0,1200) on my system.
802 // In any case, those weren't valid coordinates for CGGetDisplaysWithPoint.
803 // XTranslateCoordinates (xdpy, xwindow, NULL, 0, 0, &wx, &wy, NULL);
808 p0 = [window convertBaseToScreen:p0];
809 CGPoint p = {p0.x, p0.y};
811 CGGetDisplaysWithPoint (p, 1, &cgdpy, &n);
812 NSAssert (cgdpy, @"unable to find CGDisplay");
816 // Figure out this screen's colorspace, and use that for every CGImage.
818 CMProfileRef profile = 0;
820 // CMGetProfileByAVID is deprecated as of OS X 10.6, but there's no
821 // documented replacement as of OS X 10.9.
822 // http://lists.apple.com/archives/colorsync-dev/2012/Nov/msg00001.html
823 CMGetProfileByAVID ((CMDisplayIDType) cgdpy, &profile);
824 NSAssert (profile, @"unable to find colorspace profile");
825 colorspace = CGColorSpaceCreateWithPlatformColorSpace (profile);
826 NSAssert (colorspace, @"unable to find colorspace");
828 # elif defined(BACKBUFFER_CALAYER)
829 // Was apparently faster until 10.9.
830 colorspace = CGColorSpaceCreateDeviceRGB ();
831 # endif // BACKBUFFER_CALAYER
833 # ifdef BACKBUFFER_CGCONTEXT
834 window_ctx = [[window graphicsContext] graphicsPort];
835 CGContextRetain (window_ctx);
836 # endif // BACKBUFFER_CGCONTEXT
840 # ifdef BACKBUFFER_CGCONTEXT
842 # endif // BACKBUFFER_CGCONTEXT
843 colorspace = CGColorSpaceCreateDeviceRGB();
847 (int)backbuffer_size.width == (int)new_size.width &&
848 (int)backbuffer_size.height == (int)new_size.height)
851 CGContextRef ob = backbuffer;
853 CGSize osize = backbuffer_size; // pixels, not points.
854 backbuffer_size = new_size; // pixels, not points.
856 # if TARGET_IPHONE_SIMULATOR
857 NSLog(@"backbuffer %.0fx%.0f",
858 backbuffer_size.width, backbuffer_size.height);
861 backbuffer = CGBitmapContextCreate (NULL,
862 (int)backbuffer_size.width,
863 (int)backbuffer_size.height,
865 (int)backbuffer_size.width * 4,
867 // kCGImageAlphaPremultipliedLast
868 (kCGImageAlphaNoneSkipFirst |
869 kCGBitmapByteOrder32Host)
871 NSAssert (backbuffer, @"unable to allocate back buffer");
875 r.origin.x = r.origin.y = 0;
876 r.size = backbuffer_size;
877 CGContextSetGrayFillColor (backbuffer, 0, 1);
878 CGContextFillRect (backbuffer, r);
881 // Restore old bits, as much as possible, to the X11 upper left origin.
883 CGRect rect; // pixels, not points
885 rect.origin.y = (backbuffer_size.height - osize.height);
888 CGImageRef img = CGBitmapContextCreateImage (ob);
889 CGContextDrawImage (backbuffer, rect, img);
890 CGImageRelease (img);
891 CGContextRelease (ob);
895 #endif // USE_BACKBUFFER
898 /* Inform X11 that the size of our window has changed.
902 if (!xwindow) return; // early
904 CGSize new_size; // pixels, not points
906 # ifdef USE_BACKBUFFER
908 CGSize rotsize = ((ignore_rotation_p || ![self reshapeRotatedWindow])
911 new_size.width = rotsize.width;
912 new_size.height = rotsize.height;
913 # else // !USE_IPHONE
914 new_size = NSSizeToCGSize([self bounds].size);
915 # endif // !USE_IPHONE
917 [self createBackbuffer:new_size];
918 jwxyz_window_resized (xdpy, xwindow, 0, 0, new_size.width, new_size.height,
920 # else // !USE_BACKBUFFER
921 new_size = [self bounds].size;
922 jwxyz_window_resized (xdpy, xwindow, 0, 0, new_size.width, new_size.height,
924 # endif // !USE_BACKBUFFER
926 # if TARGET_IPHONE_SIMULATOR
927 NSLog(@"reshape %.0fx%.0f", new_size.width, new_size.height);
930 // Next time render_x11 is called, run the saver's reshape_cb.
940 if (orientation == UIDeviceOrientationUnknown)
941 [self didRotate:nil];
948 # ifdef USE_BACKBUFFER
949 NSAssert (backbuffer, @"no back buffer");
950 xdpy = jwxyz_make_display (self, backbuffer);
952 xdpy = jwxyz_make_display (self, 0);
954 xwindow = XRootWindow (xdpy, 0);
957 /* Some X11 hacks (fluidballs) want to ignore all rotation events. */
959 get_boolean_resource (xdpy, "ignoreRotation", "IgnoreRotation");
960 # endif // USE_IPHONE
968 xsft->setup_cb (xsft, xsft->setup_arg);
972 NSAssert(!xdata, @"xdata already initialized");
978 XSetWindowBackground (xdpy, xwindow,
979 get_pixel_resource (xdpy, 0,
980 "background", "Background"));
981 XClearWindow (xdpy, xwindow);
984 [[self window] setAcceptsMouseMovedEvents:YES];
987 /* In MacOS 10.5, this enables "QuartzGL", meaning that the Quartz
988 drawing primitives will run on the GPU instead of the CPU.
989 It seems like it might make things worse rather than better,
990 though... Plus it makes us binary-incompatible with 10.4.
992 # if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5
993 [[self window] setPreferredBackingLocation:
994 NSWindowBackingLocationVideoMemory];
998 /* Kludge: even though the init_cb functions are declared to take 2 args,
999 actually call them with 3, for the benefit of xlockmore_init() and
1002 void *(*init_cb) (Display *, Window, void *) =
1003 (void *(*) (Display *, Window, void *)) xsft->init_cb;
1005 xdata = init_cb (xdpy, xwindow, xsft->setup_arg);
1006 // NSAssert(xdata, @"no xdata from init");
1007 if (! xdata) abort();
1009 if (get_boolean_resource (xdpy, "doFPS", "DoFPS")) {
1010 fpst = fps_init (xdpy, xwindow);
1011 if (! xsft->fps_cb) xsft->fps_cb = screenhack_do_fps;
1017 [self checkForUpdates];
1021 /* I don't understand why we have to do this *every frame*, but we do,
1022 or else the cursor comes back on.
1025 if (![self isPreview])
1026 [NSCursor setHiddenUntilMouseMoves:YES];
1032 /* This is just a guess, but the -fps code wants to know how long
1033 we were sleeping between frames.
1035 long usecs = 1000000 * [self animationTimeInterval];
1036 usecs -= 200; // caller apparently sleeps for slightly less sometimes...
1037 if (usecs < 0) usecs = 0;
1038 fps_slept (fpst, usecs);
1042 /* It turns out that on some systems (possibly only 10.5 and older?)
1043 [ScreenSaverView setAnimationTimeInterval] does nothing. This means
1044 that we cannot rely on it.
1046 Some of the screen hacks want to delay for long periods, and letting the
1047 framework run the update function at 30 FPS when it really wanted half a
1048 minute between frames would be bad. So instead, we assume that the
1049 framework's animation timer might fire whenever, but we only invoke the
1050 screen hack's "draw frame" method when enough time has expired.
1052 This means two extra calls to gettimeofday() per frame. For fast-cycling
1053 screen savers, that might actually slow them down. Oh well.
1055 A side-effect of this is that it's not possible for a saver to request
1056 an animation interval that is faster than animationTimeInterval.
1058 HOWEVER! On modern systems where setAnimationTimeInterval is *not*
1059 ignored, it's important that it be faster than 30 FPS. 120 FPS is good.
1061 An NSTimer won't fire if the timer is already running the invocation
1062 function from a previous firing. So, if we use a 30 FPS
1063 animationTimeInterval (33333 µs) and a screenhack takes 40000 µs for a
1064 frame, there will be a 26666 µs delay until the next frame, 66666 µs
1065 after the beginning of the current frame. In other words, 25 FPS
1068 Frame rates tend to snap to values of 30/N, where N is a positive
1069 integer, i.e. 30 FPS, 15 FPS, 10, 7.5, 6. And the 'snapped' frame rate
1070 is rounded down from what it would normally be.
1072 So if we set animationTimeInterval to 1/120 instead of 1/30, frame rates
1073 become values of 60/N, 120/N, or 240/N, with coarser or finer frame rate
1074 steps for higher or lower animation time intervals respectively.
1077 gettimeofday (&tv, 0);
1078 double now = tv.tv_sec + (tv.tv_usec / 1000000.0);
1079 if (now < next_frame_time) return;
1081 [self prepareContext];
1084 // We do this here instead of in setFrame so that all the
1085 // Xlib drawing takes place under the animation timer.
1086 [self resizeContext];
1088 # ifndef USE_BACKBUFFER
1090 # else // USE_BACKBUFFER
1093 r.size.width = backbuffer_size.width;
1094 r.size.height = backbuffer_size.height;
1095 # endif // USE_BACKBUFFER
1097 xsft->reshape_cb (xdpy, xwindow, xdata, r.size.width, r.size.height);
1101 // Run any XtAppAddInput callbacks now.
1102 // (Note that XtAppAddTimeOut callbacks have already been run by
1103 // the Cocoa event loop.)
1105 jwxyz_sources_run (display_sources_data (xdpy));
1111 NSDisableScreenUpdates();
1113 // NSAssert(xdata, @"no xdata when drawing");
1114 if (! xdata) abort();
1115 unsigned long delay = xsft->draw_cb (xdpy, xwindow, xdata);
1116 if (fpst) xsft->fps_cb (xdpy, xwindow, fpst, xdata);
1118 NSEnableScreenUpdates();
1121 gettimeofday (&tv, 0);
1122 now = tv.tv_sec + (tv.tv_usec / 1000000.0);
1123 next_frame_time = now + (delay / 1000000.0);
1125 # ifdef USE_IPHONE // Allow savers on the iPhone to run full-tilt.
1126 if (delay < [self animationTimeInterval])
1127 [self setAnimationTimeInterval:(delay / 1000000.0)];
1130 # ifdef DO_GC_HACKERY
1131 /* Current theory is that the 10.6 garbage collector sucks in the
1134 It only does a collection when a threshold of outstanding
1135 collectable allocations has been surpassed. However, CoreGraphics
1136 creates lots of small collectable allocations that contain pointers
1137 to very large non-collectable allocations: a small CG object that's
1138 collectable referencing large malloc'd allocations (non-collectable)
1139 containing bitmap data. So the large allocation doesn't get freed
1140 until GC collects the small allocation, which triggers its finalizer
1141 to run which frees the large allocation. So GC is deciding that it
1142 doesn't really need to run, even though the process has gotten
1143 enormous. GC eventually runs once pageouts have happened, but by
1144 then it's too late, and the machine's resident set has been
1147 So, we force an exhaustive garbage collection in this process
1148 approximately every 5 seconds whether the system thinks it needs
1152 static int tick = 0;
1153 if (++tick > 5*30) {
1155 objc_collect (OBJC_EXHAUSTIVE_COLLECTION);
1158 # endif // DO_GC_HACKERY
1162 @catch (NSException *e) {
1163 [self handleException: e];
1165 # endif // USE_IPHONE
1169 /* drawRect always does nothing, and animateOneFrame renders bits to the
1170 screen. This is (now) true of both X11 and GL on both MacOS and iOS.
1173 - (void)drawRect:(NSRect)rect
1175 if (xwindow) // clear to the X window's bg color, not necessarily black.
1176 XClearWindow (xdpy, xwindow);
1178 [super drawRect:rect]; // early: black.
1182 #ifndef USE_BACKBUFFER
1184 - (void) animateOneFrame
1187 jwxyz_flush_context(xdpy);
1190 #else // USE_BACKBUFFER
1192 - (void) animateOneFrame
1194 // Render X11 into the backing store bitmap...
1196 NSAssert (backbuffer, @"no back buffer");
1199 UIGraphicsPushContext (backbuffer);
1205 UIGraphicsPopContext();
1209 // The rotation origin for layer.affineTransform is in the center already.
1210 CGAffineTransform t = ignore_rotation_p ?
1211 CGAffineTransformIdentity :
1212 CGAffineTransformMakeRotation (rot_current_angle / (180.0 / M_PI));
1214 // Ratio of backbuffer size in pixels to layer size in points.
1215 CGSize ssize = backbuffer_size;
1216 CGSize bsize = [self bounds].size;
1217 GLfloat s = ((ssize.width > ssize.height ? ssize.width : ssize.height) /
1218 (bsize.width > bsize.height ? bsize.width : bsize.height));
1220 self.layer.contentsScale = s;
1221 self.layer.affineTransform = t;
1223 /* Setting the layer's bounds also sets the view's bounds.
1224 The view's bounds must be in points, not pixels, and it
1225 must be rotated to the current orientation.
1228 bounds.origin.x = 0;
1229 bounds.origin.y = 0;
1230 bounds.size.width = ssize.width / s;
1231 bounds.size.height = ssize.height / s;
1232 self.layer.bounds = bounds;
1234 # endif // USE_IPHONE
1236 # if defined(BACKBUFFER_CALAYER)
1237 [self.layer setNeedsDisplay];
1238 # elif defined(BACKBUFFER_CGCONTEXT)
1240 w = CGBitmapContextGetWidth (backbuffer),
1241 h = CGBitmapContextGetHeight (backbuffer);
1243 size_t bpl = CGBitmapContextGetBytesPerRow (backbuffer);
1244 CGDataProviderRef prov = CGDataProviderCreateWithData (NULL,
1245 CGBitmapContextGetData(backbuffer),
1250 CGImageRef img = CGImageCreate (w, h,
1252 CGBitmapContextGetBytesPerRow(backbuffer),
1254 CGBitmapContextGetBitmapInfo(backbuffer),
1256 kCGRenderingIntentDefault);
1258 CGDataProviderRelease (prov);
1263 rect.size = backbuffer_size;
1264 CGContextDrawImage (window_ctx, rect, img);
1266 CGImageRelease (img);
1268 CGContextFlush (window_ctx);
1269 # endif // BACKBUFFER_CGCONTEXT
1272 # ifdef BACKBUFFER_CALAYER
1274 - (void) drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
1276 // This "isn't safe" if NULL is passed to CGBitmapCreateContext before iOS 4.
1277 char *dest_data = (char *)CGBitmapContextGetData (ctx);
1279 // The CGContext here is normally upside-down on iOS.
1281 CGBitmapContextGetBitmapInfo (ctx) ==
1282 (kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Host)
1284 && CGContextGetCTM (ctx).d < 0
1285 # endif // USE_IPHONE
1288 size_t dest_height = CGBitmapContextGetHeight (ctx);
1289 size_t dest_bpr = CGBitmapContextGetBytesPerRow (ctx);
1290 size_t src_height = CGBitmapContextGetHeight (backbuffer);
1291 size_t src_bpr = CGBitmapContextGetBytesPerRow (backbuffer);
1292 char *src_data = (char *)CGBitmapContextGetData (backbuffer);
1294 size_t height = src_height < dest_height ? src_height : dest_height;
1296 if (src_bpr == dest_bpr) {
1297 // iPad 1: 4.0 ms, iPad 2: 6.7 ms
1298 memcpy (dest_data, src_data, src_bpr * height);
1300 // iPad 1: 4.6 ms, iPad 2: 7.2 ms
1301 size_t bpr = src_bpr < dest_bpr ? src_bpr : dest_bpr;
1303 memcpy (dest_data, src_data, bpr);
1305 src_data += src_bpr;
1306 dest_data += dest_bpr;
1311 // iPad 1: 9.6 ms, iPad 2: 12.1 ms
1314 CGContextScaleCTM (ctx, 1, -1);
1315 CGFloat s = [self contentScaleFactor];
1316 CGFloat hs = [self hackedContentScaleFactor];
1317 CGContextTranslateCTM (ctx, 0, -backbuffer_size.height * hs / s);
1318 # endif // USE_IPHONE
1320 CGImageRef img = CGBitmapContextCreateImage (backbuffer);
1321 CGContextDrawImage (ctx, self.layer.bounds, img);
1322 CGImageRelease (img);
1325 # endif // BACKBUFFER_CALAYER
1327 #endif // USE_BACKBUFFER
1331 - (void) setFrame:(NSRect) newRect
1333 [super setFrame:newRect];
1335 if (xwindow) // inform Xlib that the window has changed now.
1340 # ifndef USE_IPHONE // Doesn't exist on iOS
1341 - (void) setFrameSize:(NSSize) newSize
1343 [super setFrameSize:newSize];
1347 # endif // !USE_IPHONE
1350 +(BOOL) performGammaFade
1355 - (BOOL) hasConfigureSheet
1360 + (NSString *) decompressXML: (NSData *)data
1362 if (! data) return 0;
1363 BOOL compressed_p = !!strncmp ((const char *) data.bytes, "<?xml", 5);
1365 // If it's not already XML, decompress it.
1366 NSAssert (compressed_p, @"xml isn't compressed");
1368 NSMutableData *data2 = 0;
1371 memset (&zs, 0, sizeof(zs));
1372 ret = inflateInit2 (&zs, 16 + MAX_WBITS);
1374 UInt32 usize = * (UInt32 *) (data.bytes + data.length - 4);
1375 data2 = [NSMutableData dataWithLength: usize];
1376 zs.next_in = (Bytef *) data.bytes;
1377 zs.avail_in = (uint) data.length;
1378 zs.next_out = (Bytef *) data2.bytes;
1379 zs.avail_out = (uint) data2.length;
1380 ret = inflate (&zs, Z_FINISH);
1383 if (ret == Z_OK || ret == Z_STREAM_END)
1386 NSAssert2 (0, @"gunzip error: %d: %s",
1387 ret, (zs.msg ? zs.msg : "<null>"));
1390 return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
1395 - (NSWindow *) configureSheet
1397 - (UIViewController *) configureView
1400 NSBundle *bundle = [NSBundle bundleForClass:[self class]];
1401 NSString *file = [NSString stringWithCString:xsft->progclass
1402 encoding:NSISOLatin1StringEncoding];
1403 file = [file lowercaseString];
1404 NSString *path = [bundle pathForResource:file ofType:@"xml"];
1406 NSLog (@"%@.xml does not exist in the application bundle: %@/",
1407 file, [bundle resourcePath]);
1412 UIViewController *sheet;
1413 # else // !USE_IPHONE
1415 # endif // !USE_IPHONE
1417 NSData *xmld = [NSData dataWithContentsOfFile:path];
1418 NSString *xml = [[self class] decompressXML: xmld];
1419 sheet = [[XScreenSaverConfigSheet alloc]
1420 initWithXML:[xml dataUsingEncoding:NSUTF8StringEncoding]
1421 options:xsft->options
1422 controller:[prefsReader userDefaultsController]
1423 globalController:[prefsReader globalDefaultsController]
1424 defaults:[prefsReader defaultOptions]];
1426 // #### am I expected to retain this, or not? wtf.
1427 // I thought not, but if I don't do this, we (sometimes) crash.
1428 // #### Analyze says "potential leak of an object stored into sheet"
1435 - (NSUserDefaultsController *) userDefaultsController
1437 return [prefsReader userDefaultsController];
1441 /* Announce our willingness to accept keyboard input.
1443 - (BOOL)acceptsFirstResponder
1453 # else // USE_IPHONE
1455 // There's no way to play a standard system alert sound!
1456 // We'd have to include our own WAV for that.
1458 // Or we could vibrate:
1459 // #import <AudioToolbox/AudioToolbox.h>
1460 // AudioServicesPlaySystemSound (kSystemSoundID_Vibrate);
1462 // Instead, just flash the screen white, then fade.
1464 UIView *v = [[UIView alloc] initWithFrame: [self frame]];
1465 [v setBackgroundColor: [UIColor whiteColor]];
1466 [[self window] addSubview:v];
1467 [UIView animateWithDuration: 0.1
1468 animations:^{ [v setAlpha: 0.0]; }
1469 completion:^(BOOL finished) { [v removeFromSuperview]; } ];
1471 # endif // USE_IPHONE
1475 /* Send an XEvent to the hack. Returns YES if it was handled.
1477 - (BOOL) sendEvent: (XEvent *) e
1479 if (!initted_p || ![self isAnimating]) // no event handling unless running.
1483 [self prepareContext];
1484 BOOL result = xsft->event_cb (xdpy, xwindow, xdata, e);
1492 /* Convert an NSEvent into an XEvent, and pass it along.
1493 Returns YES if it was handled.
1495 - (BOOL) convertEvent: (NSEvent *) e
1499 memset (&xe, 0, sizeof(xe));
1503 int flags = [e modifierFlags];
1504 if (flags & NSAlphaShiftKeyMask) state |= LockMask;
1505 if (flags & NSShiftKeyMask) state |= ShiftMask;
1506 if (flags & NSControlKeyMask) state |= ControlMask;
1507 if (flags & NSAlternateKeyMask) state |= Mod1Mask;
1508 if (flags & NSCommandKeyMask) state |= Mod2Mask;
1510 NSPoint p = [[[e window] contentView] convertPoint:[e locationInWindow]
1513 double s = [self hackedContentScaleFactor];
1518 int y = s * ([self bounds].size.height - p.y);
1520 xe.xany.type = type;
1526 xe.xbutton.state = state;
1527 if ([e type] == NSScrollWheel)
1528 xe.xbutton.button = ([e deltaY] > 0 ? Button4 :
1529 [e deltaY] < 0 ? Button5 :
1530 [e deltaX] > 0 ? Button6 :
1531 [e deltaX] < 0 ? Button7 :
1534 xe.xbutton.button = [e buttonNumber] + 1;
1539 xe.xmotion.state = state;
1544 NSString *ns = (([e type] == NSFlagsChanged) ? 0 :
1545 [e charactersIgnoringModifiers]);
1548 if (!ns || [ns length] == 0) // dead key
1550 // Cocoa hides the difference between left and right keys.
1551 // Also we only get KeyPress events for these, no KeyRelease
1552 // (unless we hack the mod state manually. Bleh.)
1554 if (flags & NSAlphaShiftKeyMask) k = XK_Caps_Lock;
1555 else if (flags & NSShiftKeyMask) k = XK_Shift_L;
1556 else if (flags & NSControlKeyMask) k = XK_Control_L;
1557 else if (flags & NSAlternateKeyMask) k = XK_Alt_L;
1558 else if (flags & NSCommandKeyMask) k = XK_Meta_L;
1560 else if ([ns length] == 1) // real key
1562 switch ([ns characterAtIndex:0]) {
1563 case NSLeftArrowFunctionKey: k = XK_Left; break;
1564 case NSRightArrowFunctionKey: k = XK_Right; break;
1565 case NSUpArrowFunctionKey: k = XK_Up; break;
1566 case NSDownArrowFunctionKey: k = XK_Down; break;
1567 case NSPageUpFunctionKey: k = XK_Page_Up; break;
1568 case NSPageDownFunctionKey: k = XK_Page_Down; break;
1569 case NSHomeFunctionKey: k = XK_Home; break;
1570 case NSPrevFunctionKey: k = XK_Prior; break;
1571 case NSNextFunctionKey: k = XK_Next; break;
1572 case NSBeginFunctionKey: k = XK_Begin; break;
1573 case NSEndFunctionKey: k = XK_End; break;
1577 [ns cStringUsingEncoding:NSISOLatin1StringEncoding];
1578 k = (s && *s ? *s : 0);
1584 if (! k) return YES; // E.g., "KeyRelease XK_Shift_L"
1586 xe.xkey.keycode = k;
1587 xe.xkey.state = state;
1591 NSAssert1 (0, @"unknown X11 event type: %d", type);
1595 return [self sendEvent: &xe];
1599 - (void) mouseDown: (NSEvent *) e
1601 if (! [self convertEvent:e type:ButtonPress])
1602 [super mouseDown:e];
1605 - (void) mouseUp: (NSEvent *) e
1607 if (! [self convertEvent:e type:ButtonRelease])
1611 - (void) otherMouseDown: (NSEvent *) e
1613 if (! [self convertEvent:e type:ButtonPress])
1614 [super otherMouseDown:e];
1617 - (void) otherMouseUp: (NSEvent *) e
1619 if (! [self convertEvent:e type:ButtonRelease])
1620 [super otherMouseUp:e];
1623 - (void) mouseMoved: (NSEvent *) e
1625 if (! [self convertEvent:e type:MotionNotify])
1626 [super mouseMoved:e];
1629 - (void) mouseDragged: (NSEvent *) e
1631 if (! [self convertEvent:e type:MotionNotify])
1632 [super mouseDragged:e];
1635 - (void) otherMouseDragged: (NSEvent *) e
1637 if (! [self convertEvent:e type:MotionNotify])
1638 [super otherMouseDragged:e];
1641 - (void) scrollWheel: (NSEvent *) e
1643 if (! [self convertEvent:e type:ButtonPress])
1644 [super scrollWheel:e];
1647 - (void) keyDown: (NSEvent *) e
1649 if (! [self convertEvent:e type:KeyPress])
1653 - (void) keyUp: (NSEvent *) e
1655 if (! [self convertEvent:e type:KeyRelease])
1659 - (void) flagsChanged: (NSEvent *) e
1661 if (! [self convertEvent:e type:KeyPress])
1662 [super flagsChanged:e];
1668 - (void) stopAndClose:(Bool)relaunch_p
1670 if ([self isAnimating])
1671 [self stopAnimation];
1673 /* Need to make the SaverListController be the firstResponder again
1674 so that it can continue to receive its own shake events. I
1675 suppose that this abstraction-breakage means that I'm adding
1676 XScreenSaverView to the UINavigationController wrong...
1678 // UIViewController *v = [[self window] rootViewController];
1679 // if ([v isKindOfClass: [UINavigationController class]]) {
1680 // UINavigationController *n = (UINavigationController *) v;
1681 // [[n topViewController] becomeFirstResponder];
1683 [self resignFirstResponder];
1685 // Find SaverRunner.window (as opposed to SaverRunner.saverWindow)
1686 UIWindow *listWindow = 0;
1687 for (UIWindow *w in [[UIApplication sharedApplication] windows]) {
1688 if (w != [self window]) {
1694 UIView *fader = [self superview]; // the "backgroundView" view is our parent
1696 if (relaunch_p) { // Fake a shake on the SaverListController.
1697 UIViewController *v = [listWindow rootViewController];
1698 if ([v isKindOfClass: [UINavigationController class]]) {
1699 # if TARGET_IPHONE_SIMULATOR
1700 NSLog (@"simulating shake on saver list");
1702 UINavigationController *n = (UINavigationController *) v;
1703 [[n topViewController] motionEnded: UIEventSubtypeMotionShake
1706 } else { // Not launching another, animate our return to the list.
1707 # if TARGET_IPHONE_SIMULATOR
1708 NSLog (@"fading back to saver list");
1710 UIWindow *saverWindow = [self window]; // not SaverRunner.window
1711 [listWindow setHidden:NO];
1712 [UIView animateWithDuration: 0.5
1713 animations:^{ fader.alpha = 0.0; }
1714 completion:^(BOOL finished) {
1715 [fader removeFromSuperview];
1717 [saverWindow setHidden:YES];
1718 [listWindow makeKeyAndVisible];
1719 [[[listWindow rootViewController] view] becomeFirstResponder];
1725 /* Whether the shape of the X11 Window should be changed to HxW when the
1726 device is in a landscape orientation. X11 hacks want this, but OpenGL
1729 - (BOOL)reshapeRotatedWindow
1735 /* Called after the device's orientation has changed.
1737 Rotation is complicated: the UI, X11 and OpenGL work in 3 different ways.
1739 The UI (list of savers, preferences panels) is rotated by the system,
1740 because its UIWindow is under a UINavigationController that does
1741 automatic rotation, using Core Animation.
1743 The savers are under a different UIWindow and a UINavigationController
1744 that does not do automatic rotation.
1746 We have to do it this way for OpenGL savers because using Core Animation
1747 on an EAGLContext causes the OpenGL pipeline to fall back on software
1748 rendering and performance goes to hell.
1750 For X11-only savers, we could just use Core Animation and let the system
1751 handle it, but (maybe) it's simpler to do it the same way for X11 and GL.
1753 During and after rotation, the size/shape of the X11 window changes,
1754 and ConfigureNotify events are generated.
1756 X11 code (jwxyz) continues to draw into the (reshaped) backbuffer, which
1757 rotated at the last minute via a CGAffineTransformMakeRotation when it is
1758 copied to the display hardware.
1760 GL code always recieves a portrait-oriented X11 Window whose size never
1761 changes. The GL COLOR_BUFFER is displayed on the hardware directly and
1762 unrotated, so the GL hacks themselves are responsible for rotating the
1763 GL scene to match current_device_rotation().
1765 Touch events are converted to mouse clicks, and those XEvent coordinates
1766 are reported in the coordinate system currently in use by the X11 window.
1767 Again, GL must convert those.
1769 - (void)didRotate:(NSNotification *)notification
1771 UIDeviceOrientation current = [[UIDevice currentDevice] orientation];
1773 /* If the simulator starts up in the rotated position, sometimes
1774 the UIDevice says we're in Portrait when we're not -- but it
1775 turns out that the UINavigationController knows what's up!
1776 So get it from there.
1778 if (current == UIDeviceOrientationUnknown) {
1779 switch ([[[self window] rootViewController] interfaceOrientation]) {
1780 case UIInterfaceOrientationPortrait:
1781 current = UIDeviceOrientationPortrait;
1783 case UIInterfaceOrientationPortraitUpsideDown:
1784 current = UIDeviceOrientationPortraitUpsideDown;
1786 /* It's opposite day, "because rotating the device to the left requires
1787 rotating the content to the right" */
1788 case UIInterfaceOrientationLandscapeLeft:
1789 current = UIDeviceOrientationLandscapeRight;
1791 case UIInterfaceOrientationLandscapeRight:
1792 current = UIDeviceOrientationLandscapeLeft;
1799 /* On the iPad (but not iPhone 3GS, or the simulator) sometimes we get
1800 an orientation change event with an unknown orientation. Those seem
1801 to always be immediately followed by another orientation change with
1802 a *real* orientation change, so let's try just ignoring those bogus
1803 ones and hoping that the real one comes in shortly...
1805 if (current == UIDeviceOrientationUnknown)
1808 if (rotation_ratio >= 0) return; // in the midst of rotation animation
1809 if (orientation == current) return; // no change
1811 // When transitioning to FaceUp or FaceDown, pretend there was no change.
1812 if (current == UIDeviceOrientationFaceUp ||
1813 current == UIDeviceOrientationFaceDown)
1816 new_orientation = current; // current animation target
1817 rotation_ratio = 0; // start animating
1818 rot_start_time = double_time();
1820 switch (orientation) {
1821 case UIDeviceOrientationLandscapeLeft: angle_from = 90; break;
1822 case UIDeviceOrientationLandscapeRight: angle_from = 270; break;
1823 case UIDeviceOrientationPortraitUpsideDown: angle_from = 180; break;
1824 default: angle_from = 0; break;
1827 switch (new_orientation) {
1828 case UIDeviceOrientationLandscapeLeft: angle_to = 90; break;
1829 case UIDeviceOrientationLandscapeRight: angle_to = 270; break;
1830 case UIDeviceOrientationPortraitUpsideDown: angle_to = 180; break;
1831 default: angle_to = 0; break;
1834 switch (orientation) {
1835 case UIDeviceOrientationLandscapeRight: // from landscape
1836 case UIDeviceOrientationLandscapeLeft:
1837 rot_from.width = initial_bounds.height;
1838 rot_from.height = initial_bounds.width;
1840 default: // from portrait
1841 rot_from.width = initial_bounds.width;
1842 rot_from.height = initial_bounds.height;
1846 switch (new_orientation) {
1847 case UIDeviceOrientationLandscapeRight: // to landscape
1848 case UIDeviceOrientationLandscapeLeft:
1849 rot_to.width = initial_bounds.height;
1850 rot_to.height = initial_bounds.width;
1852 default: // to portrait
1853 rot_to.width = initial_bounds.width;
1854 rot_to.height = initial_bounds.height;
1858 # if TARGET_IPHONE_SIMULATOR
1859 NSLog (@"rotation begun: %s %d -> %s %d; %d x %d",
1860 orientname(orientation), (int) rot_current_angle,
1861 orientname(new_orientation), (int) angle_to,
1862 (int) rot_current_size.width, (int) rot_current_size.height);
1866 // If we've done a rotation but the saver hasn't been initialized yet,
1867 // don't bother going through an X11 resize, but just do it now.
1868 rot_start_time = 0; // dawn of time
1869 [self hackRotation];
1874 /* We distinguish between taps and drags.
1876 - Drags/pans (down, motion, up) are sent to the saver to handle.
1877 - Single-taps exit the saver.
1878 - Double-taps are sent to the saver as a "Space" keypress.
1879 - Swipes (really, two-finger drags/pans) send Up/Down/Left/RightArrow keys.
1881 This means a saver cannot respond to a single-tap. Only a few try to.
1884 - (void)initGestures
1886 UITapGestureRecognizer *dtap = [[UITapGestureRecognizer alloc]
1888 action:@selector(handleDoubleTap)];
1889 dtap.numberOfTapsRequired = 2;
1890 dtap.numberOfTouchesRequired = 1;
1892 UITapGestureRecognizer *stap = [[UITapGestureRecognizer alloc]
1894 action:@selector(handleTap)];
1895 stap.numberOfTapsRequired = 1;
1896 stap.numberOfTouchesRequired = 1;
1898 UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc]
1900 action:@selector(handlePan:)];
1901 pan.maximumNumberOfTouches = 1;
1902 pan.minimumNumberOfTouches = 1;
1904 // I couldn't get Swipe to work, but using a second Pan recognizer works.
1905 UIPanGestureRecognizer *pan2 = [[UIPanGestureRecognizer alloc]
1907 action:@selector(handlePan2:)];
1908 pan2.maximumNumberOfTouches = 2;
1909 pan2.minimumNumberOfTouches = 2;
1911 // Also handle long-touch, and treat that the same as Pan.
1912 // Without this, panning doesn't start until there's motion, so the trick
1913 // of holding down your finger to freeze the scene doesn't work.
1915 UILongPressGestureRecognizer *hold = [[UILongPressGestureRecognizer alloc]
1917 action:@selector(handleLongPress:)];
1918 hold.numberOfTapsRequired = 0;
1919 hold.numberOfTouchesRequired = 1;
1920 hold.minimumPressDuration = 0.25; /* 1/4th second */
1922 [stap requireGestureRecognizerToFail: dtap];
1923 [stap requireGestureRecognizerToFail: hold];
1924 [dtap requireGestureRecognizerToFail: hold];
1925 [pan requireGestureRecognizerToFail: hold];
1927 [self setMultipleTouchEnabled:YES];
1929 [self addGestureRecognizer: dtap];
1930 [self addGestureRecognizer: stap];
1931 [self addGestureRecognizer: pan];
1932 [self addGestureRecognizer: pan2];
1933 [self addGestureRecognizer: hold];
1943 /* Given a mouse (touch) coordinate in unrotated, unscaled view coordinates,
1944 convert it to what X11 and OpenGL expect.
1946 - (void) convertMouse:(int)rot x:(int*)x y:(int *)y
1948 int w = [self frame].size.width;
1949 int h = [self frame].size.height;
1950 int xx = *x, yy = *y;
1953 if (ignore_rotation_p) {
1954 // We need to rotate the coordinates to match the unrotated X11 window.
1955 switch (orientation) {
1956 case UIDeviceOrientationLandscapeRight:
1957 swap = xx; xx = h-yy; yy = swap;
1959 case UIDeviceOrientationLandscapeLeft:
1960 swap = xx; xx = yy; yy = w-swap;
1962 case UIDeviceOrientationPortraitUpsideDown:
1963 xx = w-xx; yy = h-yy;
1969 double s = [self contentScaleFactor];
1973 # if TARGET_IPHONE_SIMULATOR
1974 NSLog (@"touch %4d, %-4d in %4d x %-4d %d %d\n",
1975 *x, *y, (int)(w*s), (int)(h*s),
1976 ignore_rotation_p, [self reshapeRotatedWindow]);
1981 /* Single click exits saver.
1985 [self stopAndClose:NO];
1989 /* Double click sends Space KeyPress.
1991 - (void) handleDoubleTap
1993 if (!xsft->event_cb || !xwindow) return;
1996 memset (&xe, 0, sizeof(xe));
1997 xe.xkey.keycode = ' ';
1998 xe.xany.type = KeyPress;
1999 BOOL ok1 = [self sendEvent: &xe];
2000 xe.xany.type = KeyRelease;
2001 BOOL ok2 = [self sendEvent: &xe];
2007 /* Drag with one finger down: send MotionNotify.
2009 - (void) handlePan:(UIGestureRecognizer *)sender
2011 if (!xsft->event_cb || !xwindow) return;
2014 memset (&xe, 0, sizeof(xe));
2016 CGPoint p = [sender locationInView:self]; // this is in points, not pixels
2019 [self convertMouse: rot_current_angle x:&x y:&y];
2020 jwxyz_mouse_moved (xdpy, xwindow, x, y);
2022 switch (sender.state) {
2023 case UIGestureRecognizerStateBegan:
2024 xe.xany.type = ButtonPress;
2025 xe.xbutton.button = 1;
2030 case UIGestureRecognizerStateEnded:
2031 xe.xany.type = ButtonRelease;
2032 xe.xbutton.button = 1;
2037 case UIGestureRecognizerStateChanged:
2038 xe.xany.type = MotionNotify;
2047 BOOL ok = [self sendEvent: &xe];
2048 if (!ok && xe.xany.type == ButtonRelease)
2053 /* Hold one finger down: assume we're about to start dragging.
2054 Treat the same as Pan.
2056 - (void) handleLongPress:(UIGestureRecognizer *)sender
2058 [self handlePan:sender];
2063 /* Drag with 2 fingers down: send arrow keys.
2065 - (void) handlePan2:(UIPanGestureRecognizer *)sender
2067 if (!xsft->event_cb || !xwindow) return;
2069 if (sender.state != UIGestureRecognizerStateEnded)
2073 memset (&xe, 0, sizeof(xe));
2075 CGPoint p = [sender locationInView:self]; // this is in points, not pixels
2078 [self convertMouse: rot_current_angle x:&x y:&y];
2080 if (abs(x) > abs(y))
2081 xe.xkey.keycode = (x > 0 ? XK_Right : XK_Left);
2083 xe.xkey.keycode = (y > 0 ? XK_Down : XK_Up);
2085 BOOL ok1 = [self sendEvent: &xe];
2086 xe.xany.type = KeyRelease;
2087 BOOL ok2 = [self sendEvent: &xe];
2093 /* We need this to respond to "shake" gestures
2095 - (BOOL)canBecomeFirstResponder
2100 - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
2105 - (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event
2109 /* Shake means exit and launch a new saver.
2111 - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
2113 [self stopAndClose:YES];
2117 - (void)setScreenLocked:(BOOL)locked
2119 if (screenLocked == locked) return;
2120 screenLocked = locked;
2122 if ([self isAnimating])
2123 [self stopAnimation];
2125 if (! [self isAnimating])
2126 [self startAnimation];
2130 #endif // USE_IPHONE
2133 - (void) checkForUpdates
2136 // We only check once at startup, even if there are multiple screens,
2137 // and even if this saver is running for many days.
2138 // (Uh, except this doesn't work because this static isn't shared,
2139 // even if we make it an exported global. Not sure why. Oh well.)
2140 static BOOL checked_p = NO;
2141 if (checked_p) return;
2144 // If it's off, don't bother running the updater. Otherwise, the
2145 // updater will decide if it's time to hit the network.
2146 if (! get_boolean_resource (xdpy,
2147 SUSUEnableAutomaticChecksKey,
2148 SUSUEnableAutomaticChecksKey))
2151 NSString *updater = @"XScreenSaverUpdater.app";
2153 // There may be multiple copies of the updater: e.g., one in /Applications
2154 // and one in the mounted installer DMG! It's important that we run the
2155 // one from the disk and not the DMG, so search for the right one.
2157 NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
2158 NSBundle *bundle = [NSBundle bundleForClass:[self class]];
2160 @[[[bundle bundlePath] stringByDeletingLastPathComponent],
2161 [@"~/Library/Screen Savers" stringByExpandingTildeInPath],
2162 @"/Library/Screen Savers",
2163 @"/System/Library/Screen Savers",
2165 @"/Applications/Utilities"];
2166 NSString *app_path = nil;
2167 for (NSString *dir in search) {
2168 NSString *p = [dir stringByAppendingPathComponent:updater];
2169 if ([[NSFileManager defaultManager] fileExistsAtPath:p]) {
2176 app_path = [workspace fullPathForApplication:updater];
2178 if (app_path && [app_path hasPrefix:@"/Volumes/XScreenSaver "])
2179 app_path = 0; // The DMG version will not do.
2182 NSLog(@"Unable to find %@", updater);
2187 if (! [workspace launchApplicationAtURL:[NSURL fileURLWithPath:app_path]
2188 options:(NSWorkspaceLaunchWithoutAddingToRecents |
2189 NSWorkspaceLaunchWithoutActivation |
2190 NSWorkspaceLaunchAndHide)
2193 NSLog(@"Unable to launch %@: %@", app_path, err);
2196 # endif // !USE_IPHONE
2202 /* Utility functions...
2205 static PrefsReader *
2206 get_prefsReader (Display *dpy)
2208 XScreenSaverView *view = jwxyz_window_view (XRootWindow (dpy, 0));
2209 if (!view) return 0;
2210 return [view prefsReader];
2215 get_string_resource (Display *dpy, char *name, char *class)
2217 return [get_prefsReader(dpy) getStringResource:name];
2221 get_boolean_resource (Display *dpy, char *name, char *class)
2223 return [get_prefsReader(dpy) getBooleanResource:name];
2227 get_integer_resource (Display *dpy, char *name, char *class)
2229 return [get_prefsReader(dpy) getIntegerResource:name];
2233 get_float_resource (Display *dpy, char *name, char *class)
2235 return [get_prefsReader(dpy) getFloatResource:name];