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 extern NSDictionary *make_function_table_dict(void); // ios-function-table.m
52 /* Stub definition of the superclass, for iPhone.
54 @implementation ScreenSaverView
56 NSTimeInterval anim_interval;
61 - (id)initWithFrame:(NSRect)frame isPreview:(BOOL)isPreview {
62 self = [super initWithFrame:frame];
64 anim_interval = 1.0/30;
67 - (NSTimeInterval)animationTimeInterval { return anim_interval; }
68 - (void)setAnimationTimeInterval:(NSTimeInterval)i { anim_interval = i; }
69 - (BOOL)hasConfigureSheet { return NO; }
70 - (NSWindow *)configureSheet { return nil; }
71 - (NSView *)configureView { return nil; }
72 - (BOOL)isPreview { return NO; }
73 - (BOOL)isAnimating { return animating_p; }
74 - (void)animateOneFrame { }
76 - (void)startAnimation {
77 if (animating_p) return;
79 anim_timer = [NSTimer scheduledTimerWithTimeInterval: anim_interval
81 selector:@selector(animateOneFrame)
86 - (void)stopAnimation {
88 [anim_timer invalidate];
95 # endif // !USE_IPHONE
99 @interface XScreenSaverView (Private)
100 - (void) stopAndClose:(Bool)relaunch;
103 @implementation XScreenSaverView
105 // Given a lower-cased saver name, returns the function table for it.
106 // If no name, guess the name from the class's bundle name.
108 - (struct xscreensaver_function_table *) findFunctionTable:(NSString *)name
110 NSBundle *nsb = [NSBundle bundleForClass:[self class]];
111 NSAssert1 (nsb, @"no bundle for class %@", [self class]);
113 NSString *path = [nsb bundlePath];
114 CFURLRef url = CFURLCreateWithFileSystemPath(kCFAllocatorDefault,
116 kCFURLPOSIXPathStyle,
118 CFBundleRef cfb = CFBundleCreate (kCFAllocatorDefault, url);
120 NSAssert1 (cfb, @"no CFBundle for \"%@\"", path);
121 // #### Analyze says "Potential leak of an object stored into cfb"
124 name = [[path lastPathComponent] stringByDeletingPathExtension];
126 name = [[name lowercaseString]
127 stringByReplacingOccurrencesOfString:@" "
131 // CFBundleGetDataPointerForName doesn't work in "Archive" builds.
132 // I'm guessing that symbol-stripping is mandatory. Fuck.
133 NSString *table_name = [name stringByAppendingString:
134 @"_xscreensaver_function_table"];
135 void *addr = CFBundleGetDataPointerForName (cfb, (CFStringRef) table_name);
139 NSLog (@"no symbol \"%@\" for \"%@\"", table_name, path);
142 // Remember: any time you add a new saver to the iOS app,
143 // manually run "make ios-function-table.m"!
144 if (! function_tables)
145 function_tables = [make_function_table_dict() retain];
146 NSValue *v = [function_tables objectForKey: name];
147 void *addr = v ? [v pointerValue] : 0;
148 # endif // USE_IPHONE
150 return (struct xscreensaver_function_table *) addr;
154 // Add the "Contents/Resources/" subdirectory of this screen saver's .bundle
155 // to $PATH for the benefit of savers that include helper shell scripts.
157 - (void) setShellPath
159 NSBundle *nsb = [NSBundle bundleForClass:[self class]];
160 NSAssert1 (nsb, @"no bundle for class %@", [self class]);
162 NSString *nsdir = [nsb resourcePath];
163 NSAssert1 (nsdir, @"no resourcePath for class %@", [self class]);
164 const char *dir = [nsdir cStringUsingEncoding:NSUTF8StringEncoding];
165 const char *opath = getenv ("PATH");
166 if (!opath) opath = "/bin"; // $PATH is unset when running under Shark!
167 char *npath = (char *) malloc (strlen (opath) + strlen (dir) + 30);
168 strcpy (npath, "PATH=");
171 strcat (npath, opath);
172 if (putenv (npath)) {
174 NSAssert1 (0, @"putenv \"%s\" failed", npath);
177 /* Don't free (npath) -- MacOS's putenv() does not copy it. */
181 // set an $XSCREENSAVER_CLASSPATH variable so that included shell scripts
182 // (e.g., "xscreensaver-text") know how to look up resources.
184 - (void) setResourcesEnv:(NSString *) name
186 NSBundle *nsb = [NSBundle bundleForClass:[self class]];
187 NSAssert1 (nsb, @"no bundle for class %@", [self class]);
189 const char *s = [name cStringUsingEncoding:NSUTF8StringEncoding];
190 char *env = (char *) malloc (strlen (s) + 40);
191 strcpy (env, "XSCREENSAVER_CLASSPATH=");
195 NSAssert1 (0, @"putenv \"%s\" failed", env);
197 /* Don't free (env) -- MacOS's putenv() does not copy it. */
202 add_default_options (const XrmOptionDescRec *opts,
203 const char * const *defs,
204 XrmOptionDescRec **opts_ret,
205 const char ***defs_ret)
207 /* These aren't "real" command-line options (there are no actual command-line
208 options in the Cocoa version); but this is the somewhat kludgey way that
209 the <xscreensaver-text /> and <xscreensaver-image /> tags in the
210 ../hacks/config/\*.xml files communicate with the preferences database.
212 static const XrmOptionDescRec default_options [] = {
213 { "-text-mode", ".textMode", XrmoptionSepArg, 0 },
214 { "-text-literal", ".textLiteral", XrmoptionSepArg, 0 },
215 { "-text-file", ".textFile", XrmoptionSepArg, 0 },
216 { "-text-url", ".textURL", XrmoptionSepArg, 0 },
217 { "-text-program", ".textProgram", XrmoptionSepArg, 0 },
218 { "-grab-desktop", ".grabDesktopImages", XrmoptionNoArg, "True" },
219 { "-no-grab-desktop", ".grabDesktopImages", XrmoptionNoArg, "False"},
220 { "-choose-random-images", ".chooseRandomImages",XrmoptionNoArg, "True" },
221 { "-no-choose-random-images",".chooseRandomImages",XrmoptionNoArg, "False"},
222 { "-image-directory", ".imageDirectory", XrmoptionSepArg, 0 },
223 { "-fps", ".doFPS", XrmoptionNoArg, "True" },
224 { "-no-fps", ".doFPS", XrmoptionNoArg, "False"},
225 { "-foreground", ".foreground", XrmoptionSepArg, 0 },
226 { "-fg", ".foreground", XrmoptionSepArg, 0 },
227 { "-background", ".background", XrmoptionSepArg, 0 },
228 { "-bg", ".background", XrmoptionSepArg, 0 },
231 // <xscreensaver-updater />
232 { "-" SUSUEnableAutomaticChecksKey,
233 "." SUSUEnableAutomaticChecksKey, XrmoptionNoArg, "True" },
234 { "-no-" SUSUEnableAutomaticChecksKey,
235 "." SUSUEnableAutomaticChecksKey, XrmoptionNoArg, "False" },
236 { "-" SUAutomaticallyUpdateKey,
237 "." SUAutomaticallyUpdateKey, XrmoptionNoArg, "True" },
238 { "-no-" SUAutomaticallyUpdateKey,
239 "." SUAutomaticallyUpdateKey, XrmoptionNoArg, "False" },
240 { "-" SUSendProfileInfoKey,
241 "." SUSendProfileInfoKey, XrmoptionNoArg,"True" },
242 { "-no-" SUSendProfileInfoKey,
243 "." SUSendProfileInfoKey, XrmoptionNoArg,"False"},
244 { "-" SUScheduledCheckIntervalKey,
245 "." SUScheduledCheckIntervalKey, XrmoptionSepArg, 0 },
246 # endif // !USE_IPHONE
250 static const char *default_defaults [] = {
252 ".doubleBuffer: True",
253 ".multiSample: False",
261 ".textURL: http://en.wikipedia.org/w/index.php?title=Special:NewPages&feed=rss",
263 ".grabDesktopImages: yes",
265 ".chooseRandomImages: no",
267 ".chooseRandomImages: yes",
269 ".imageDirectory: ~/Pictures",
274 # define STR(S) STR1(S)
275 # define __objc_yes Yes
276 # define __objc_no No
277 "." SUSUEnableAutomaticChecksKey ": " STR(SUSUEnableAutomaticChecksDef),
278 "." SUAutomaticallyUpdateKey ": " STR(SUAutomaticallyUpdateDef),
279 "." SUSendProfileInfoKey ": " STR(SUSendProfileInfoDef),
280 "." SUScheduledCheckIntervalKey ": " STR(SUScheduledCheckIntervalDef),
285 # endif // USE_IPHONE
290 for (i = 0; default_options[i].option; i++)
292 for (i = 0; opts[i].option; i++)
295 XrmOptionDescRec *opts2 = (XrmOptionDescRec *)
296 calloc (count + 1, sizeof (*opts2));
300 while (default_options[j].option) {
301 opts2[i] = default_options[j];
305 while (opts[j].option) {
316 for (i = 0; default_defaults[i]; i++)
318 for (i = 0; defs[i]; i++)
321 const char **defs2 = (const char **) calloc (count + 1, sizeof (*defs2));
325 while (default_defaults[j]) {
326 defs2[i] = default_defaults[j];
340 /* Returns the current time in seconds as a double.
346 # ifdef GETTIMEOFDAY_TWO_ARGS
348 gettimeofday(&now, &tzp);
353 return (now.tv_sec + ((double) now.tv_usec * 0.000001));
358 - (id) initWithFrame:(NSRect)frame
359 saverName:(NSString *)saverName
360 isPreview:(BOOL)isPreview
363 initial_bounds = frame.size;
364 rot_current_size = frame.size; // needs to be early, because
365 rot_from = rot_current_size; // [self setFrame] is called by
366 rot_to = rot_current_size; // [super initWithFrame].
370 if (! (self = [super initWithFrame:frame isPreview:isPreview]))
373 xsft = [self findFunctionTable: saverName];
382 [self setMultipleTouchEnabled:YES];
383 orientation = UIDeviceOrientationUnknown;
384 [self didRotate:nil];
386 # endif // USE_IPHONE
390 xsft->setup_cb (xsft, xsft->setup_arg);
392 /* The plist files for these preferences show up in
393 $HOME/Library/Preferences/ByHost/ in a file named like
394 "org.jwz.xscreensaver.<SAVERNAME>.<NUMBERS>.plist"
396 NSString *name = [NSString stringWithCString:xsft->progclass
397 encoding:NSISOLatin1StringEncoding];
398 name = [@"org.jwz.xscreensaver." stringByAppendingString:name];
399 [self setResourcesEnv:name];
402 XrmOptionDescRec *opts = 0;
403 const char **defs = 0;
404 add_default_options (xsft->options, xsft->defaults, &opts, &defs);
405 prefsReader = [[PrefsReader alloc]
406 initWithName:name xrmKeys:opts defaults:defs];
408 // free (opts); // bah, we need these! #### leak!
409 xsft->options = opts;
411 progname = progclass = xsft->progclass;
415 # ifdef USE_BACKBUFFER
416 [self createBackbuffer];
421 // So we can tell when we're docked.
422 [UIDevice currentDevice].batteryMonitoringEnabled = YES;
423 # endif // USE_IPHONE
430 # if !defined(USE_IPHONE) && defined(BACKBUFFER_CALAYER)
431 [self setLayer: [CALayer layer]];
432 self.layer.delegate = self;
433 self.layer.opaque = YES;
434 [self setWantsLayer: YES];
435 # endif // !USE_IPHONE && BACKBUFFER_CALAYER
439 - (id) initWithFrame:(NSRect)frame isPreview:(BOOL)p
441 return [self initWithFrame:frame saverName:0 isPreview:p];
447 NSAssert(![self isAnimating], @"still animating");
448 NSAssert(!xdata, @"xdata not yet freed");
449 NSAssert(!xdpy, @"xdpy not yet freed");
451 # ifdef USE_BACKBUFFER
453 CGContextRelease (backbuffer);
456 CGColorSpaceRelease (colorspace);
458 # ifdef BACKBUFFER_CGCONTEXT
460 CGContextRelease (window_ctx);
461 # endif // BACKBUFFER_CGCONTEXT
463 # endif // USE_BACKBUFFER
465 [prefsReader release];
473 - (PrefsReader *) prefsReader
480 - (void) lockFocus { }
481 - (void) unlockFocus { }
487 /* A few seconds after the saver launches, we store the "wasRunning"
488 preference. This is so that if the saver is crashing at startup,
489 we don't launch it again next time, getting stuck in a crash loop.
491 - (void) allSystemsGo: (NSTimer *) timer
493 NSAssert (timer == crash_timer, @"crash timer screwed up");
496 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
497 [prefs setBool:YES forKey:@"wasRunning"];
503 - (void) startAnimation
505 NSAssert(![self isAnimating], @"already animating");
506 NSAssert(!initted_p && !xdata, @"already initialized");
508 // See comment in render_x11() for why this value is important:
509 [self setAnimationTimeInterval: 1.0 / 120.0];
511 [super startAnimation];
512 /* We can't draw on the window from this method, so we actually do the
513 initialization of the screen saver (xsft->init_cb) in the first call
514 to animateOneFrame() instead.
519 [crash_timer invalidate];
521 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
522 [prefs removeObjectForKey:@"wasRunning"];
525 crash_timer = [NSTimer scheduledTimerWithTimeInterval: 5
527 selector:@selector(allSystemsGo:)
531 # endif // USE_IPHONE
533 // Never automatically turn the screen off if we are docked,
534 // and an animation is running.
537 [UIApplication sharedApplication].idleTimerDisabled =
538 ([UIDevice currentDevice].batteryState != UIDeviceBatteryStateUnplugged);
539 [[UIApplication sharedApplication]
540 setStatusBarHidden:YES withAnimation:UIStatusBarAnimationNone];
545 - (void)stopAnimation
547 NSAssert([self isAnimating], @"not animating");
551 [self lockFocus]; // in case something tries to draw from here
552 [self prepareContext];
554 /* I considered just not even calling the free callback at all...
555 But webcollage-cocoa needs it, to kill the inferior webcollage
556 processes (since the screen saver framework never generates a
557 SIGPIPE for them...) Instead, I turned off the free call in
558 xlockmore.c, which is where all of the bogus calls are anyway.
560 xsft->free_cb (xdpy, xwindow, xdata);
563 // xdpy must be freed before dealloc is called, because xdpy owns a
564 // circular reference to the parent XScreenSaverView.
565 jwxyz_free_display (xdpy);
569 // setup_p = NO; // #### wait, do we need this?
576 [crash_timer invalidate];
578 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
579 [prefs removeObjectForKey:@"wasRunning"];
581 # endif // USE_IPHONE
583 [super stopAnimation];
585 // When an animation is no longer running (e.g., looking at the list)
586 // then it's ok to power off the screen when docked.
589 [UIApplication sharedApplication].idleTimerDisabled = NO;
590 [[UIApplication sharedApplication]
591 setStatusBarHidden:NO withAnimation:UIStatusBarAnimationNone];
596 /* Hook for the XScreenSaverGLView subclass
598 - (void) prepareContext
602 /* Hook for the XScreenSaverGLView subclass
604 - (void) resizeContext
610 screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
612 fps_compute (fpst, 0, -1);
619 /* On iPhones with Retina displays, we can draw the savers in "real"
620 pixels, and that works great. The 320x480 "point" screen is really
621 a 640x960 *pixel* screen. However, Retina iPads have 768x1024
622 point screens which are 1536x2048 pixels, and apparently that's
623 enough pixels that copying those bits to the screen is slow. Like,
624 drops us from 15fps to 7fps. So, on Retina iPads, we don't draw in
625 real pixels. This will probably make the savers look better
626 anyway, since that's a higher resolution than most desktop monitors
627 have even today. (This is only true for X11 programs, not GL
628 programs. Those are fine at full rez.)
630 This method is overridden in XScreenSaverGLView, since this kludge
631 isn't necessary for GL programs, being resolution independent by
634 - (CGFloat) hackedContentScaleFactor
636 GLfloat s = [self contentScaleFactor];
637 if (initial_bounds.width >= 1024 ||
638 initial_bounds.height >= 1024)
644 static GLfloat _global_rot_current_angle_kludge;
646 double current_device_rotation (void)
648 return -_global_rot_current_angle_kludge;
652 - (void) hackRotation
654 if (rotation_ratio >= 0) { // in the midst of a rotation animation
656 # define CLAMP180(N) while (N < 0) N += 360; while (N > 180) N -= 360
657 GLfloat f = angle_from;
658 GLfloat t = angle_to;
661 GLfloat dist = -(t-f);
664 // Intermediate angle.
665 rot_current_angle = f - rotation_ratio * dist;
667 // Intermediate frame size.
668 rot_current_size.width = rot_from.width +
669 rotation_ratio * (rot_to.width - rot_from.width);
670 rot_current_size.height = rot_from.height +
671 rotation_ratio * (rot_to.height - rot_from.height);
673 // Tick animation. Complete rotation in 1/6th sec.
674 double now = double_time();
675 double duration = 1/6.0;
676 rotation_ratio = 1 - ((rot_start_time + duration - now) / duration);
678 if (rotation_ratio > 1) { // Done animating.
679 orientation = new_orientation;
680 rot_current_angle = angle_to;
681 rot_current_size = rot_to;
684 // Check orientation again in case we rotated again while rotating:
685 // this is a no-op if nothing has changed.
686 [self didRotate:nil];
688 } else { // Not animating a rotation.
689 rot_current_angle = angle_to;
690 rot_current_size = rot_to;
693 CLAMP180(rot_current_angle);
694 _global_rot_current_angle_kludge = rot_current_angle;
698 double s = [self hackedContentScaleFactor];
699 if (!ignore_rotation_p &&
700 /* rotation_ratio && */
701 ((int) backbuffer_size.width != (int) (s * rot_current_size.width) ||
702 (int) backbuffer_size.height != (int) (s * rot_current_size.height)))
707 - (void)alertView:(UIAlertView *)av clickedButtonAtIndex:(NSInteger)i
709 if (i == 0) exit (-1); // Cancel
710 [self stopAndClose:NO]; // Keep going
713 - (void) handleException: (NSException *)e
715 NSLog (@"Caught exception: %@", e);
716 [[[UIAlertView alloc] initWithTitle:
717 [NSString stringWithFormat: @"%s crashed!",
720 [NSString stringWithFormat:
721 @"The error message was:"
723 "If it keeps crashing, try "
724 "resetting its options.",
727 cancelButtonTitle: @"Exit"
728 otherButtonTitles: @"Keep going", nil]
730 [self stopAnimation];
736 #ifdef USE_BACKBUFFER
738 /* Create a bitmap context into which we render everything.
739 If the desired size has changed, re-created it.
741 - (void) createBackbuffer
744 double s = [self hackedContentScaleFactor];
745 CGSize rotsize = ignore_rotation_p ? initial_bounds : rot_current_size;
746 int new_w = s * rotsize.width;
747 int new_h = s * rotsize.height;
749 int new_w = [self bounds].size.width;
750 int new_h = [self bounds].size.height;
753 // Colorspaces and CGContexts only happen with non-GL hacks.
755 CGColorSpaceRelease (colorspace);
756 # ifdef BACKBUFFER_CGCONTEXT
758 CGContextRelease (window_ctx);
761 NSWindow *window = [self window];
763 if (window && xdpy) {
766 # if defined(BACKBUFFER_CGCONTEXT)
767 // TODO: This was borrowed from jwxyz_window_resized, and should
768 // probably be refactored.
770 // Figure out which screen the window is currently on.
771 CGDirectDisplayID cgdpy = 0;
775 // TODO: XTranslateCoordinates is returning (0,1200) on my system.
777 // In any case, those weren't valid coordinates for CGGetDisplaysWithPoint.
778 // XTranslateCoordinates (xdpy, xwindow, NULL, 0, 0, &wx, &wy, NULL);
783 p0 = [window convertBaseToScreen:p0];
784 CGPoint p = {p0.x, p0.y};
786 CGGetDisplaysWithPoint (p, 1, &cgdpy, &n);
787 NSAssert (cgdpy, @"unable to find CGDisplay");
791 // Figure out this screen's colorspace, and use that for every CGImage.
793 CMProfileRef profile = 0;
795 // CMGetProfileByAVID is deprecated as of OS X 10.6, but there's no
796 // documented replacement as of OS X 10.9.
797 // http://lists.apple.com/archives/colorsync-dev/2012/Nov/msg00001.html
798 CMGetProfileByAVID ((CMDisplayIDType) cgdpy, &profile);
799 NSAssert (profile, @"unable to find colorspace profile");
800 colorspace = CGColorSpaceCreateWithPlatformColorSpace (profile);
801 NSAssert (colorspace, @"unable to find colorspace");
803 # elif defined(BACKBUFFER_CALAYER)
804 // Was apparently faster until 10.9.
805 colorspace = CGColorSpaceCreateDeviceRGB ();
806 # endif // BACKBUFFER_CALAYER
808 # ifdef BACKBUFFER_CGCONTEXT
809 window_ctx = [[window graphicsContext] graphicsPort];
810 CGContextRetain (window_ctx);
811 # endif // BACKBUFFER_CGCONTEXT
815 # ifdef BACKBUFFER_CGCONTEXT
817 # endif // BACKBUFFER_CGCONTEXT
818 colorspace = CGColorSpaceCreateDeviceRGB();
822 backbuffer_size.width == new_w &&
823 backbuffer_size.height == new_h)
826 CGSize osize = backbuffer_size;
827 CGContextRef ob = backbuffer;
829 backbuffer_size.width = new_w;
830 backbuffer_size.height = new_h;
832 backbuffer = CGBitmapContextCreate (NULL,
833 backbuffer_size.width,
834 backbuffer_size.height,
836 backbuffer_size.width * 4,
838 // kCGImageAlphaPremultipliedLast
839 (kCGImageAlphaNoneSkipFirst |
840 kCGBitmapByteOrder32Host)
842 NSAssert (backbuffer, @"unable to allocate back buffer");
846 r.origin.x = r.origin.y = 0;
847 r.size = backbuffer_size;
848 CGContextSetGrayFillColor (backbuffer, 0, 1);
849 CGContextFillRect (backbuffer, r);
852 // Restore old bits, as much as possible, to the X11 upper left origin.
855 rect.origin.y = (backbuffer_size.height - osize.height);
857 CGImageRef img = CGBitmapContextCreateImage (ob);
858 CGContextDrawImage (backbuffer, rect, img);
859 CGImageRelease (img);
860 CGContextRelease (ob);
864 #endif // USE_BACKBUFFER
867 /* Inform X11 that the size of our window has changed.
871 if (!xwindow) return; // early
873 # ifdef USE_BACKBUFFER
874 [self createBackbuffer];
875 jwxyz_window_resized (xdpy, xwindow,
877 backbuffer_size.width, backbuffer_size.height,
879 # else // !USE_BACKBUFFER
880 NSRect r = [self frame]; // ignoring rotation is closer
881 r.size = [self bounds].size; // to what XGetGeometry expects.
882 jwxyz_window_resized (xdpy, xwindow,
883 r.origin.x, r.origin.y,
884 r.size.width, r.size.height,
886 # endif // !USE_BACKBUFFER
888 // Next time render_x11 is called, run the saver's reshape_cb.
898 if (orientation == UIDeviceOrientationUnknown)
899 [self didRotate:nil];
906 # ifdef USE_BACKBUFFER
907 NSAssert (backbuffer, @"no back buffer");
908 xdpy = jwxyz_make_display (self, backbuffer);
910 xdpy = jwxyz_make_display (self, 0);
912 xwindow = XRootWindow (xdpy, 0);
915 /* Some X11 hacks (fluidballs) want to ignore all rotation events. */
917 get_boolean_resource (xdpy, "ignoreRotation", "IgnoreRotation");
918 # endif // USE_IPHONE
926 xsft->setup_cb (xsft, xsft->setup_arg);
930 NSAssert(!xdata, @"xdata already initialized");
936 XSetWindowBackground (xdpy, xwindow,
937 get_pixel_resource (xdpy, 0,
938 "background", "Background"));
939 XClearWindow (xdpy, xwindow);
942 [[self window] setAcceptsMouseMovedEvents:YES];
945 /* In MacOS 10.5, this enables "QuartzGL", meaning that the Quartz
946 drawing primitives will run on the GPU instead of the CPU.
947 It seems like it might make things worse rather than better,
948 though... Plus it makes us binary-incompatible with 10.4.
950 # if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5
951 [[self window] setPreferredBackingLocation:
952 NSWindowBackingLocationVideoMemory];
956 /* Kludge: even though the init_cb functions are declared to take 2 args,
957 actually call them with 3, for the benefit of xlockmore_init() and
960 void *(*init_cb) (Display *, Window, void *) =
961 (void *(*) (Display *, Window, void *)) xsft->init_cb;
963 xdata = init_cb (xdpy, xwindow, xsft->setup_arg);
964 // NSAssert(xdata, @"no xdata from init");
965 if (! xdata) abort();
967 if (get_boolean_resource (xdpy, "doFPS", "DoFPS")) {
968 fpst = fps_init (xdpy, xwindow);
969 if (! xsft->fps_cb) xsft->fps_cb = screenhack_do_fps;
975 [self checkForUpdates];
979 /* I don't understand why we have to do this *every frame*, but we do,
980 or else the cursor comes back on.
983 if (![self isPreview])
984 [NSCursor setHiddenUntilMouseMoves:YES];
990 /* This is just a guess, but the -fps code wants to know how long
991 we were sleeping between frames.
993 long usecs = 1000000 * [self animationTimeInterval];
994 usecs -= 200; // caller apparently sleeps for slightly less sometimes...
995 if (usecs < 0) usecs = 0;
996 fps_slept (fpst, usecs);
1000 /* It turns out that on some systems (possibly only 10.5 and older?)
1001 [ScreenSaverView setAnimationTimeInterval] does nothing. This means
1002 that we cannot rely on it.
1004 Some of the screen hacks want to delay for long periods, and letting the
1005 framework run the update function at 30 FPS when it really wanted half a
1006 minute between frames would be bad. So instead, we assume that the
1007 framework's animation timer might fire whenever, but we only invoke the
1008 screen hack's "draw frame" method when enough time has expired.
1010 This means two extra calls to gettimeofday() per frame. For fast-cycling
1011 screen savers, that might actually slow them down. Oh well.
1013 A side-effect of this is that it's not possible for a saver to request
1014 an animation interval that is faster than animationTimeInterval.
1016 HOWEVER! On modern systems where setAnimationTimeInterval is *not*
1017 ignored, it's important that it be faster than 30 FPS. 120 FPS is good.
1019 An NSTimer won't fire if the timer is already running the invocation
1020 function from a previous firing. So, if we use a 30 FPS
1021 animationTimeInterval (33333 µs) and a screenhack takes 40000 µs for a
1022 frame, there will be a 26666 µs delay until the next frame, 66666 µs
1023 after the beginning of the current frame. In other words, 25 FPS
1026 Frame rates tend to snap to values of 30/N, where N is a positive
1027 integer, i.e. 30 FPS, 15 FPS, 10, 7.5, 6. And the 'snapped' frame rate
1028 is rounded down from what it would normally be.
1030 So if we set animationTimeInterval to 1/120 instead of 1/30, frame rates
1031 become values of 60/N, 120/N, or 240/N, with coarser or finer frame rate
1032 steps for higher or lower animation time intervals respectively.
1035 gettimeofday (&tv, 0);
1036 double now = tv.tv_sec + (tv.tv_usec / 1000000.0);
1037 if (now < next_frame_time) return;
1039 [self prepareContext];
1042 // We do this here instead of in setFrame so that all the
1043 // Xlib drawing takes place under the animation timer.
1044 [self resizeContext];
1046 # ifndef USE_BACKBUFFER
1048 # else // USE_BACKBUFFER
1051 r.size.width = backbuffer_size.width;
1052 r.size.height = backbuffer_size.height;
1053 # endif // USE_BACKBUFFER
1055 xsft->reshape_cb (xdpy, xwindow, xdata, r.size.width, r.size.height);
1059 // Run any XtAppAddInput callbacks now.
1060 // (Note that XtAppAddTimeOut callbacks have already been run by
1061 // the Cocoa event loop.)
1063 jwxyz_sources_run (display_sources_data (xdpy));
1069 NSDisableScreenUpdates();
1071 // NSAssert(xdata, @"no xdata when drawing");
1072 if (! xdata) abort();
1073 unsigned long delay = xsft->draw_cb (xdpy, xwindow, xdata);
1074 if (fpst) xsft->fps_cb (xdpy, xwindow, fpst, xdata);
1076 NSEnableScreenUpdates();
1079 gettimeofday (&tv, 0);
1080 now = tv.tv_sec + (tv.tv_usec / 1000000.0);
1081 next_frame_time = now + (delay / 1000000.0);
1083 # ifdef USE_IPHONE // Allow savers on the iPhone to run full-tilt.
1084 if (delay < [self animationTimeInterval])
1085 [self setAnimationTimeInterval:(delay / 1000000.0)];
1088 # ifdef DO_GC_HACKERY
1089 /* Current theory is that the 10.6 garbage collector sucks in the
1092 It only does a collection when a threshold of outstanding
1093 collectable allocations has been surpassed. However, CoreGraphics
1094 creates lots of small collectable allocations that contain pointers
1095 to very large non-collectable allocations: a small CG object that's
1096 collectable referencing large malloc'd allocations (non-collectable)
1097 containing bitmap data. So the large allocation doesn't get freed
1098 until GC collects the small allocation, which triggers its finalizer
1099 to run which frees the large allocation. So GC is deciding that it
1100 doesn't really need to run, even though the process has gotten
1101 enormous. GC eventually runs once pageouts have happened, but by
1102 then it's too late, and the machine's resident set has been
1105 So, we force an exhaustive garbage collection in this process
1106 approximately every 5 seconds whether the system thinks it needs
1110 static int tick = 0;
1111 if (++tick > 5*30) {
1113 objc_collect (OBJC_EXHAUSTIVE_COLLECTION);
1116 # endif // DO_GC_HACKERY
1120 @catch (NSException *e) {
1121 [self handleException: e];
1123 # endif // USE_IPHONE
1127 /* drawRect always does nothing, and animateOneFrame renders bits to the
1128 screen. This is (now) true of both X11 and GL on both MacOS and iOS.
1131 - (void)drawRect:(NSRect)rect
1133 if (xwindow) // clear to the X window's bg color, not necessarily black.
1134 XClearWindow (xdpy, xwindow);
1136 [super drawRect:rect]; // early: black.
1140 #ifndef USE_BACKBUFFER
1142 - (void) animateOneFrame
1145 jwxyz_flush_context(xdpy);
1148 #else // USE_BACKBUFFER
1150 - (void) animateOneFrame
1152 // Render X11 into the backing store bitmap...
1154 NSAssert (backbuffer, @"no back buffer");
1157 UIGraphicsPushContext (backbuffer);
1163 UIGraphicsPopContext();
1167 // Then compute the transformations for rotation.
1168 double hs = [self hackedContentScaleFactor];
1169 double s = [self contentScaleFactor];
1171 // The rotation origin for layer.affineTransform is in the center already.
1172 CGAffineTransform t = ignore_rotation_p ?
1173 CGAffineTransformIdentity :
1174 CGAffineTransformMakeRotation (rot_current_angle / (180.0 / M_PI));
1177 self.layer.affineTransform = CGAffineTransformScale(t, f, f);
1180 bounds.origin.x = 0;
1181 bounds.origin.y = 0;
1182 bounds.size.width = backbuffer_size.width / s;
1183 bounds.size.height = backbuffer_size.height / s;
1184 self.layer.bounds = bounds;
1185 # endif // USE_IPHONE
1187 # if defined(BACKBUFFER_CALAYER)
1188 [self.layer setNeedsDisplay];
1189 # elif defined(BACKBUFFER_CGCONTEXT)
1191 w = CGBitmapContextGetWidth (backbuffer),
1192 h = CGBitmapContextGetHeight (backbuffer);
1194 size_t bpl = CGBitmapContextGetBytesPerRow (backbuffer);
1195 CGDataProviderRef prov = CGDataProviderCreateWithData (NULL,
1196 CGBitmapContextGetData(backbuffer),
1201 CGImageRef img = CGImageCreate (w, h,
1203 CGBitmapContextGetBytesPerRow(backbuffer),
1205 CGBitmapContextGetBitmapInfo(backbuffer),
1207 kCGRenderingIntentDefault);
1209 CGDataProviderRelease (prov);
1214 rect.size = backbuffer_size;
1215 CGContextDrawImage (window_ctx, rect, img);
1217 CGImageRelease (img);
1219 CGContextFlush (window_ctx);
1220 # endif // BACKBUFFER_CGCONTEXT
1223 # ifdef BACKBUFFER_CALAYER
1225 - (void) drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
1227 // This "isn't safe" if NULL is passed to CGBitmapCreateContext before iOS 4.
1228 char *dest_data = (char *)CGBitmapContextGetData (ctx);
1230 // The CGContext here is normally upside-down on iOS.
1232 CGBitmapContextGetBitmapInfo (ctx) ==
1233 (kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Host)
1235 && CGContextGetCTM (ctx).d < 0
1236 # endif // USE_IPHONE
1239 size_t dest_height = CGBitmapContextGetHeight (ctx);
1240 size_t dest_bpr = CGBitmapContextGetBytesPerRow (ctx);
1241 size_t src_height = CGBitmapContextGetHeight (backbuffer);
1242 size_t src_bpr = CGBitmapContextGetBytesPerRow (backbuffer);
1243 char *src_data = (char *)CGBitmapContextGetData (backbuffer);
1245 size_t height = src_height < dest_height ? src_height : dest_height;
1247 if (src_bpr == dest_bpr) {
1248 // iPad 1: 4.0 ms, iPad 2: 6.7 ms
1249 memcpy (dest_data, src_data, src_bpr * height);
1251 // iPad 1: 4.6 ms, iPad 2: 7.2 ms
1252 size_t bpr = src_bpr < dest_bpr ? src_bpr : dest_bpr;
1254 memcpy (dest_data, src_data, bpr);
1256 src_data += src_bpr;
1257 dest_data += dest_bpr;
1262 // iPad 1: 9.6 ms, iPad 2: 12.1 ms
1265 CGContextScaleCTM (ctx, 1, -1);
1266 CGFloat s = [self contentScaleFactor];
1267 CGFloat hs = [self hackedContentScaleFactor];
1268 CGContextTranslateCTM (ctx, 0, -backbuffer_size.height * hs / s);
1269 # endif // USE_IPHONE
1271 CGImageRef img = CGBitmapContextCreateImage (backbuffer);
1272 CGContextDrawImage (ctx, self.layer.bounds, img);
1273 CGImageRelease (img);
1276 # endif // BACKBUFFER_CALAYER
1278 #endif // USE_BACKBUFFER
1282 - (void) setFrame:(NSRect) newRect
1284 [super setFrame:newRect];
1286 if (xwindow) // inform Xlib that the window has changed now.
1291 # ifndef USE_IPHONE // Doesn't exist on iOS
1292 - (void) setFrameSize:(NSSize) newSize
1294 [super setFrameSize:newSize];
1298 # endif // !USE_IPHONE
1301 +(BOOL) performGammaFade
1306 - (BOOL) hasConfigureSheet
1311 + (NSString *) decompressXML: (NSData *)data
1313 if (! data) return 0;
1314 BOOL compressed_p = !!strncmp ((const char *) data.bytes, "<?xml", 5);
1316 // If it's not already XML, decompress it.
1317 NSAssert (compressed_p, @"xml isn't compressed");
1319 NSMutableData *data2 = 0;
1322 memset (&zs, 0, sizeof(zs));
1323 ret = inflateInit2 (&zs, 16 + MAX_WBITS);
1325 UInt32 usize = * (UInt32 *) (data.bytes + data.length - 4);
1326 data2 = [NSMutableData dataWithLength: usize];
1327 zs.next_in = (Bytef *) data.bytes;
1328 zs.avail_in = (uint) data.length;
1329 zs.next_out = (Bytef *) data2.bytes;
1330 zs.avail_out = (uint) data2.length;
1331 ret = inflate (&zs, Z_FINISH);
1334 if (ret == Z_OK || ret == Z_STREAM_END)
1337 NSAssert2 (0, @"gunzip error: %d: %s",
1338 ret, (zs.msg ? zs.msg : "<null>"));
1341 return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
1346 - (NSWindow *) configureSheet
1348 - (UIViewController *) configureView
1351 NSBundle *bundle = [NSBundle bundleForClass:[self class]];
1352 NSString *file = [NSString stringWithCString:xsft->progclass
1353 encoding:NSISOLatin1StringEncoding];
1354 file = [file lowercaseString];
1355 NSString *path = [bundle pathForResource:file ofType:@"xml"];
1357 NSLog (@"%@.xml does not exist in the application bundle: %@/",
1358 file, [bundle resourcePath]);
1363 UIViewController *sheet;
1364 # else // !USE_IPHONE
1366 # endif // !USE_IPHONE
1368 NSData *xmld = [NSData dataWithContentsOfFile:path];
1369 NSString *xml = [[self class] decompressXML: xmld];
1370 sheet = [[XScreenSaverConfigSheet alloc]
1371 initWithXML:[xml dataUsingEncoding:NSUTF8StringEncoding]
1372 options:xsft->options
1373 controller:[prefsReader userDefaultsController]
1374 globalController:[prefsReader globalDefaultsController]
1375 defaults:[prefsReader defaultOptions]];
1377 // #### am I expected to retain this, or not? wtf.
1378 // I thought not, but if I don't do this, we (sometimes) crash.
1379 // #### Analyze says "potential leak of an object stored into sheet"
1386 - (NSUserDefaultsController *) userDefaultsController
1388 return [prefsReader userDefaultsController];
1392 /* Announce our willingness to accept keyboard input.
1394 - (BOOL)acceptsFirstResponder
1404 # else // USE_IPHONE
1406 // There's no way to play a standard system alert sound!
1407 // We'd have to include our own WAV for that.
1409 // Or we could vibrate:
1410 // #import <AudioToolbox/AudioToolbox.h>
1411 // AudioServicesPlaySystemSound (kSystemSoundID_Vibrate);
1413 // Instead, just flash the screen white, then fade.
1415 UIView *v = [[UIView alloc] initWithFrame: [self frame]];
1416 [v setBackgroundColor: [UIColor whiteColor]];
1417 [[self window] addSubview:v];
1418 [UIView animateWithDuration: 0.1
1419 animations:^{ [v setAlpha: 0.0]; }
1420 completion:^(BOOL finished) { [v removeFromSuperview]; } ];
1422 # endif // USE_IPHONE
1426 /* Send an XEvent to the hack. Returns YES if it was handled.
1428 - (BOOL) sendEvent: (XEvent *) e
1430 if (!initted_p || ![self isAnimating]) // no event handling unless running.
1434 [self prepareContext];
1435 BOOL result = xsft->event_cb (xdpy, xwindow, xdata, e);
1443 /* Convert an NSEvent into an XEvent, and pass it along.
1444 Returns YES if it was handled.
1446 - (BOOL) convertEvent: (NSEvent *) e
1450 memset (&xe, 0, sizeof(xe));
1454 int flags = [e modifierFlags];
1455 if (flags & NSAlphaShiftKeyMask) state |= LockMask;
1456 if (flags & NSShiftKeyMask) state |= ShiftMask;
1457 if (flags & NSControlKeyMask) state |= ControlMask;
1458 if (flags & NSAlternateKeyMask) state |= Mod1Mask;
1459 if (flags & NSCommandKeyMask) state |= Mod2Mask;
1461 NSPoint p = [[[e window] contentView] convertPoint:[e locationInWindow]
1464 double s = [self hackedContentScaleFactor];
1469 int y = s * ([self bounds].size.height - p.y);
1471 xe.xany.type = type;
1477 xe.xbutton.state = state;
1478 if ([e type] == NSScrollWheel)
1479 xe.xbutton.button = ([e deltaY] > 0 ? Button4 :
1480 [e deltaY] < 0 ? Button5 :
1481 [e deltaX] > 0 ? Button6 :
1482 [e deltaX] < 0 ? Button7 :
1485 xe.xbutton.button = [e buttonNumber] + 1;
1490 xe.xmotion.state = state;
1495 NSString *ns = (([e type] == NSFlagsChanged) ? 0 :
1496 [e charactersIgnoringModifiers]);
1499 if (!ns || [ns length] == 0) // dead key
1501 // Cocoa hides the difference between left and right keys.
1502 // Also we only get KeyPress events for these, no KeyRelease
1503 // (unless we hack the mod state manually. Bleh.)
1505 if (flags & NSAlphaShiftKeyMask) k = XK_Caps_Lock;
1506 else if (flags & NSShiftKeyMask) k = XK_Shift_L;
1507 else if (flags & NSControlKeyMask) k = XK_Control_L;
1508 else if (flags & NSAlternateKeyMask) k = XK_Alt_L;
1509 else if (flags & NSCommandKeyMask) k = XK_Meta_L;
1511 else if ([ns length] == 1) // real key
1513 switch ([ns characterAtIndex:0]) {
1514 case NSLeftArrowFunctionKey: k = XK_Left; break;
1515 case NSRightArrowFunctionKey: k = XK_Right; break;
1516 case NSUpArrowFunctionKey: k = XK_Up; break;
1517 case NSDownArrowFunctionKey: k = XK_Down; break;
1518 case NSPageUpFunctionKey: k = XK_Page_Up; break;
1519 case NSPageDownFunctionKey: k = XK_Page_Down; break;
1520 case NSHomeFunctionKey: k = XK_Home; break;
1521 case NSPrevFunctionKey: k = XK_Prior; break;
1522 case NSNextFunctionKey: k = XK_Next; break;
1523 case NSBeginFunctionKey: k = XK_Begin; break;
1524 case NSEndFunctionKey: k = XK_End; break;
1528 [ns cStringUsingEncoding:NSISOLatin1StringEncoding];
1529 k = (s && *s ? *s : 0);
1535 if (! k) return YES; // E.g., "KeyRelease XK_Shift_L"
1537 xe.xkey.keycode = k;
1538 xe.xkey.state = state;
1542 NSAssert1 (0, @"unknown X11 event type: %d", type);
1546 return [self sendEvent: &xe];
1550 - (void) mouseDown: (NSEvent *) e
1552 if (! [self convertEvent:e type:ButtonPress])
1553 [super mouseDown:e];
1556 - (void) mouseUp: (NSEvent *) e
1558 if (! [self convertEvent:e type:ButtonRelease])
1562 - (void) otherMouseDown: (NSEvent *) e
1564 if (! [self convertEvent:e type:ButtonPress])
1565 [super otherMouseDown:e];
1568 - (void) otherMouseUp: (NSEvent *) e
1570 if (! [self convertEvent:e type:ButtonRelease])
1571 [super otherMouseUp:e];
1574 - (void) mouseMoved: (NSEvent *) e
1576 if (! [self convertEvent:e type:MotionNotify])
1577 [super mouseMoved:e];
1580 - (void) mouseDragged: (NSEvent *) e
1582 if (! [self convertEvent:e type:MotionNotify])
1583 [super mouseDragged:e];
1586 - (void) otherMouseDragged: (NSEvent *) e
1588 if (! [self convertEvent:e type:MotionNotify])
1589 [super otherMouseDragged:e];
1592 - (void) scrollWheel: (NSEvent *) e
1594 if (! [self convertEvent:e type:ButtonPress])
1595 [super scrollWheel:e];
1598 - (void) keyDown: (NSEvent *) e
1600 if (! [self convertEvent:e type:KeyPress])
1604 - (void) keyUp: (NSEvent *) e
1606 if (! [self convertEvent:e type:KeyRelease])
1610 - (void) flagsChanged: (NSEvent *) e
1612 if (! [self convertEvent:e type:KeyPress])
1613 [super flagsChanged:e];
1619 - (void) stopAndClose:(Bool)relaunch_p
1621 if ([self isAnimating])
1622 [self stopAnimation];
1624 /* Need to make the SaverListController be the firstResponder again
1625 so that it can continue to receive its own shake events. I
1626 suppose that this abstraction-breakage means that I'm adding
1627 XScreenSaverView to the UINavigationController wrong...
1629 UIViewController *v = [[self window] rootViewController];
1630 if ([v isKindOfClass: [UINavigationController class]]) {
1631 UINavigationController *n = (UINavigationController *) v;
1632 [[n topViewController] becomeFirstResponder];
1635 UIView *fader = [self superview]; // the "backgroundView" view is our parent
1637 if (relaunch_p) { // Fake a shake on the SaverListController.
1638 // Why is [self window] sometimes null here?
1639 UIWindow *w = [[UIApplication sharedApplication] keyWindow];
1640 UIViewController *v = [w rootViewController];
1641 if ([v isKindOfClass: [UINavigationController class]]) {
1642 UINavigationController *n = (UINavigationController *) v;
1643 [[n topViewController] motionEnded: UIEventSubtypeMotionShake
1646 } else { // Not launching another, animate our return to the list.
1647 [UIView animateWithDuration: 0.5
1648 animations:^{ fader.alpha = 0.0; }
1649 completion:^(BOOL finished) {
1650 [fader removeFromSuperview];
1657 /* Called after the device's orientation has changed.
1659 Note: we could include a subclass of UIViewController which
1660 contains a shouldAutorotateToInterfaceOrientation method that
1661 returns YES, in which case Core Animation would auto-rotate our
1662 View for us in response to rotation events... but, that interacts
1663 badly with the EAGLContext -- if you introduce Core Animation into
1664 the path, the OpenGL pipeline probably falls back on software
1665 rendering and performance goes to hell. Also, the scaling and
1666 rotation that Core Animation does interacts incorrectly with the GL
1669 So, we have to hack the rotation animation manually, in the GL world.
1671 Possibly XScreenSaverView should use Core Animation, and
1672 XScreenSaverGLView should override that.
1674 - (void)didRotate:(NSNotification *)notification
1676 UIDeviceOrientation current = [[UIDevice currentDevice] orientation];
1678 /* If the simulator starts up in the rotated position, sometimes
1679 the UIDevice says we're in Portrait when we're not -- but it
1680 turns out that the UINavigationController knows what's up!
1681 So get it from there.
1683 if (current == UIDeviceOrientationUnknown) {
1684 switch ([[[self window] rootViewController] interfaceOrientation]) {
1685 case UIInterfaceOrientationPortrait:
1686 current = UIDeviceOrientationPortrait;
1688 case UIInterfaceOrientationPortraitUpsideDown:
1689 current = UIDeviceOrientationPortraitUpsideDown;
1691 case UIInterfaceOrientationLandscapeLeft: // It's opposite day
1692 current = UIDeviceOrientationLandscapeRight;
1694 case UIInterfaceOrientationLandscapeRight:
1695 current = UIDeviceOrientationLandscapeLeft;
1702 /* On the iPad (but not iPhone 3GS, or the simulator) sometimes we get
1703 an orientation change event with an unknown orientation. Those seem
1704 to always be immediately followed by another orientation change with
1705 a *real* orientation change, so let's try just ignoring those bogus
1706 ones and hoping that the real one comes in shortly...
1708 if (current == UIDeviceOrientationUnknown)
1711 if (rotation_ratio >= 0) return; // in the midst of rotation animation
1712 if (orientation == current) return; // no change
1714 // When transitioning to FaceUp or FaceDown, pretend there was no change.
1715 if (current == UIDeviceOrientationFaceUp ||
1716 current == UIDeviceOrientationFaceDown)
1719 new_orientation = current; // current animation target
1720 rotation_ratio = 0; // start animating
1721 rot_start_time = double_time();
1723 switch (orientation) {
1724 case UIDeviceOrientationLandscapeLeft: angle_from = 90; break;
1725 case UIDeviceOrientationLandscapeRight: angle_from = 270; break;
1726 case UIDeviceOrientationPortraitUpsideDown: angle_from = 180; break;
1727 default: angle_from = 0; break;
1730 switch (new_orientation) {
1731 case UIDeviceOrientationLandscapeLeft: angle_to = 90; break;
1732 case UIDeviceOrientationLandscapeRight: angle_to = 270; break;
1733 case UIDeviceOrientationPortraitUpsideDown: angle_to = 180; break;
1734 default: angle_to = 0; break;
1737 switch (orientation) {
1738 case UIDeviceOrientationLandscapeRight: // from landscape
1739 case UIDeviceOrientationLandscapeLeft:
1740 rot_from.width = initial_bounds.height;
1741 rot_from.height = initial_bounds.width;
1743 default: // from portrait
1744 rot_from.width = initial_bounds.width;
1745 rot_from.height = initial_bounds.height;
1749 switch (new_orientation) {
1750 case UIDeviceOrientationLandscapeRight: // to landscape
1751 case UIDeviceOrientationLandscapeLeft:
1752 rot_to.width = initial_bounds.height;
1753 rot_to.height = initial_bounds.width;
1755 default: // to portrait
1756 rot_to.width = initial_bounds.width;
1757 rot_to.height = initial_bounds.height;
1762 // If we've done a rotation but the saver hasn't been initialized yet,
1763 // don't bother going through an X11 resize, but just do it now.
1764 rot_start_time = 0; // dawn of time
1765 [self hackRotation];
1770 /* We distinguish between taps and drags.
1772 - Drags/pans (down, motion, up) are sent to the saver to handle.
1773 - Single-taps exit the saver.
1774 - Double-taps are sent to the saver as a "Space" keypress.
1775 - Swipes (really, two-finger drags/pans) send Up/Down/Left/RightArrow keys.
1777 This means a saver cannot respond to a single-tap. Only a few try to.
1780 - (void)initGestures
1782 UITapGestureRecognizer *dtap = [[UITapGestureRecognizer alloc]
1784 action:@selector(handleDoubleTap)];
1785 dtap.numberOfTapsRequired = 2;
1786 dtap.numberOfTouchesRequired = 1;
1788 UITapGestureRecognizer *stap = [[UITapGestureRecognizer alloc]
1790 action:@selector(handleTap)];
1791 stap.numberOfTapsRequired = 1;
1792 stap.numberOfTouchesRequired = 1;
1794 UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc]
1796 action:@selector(handlePan:)];
1797 pan.maximumNumberOfTouches = 1;
1798 pan.minimumNumberOfTouches = 1;
1800 // I couldn't get Swipe to work, but using a second Pan recognizer works.
1801 UIPanGestureRecognizer *pan2 = [[UIPanGestureRecognizer alloc]
1803 action:@selector(handlePan2:)];
1804 pan2.maximumNumberOfTouches = 2;
1805 pan2.minimumNumberOfTouches = 2;
1807 // Also handle long-touch, and treat that the same as Pan.
1808 // Without this, panning doesn't start until there's motion, so the trick
1809 // of holding down your finger to freeze the scene doesn't work.
1811 UILongPressGestureRecognizer *hold = [[UILongPressGestureRecognizer alloc]
1813 action:@selector(handleLongPress:)];
1814 hold.numberOfTapsRequired = 0;
1815 hold.numberOfTouchesRequired = 1;
1816 hold.minimumPressDuration = 0.25; /* 1/4th second */
1818 [stap requireGestureRecognizerToFail: dtap];
1819 [stap requireGestureRecognizerToFail: hold];
1820 [dtap requireGestureRecognizerToFail: hold];
1821 [pan requireGestureRecognizerToFail: hold];
1823 [self addGestureRecognizer: dtap];
1824 [self addGestureRecognizer: stap];
1825 [self addGestureRecognizer: pan];
1826 [self addGestureRecognizer: pan2];
1827 [self addGestureRecognizer: hold];
1838 - (void) rotateMouse:(int)rot x:(int*)x y:(int *)y w:(int)w h:(int)h
1840 // This is a no-op unless contentScaleFactor != hackedContentScaleFactor.
1841 // Currently, this is the iPad Retina only.
1842 CGRect frame = [self bounds]; // Scale.
1843 double s = [self hackedContentScaleFactor];
1844 *x *= (backbuffer_size.width / frame.size.width) / s;
1845 *y *= (backbuffer_size.height / frame.size.height) / s;
1849 /* Single click exits saver.
1853 [self stopAndClose:NO];
1857 /* Double click sends Space KeyPress.
1859 - (void) handleDoubleTap
1861 if (!xsft->event_cb || !xwindow) return;
1864 memset (&xe, 0, sizeof(xe));
1865 xe.xkey.keycode = ' ';
1866 xe.xany.type = KeyPress;
1867 BOOL ok1 = [self sendEvent: &xe];
1868 xe.xany.type = KeyRelease;
1869 BOOL ok2 = [self sendEvent: &xe];
1875 /* Drag with one finger down: send MotionNotify.
1877 - (void) handlePan:(UIGestureRecognizer *)sender
1879 if (!xsft->event_cb || !xwindow) return;
1881 double s = [self hackedContentScaleFactor];
1883 memset (&xe, 0, sizeof(xe));
1885 CGPoint p = [sender locationInView:self];
1888 int w = s * [self frame].size.width; // #### 'frame' here or 'bounds'?
1889 int h = s * [self frame].size.height;
1890 [self rotateMouse: rot_current_angle x:&x y:&y w:w h:h];
1891 jwxyz_mouse_moved (xdpy, xwindow, x, y);
1893 switch (sender.state) {
1894 case UIGestureRecognizerStateBegan:
1895 xe.xany.type = ButtonPress;
1896 xe.xbutton.button = 1;
1901 case UIGestureRecognizerStateEnded:
1902 xe.xany.type = ButtonRelease;
1903 xe.xbutton.button = 1;
1908 case UIGestureRecognizerStateChanged:
1909 xe.xany.type = MotionNotify;
1918 BOOL ok = [self sendEvent: &xe];
1919 if (!ok && xe.xany.type == ButtonRelease)
1924 /* Hold one finger down: assume we're about to start dragging.
1925 Treat the same as Pan.
1927 - (void) handleLongPress:(UIGestureRecognizer *)sender
1929 [self handlePan:sender];
1934 /* Drag with 2 fingers down: send arrow keys.
1936 - (void) handlePan2:(UIPanGestureRecognizer *)sender
1938 if (!xsft->event_cb || !xwindow) return;
1940 if (sender.state != UIGestureRecognizerStateEnded)
1943 double s = [self hackedContentScaleFactor];
1945 memset (&xe, 0, sizeof(xe));
1947 CGPoint p = [sender translationInView:self];
1950 int w = s * [self frame].size.width; // #### 'frame' here or 'bounds'?
1951 int h = s * [self frame].size.height;
1952 [self rotateMouse: rot_current_angle x:&x y:&y w:w h:h];
1953 // jwxyz_mouse_moved (xdpy, xwindow, x, y);
1955 if (abs(x) > abs(y))
1956 xe.xkey.keycode = (x > 0 ? XK_Right : XK_Left);
1958 xe.xkey.keycode = (y > 0 ? XK_Down : XK_Up);
1960 BOOL ok1 = [self sendEvent: &xe];
1961 xe.xany.type = KeyRelease;
1962 BOOL ok2 = [self sendEvent: &xe];
1968 /* We need this to respond to "shake" gestures
1970 - (BOOL)canBecomeFirstResponder
1975 - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
1980 - (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event
1984 /* Shake means exit and launch a new saver.
1986 - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
1988 [self stopAndClose:YES];
1992 - (void)setScreenLocked:(BOOL)locked
1994 if (screenLocked == locked) return;
1995 screenLocked = locked;
1997 if ([self isAnimating])
1998 [self stopAnimation];
2000 if (! [self isAnimating])
2001 [self startAnimation];
2005 #endif // USE_IPHONE
2008 - (void) checkForUpdates
2011 // We only check once at startup, even if there are multiple screens,
2012 // and even if this saver is running for many days.
2013 // (Uh, except this doesn't work because this static isn't shared,
2014 // even if we make it an exported global. Not sure why. Oh well.)
2015 static BOOL checked_p = NO;
2016 if (checked_p) return;
2019 // If it's off, don't bother running the updater. Otherwise, the
2020 // updater will decide if it's time to hit the network.
2021 if (! get_boolean_resource (xdpy,
2022 SUSUEnableAutomaticChecksKey,
2023 SUSUEnableAutomaticChecksKey))
2026 NSString *updater = @"XScreenSaverUpdater.app";
2028 // There may be multiple copies of the updater: e.g., one in /Applications
2029 // and one in the mounted installer DMG! It's important that we run the
2030 // one from the disk and not the DMG, so search for the right one.
2032 NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
2033 NSBundle *bundle = [NSBundle bundleForClass:[self class]];
2035 @[[[bundle bundlePath] stringByDeletingLastPathComponent],
2036 [@"~/Library/Screen Savers" stringByExpandingTildeInPath],
2037 @"/Library/Screen Savers",
2038 @"/System/Library/Screen Savers",
2040 @"/Applications/Utilities"];
2041 NSString *app_path = nil;
2042 for (NSString *dir in search) {
2043 NSString *p = [dir stringByAppendingPathComponent:updater];
2044 if ([[NSFileManager defaultManager] fileExistsAtPath:p]) {
2051 app_path = [workspace fullPathForApplication:updater];
2053 if (app_path && [app_path hasPrefix:@"/Volumes/XScreenSaver "])
2054 app_path = 0; // The DMG version will not do.
2057 NSLog(@"Unable to find %@", updater);
2062 if (! [workspace launchApplicationAtURL:[NSURL fileURLWithPath:app_path]
2063 options:(NSWorkspaceLaunchWithoutAddingToRecents |
2064 NSWorkspaceLaunchWithoutActivation |
2065 NSWorkspaceLaunchAndHide)
2068 NSLog(@"Unable to launch %@: %@", app_path, err);
2071 # endif // !USE_IPHONE
2077 /* Utility functions...
2080 static PrefsReader *
2081 get_prefsReader (Display *dpy)
2083 XScreenSaverView *view = jwxyz_window_view (XRootWindow (dpy, 0));
2084 if (!view) return 0;
2085 return [view prefsReader];
2090 get_string_resource (Display *dpy, char *name, char *class)
2092 return [get_prefsReader(dpy) getStringResource:name];
2096 get_boolean_resource (Display *dpy, char *name, char *class)
2098 return [get_prefsReader(dpy) getBooleanResource:name];
2102 get_integer_resource (Display *dpy, char *name, char *class)
2104 return [get_prefsReader(dpy) getIntegerResource:name];
2108 get_float_resource (Display *dpy, char *name, char *class)
2110 return [get_prefsReader(dpy) getFloatResource:name];