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 // Remember: any time you add a new saver to the iOS app,
145 // manually run "make ios-function-table.m"!
146 if (! function_tables)
147 function_tables = [make_function_table_dict() retain];
148 NSValue *v = [function_tables objectForKey: name];
149 void *addr = v ? [v pointerValue] : 0;
150 # endif // USE_IPHONE
152 return (struct xscreensaver_function_table *) addr;
156 // Add the "Contents/Resources/" subdirectory of this screen saver's .bundle
157 // to $PATH for the benefit of savers that include helper shell scripts.
159 - (void) setShellPath
161 NSBundle *nsb = [NSBundle bundleForClass:[self class]];
162 NSAssert1 (nsb, @"no bundle for class %@", [self class]);
164 NSString *nsdir = [nsb resourcePath];
165 NSAssert1 (nsdir, @"no resourcePath for class %@", [self class]);
166 const char *dir = [nsdir cStringUsingEncoding:NSUTF8StringEncoding];
167 const char *opath = getenv ("PATH");
168 if (!opath) opath = "/bin"; // $PATH is unset when running under Shark!
169 char *npath = (char *) malloc (strlen (opath) + strlen (dir) + 30);
170 strcpy (npath, "PATH=");
173 strcat (npath, opath);
174 if (putenv (npath)) {
176 NSAssert1 (0, @"putenv \"%s\" failed", npath);
179 /* Don't free (npath) -- MacOS's putenv() does not copy it. */
183 // set an $XSCREENSAVER_CLASSPATH variable so that included shell scripts
184 // (e.g., "xscreensaver-text") know how to look up resources.
186 - (void) setResourcesEnv:(NSString *) name
188 NSBundle *nsb = [NSBundle bundleForClass:[self class]];
189 NSAssert1 (nsb, @"no bundle for class %@", [self class]);
191 const char *s = [name cStringUsingEncoding:NSUTF8StringEncoding];
192 char *env = (char *) malloc (strlen (s) + 40);
193 strcpy (env, "XSCREENSAVER_CLASSPATH=");
197 NSAssert1 (0, @"putenv \"%s\" failed", env);
199 /* Don't free (env) -- MacOS's putenv() does not copy it. */
204 add_default_options (const XrmOptionDescRec *opts,
205 const char * const *defs,
206 XrmOptionDescRec **opts_ret,
207 const char ***defs_ret)
209 /* These aren't "real" command-line options (there are no actual command-line
210 options in the Cocoa version); but this is the somewhat kludgey way that
211 the <xscreensaver-text /> and <xscreensaver-image /> tags in the
212 ../hacks/config/\*.xml files communicate with the preferences database.
214 static const XrmOptionDescRec default_options [] = {
215 { "-text-mode", ".textMode", XrmoptionSepArg, 0 },
216 { "-text-literal", ".textLiteral", XrmoptionSepArg, 0 },
217 { "-text-file", ".textFile", XrmoptionSepArg, 0 },
218 { "-text-url", ".textURL", XrmoptionSepArg, 0 },
219 { "-text-program", ".textProgram", XrmoptionSepArg, 0 },
220 { "-grab-desktop", ".grabDesktopImages", XrmoptionNoArg, "True" },
221 { "-no-grab-desktop", ".grabDesktopImages", XrmoptionNoArg, "False"},
222 { "-choose-random-images", ".chooseRandomImages",XrmoptionNoArg, "True" },
223 { "-no-choose-random-images",".chooseRandomImages",XrmoptionNoArg, "False"},
224 { "-image-directory", ".imageDirectory", XrmoptionSepArg, 0 },
225 { "-fps", ".doFPS", XrmoptionNoArg, "True" },
226 { "-no-fps", ".doFPS", XrmoptionNoArg, "False"},
227 { "-foreground", ".foreground", XrmoptionSepArg, 0 },
228 { "-fg", ".foreground", XrmoptionSepArg, 0 },
229 { "-background", ".background", XrmoptionSepArg, 0 },
230 { "-bg", ".background", XrmoptionSepArg, 0 },
233 // <xscreensaver-updater />
234 { "-" SUSUEnableAutomaticChecksKey,
235 "." SUSUEnableAutomaticChecksKey, XrmoptionNoArg, "True" },
236 { "-no-" SUSUEnableAutomaticChecksKey,
237 "." SUSUEnableAutomaticChecksKey, XrmoptionNoArg, "False" },
238 { "-" SUAutomaticallyUpdateKey,
239 "." SUAutomaticallyUpdateKey, XrmoptionNoArg, "True" },
240 { "-no-" SUAutomaticallyUpdateKey,
241 "." SUAutomaticallyUpdateKey, XrmoptionNoArg, "False" },
242 { "-" SUSendProfileInfoKey,
243 "." SUSendProfileInfoKey, XrmoptionNoArg,"True" },
244 { "-no-" SUSendProfileInfoKey,
245 "." SUSendProfileInfoKey, XrmoptionNoArg,"False"},
246 { "-" SUScheduledCheckIntervalKey,
247 "." SUScheduledCheckIntervalKey, XrmoptionSepArg, 0 },
248 # endif // !USE_IPHONE
252 static const char *default_defaults [] = {
254 ".doubleBuffer: True",
255 ".multiSample: False",
263 ".textURL: http://en.wikipedia.org/w/index.php?title=Special:NewPages&feed=rss",
265 ".grabDesktopImages: yes",
267 ".chooseRandomImages: no",
269 ".chooseRandomImages: yes",
271 ".imageDirectory: ~/Pictures",
273 ".texFontCacheSize: 30",
277 # define STR(S) STR1(S)
278 # define __objc_yes Yes
279 # define __objc_no No
280 "." SUSUEnableAutomaticChecksKey ": " STR(SUSUEnableAutomaticChecksDef),
281 "." SUAutomaticallyUpdateKey ": " STR(SUAutomaticallyUpdateDef),
282 "." SUSendProfileInfoKey ": " STR(SUSendProfileInfoDef),
283 "." SUScheduledCheckIntervalKey ": " STR(SUScheduledCheckIntervalDef),
288 # endif // USE_IPHONE
293 for (i = 0; default_options[i].option; i++)
295 for (i = 0; opts[i].option; i++)
298 XrmOptionDescRec *opts2 = (XrmOptionDescRec *)
299 calloc (count + 1, sizeof (*opts2));
303 while (default_options[j].option) {
304 opts2[i] = default_options[j];
308 while (opts[j].option) {
319 for (i = 0; default_defaults[i]; i++)
321 for (i = 0; defs[i]; i++)
324 const char **defs2 = (const char **) calloc (count + 1, sizeof (*defs2));
328 while (default_defaults[j]) {
329 defs2[i] = default_defaults[j];
343 /* Returns the current time in seconds as a double.
349 # ifdef GETTIMEOFDAY_TWO_ARGS
351 gettimeofday(&now, &tzp);
356 return (now.tv_sec + ((double) now.tv_usec * 0.000001));
360 #if TARGET_IPHONE_SIMULATOR
362 orientname(unsigned long o)
365 case UIDeviceOrientationUnknown: return "Unknown";
366 case UIDeviceOrientationPortrait: return "Portrait";
367 case UIDeviceOrientationPortraitUpsideDown: return "PortraitUpsideDown";
368 case UIDeviceOrientationLandscapeLeft: return "LandscapeLeft";
369 case UIDeviceOrientationLandscapeRight: return "LandscapeRight";
370 case UIDeviceOrientationFaceUp: return "FaceUp";
371 case UIDeviceOrientationFaceDown: return "FaceDown";
372 default: return "ERROR";
375 #endif // TARGET_IPHONE_SIMULATOR
378 - (id) initWithFrame:(NSRect)frame
379 saverName:(NSString *)saverName
380 isPreview:(BOOL)isPreview
383 initial_bounds = frame.size;
384 rot_current_size = frame.size; // needs to be early, because
385 rot_from = rot_current_size; // [self setFrame] is called by
386 rot_to = rot_current_size; // [super initWithFrame].
390 if (! (self = [super initWithFrame:frame isPreview:isPreview]))
393 xsft = [self findFunctionTable: saverName];
402 [self setMultipleTouchEnabled:YES];
403 orientation = UIDeviceOrientationUnknown;
404 [self didRotate:nil];
406 # endif // USE_IPHONE
410 xsft->setup_cb (xsft, xsft->setup_arg);
412 /* The plist files for these preferences show up in
413 $HOME/Library/Preferences/ByHost/ in a file named like
414 "org.jwz.xscreensaver.<SAVERNAME>.<NUMBERS>.plist"
416 NSString *name = [NSString stringWithCString:xsft->progclass
417 encoding:NSISOLatin1StringEncoding];
418 name = [@"org.jwz.xscreensaver." stringByAppendingString:name];
419 [self setResourcesEnv:name];
422 XrmOptionDescRec *opts = 0;
423 const char **defs = 0;
424 add_default_options (xsft->options, xsft->defaults, &opts, &defs);
425 prefsReader = [[PrefsReader alloc]
426 initWithName:name xrmKeys:opts defaults:defs];
428 // free (opts); // bah, we need these! #### leak!
429 xsft->options = opts;
431 progname = progclass = xsft->progclass;
435 # ifdef USE_BACKBUFFER
436 [self createBackbuffer:NSSizeToCGSize(frame.size)];
441 // So we can tell when we're docked.
442 [UIDevice currentDevice].batteryMonitoringEnabled = YES;
443 # endif // USE_IPHONE
450 # if !defined(USE_IPHONE) && defined(BACKBUFFER_CALAYER)
451 [self setLayer: [CALayer layer]];
452 self.layer.delegate = self;
453 self.layer.opaque = YES;
454 [self setWantsLayer: YES];
455 # endif // !USE_IPHONE && BACKBUFFER_CALAYER
459 - (id) initWithFrame:(NSRect)frame isPreview:(BOOL)p
461 return [self initWithFrame:frame saverName:0 isPreview:p];
467 NSAssert(![self isAnimating], @"still animating");
468 NSAssert(!xdata, @"xdata not yet freed");
469 NSAssert(!xdpy, @"xdpy not yet freed");
471 # ifdef USE_BACKBUFFER
473 CGContextRelease (backbuffer);
476 CGColorSpaceRelease (colorspace);
478 # ifdef BACKBUFFER_CGCONTEXT
480 CGContextRelease (window_ctx);
481 # endif // BACKBUFFER_CGCONTEXT
483 # endif // USE_BACKBUFFER
485 [prefsReader release];
493 - (PrefsReader *) prefsReader
500 - (void) lockFocus { }
501 - (void) unlockFocus { }
507 /* A few seconds after the saver launches, we store the "wasRunning"
508 preference. This is so that if the saver is crashing at startup,
509 we don't launch it again next time, getting stuck in a crash loop.
511 - (void) allSystemsGo: (NSTimer *) timer
513 NSAssert (timer == crash_timer, @"crash timer screwed up");
516 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
517 [prefs setBool:YES forKey:@"wasRunning"];
523 - (void) startAnimation
525 NSAssert(![self isAnimating], @"already animating");
526 NSAssert(!initted_p && !xdata, @"already initialized");
528 // See comment in render_x11() for why this value is important:
529 [self setAnimationTimeInterval: 1.0 / 120.0];
531 [super startAnimation];
532 /* We can't draw on the window from this method, so we actually do the
533 initialization of the screen saver (xsft->init_cb) in the first call
534 to animateOneFrame() instead.
539 [crash_timer invalidate];
541 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
542 [prefs removeObjectForKey:@"wasRunning"];
545 crash_timer = [NSTimer scheduledTimerWithTimeInterval: 5
547 selector:@selector(allSystemsGo:)
551 # endif // USE_IPHONE
553 // Never automatically turn the screen off if we are docked,
554 // and an animation is running.
557 [UIApplication sharedApplication].idleTimerDisabled =
558 ([UIDevice currentDevice].batteryState != UIDeviceBatteryStateUnplugged);
559 [[UIApplication sharedApplication]
560 setStatusBarHidden:YES withAnimation:UIStatusBarAnimationNone];
565 - (void)stopAnimation
567 NSAssert([self isAnimating], @"not animating");
571 [self lockFocus]; // in case something tries to draw from here
572 [self prepareContext];
574 /* I considered just not even calling the free callback at all...
575 But webcollage-cocoa needs it, to kill the inferior webcollage
576 processes (since the screen saver framework never generates a
577 SIGPIPE for them...) Instead, I turned off the free call in
578 xlockmore.c, which is where all of the bogus calls are anyway.
580 xsft->free_cb (xdpy, xwindow, xdata);
583 // xdpy must be freed before dealloc is called, because xdpy owns a
584 // circular reference to the parent XScreenSaverView.
585 jwxyz_free_display (xdpy);
589 // setup_p = NO; // #### wait, do we need this?
596 [crash_timer invalidate];
598 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
599 [prefs removeObjectForKey:@"wasRunning"];
601 # endif // USE_IPHONE
603 [super stopAnimation];
605 // When an animation is no longer running (e.g., looking at the list)
606 // then it's ok to power off the screen when docked.
609 [UIApplication sharedApplication].idleTimerDisabled = NO;
610 [[UIApplication sharedApplication]
611 setStatusBarHidden:NO withAnimation:UIStatusBarAnimationNone];
616 /* Hook for the XScreenSaverGLView subclass
618 - (void) prepareContext
622 /* Hook for the XScreenSaverGLView subclass
624 - (void) resizeContext
630 screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
632 fps_compute (fpst, 0, -1);
639 /* On iPhones with Retina displays, we can draw the savers in "real"
640 pixels, and that works great. The 320x480 "point" screen is really
641 a 640x960 *pixel* screen. However, Retina iPads have 768x1024
642 point screens which are 1536x2048 pixels, and apparently that's
643 enough pixels that copying those bits to the screen is slow. Like,
644 drops us from 15fps to 7fps. So, on Retina iPads, we don't draw in
645 real pixels. This will probably make the savers look better
646 anyway, since that's a higher resolution than most desktop monitors
647 have even today. (This is only true for X11 programs, not GL
648 programs. Those are fine at full rez.)
650 This method is overridden in XScreenSaverGLView, since this kludge
651 isn't necessary for GL programs, being resolution independent by
654 - (CGFloat) hackedContentScaleFactor
656 GLfloat s = [self contentScaleFactor];
657 if (initial_bounds.width >= 1024 ||
658 initial_bounds.height >= 1024)
664 static GLfloat _global_rot_current_angle_kludge;
666 double current_device_rotation (void)
668 return -_global_rot_current_angle_kludge;
672 - (void) hackRotation
674 if (rotation_ratio >= 0) { // in the midst of a rotation animation
676 # define CLAMP180(N) while (N < 0) N += 360; while (N > 180) N -= 360
677 GLfloat f = angle_from;
678 GLfloat t = angle_to;
681 GLfloat dist = -(t-f);
684 // Intermediate angle.
685 rot_current_angle = f - rotation_ratio * dist;
687 // Intermediate frame size.
688 rot_current_size.width = rot_from.width +
689 rotation_ratio * (rot_to.width - rot_from.width);
690 rot_current_size.height = rot_from.height +
691 rotation_ratio * (rot_to.height - rot_from.height);
693 // Tick animation. Complete rotation in 1/6th sec.
694 double now = double_time();
695 double duration = 1/6.0;
696 rotation_ratio = 1 - ((rot_start_time + duration - now) / duration);
698 if (rotation_ratio > 1 || ignore_rotation_p) { // Done animating.
699 orientation = new_orientation;
700 rot_current_angle = angle_to;
701 rot_current_size = rot_to;
704 # if TARGET_IPHONE_SIMULATOR
705 NSLog (@"rotation ended: %s %d, %d x %d",
706 orientname(orientation), (int) rot_current_angle,
707 (int) rot_current_size.width, (int) rot_current_size.height);
710 // Check orientation again in case we rotated again while rotating:
711 // this is a no-op if nothing has changed.
712 [self didRotate:nil];
714 } else { // Not animating a rotation.
715 rot_current_angle = angle_to;
716 rot_current_size = rot_to;
719 CLAMP180(rot_current_angle);
720 _global_rot_current_angle_kludge = rot_current_angle;
724 double s = [self hackedContentScaleFactor];
725 CGSize rotsize = ((ignore_rotation_p || ![self reshapeRotatedWindow])
728 if ((int) backbuffer_size.width != (int) (s * rotsize.width) ||
729 (int) backbuffer_size.height != (int) (s * rotsize.height))
734 - (void)alertView:(UIAlertView *)av clickedButtonAtIndex:(NSInteger)i
736 if (i == 0) exit (-1); // Cancel
737 [self stopAndClose:NO]; // Keep going
740 - (void) handleException: (NSException *)e
742 NSLog (@"Caught exception: %@", e);
743 [[[UIAlertView alloc] initWithTitle:
744 [NSString stringWithFormat: @"%s crashed!",
747 [NSString stringWithFormat:
748 @"The error message was:"
750 "If it keeps crashing, try "
751 "resetting its options.",
754 cancelButtonTitle: @"Exit"
755 otherButtonTitles: @"Keep going", nil]
757 [self stopAnimation];
763 #ifdef USE_BACKBUFFER
765 /* Create a bitmap context into which we render everything.
766 If the desired size has changed, re-created it.
768 - (void) createBackbuffer:(CGSize)new_size
770 // Colorspaces and CGContexts only happen with non-GL hacks.
772 CGColorSpaceRelease (colorspace);
773 # ifdef BACKBUFFER_CGCONTEXT
775 CGContextRelease (window_ctx);
778 NSWindow *window = [self window];
780 if (window && xdpy) {
783 # if defined(BACKBUFFER_CGCONTEXT)
784 // TODO: This was borrowed from jwxyz_window_resized, and should
785 // probably be refactored.
787 // Figure out which screen the window is currently on.
788 CGDirectDisplayID cgdpy = 0;
792 // TODO: XTranslateCoordinates is returning (0,1200) on my system.
794 // In any case, those weren't valid coordinates for CGGetDisplaysWithPoint.
795 // XTranslateCoordinates (xdpy, xwindow, NULL, 0, 0, &wx, &wy, NULL);
800 p0 = [window convertBaseToScreen:p0];
801 CGPoint p = {p0.x, p0.y};
803 CGGetDisplaysWithPoint (p, 1, &cgdpy, &n);
804 NSAssert (cgdpy, @"unable to find CGDisplay");
808 // Figure out this screen's colorspace, and use that for every CGImage.
810 CMProfileRef profile = 0;
812 // CMGetProfileByAVID is deprecated as of OS X 10.6, but there's no
813 // documented replacement as of OS X 10.9.
814 // http://lists.apple.com/archives/colorsync-dev/2012/Nov/msg00001.html
815 CMGetProfileByAVID ((CMDisplayIDType) cgdpy, &profile);
816 NSAssert (profile, @"unable to find colorspace profile");
817 colorspace = CGColorSpaceCreateWithPlatformColorSpace (profile);
818 NSAssert (colorspace, @"unable to find colorspace");
820 # elif defined(BACKBUFFER_CALAYER)
821 // Was apparently faster until 10.9.
822 colorspace = CGColorSpaceCreateDeviceRGB ();
823 # endif // BACKBUFFER_CALAYER
825 # ifdef BACKBUFFER_CGCONTEXT
826 window_ctx = [[window graphicsContext] graphicsPort];
827 CGContextRetain (window_ctx);
828 # endif // BACKBUFFER_CGCONTEXT
832 # ifdef BACKBUFFER_CGCONTEXT
834 # endif // BACKBUFFER_CGCONTEXT
835 colorspace = CGColorSpaceCreateDeviceRGB();
839 (int)backbuffer_size.width == (int)new_size.width &&
840 (int)backbuffer_size.height == (int)new_size.height)
843 CGSize osize = backbuffer_size;
844 CGContextRef ob = backbuffer;
846 backbuffer_size = new_size;
848 # if TARGET_IPHONE_SIMULATOR
849 NSLog(@"backbuffer %.0f %.0f",
850 backbuffer_size.width, backbuffer_size.height);
853 backbuffer = CGBitmapContextCreate (NULL,
854 (int)backbuffer_size.width,
855 (int)backbuffer_size.height,
857 (int)backbuffer_size.width * 4,
859 // kCGImageAlphaPremultipliedLast
860 (kCGImageAlphaNoneSkipFirst |
861 kCGBitmapByteOrder32Host)
863 NSAssert (backbuffer, @"unable to allocate back buffer");
867 r.origin.x = r.origin.y = 0;
868 r.size = backbuffer_size;
869 CGContextSetGrayFillColor (backbuffer, 0, 1);
870 CGContextFillRect (backbuffer, r);
873 // Restore old bits, as much as possible, to the X11 upper left origin.
876 rect.origin.y = (backbuffer_size.height - osize.height);
878 CGImageRef img = CGBitmapContextCreateImage (ob);
879 CGContextDrawImage (backbuffer, rect, img);
880 CGImageRelease (img);
881 CGContextRelease (ob);
885 #endif // USE_BACKBUFFER
888 /* Inform X11 that the size of our window has changed.
892 if (!xwindow) return; // early
895 # ifdef USE_BACKBUFFER
897 double s = [self hackedContentScaleFactor];
898 CGSize rotsize = ((ignore_rotation_p || ![self reshapeRotatedWindow])
901 new_size.width = s * rotsize.width;
902 new_size.height = s * rotsize.height;
904 new_size = NSSizeToCGSize([self bounds].size);
907 [self createBackbuffer:new_size];
908 jwxyz_window_resized (xdpy, xwindow, 0, 0, new_size.width, new_size.height,
910 # else // !USE_BACKBUFFER
911 new_size = [self bounds].size;
912 jwxyz_window_resized (xdpy, xwindow, 0, 0, new_size.width, new_size.height,
914 # endif // !USE_BACKBUFFER
916 # if TARGET_IPHONE_SIMULATOR
917 NSLog(@"reshape %.0f x %.0f", new_size.width, new_size.height);
920 // Next time render_x11 is called, run the saver's reshape_cb.
930 if (orientation == UIDeviceOrientationUnknown)
931 [self didRotate:nil];
938 # ifdef USE_BACKBUFFER
939 NSAssert (backbuffer, @"no back buffer");
940 xdpy = jwxyz_make_display (self, backbuffer);
942 xdpy = jwxyz_make_display (self, 0);
944 xwindow = XRootWindow (xdpy, 0);
947 /* Some X11 hacks (fluidballs) want to ignore all rotation events. */
949 get_boolean_resource (xdpy, "ignoreRotation", "IgnoreRotation");
950 # endif // USE_IPHONE
958 xsft->setup_cb (xsft, xsft->setup_arg);
962 NSAssert(!xdata, @"xdata already initialized");
968 XSetWindowBackground (xdpy, xwindow,
969 get_pixel_resource (xdpy, 0,
970 "background", "Background"));
971 XClearWindow (xdpy, xwindow);
974 [[self window] setAcceptsMouseMovedEvents:YES];
977 /* In MacOS 10.5, this enables "QuartzGL", meaning that the Quartz
978 drawing primitives will run on the GPU instead of the CPU.
979 It seems like it might make things worse rather than better,
980 though... Plus it makes us binary-incompatible with 10.4.
982 # if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5
983 [[self window] setPreferredBackingLocation:
984 NSWindowBackingLocationVideoMemory];
988 /* Kludge: even though the init_cb functions are declared to take 2 args,
989 actually call them with 3, for the benefit of xlockmore_init() and
992 void *(*init_cb) (Display *, Window, void *) =
993 (void *(*) (Display *, Window, void *)) xsft->init_cb;
995 xdata = init_cb (xdpy, xwindow, xsft->setup_arg);
996 // NSAssert(xdata, @"no xdata from init");
997 if (! xdata) abort();
999 if (get_boolean_resource (xdpy, "doFPS", "DoFPS")) {
1000 fpst = fps_init (xdpy, xwindow);
1001 if (! xsft->fps_cb) xsft->fps_cb = screenhack_do_fps;
1007 [self checkForUpdates];
1011 /* I don't understand why we have to do this *every frame*, but we do,
1012 or else the cursor comes back on.
1015 if (![self isPreview])
1016 [NSCursor setHiddenUntilMouseMoves:YES];
1022 /* This is just a guess, but the -fps code wants to know how long
1023 we were sleeping between frames.
1025 long usecs = 1000000 * [self animationTimeInterval];
1026 usecs -= 200; // caller apparently sleeps for slightly less sometimes...
1027 if (usecs < 0) usecs = 0;
1028 fps_slept (fpst, usecs);
1032 /* It turns out that on some systems (possibly only 10.5 and older?)
1033 [ScreenSaverView setAnimationTimeInterval] does nothing. This means
1034 that we cannot rely on it.
1036 Some of the screen hacks want to delay for long periods, and letting the
1037 framework run the update function at 30 FPS when it really wanted half a
1038 minute between frames would be bad. So instead, we assume that the
1039 framework's animation timer might fire whenever, but we only invoke the
1040 screen hack's "draw frame" method when enough time has expired.
1042 This means two extra calls to gettimeofday() per frame. For fast-cycling
1043 screen savers, that might actually slow them down. Oh well.
1045 A side-effect of this is that it's not possible for a saver to request
1046 an animation interval that is faster than animationTimeInterval.
1048 HOWEVER! On modern systems where setAnimationTimeInterval is *not*
1049 ignored, it's important that it be faster than 30 FPS. 120 FPS is good.
1051 An NSTimer won't fire if the timer is already running the invocation
1052 function from a previous firing. So, if we use a 30 FPS
1053 animationTimeInterval (33333 µs) and a screenhack takes 40000 µs for a
1054 frame, there will be a 26666 µs delay until the next frame, 66666 µs
1055 after the beginning of the current frame. In other words, 25 FPS
1058 Frame rates tend to snap to values of 30/N, where N is a positive
1059 integer, i.e. 30 FPS, 15 FPS, 10, 7.5, 6. And the 'snapped' frame rate
1060 is rounded down from what it would normally be.
1062 So if we set animationTimeInterval to 1/120 instead of 1/30, frame rates
1063 become values of 60/N, 120/N, or 240/N, with coarser or finer frame rate
1064 steps for higher or lower animation time intervals respectively.
1067 gettimeofday (&tv, 0);
1068 double now = tv.tv_sec + (tv.tv_usec / 1000000.0);
1069 if (now < next_frame_time) return;
1071 [self prepareContext];
1074 // We do this here instead of in setFrame so that all the
1075 // Xlib drawing takes place under the animation timer.
1076 [self resizeContext];
1078 # ifndef USE_BACKBUFFER
1080 # else // USE_BACKBUFFER
1083 r.size.width = backbuffer_size.width;
1084 r.size.height = backbuffer_size.height;
1085 # endif // USE_BACKBUFFER
1087 xsft->reshape_cb (xdpy, xwindow, xdata, r.size.width, r.size.height);
1091 // Run any XtAppAddInput callbacks now.
1092 // (Note that XtAppAddTimeOut callbacks have already been run by
1093 // the Cocoa event loop.)
1095 jwxyz_sources_run (display_sources_data (xdpy));
1101 NSDisableScreenUpdates();
1103 // NSAssert(xdata, @"no xdata when drawing");
1104 if (! xdata) abort();
1105 unsigned long delay = xsft->draw_cb (xdpy, xwindow, xdata);
1106 if (fpst) xsft->fps_cb (xdpy, xwindow, fpst, xdata);
1108 NSEnableScreenUpdates();
1111 gettimeofday (&tv, 0);
1112 now = tv.tv_sec + (tv.tv_usec / 1000000.0);
1113 next_frame_time = now + (delay / 1000000.0);
1115 # ifdef USE_IPHONE // Allow savers on the iPhone to run full-tilt.
1116 if (delay < [self animationTimeInterval])
1117 [self setAnimationTimeInterval:(delay / 1000000.0)];
1120 # ifdef DO_GC_HACKERY
1121 /* Current theory is that the 10.6 garbage collector sucks in the
1124 It only does a collection when a threshold of outstanding
1125 collectable allocations has been surpassed. However, CoreGraphics
1126 creates lots of small collectable allocations that contain pointers
1127 to very large non-collectable allocations: a small CG object that's
1128 collectable referencing large malloc'd allocations (non-collectable)
1129 containing bitmap data. So the large allocation doesn't get freed
1130 until GC collects the small allocation, which triggers its finalizer
1131 to run which frees the large allocation. So GC is deciding that it
1132 doesn't really need to run, even though the process has gotten
1133 enormous. GC eventually runs once pageouts have happened, but by
1134 then it's too late, and the machine's resident set has been
1137 So, we force an exhaustive garbage collection in this process
1138 approximately every 5 seconds whether the system thinks it needs
1142 static int tick = 0;
1143 if (++tick > 5*30) {
1145 objc_collect (OBJC_EXHAUSTIVE_COLLECTION);
1148 # endif // DO_GC_HACKERY
1152 @catch (NSException *e) {
1153 [self handleException: e];
1155 # endif // USE_IPHONE
1159 /* drawRect always does nothing, and animateOneFrame renders bits to the
1160 screen. This is (now) true of both X11 and GL on both MacOS and iOS.
1163 - (void)drawRect:(NSRect)rect
1165 if (xwindow) // clear to the X window's bg color, not necessarily black.
1166 XClearWindow (xdpy, xwindow);
1168 [super drawRect:rect]; // early: black.
1172 #ifndef USE_BACKBUFFER
1174 - (void) animateOneFrame
1177 jwxyz_flush_context(xdpy);
1180 #else // USE_BACKBUFFER
1182 - (void) animateOneFrame
1184 // Render X11 into the backing store bitmap...
1186 NSAssert (backbuffer, @"no back buffer");
1189 UIGraphicsPushContext (backbuffer);
1195 UIGraphicsPopContext();
1199 // Then compute the transformations for rotation.
1200 double hs = [self hackedContentScaleFactor];
1201 double s = [self contentScaleFactor];
1203 // The rotation origin for layer.affineTransform is in the center already.
1204 CGAffineTransform t = ignore_rotation_p ?
1205 CGAffineTransformIdentity :
1206 CGAffineTransformMakeRotation (rot_current_angle / (180.0 / M_PI));
1209 self.layer.affineTransform = CGAffineTransformScale(t, f, f);
1212 bounds.origin.x = 0;
1213 bounds.origin.y = 0;
1215 bounds.size.width = backbuffer_size.width / s;
1216 bounds.size.height = backbuffer_size.height / s;
1217 self.layer.bounds = bounds;
1218 # endif // USE_IPHONE
1220 # if defined(BACKBUFFER_CALAYER)
1221 [self.layer setNeedsDisplay];
1222 # elif defined(BACKBUFFER_CGCONTEXT)
1224 w = CGBitmapContextGetWidth (backbuffer),
1225 h = CGBitmapContextGetHeight (backbuffer);
1227 size_t bpl = CGBitmapContextGetBytesPerRow (backbuffer);
1228 CGDataProviderRef prov = CGDataProviderCreateWithData (NULL,
1229 CGBitmapContextGetData(backbuffer),
1234 CGImageRef img = CGImageCreate (w, h,
1236 CGBitmapContextGetBytesPerRow(backbuffer),
1238 CGBitmapContextGetBitmapInfo(backbuffer),
1240 kCGRenderingIntentDefault);
1242 CGDataProviderRelease (prov);
1247 rect.size = backbuffer_size;
1248 CGContextDrawImage (window_ctx, rect, img);
1250 CGImageRelease (img);
1252 CGContextFlush (window_ctx);
1253 # endif // BACKBUFFER_CGCONTEXT
1256 # ifdef BACKBUFFER_CALAYER
1258 - (void) drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
1260 // This "isn't safe" if NULL is passed to CGBitmapCreateContext before iOS 4.
1261 char *dest_data = (char *)CGBitmapContextGetData (ctx);
1263 // The CGContext here is normally upside-down on iOS.
1265 CGBitmapContextGetBitmapInfo (ctx) ==
1266 (kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Host)
1268 && CGContextGetCTM (ctx).d < 0
1269 # endif // USE_IPHONE
1272 size_t dest_height = CGBitmapContextGetHeight (ctx);
1273 size_t dest_bpr = CGBitmapContextGetBytesPerRow (ctx);
1274 size_t src_height = CGBitmapContextGetHeight (backbuffer);
1275 size_t src_bpr = CGBitmapContextGetBytesPerRow (backbuffer);
1276 char *src_data = (char *)CGBitmapContextGetData (backbuffer);
1278 size_t height = src_height < dest_height ? src_height : dest_height;
1280 if (src_bpr == dest_bpr) {
1281 // iPad 1: 4.0 ms, iPad 2: 6.7 ms
1282 memcpy (dest_data, src_data, src_bpr * height);
1284 // iPad 1: 4.6 ms, iPad 2: 7.2 ms
1285 size_t bpr = src_bpr < dest_bpr ? src_bpr : dest_bpr;
1287 memcpy (dest_data, src_data, bpr);
1289 src_data += src_bpr;
1290 dest_data += dest_bpr;
1295 // iPad 1: 9.6 ms, iPad 2: 12.1 ms
1298 CGContextScaleCTM (ctx, 1, -1);
1299 CGFloat s = [self contentScaleFactor];
1300 CGFloat hs = [self hackedContentScaleFactor];
1301 CGContextTranslateCTM (ctx, 0, -backbuffer_size.height * hs / s);
1302 # endif // USE_IPHONE
1304 CGImageRef img = CGBitmapContextCreateImage (backbuffer);
1305 CGContextDrawImage (ctx, self.layer.bounds, img);
1306 CGImageRelease (img);
1309 # endif // BACKBUFFER_CALAYER
1311 #endif // USE_BACKBUFFER
1315 - (void) setFrame:(NSRect) newRect
1317 [super setFrame:newRect];
1319 if (xwindow) // inform Xlib that the window has changed now.
1324 # ifndef USE_IPHONE // Doesn't exist on iOS
1325 - (void) setFrameSize:(NSSize) newSize
1327 [super setFrameSize:newSize];
1331 # endif // !USE_IPHONE
1334 +(BOOL) performGammaFade
1339 - (BOOL) hasConfigureSheet
1344 + (NSString *) decompressXML: (NSData *)data
1346 if (! data) return 0;
1347 BOOL compressed_p = !!strncmp ((const char *) data.bytes, "<?xml", 5);
1349 // If it's not already XML, decompress it.
1350 NSAssert (compressed_p, @"xml isn't compressed");
1352 NSMutableData *data2 = 0;
1355 memset (&zs, 0, sizeof(zs));
1356 ret = inflateInit2 (&zs, 16 + MAX_WBITS);
1358 UInt32 usize = * (UInt32 *) (data.bytes + data.length - 4);
1359 data2 = [NSMutableData dataWithLength: usize];
1360 zs.next_in = (Bytef *) data.bytes;
1361 zs.avail_in = (uint) data.length;
1362 zs.next_out = (Bytef *) data2.bytes;
1363 zs.avail_out = (uint) data2.length;
1364 ret = inflate (&zs, Z_FINISH);
1367 if (ret == Z_OK || ret == Z_STREAM_END)
1370 NSAssert2 (0, @"gunzip error: %d: %s",
1371 ret, (zs.msg ? zs.msg : "<null>"));
1374 return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
1379 - (NSWindow *) configureSheet
1381 - (UIViewController *) configureView
1384 NSBundle *bundle = [NSBundle bundleForClass:[self class]];
1385 NSString *file = [NSString stringWithCString:xsft->progclass
1386 encoding:NSISOLatin1StringEncoding];
1387 file = [file lowercaseString];
1388 NSString *path = [bundle pathForResource:file ofType:@"xml"];
1390 NSLog (@"%@.xml does not exist in the application bundle: %@/",
1391 file, [bundle resourcePath]);
1396 UIViewController *sheet;
1397 # else // !USE_IPHONE
1399 # endif // !USE_IPHONE
1401 NSData *xmld = [NSData dataWithContentsOfFile:path];
1402 NSString *xml = [[self class] decompressXML: xmld];
1403 sheet = [[XScreenSaverConfigSheet alloc]
1404 initWithXML:[xml dataUsingEncoding:NSUTF8StringEncoding]
1405 options:xsft->options
1406 controller:[prefsReader userDefaultsController]
1407 globalController:[prefsReader globalDefaultsController]
1408 defaults:[prefsReader defaultOptions]];
1410 // #### am I expected to retain this, or not? wtf.
1411 // I thought not, but if I don't do this, we (sometimes) crash.
1412 // #### Analyze says "potential leak of an object stored into sheet"
1419 - (NSUserDefaultsController *) userDefaultsController
1421 return [prefsReader userDefaultsController];
1425 /* Announce our willingness to accept keyboard input.
1427 - (BOOL)acceptsFirstResponder
1437 # else // USE_IPHONE
1439 // There's no way to play a standard system alert sound!
1440 // We'd have to include our own WAV for that.
1442 // Or we could vibrate:
1443 // #import <AudioToolbox/AudioToolbox.h>
1444 // AudioServicesPlaySystemSound (kSystemSoundID_Vibrate);
1446 // Instead, just flash the screen white, then fade.
1448 UIView *v = [[UIView alloc] initWithFrame: [self frame]];
1449 [v setBackgroundColor: [UIColor whiteColor]];
1450 [[self window] addSubview:v];
1451 [UIView animateWithDuration: 0.1
1452 animations:^{ [v setAlpha: 0.0]; }
1453 completion:^(BOOL finished) { [v removeFromSuperview]; } ];
1455 # endif // USE_IPHONE
1459 /* Send an XEvent to the hack. Returns YES if it was handled.
1461 - (BOOL) sendEvent: (XEvent *) e
1463 if (!initted_p || ![self isAnimating]) // no event handling unless running.
1467 [self prepareContext];
1468 BOOL result = xsft->event_cb (xdpy, xwindow, xdata, e);
1476 /* Convert an NSEvent into an XEvent, and pass it along.
1477 Returns YES if it was handled.
1479 - (BOOL) convertEvent: (NSEvent *) e
1483 memset (&xe, 0, sizeof(xe));
1487 int flags = [e modifierFlags];
1488 if (flags & NSAlphaShiftKeyMask) state |= LockMask;
1489 if (flags & NSShiftKeyMask) state |= ShiftMask;
1490 if (flags & NSControlKeyMask) state |= ControlMask;
1491 if (flags & NSAlternateKeyMask) state |= Mod1Mask;
1492 if (flags & NSCommandKeyMask) state |= Mod2Mask;
1494 NSPoint p = [[[e window] contentView] convertPoint:[e locationInWindow]
1497 double s = [self hackedContentScaleFactor];
1502 int y = s * ([self bounds].size.height - p.y);
1504 xe.xany.type = type;
1510 xe.xbutton.state = state;
1511 if ([e type] == NSScrollWheel)
1512 xe.xbutton.button = ([e deltaY] > 0 ? Button4 :
1513 [e deltaY] < 0 ? Button5 :
1514 [e deltaX] > 0 ? Button6 :
1515 [e deltaX] < 0 ? Button7 :
1518 xe.xbutton.button = [e buttonNumber] + 1;
1523 xe.xmotion.state = state;
1528 NSString *ns = (([e type] == NSFlagsChanged) ? 0 :
1529 [e charactersIgnoringModifiers]);
1532 if (!ns || [ns length] == 0) // dead key
1534 // Cocoa hides the difference between left and right keys.
1535 // Also we only get KeyPress events for these, no KeyRelease
1536 // (unless we hack the mod state manually. Bleh.)
1538 if (flags & NSAlphaShiftKeyMask) k = XK_Caps_Lock;
1539 else if (flags & NSShiftKeyMask) k = XK_Shift_L;
1540 else if (flags & NSControlKeyMask) k = XK_Control_L;
1541 else if (flags & NSAlternateKeyMask) k = XK_Alt_L;
1542 else if (flags & NSCommandKeyMask) k = XK_Meta_L;
1544 else if ([ns length] == 1) // real key
1546 switch ([ns characterAtIndex:0]) {
1547 case NSLeftArrowFunctionKey: k = XK_Left; break;
1548 case NSRightArrowFunctionKey: k = XK_Right; break;
1549 case NSUpArrowFunctionKey: k = XK_Up; break;
1550 case NSDownArrowFunctionKey: k = XK_Down; break;
1551 case NSPageUpFunctionKey: k = XK_Page_Up; break;
1552 case NSPageDownFunctionKey: k = XK_Page_Down; break;
1553 case NSHomeFunctionKey: k = XK_Home; break;
1554 case NSPrevFunctionKey: k = XK_Prior; break;
1555 case NSNextFunctionKey: k = XK_Next; break;
1556 case NSBeginFunctionKey: k = XK_Begin; break;
1557 case NSEndFunctionKey: k = XK_End; break;
1561 [ns cStringUsingEncoding:NSISOLatin1StringEncoding];
1562 k = (s && *s ? *s : 0);
1568 if (! k) return YES; // E.g., "KeyRelease XK_Shift_L"
1570 xe.xkey.keycode = k;
1571 xe.xkey.state = state;
1575 NSAssert1 (0, @"unknown X11 event type: %d", type);
1579 return [self sendEvent: &xe];
1583 - (void) mouseDown: (NSEvent *) e
1585 if (! [self convertEvent:e type:ButtonPress])
1586 [super mouseDown:e];
1589 - (void) mouseUp: (NSEvent *) e
1591 if (! [self convertEvent:e type:ButtonRelease])
1595 - (void) otherMouseDown: (NSEvent *) e
1597 if (! [self convertEvent:e type:ButtonPress])
1598 [super otherMouseDown:e];
1601 - (void) otherMouseUp: (NSEvent *) e
1603 if (! [self convertEvent:e type:ButtonRelease])
1604 [super otherMouseUp:e];
1607 - (void) mouseMoved: (NSEvent *) e
1609 if (! [self convertEvent:e type:MotionNotify])
1610 [super mouseMoved:e];
1613 - (void) mouseDragged: (NSEvent *) e
1615 if (! [self convertEvent:e type:MotionNotify])
1616 [super mouseDragged:e];
1619 - (void) otherMouseDragged: (NSEvent *) e
1621 if (! [self convertEvent:e type:MotionNotify])
1622 [super otherMouseDragged:e];
1625 - (void) scrollWheel: (NSEvent *) e
1627 if (! [self convertEvent:e type:ButtonPress])
1628 [super scrollWheel:e];
1631 - (void) keyDown: (NSEvent *) e
1633 if (! [self convertEvent:e type:KeyPress])
1637 - (void) keyUp: (NSEvent *) e
1639 if (! [self convertEvent:e type:KeyRelease])
1643 - (void) flagsChanged: (NSEvent *) e
1645 if (! [self convertEvent:e type:KeyPress])
1646 [super flagsChanged:e];
1652 - (void) stopAndClose:(Bool)relaunch_p
1654 if ([self isAnimating])
1655 [self stopAnimation];
1657 /* Need to make the SaverListController be the firstResponder again
1658 so that it can continue to receive its own shake events. I
1659 suppose that this abstraction-breakage means that I'm adding
1660 XScreenSaverView to the UINavigationController wrong...
1662 // UIViewController *v = [[self window] rootViewController];
1663 // if ([v isKindOfClass: [UINavigationController class]]) {
1664 // UINavigationController *n = (UINavigationController *) v;
1665 // [[n topViewController] becomeFirstResponder];
1667 [self resignFirstResponder];
1669 // Find SaverRunner.window (as opposed to SaverRunner.saverWindow)
1670 UIWindow *listWindow = 0;
1671 for (UIWindow *w in [[UIApplication sharedApplication] windows]) {
1672 if (w != [self window]) {
1678 UIView *fader = [self superview]; // the "backgroundView" view is our parent
1680 if (relaunch_p) { // Fake a shake on the SaverListController.
1681 UIViewController *v = [listWindow rootViewController];
1682 if ([v isKindOfClass: [UINavigationController class]]) {
1683 # if TARGET_IPHONE_SIMULATOR
1684 NSLog (@"simulating shake on saver list");
1686 UINavigationController *n = (UINavigationController *) v;
1687 [[n topViewController] motionEnded: UIEventSubtypeMotionShake
1690 } else { // Not launching another, animate our return to the list.
1691 # if TARGET_IPHONE_SIMULATOR
1692 NSLog (@"fading back to saver list");
1694 UIWindow *saverWindow = [self window]; // not SaverRunner.window
1695 [listWindow setHidden:NO];
1696 [UIView animateWithDuration: 0.5
1697 animations:^{ fader.alpha = 0.0; }
1698 completion:^(BOOL finished) {
1699 [fader removeFromSuperview];
1701 [saverWindow setHidden:YES];
1702 [listWindow makeKeyAndVisible];
1703 [[[listWindow rootViewController] view] becomeFirstResponder];
1709 /* Whether the shape of the X11 Window should be changed to HxW when the
1710 device is in a landscape orientation. X11 hacks want this, but OpenGL
1713 - (BOOL)reshapeRotatedWindow
1719 /* Called after the device's orientation has changed.
1721 Rotation is complicated: the UI, X11 and OpenGL work in 3 different ways.
1723 The UI (list of savers, preferences panels) is rotated by the system,
1724 because its UIWindow is under a UINavigationController that does
1725 automatic rotation, using Core Animation.
1727 The savers are under a different UIWindow and a UINavigationController
1728 that does not do automatic rotation.
1730 We have to do it this way for OpenGL savers because using Core Animation
1731 on an EAGLContext causes the OpenGL pipeline to fall back on software
1732 rendering and performance goes to hell.
1734 For X11-only savers, we could just use Core Animation and let the system
1735 handle it, but (maybe) it's simpler to do it the same way for X11 and GL.
1737 During and after rotation, the size/shape of the X11 window changes,
1738 and ConfigureNotify events are generated.
1740 X11 code (jwxyz) continues to draw into the (reshaped) backbuffer, which
1741 rotated at the last minute via a CGAffineTransformMakeRotation when it is
1742 copied to the display hardware.
1744 GL code always recieves a portrait-oriented X11 Window whose size never
1745 changes. The GL COLOR_BUFFER is displayed on the hardware directly and
1746 unrotated, so the GL hacks themselves are responsible for rotating the
1747 GL scene to match current_device_rotation().
1749 Touch events are converted to mouse clicks, and those XEvent coordinates
1750 are reported in the coordinate system currently in use by the X11 window.
1751 Again, GL must convert those.
1753 - (void)didRotate:(NSNotification *)notification
1755 UIDeviceOrientation current = [[UIDevice currentDevice] orientation];
1757 /* If the simulator starts up in the rotated position, sometimes
1758 the UIDevice says we're in Portrait when we're not -- but it
1759 turns out that the UINavigationController knows what's up!
1760 So get it from there.
1762 if (current == UIDeviceOrientationUnknown) {
1763 switch ([[[self window] rootViewController] interfaceOrientation]) {
1764 case UIInterfaceOrientationPortrait:
1765 current = UIDeviceOrientationPortrait;
1767 case UIInterfaceOrientationPortraitUpsideDown:
1768 current = UIDeviceOrientationPortraitUpsideDown;
1770 /* It's opposite day, "because rotating the device to the left requires
1771 rotating the content to the right" */
1772 case UIInterfaceOrientationLandscapeLeft:
1773 current = UIDeviceOrientationLandscapeRight;
1775 case UIInterfaceOrientationLandscapeRight:
1776 current = UIDeviceOrientationLandscapeLeft;
1783 /* On the iPad (but not iPhone 3GS, or the simulator) sometimes we get
1784 an orientation change event with an unknown orientation. Those seem
1785 to always be immediately followed by another orientation change with
1786 a *real* orientation change, so let's try just ignoring those bogus
1787 ones and hoping that the real one comes in shortly...
1789 if (current == UIDeviceOrientationUnknown)
1792 if (rotation_ratio >= 0) return; // in the midst of rotation animation
1793 if (orientation == current) return; // no change
1795 // When transitioning to FaceUp or FaceDown, pretend there was no change.
1796 if (current == UIDeviceOrientationFaceUp ||
1797 current == UIDeviceOrientationFaceDown)
1800 new_orientation = current; // current animation target
1801 rotation_ratio = 0; // start animating
1802 rot_start_time = double_time();
1804 switch (orientation) {
1805 case UIDeviceOrientationLandscapeLeft: angle_from = 90; break;
1806 case UIDeviceOrientationLandscapeRight: angle_from = 270; break;
1807 case UIDeviceOrientationPortraitUpsideDown: angle_from = 180; break;
1808 default: angle_from = 0; break;
1811 switch (new_orientation) {
1812 case UIDeviceOrientationLandscapeLeft: angle_to = 90; break;
1813 case UIDeviceOrientationLandscapeRight: angle_to = 270; break;
1814 case UIDeviceOrientationPortraitUpsideDown: angle_to = 180; break;
1815 default: angle_to = 0; break;
1818 switch (orientation) {
1819 case UIDeviceOrientationLandscapeRight: // from landscape
1820 case UIDeviceOrientationLandscapeLeft:
1821 rot_from.width = initial_bounds.height;
1822 rot_from.height = initial_bounds.width;
1824 default: // from portrait
1825 rot_from.width = initial_bounds.width;
1826 rot_from.height = initial_bounds.height;
1830 switch (new_orientation) {
1831 case UIDeviceOrientationLandscapeRight: // to landscape
1832 case UIDeviceOrientationLandscapeLeft:
1833 rot_to.width = initial_bounds.height;
1834 rot_to.height = initial_bounds.width;
1836 default: // to portrait
1837 rot_to.width = initial_bounds.width;
1838 rot_to.height = initial_bounds.height;
1842 # if TARGET_IPHONE_SIMULATOR
1843 NSLog (@"rotation begun: %s %d -> %s %d; %d x %d",
1844 orientname(orientation), (int) rot_current_angle,
1845 orientname(new_orientation), (int) angle_to,
1846 (int) rot_current_size.width, (int) rot_current_size.height);
1850 // If we've done a rotation but the saver hasn't been initialized yet,
1851 // don't bother going through an X11 resize, but just do it now.
1852 rot_start_time = 0; // dawn of time
1853 [self hackRotation];
1858 /* We distinguish between taps and drags.
1860 - Drags/pans (down, motion, up) are sent to the saver to handle.
1861 - Single-taps exit the saver.
1862 - Double-taps are sent to the saver as a "Space" keypress.
1863 - Swipes (really, two-finger drags/pans) send Up/Down/Left/RightArrow keys.
1865 This means a saver cannot respond to a single-tap. Only a few try to.
1868 - (void)initGestures
1870 UITapGestureRecognizer *dtap = [[UITapGestureRecognizer alloc]
1872 action:@selector(handleDoubleTap)];
1873 dtap.numberOfTapsRequired = 2;
1874 dtap.numberOfTouchesRequired = 1;
1876 UITapGestureRecognizer *stap = [[UITapGestureRecognizer alloc]
1878 action:@selector(handleTap)];
1879 stap.numberOfTapsRequired = 1;
1880 stap.numberOfTouchesRequired = 1;
1882 UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc]
1884 action:@selector(handlePan:)];
1885 pan.maximumNumberOfTouches = 1;
1886 pan.minimumNumberOfTouches = 1;
1888 // I couldn't get Swipe to work, but using a second Pan recognizer works.
1889 UIPanGestureRecognizer *pan2 = [[UIPanGestureRecognizer alloc]
1891 action:@selector(handlePan2:)];
1892 pan2.maximumNumberOfTouches = 2;
1893 pan2.minimumNumberOfTouches = 2;
1895 // Also handle long-touch, and treat that the same as Pan.
1896 // Without this, panning doesn't start until there's motion, so the trick
1897 // of holding down your finger to freeze the scene doesn't work.
1899 UILongPressGestureRecognizer *hold = [[UILongPressGestureRecognizer alloc]
1901 action:@selector(handleLongPress:)];
1902 hold.numberOfTapsRequired = 0;
1903 hold.numberOfTouchesRequired = 1;
1904 hold.minimumPressDuration = 0.25; /* 1/4th second */
1906 [stap requireGestureRecognizerToFail: dtap];
1907 [stap requireGestureRecognizerToFail: hold];
1908 [dtap requireGestureRecognizerToFail: hold];
1909 [pan requireGestureRecognizerToFail: hold];
1911 [self addGestureRecognizer: dtap];
1912 [self addGestureRecognizer: stap];
1913 [self addGestureRecognizer: pan];
1914 [self addGestureRecognizer: pan2];
1915 [self addGestureRecognizer: hold];
1925 /* Given a mouse (touch) coordinate in unrotated, unscaled view coordinates,
1926 convert it to what X11 and OpenGL expect.
1928 - (void) convertMouse:(int)rot x:(int*)x y:(int *)y
1930 int w = [self frame].size.width;
1931 int h = [self frame].size.height;
1932 int xx = *x, yy = *y;
1935 if (ignore_rotation_p) {
1936 // We need to rotate the coordinates to match the unrotated X11 window.
1937 switch (orientation) {
1938 case UIDeviceOrientationLandscapeRight:
1939 swap = xx; xx = h-yy; yy = swap;
1941 case UIDeviceOrientationLandscapeLeft:
1942 swap = xx; xx = yy; yy = w-swap;
1944 case UIDeviceOrientationPortraitUpsideDown:
1945 xx = w-xx; yy = h-yy;
1951 double s = [self contentScaleFactor];
1955 # if TARGET_IPHONE_SIMULATOR
1956 NSLog (@"touch %4d, %-4d in %4d x %-4d %d %d\n",
1957 *x, *y, (int)(w*s), (int)(h*s),
1958 ignore_rotation_p, [self reshapeRotatedWindow]);
1963 /* Single click exits saver.
1967 [self stopAndClose:NO];
1971 /* Double click sends Space KeyPress.
1973 - (void) handleDoubleTap
1975 if (!xsft->event_cb || !xwindow) return;
1978 memset (&xe, 0, sizeof(xe));
1979 xe.xkey.keycode = ' ';
1980 xe.xany.type = KeyPress;
1981 BOOL ok1 = [self sendEvent: &xe];
1982 xe.xany.type = KeyRelease;
1983 BOOL ok2 = [self sendEvent: &xe];
1989 /* Drag with one finger down: send MotionNotify.
1991 - (void) handlePan:(UIGestureRecognizer *)sender
1993 if (!xsft->event_cb || !xwindow) return;
1996 memset (&xe, 0, sizeof(xe));
1998 CGPoint p = [sender locationInView:self]; // this is in points, not pixels
2001 [self convertMouse: rot_current_angle x:&x y:&y];
2002 jwxyz_mouse_moved (xdpy, xwindow, x, y);
2004 switch (sender.state) {
2005 case UIGestureRecognizerStateBegan:
2006 xe.xany.type = ButtonPress;
2007 xe.xbutton.button = 1;
2012 case UIGestureRecognizerStateEnded:
2013 xe.xany.type = ButtonRelease;
2014 xe.xbutton.button = 1;
2019 case UIGestureRecognizerStateChanged:
2020 xe.xany.type = MotionNotify;
2029 BOOL ok = [self sendEvent: &xe];
2030 if (!ok && xe.xany.type == ButtonRelease)
2035 /* Hold one finger down: assume we're about to start dragging.
2036 Treat the same as Pan.
2038 - (void) handleLongPress:(UIGestureRecognizer *)sender
2040 [self handlePan:sender];
2045 /* Drag with 2 fingers down: send arrow keys.
2047 - (void) handlePan2:(UIPanGestureRecognizer *)sender
2049 if (!xsft->event_cb || !xwindow) return;
2051 if (sender.state != UIGestureRecognizerStateEnded)
2055 memset (&xe, 0, sizeof(xe));
2057 CGPoint p = [sender locationInView:self]; // this is in points, not pixels
2060 [self convertMouse: rot_current_angle x:&x y:&y];
2062 if (abs(x) > abs(y))
2063 xe.xkey.keycode = (x > 0 ? XK_Right : XK_Left);
2065 xe.xkey.keycode = (y > 0 ? XK_Down : XK_Up);
2067 BOOL ok1 = [self sendEvent: &xe];
2068 xe.xany.type = KeyRelease;
2069 BOOL ok2 = [self sendEvent: &xe];
2075 /* We need this to respond to "shake" gestures
2077 - (BOOL)canBecomeFirstResponder
2082 - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
2087 - (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event
2091 /* Shake means exit and launch a new saver.
2093 - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
2095 [self stopAndClose:YES];
2099 - (void)setScreenLocked:(BOOL)locked
2101 if (screenLocked == locked) return;
2102 screenLocked = locked;
2104 if ([self isAnimating])
2105 [self stopAnimation];
2107 if (! [self isAnimating])
2108 [self startAnimation];
2112 #endif // USE_IPHONE
2115 - (void) checkForUpdates
2118 // We only check once at startup, even if there are multiple screens,
2119 // and even if this saver is running for many days.
2120 // (Uh, except this doesn't work because this static isn't shared,
2121 // even if we make it an exported global. Not sure why. Oh well.)
2122 static BOOL checked_p = NO;
2123 if (checked_p) return;
2126 // If it's off, don't bother running the updater. Otherwise, the
2127 // updater will decide if it's time to hit the network.
2128 if (! get_boolean_resource (xdpy,
2129 SUSUEnableAutomaticChecksKey,
2130 SUSUEnableAutomaticChecksKey))
2133 NSString *updater = @"XScreenSaverUpdater.app";
2135 // There may be multiple copies of the updater: e.g., one in /Applications
2136 // and one in the mounted installer DMG! It's important that we run the
2137 // one from the disk and not the DMG, so search for the right one.
2139 NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
2140 NSBundle *bundle = [NSBundle bundleForClass:[self class]];
2142 @[[[bundle bundlePath] stringByDeletingLastPathComponent],
2143 [@"~/Library/Screen Savers" stringByExpandingTildeInPath],
2144 @"/Library/Screen Savers",
2145 @"/System/Library/Screen Savers",
2147 @"/Applications/Utilities"];
2148 NSString *app_path = nil;
2149 for (NSString *dir in search) {
2150 NSString *p = [dir stringByAppendingPathComponent:updater];
2151 if ([[NSFileManager defaultManager] fileExistsAtPath:p]) {
2158 app_path = [workspace fullPathForApplication:updater];
2160 if (app_path && [app_path hasPrefix:@"/Volumes/XScreenSaver "])
2161 app_path = 0; // The DMG version will not do.
2164 NSLog(@"Unable to find %@", updater);
2169 if (! [workspace launchApplicationAtURL:[NSURL fileURLWithPath:app_path]
2170 options:(NSWorkspaceLaunchWithoutAddingToRecents |
2171 NSWorkspaceLaunchWithoutActivation |
2172 NSWorkspaceLaunchAndHide)
2175 NSLog(@"Unable to launch %@: %@", app_path, err);
2178 # endif // !USE_IPHONE
2184 /* Utility functions...
2187 static PrefsReader *
2188 get_prefsReader (Display *dpy)
2190 XScreenSaverView *view = jwxyz_window_view (XRootWindow (dpy, 0));
2191 if (!view) return 0;
2192 return [view prefsReader];
2197 get_string_resource (Display *dpy, char *name, char *class)
2199 return [get_prefsReader(dpy) getStringResource:name];
2203 get_boolean_resource (Display *dpy, char *name, char *class)
2205 return [get_prefsReader(dpy) getBooleanResource:name];
2209 get_integer_resource (Display *dpy, char *name, char *class)
2211 return [get_prefsReader(dpy) getIntegerResource:name];
2215 get_float_resource (Display *dpy, char *name, char *class)
2217 return [get_prefsReader(dpy) getFloatResource:name];